mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1007979 - Refactor nsSearchSuggestions into a reusable JSM. r=adw
Original JSM by mconnor.
This commit is contained in:
parent
0da1203900
commit
51f01f7794
349
toolkit/components/search/SearchSuggestionController.jsm
Normal file
349
toolkit/components/search/SearchSuggestionController.jsm
Normal file
@ -0,0 +1,349 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["SearchSuggestionController"];
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NS_ASSERT", "resource://gre/modules/debug.js");
|
||||
|
||||
const SEARCH_RESPONSE_SUGGESTION_JSON = "application/x-suggestions+json";
|
||||
const DEFAULT_FORM_HISTORY_PARAM = "searchbar-history";
|
||||
const HTTP_OK = 200;
|
||||
const REMOTE_TIMEOUT = 500; // maximum time (ms) to wait before giving up on a remote suggestions
|
||||
|
||||
/**
|
||||
* SearchSuggestionController.jsm exists as a helper module to allow multiple consumers to request and display
|
||||
* search suggestions from a given engine, regardless of the base implementation. Much of this
|
||||
* code was originally in nsSearchSuggestions.js until it was refactored to separate it from the
|
||||
* nsIAutoCompleteSearch dependency.
|
||||
* One instance of SearchSuggestionController should be used per field since form history results are cached.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {function} [callback] - Callback for search suggestion results. You can use the promise
|
||||
* returned by the search method instead if you prefer.
|
||||
* @constructor
|
||||
*/
|
||||
this.SearchSuggestionController = function SearchSuggestionController(callback = null) {
|
||||
this._callback = callback;
|
||||
};
|
||||
|
||||
this.SearchSuggestionController.prototype = {
|
||||
/**
|
||||
* The maximum number of local form history results to return.
|
||||
*/
|
||||
maxLocalResults: 7,
|
||||
|
||||
/**
|
||||
* The maximum number of remote search engine results to return.
|
||||
*/
|
||||
maxRemoteResults: 10,
|
||||
|
||||
// Private properties
|
||||
/**
|
||||
* The last form history result used to improve the performance of subsequent searches.
|
||||
* This shouldn't be used for any other purpose as it is never cleared and therefore could be stale.
|
||||
*/
|
||||
_formHistoryResult: null,
|
||||
|
||||
/**
|
||||
* The remote server timeout timer, if applicable. The timer starts when form history
|
||||
* search is completed.
|
||||
*/
|
||||
_remoteResultTimer: null,
|
||||
|
||||
/**
|
||||
* The deferred for the remote results before its promise is resolved.
|
||||
*/
|
||||
_deferredRemoteResult: null,
|
||||
|
||||
/**
|
||||
* The optional result callback registered from the constructor.
|
||||
*/
|
||||
_callback: null,
|
||||
|
||||
/**
|
||||
* The XMLHttpRequest object for remote results.
|
||||
*/
|
||||
_request: null,
|
||||
|
||||
// Public methods
|
||||
|
||||
/**
|
||||
* Fetch search suggestions from all of the providers. Fetches in progress will be stopped and
|
||||
* results from them will not be provided.
|
||||
*
|
||||
* @param {string} searchTerm - the term to provide suggestions for
|
||||
* @param {bool} privateMode - whether the request is being made in the context of private browsing
|
||||
* @param {nsISearchEngine} engine - search engine for the suggestions.
|
||||
*
|
||||
* @return {Promise} resolving to an object containing results or null.
|
||||
*/
|
||||
fetch: function(searchTerm, privateMode, engine) {
|
||||
// There is no smart filtering from previous results here (as there is when looking through
|
||||
// history/form data) because the result set returned by the server is different for every typed
|
||||
// value - e.g. "ocean breathes" does not return a subset of the results returned for "ocean".
|
||||
|
||||
this.stop();
|
||||
|
||||
if (!Services.search.isInitialized) {
|
||||
throw new Error("Search not initialized yet (how did you get here?)");
|
||||
}
|
||||
if (typeof privateMode === "undefined") {
|
||||
throw new Error("The privateMode argument is required to avoid unintentional privacy leaks");
|
||||
}
|
||||
if (!(engine instanceof Ci.nsISearchEngine)) {
|
||||
throw new Error("Invalid search engine");
|
||||
}
|
||||
if (!this.maxLocalResults && !this.maxRemoteResults) {
|
||||
throw new Error("Zero results expected, what are you trying to do?");
|
||||
}
|
||||
if (this.maxLocalResults < 0 || this.remoteResult < 0) {
|
||||
throw new Error("Number of requested results must be positive");
|
||||
}
|
||||
|
||||
// Array of promises to resolve before returning results.
|
||||
let promises = [];
|
||||
this._searchString = searchTerm;
|
||||
|
||||
// Remote results
|
||||
if (searchTerm && this.maxRemoteResults &&
|
||||
engine.supportsResponseType(SEARCH_RESPONSE_SUGGESTION_JSON)) {
|
||||
this._deferredRemoteResult = this._fetchRemote(searchTerm, engine, privateMode);
|
||||
promises.push(this._deferredRemoteResult.promise);
|
||||
}
|
||||
|
||||
// Local results from form history
|
||||
if (this.maxLocalResults) {
|
||||
let deferredHistoryResult = this._fetchFormHistory(searchTerm);
|
||||
promises.push(deferredHistoryResult.promise);
|
||||
}
|
||||
|
||||
function handleRejection(reason) {
|
||||
if (reason == "HTTP request aborted") {
|
||||
// Do nothing since this is normal.
|
||||
return null;
|
||||
}
|
||||
Cu.reportError("SearchSuggestionController rejection: " + reason);
|
||||
return null;
|
||||
}
|
||||
return Promise.all(promises).then(this._dedupeAndReturnResults.bind(this), handleRejection);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop pending fetches so no results are returned from them.
|
||||
*
|
||||
* Note: If there was no remote results fetched, the fetching cannot be stopped and local results
|
||||
* will still be returned because stopping relies on aborting the XMLHTTPRequest to reject the
|
||||
* promise for Promise.all.
|
||||
*/
|
||||
stop: function() {
|
||||
if (this._request) {
|
||||
this._request.abort();
|
||||
} else if (!this.maxRemoteResults) {
|
||||
Cu.reportError("SearchSuggestionController: Cannot stop fetching if remote results were not "+
|
||||
"requested");
|
||||
}
|
||||
this._reset();
|
||||
},
|
||||
|
||||
// Private methods
|
||||
|
||||
_fetchFormHistory: function(searchTerm) {
|
||||
let deferredFormHistory = Promise.defer();
|
||||
|
||||
let acSearchObserver = {
|
||||
// Implements nsIAutoCompleteSearch
|
||||
onSearchResult: (search, result) => {
|
||||
this._formHistoryResult = result;
|
||||
|
||||
if (this._request) {
|
||||
this._remoteResultTimer = Cc["@mozilla.org/timer;1"].
|
||||
createInstance(Ci.nsITimer);
|
||||
this._remoteResultTimer.initWithCallback(this._onRemoteTimeout.bind(this),
|
||||
REMOTE_TIMEOUT,
|
||||
Ci.nsITimer.TYPE_ONE_SHOT);
|
||||
}
|
||||
|
||||
switch (result.searchResult) {
|
||||
case Ci.nsIAutoCompleteResult.RESULT_SUCCESS:
|
||||
case Ci.nsIAutoCompleteResult.RESULT_NOMATCH:
|
||||
if (result.searchString !== this._searchString) {
|
||||
deferredFormHistory.resolve("Unexpected response, this._searchString does not match form history response");
|
||||
return;
|
||||
}
|
||||
let fhEntries = [];
|
||||
let maxHistoryItems = Math.min(result.matchCount, this.maxLocalResults);
|
||||
for (let i = 0; i < maxHistoryItems; ++i) {
|
||||
fhEntries.push(result.getValueAt(i));
|
||||
}
|
||||
deferredFormHistory.resolve({
|
||||
result: fhEntries,
|
||||
formHistoryResult: result,
|
||||
});
|
||||
break;
|
||||
case Ci.nsIAutoCompleteResult.RESULT_FAILURE:
|
||||
case Ci.nsIAutoCompleteResult.RESULT_IGNORED:
|
||||
deferredFormHistory.resolve("Form History returned RESULT_FAILURE or RESULT_IGNORED");
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let formHistory = Cc["@mozilla.org/autocomplete/search;1?name=form-history"].
|
||||
createInstance(Ci.nsIAutoCompleteSearch);
|
||||
formHistory.startSearch(searchTerm, DEFAULT_FORM_HISTORY_PARAM, this._formHistoryResult,
|
||||
acSearchObserver);
|
||||
return deferredFormHistory;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch suggestions from the search engine over the network.
|
||||
*/
|
||||
_fetchRemote: function(searchTerm, engine, privateMode) {
|
||||
let deferredResponse = Promise.defer();
|
||||
this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
|
||||
createInstance(Ci.nsIXMLHttpRequest);
|
||||
let submission = engine.getSubmission(searchTerm,
|
||||
SEARCH_RESPONSE_SUGGESTION_JSON);
|
||||
let method = (submission.postData ? "POST" : "GET");
|
||||
this._request.open(method, submission.uri.spec, true);
|
||||
if (this._request.channel instanceof Ci.nsIPrivateBrowsingChannel) {
|
||||
this._request.channel.setPrivate(privateMode);
|
||||
}
|
||||
this._request.mozBackgroundRequest = true; // suppress dialogs and fail silently
|
||||
|
||||
this._request.addEventListener("load", this._onRemoteLoaded.bind(this, deferredResponse));
|
||||
this._request.addEventListener("error", (evt) => deferredResponse.resolve("HTTP error"));
|
||||
// Reject for an abort assuming it's always from .stop() in which case we shouldn't return local
|
||||
// or remote results for existing searches.
|
||||
this._request.addEventListener("abort", (evt) => deferredResponse.reject("HTTP request aborted"));
|
||||
|
||||
this._request.send(submission.postData);
|
||||
|
||||
return deferredResponse;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the request completed successfully (thought the HTTP status could be anything)
|
||||
* so we can handle the response data.
|
||||
* @private
|
||||
*/
|
||||
_onRemoteLoaded: function(deferredResponse) {
|
||||
if (!this._request) {
|
||||
deferredResponse.resolve("Got HTTP response after the request was cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
let status, serverResults;
|
||||
try {
|
||||
status = this._request.status;
|
||||
} catch (e) {
|
||||
// The XMLHttpRequest can throw NS_ERROR_NOT_AVAILABLE.
|
||||
deferredResponse.resolve("Unknown HTTP status: " + e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status != HTTP_OK || this._request.responseText == "") {
|
||||
deferredResponse.resolve("Non-200 status or empty HTTP response: " + status);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
serverResults = JSON.parse(this._request.responseText);
|
||||
} catch(ex) {
|
||||
deferredResponse.resolve("Failed to parse suggestion JSON: " + ex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._searchString !== serverResults[0]) {
|
||||
// something is wrong here so drop remote results
|
||||
deferredResponse.resolve("Unexpected response, this._searchString does not match remote response");
|
||||
return;
|
||||
}
|
||||
let results = serverResults[1] || [];
|
||||
deferredResponse.resolve({ result: results });
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when this._remoteResultTimer fires indicating the remote request took too long.
|
||||
*/
|
||||
_onRemoteTimeout: function () {
|
||||
this._request = null;
|
||||
|
||||
// FIXME: bug 387341
|
||||
// Need to break the cycle between us and the timer.
|
||||
this._remoteResultTimer = null;
|
||||
|
||||
// The XMLHTTPRequest for suggest results is taking too long
|
||||
// so send out the form history results and cancel the request.
|
||||
if (this._deferredRemoteResult) {
|
||||
this._deferredRemoteResult.resolve("HTTP Timeout");
|
||||
this._deferredRemoteResult = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Array} suggestResults - an array of result objects from different sources (local or remote)
|
||||
* @return {Object}
|
||||
*/
|
||||
_dedupeAndReturnResults: function(suggestResults) {
|
||||
NS_ASSERT(this._searchString !== null, "this._searchString shouldn't be null when returning results");
|
||||
let results = {
|
||||
term: this._searchString,
|
||||
remote: [],
|
||||
local: [],
|
||||
formHistoryResult: null,
|
||||
};
|
||||
|
||||
for (let result of suggestResults) {
|
||||
if (typeof result === "string") { // Failure message
|
||||
Cu.reportError("SearchSuggestionController: " + result);
|
||||
} else if (result.formHistoryResult) { // Local results have a formHistoryResult property.
|
||||
results.formHistoryResult = result.formHistoryResult;
|
||||
results.local = result.result || [];
|
||||
} else { // Remote result
|
||||
results.remote = result.result || [];
|
||||
}
|
||||
}
|
||||
|
||||
// We don't want things to appear in both history and suggestions so remove entries from
|
||||
// remote results that are alrady in local.
|
||||
if (results.remote.length && results.local.length) {
|
||||
for (let i = 0; i < results.local.length; ++i) {
|
||||
let term = results.local[i];
|
||||
let dupIndex = results.remote.indexOf(term);
|
||||
if (dupIndex != -1) {
|
||||
results.remote.splice(dupIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trim the number of results to the maximum requested (now that we've pruned dupes).
|
||||
results.remote = results.remote.slice(0, this.maxRemoteResults);
|
||||
|
||||
if (this._callback) {
|
||||
this._callback(results);
|
||||
}
|
||||
this._reset();
|
||||
|
||||
return results;
|
||||
},
|
||||
|
||||
_reset: function() {
|
||||
this._request = null;
|
||||
if (this._remoteResultTimer) {
|
||||
this._remoteResultTimer.cancel();
|
||||
this._remoteResultTimer = null;
|
||||
}
|
||||
this._deferredRemoteResult = null;
|
||||
this._searchString = null;
|
||||
},
|
||||
};
|
@ -11,6 +11,10 @@ EXTRA_COMPONENTS += [
|
||||
'toolkitsearch.manifest',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'SearchSuggestionController.jsm',
|
||||
]
|
||||
|
||||
EXTRA_PP_COMPONENTS += [
|
||||
'nsSearchService.js',
|
||||
]
|
||||
|
@ -2,32 +2,24 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const SEARCH_RESPONSE_SUGGESTION_JSON = "application/x-suggestions+json";
|
||||
|
||||
const BROWSER_SUGGEST_PREF = "browser.search.suggest.enabled";
|
||||
const XPCOM_SHUTDOWN_TOPIC = "xpcom-shutdown";
|
||||
const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
const HTTP_OK = 200;
|
||||
const HTTP_INTERNAL_SERVER_ERROR = 500;
|
||||
const HTTP_BAD_GATEWAY = 502;
|
||||
const HTTP_SERVICE_UNAVAILABLE = 503;
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/nsFormAutoCompleteResult.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SearchSuggestionController",
|
||||
"resource://gre/modules/SearchSuggestionController.jsm");
|
||||
|
||||
/**
|
||||
* SuggestAutoComplete is a base class that implements nsIAutoCompleteSearch
|
||||
* and can collect results for a given search by using the search URL supplied
|
||||
* by the subclass. We do it this way since the AutoCompleteController in
|
||||
* Mozilla requires a unique XPCOM Service for every search provider, even if
|
||||
* the logic for two providers is identical.
|
||||
* and can collect results for a given search by using this._suggestionController.
|
||||
* We do it this way since the AutoCompleteController in Mozilla requires a
|
||||
* unique XPCOM Service for every search provider, even if the logic for two
|
||||
* providers is identical.
|
||||
* @constructor
|
||||
*/
|
||||
function SuggestAutoComplete() {
|
||||
@ -38,6 +30,7 @@ SuggestAutoComplete.prototype = {
|
||||
_init: function() {
|
||||
this._addObservers();
|
||||
this._suggestEnabled = Services.prefs.getBoolPref(BROWSER_SUGGEST_PREF);
|
||||
this._suggestionController = new SearchSuggestionController(obj => this.onResultsReturned(obj));
|
||||
},
|
||||
|
||||
get _suggestionLabel() {
|
||||
@ -51,59 +44,6 @@ SuggestAutoComplete.prototype = {
|
||||
*/
|
||||
_suggestEnabled: null,
|
||||
|
||||
/*************************************************************************
|
||||
* Server request backoff implementation fields below
|
||||
* These allow us to throttle requests if the server is getting hammered.
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* This is an array that contains the timestamps (in unixtime) of
|
||||
* the last few backoff-triggering errors.
|
||||
*/
|
||||
_serverErrorLog: [],
|
||||
|
||||
/**
|
||||
* If we receive this number of backoff errors within the amount of time
|
||||
* specified by _serverErrorPeriod, then we initiate backoff.
|
||||
*/
|
||||
_maxErrorsBeforeBackoff: 3,
|
||||
|
||||
/**
|
||||
* If we receive enough consecutive errors (where "enough" is defined by
|
||||
* _maxErrorsBeforeBackoff above) within this time period,
|
||||
* we trigger the backoff behavior.
|
||||
*/
|
||||
_serverErrorPeriod: 600000, // 10 minutes in milliseconds
|
||||
|
||||
/**
|
||||
* If we get another backoff error immediately after timeout, we increase the
|
||||
* backoff to (2 x old period) + this value.
|
||||
*/
|
||||
_serverErrorTimeoutIncrement: 600000, // 10 minutes in milliseconds
|
||||
|
||||
/**
|
||||
* The current amount of time to wait before trying a server request
|
||||
* after receiving a backoff error.
|
||||
*/
|
||||
_serverErrorTimeout: 0,
|
||||
|
||||
/**
|
||||
* Time (in unixtime) after which we're allowed to try requesting again.
|
||||
*/
|
||||
_nextRequestTime: 0,
|
||||
|
||||
/**
|
||||
* The last engine we requested against (so that we can tell if the
|
||||
* user switched engines).
|
||||
*/
|
||||
_serverErrorEngine: null,
|
||||
|
||||
/**
|
||||
* The XMLHttpRequest object.
|
||||
* @private
|
||||
*/
|
||||
_request: null,
|
||||
|
||||
/**
|
||||
* The object implementing nsIAutoCompleteObserver that we notify when
|
||||
* we have found results
|
||||
@ -111,81 +51,6 @@ SuggestAutoComplete.prototype = {
|
||||
*/
|
||||
_listener: null,
|
||||
|
||||
/**
|
||||
* If this is true, we'll integrate form history results with the
|
||||
* suggest results.
|
||||
*/
|
||||
_includeFormHistory: true,
|
||||
|
||||
/**
|
||||
* True if a request for remote suggestions was sent. This is used to
|
||||
* differentiate between the "_request is null because the request has
|
||||
* already returned a result" and "_request is null because no request was
|
||||
* sent" cases.
|
||||
*/
|
||||
_sentSuggestRequest: false,
|
||||
|
||||
/**
|
||||
* This is the callback for the suggest timeout timer.
|
||||
*/
|
||||
notify: function SAC_notify(timer) {
|
||||
// FIXME: bug 387341
|
||||
// Need to break the cycle between us and the timer.
|
||||
this._formHistoryTimer = null;
|
||||
|
||||
// If this._listener is null, we've already sent out suggest results, so
|
||||
// nothing left to do here.
|
||||
if (!this._listener)
|
||||
return;
|
||||
|
||||
// Otherwise, the XMLHTTPRequest for suggest results is taking too long,
|
||||
// so send out the form history results and cancel the request.
|
||||
this._listener.onSearchResult(this, this._formHistoryResult);
|
||||
this._reset();
|
||||
},
|
||||
|
||||
/**
|
||||
* This determines how long (in ms) we should wait before giving up on
|
||||
* the suggestions and just showing local form history results.
|
||||
*/
|
||||
_suggestionTimeout: 500,
|
||||
|
||||
/**
|
||||
* This is the callback for that the form history service uses to
|
||||
* send us results.
|
||||
*/
|
||||
onSearchResult: function SAC_onSearchResult(search, result) {
|
||||
this._formHistoryResult = result;
|
||||
|
||||
if (this._request) {
|
||||
// We still have a pending request, wait a bit to give it a chance to
|
||||
// finish.
|
||||
this._formHistoryTimer = Cc["@mozilla.org/timer;1"].
|
||||
createInstance(Ci.nsITimer);
|
||||
this._formHistoryTimer.initWithCallback(this, this._suggestionTimeout,
|
||||
Ci.nsITimer.TYPE_ONE_SHOT);
|
||||
} else if (!this._sentSuggestRequest) {
|
||||
// We didn't send a request, so just send back the form history results.
|
||||
this._listener.onSearchResult(this, this._formHistoryResult);
|
||||
this._reset();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This is the URI that the last suggest request was sent to.
|
||||
*/
|
||||
_suggestURI: null,
|
||||
|
||||
/**
|
||||
* Autocomplete results from the form history service get stored here.
|
||||
*/
|
||||
_formHistoryResult: null,
|
||||
|
||||
/**
|
||||
* This holds the suggest server timeout timer, if applicable.
|
||||
*/
|
||||
_formHistoryTimer: null,
|
||||
|
||||
/**
|
||||
* Maximum number of history items displayed. This is capped at 7
|
||||
* because the primary consumer (Firefox search bar) displays 10 rows
|
||||
@ -195,176 +60,32 @@ SuggestAutoComplete.prototype = {
|
||||
_historyLimit: 7,
|
||||
|
||||
/**
|
||||
* This clears all the per-request state.
|
||||
*/
|
||||
_reset: function SAC_reset() {
|
||||
// Don't let go of our listener and form history result if the timer is
|
||||
// still pending, the timer will call _reset() when it fires.
|
||||
if (!this._formHistoryTimer) {
|
||||
this._listener = null;
|
||||
this._formHistoryResult = null;
|
||||
}
|
||||
this._request = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* This sends an autocompletion request to the form history service,
|
||||
* which will call onSearchResults with the results of the query.
|
||||
*/
|
||||
_startHistorySearch: function SAC_SHSearch(searchString, searchParam) {
|
||||
var formHistory =
|
||||
Cc["@mozilla.org/autocomplete/search;1?name=form-history"].
|
||||
createInstance(Ci.nsIAutoCompleteSearch);
|
||||
formHistory.startSearch(searchString, searchParam, this._formHistoryResult, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Makes a note of the fact that we've received a backoff-triggering
|
||||
* response, so that we can adjust the backoff behavior appropriately.
|
||||
*/
|
||||
_noteServerError: function SAC__noteServeError() {
|
||||
var currentTime = Date.now();
|
||||
|
||||
this._serverErrorLog.push(currentTime);
|
||||
if (this._serverErrorLog.length > this._maxErrorsBeforeBackoff)
|
||||
this._serverErrorLog.shift();
|
||||
|
||||
if ((this._serverErrorLog.length == this._maxErrorsBeforeBackoff) &&
|
||||
((currentTime - this._serverErrorLog[0]) < this._serverErrorPeriod)) {
|
||||
// increase timeout, and then don't request until timeout is over
|
||||
this._serverErrorTimeout = (this._serverErrorTimeout * 2) +
|
||||
this._serverErrorTimeoutIncrement;
|
||||
this._nextRequestTime = currentTime + this._serverErrorTimeout;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets the backoff behavior; called when we get a successful response.
|
||||
*/
|
||||
_clearServerErrors: function SAC__clearServerErrors() {
|
||||
this._serverErrorLog = [];
|
||||
this._serverErrorTimeout = 0;
|
||||
this._nextRequestTime = 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* This checks whether we should send a server request (i.e. we're not
|
||||
* in a error-triggered backoff period.
|
||||
*
|
||||
* Callback for handling results from SearchSuggestionController.jsm
|
||||
* @private
|
||||
*/
|
||||
_okToRequest: function SAC__okToRequest() {
|
||||
return Date.now() > this._nextRequestTime;
|
||||
},
|
||||
onResultsReturned: function(results) {
|
||||
let finalResults = [];
|
||||
let finalComments = [];
|
||||
|
||||
/**
|
||||
* This checks to see if the new search engine is different
|
||||
* from the previous one, and if so clears any error state that might
|
||||
* have accumulated for the old engine.
|
||||
*
|
||||
* @param engine The engine that the suggestion request would be sent to.
|
||||
* @private
|
||||
*/
|
||||
_checkForEngineSwitch: function SAC__checkForEngineSwitch(engine) {
|
||||
if (engine == this._serverErrorEngine)
|
||||
return;
|
||||
|
||||
// must've switched search providers, clear old errors
|
||||
this._serverErrorEngine = engine;
|
||||
this._clearServerErrors();
|
||||
},
|
||||
|
||||
/**
|
||||
* This returns true if the status code of the HTTP response
|
||||
* represents a backoff-triggering error.
|
||||
*
|
||||
* @param status The status code from the HTTP response
|
||||
* @private
|
||||
*/
|
||||
_isBackoffError: function SAC__isBackoffError(status) {
|
||||
return ((status == HTTP_INTERNAL_SERVER_ERROR) ||
|
||||
(status == HTTP_BAD_GATEWAY) ||
|
||||
(status == HTTP_SERVICE_UNAVAILABLE));
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the 'readyState' of the XMLHttpRequest changes. We only care
|
||||
* about state 4 (COMPLETED) - handle the response data.
|
||||
* @private
|
||||
*/
|
||||
onReadyStateChange: function() {
|
||||
// xxx use the real const here
|
||||
if (!this._request || this._request.readyState != 4)
|
||||
return;
|
||||
|
||||
try {
|
||||
var status = this._request.status;
|
||||
} catch (e) {
|
||||
// The XML HttpRequest can throw NS_ERROR_NOT_AVAILABLE.
|
||||
return;
|
||||
// If form history has results, add them to the list.
|
||||
let maxHistoryItems = Math.min(results.local.length, this._historyLimit);
|
||||
for (let i = 0; i < maxHistoryItems; ++i) {
|
||||
finalResults.push(results.local[i]);
|
||||
finalComments.push("");
|
||||
}
|
||||
|
||||
if (this._isBackoffError(status)) {
|
||||
this._noteServerError();
|
||||
return;
|
||||
}
|
||||
|
||||
var responseText = this._request.responseText;
|
||||
if (status != HTTP_OK || responseText == "")
|
||||
return;
|
||||
|
||||
this._clearServerErrors();
|
||||
|
||||
try {
|
||||
var serverResults = JSON.parse(responseText);
|
||||
} catch(ex) {
|
||||
Components.utils.reportError("Failed to parse JSON from " + this._suggestURI.spec + ": " + ex);
|
||||
return;
|
||||
}
|
||||
|
||||
var searchString = serverResults[0] || "";
|
||||
var results = serverResults[1] || [];
|
||||
|
||||
var comments = []; // "comments" column values for suggestions
|
||||
var historyResults = [];
|
||||
var historyComments = [];
|
||||
|
||||
// If form history is enabled and has results, add them to the list.
|
||||
if (this._includeFormHistory && this._formHistoryResult &&
|
||||
(this._formHistoryResult.searchResult ==
|
||||
Ci.nsIAutoCompleteResult.RESULT_SUCCESS)) {
|
||||
var maxHistoryItems = Math.min(this._formHistoryResult.matchCount, this._historyLimit);
|
||||
for (var i = 0; i < maxHistoryItems; ++i) {
|
||||
var term = this._formHistoryResult.getValueAt(i);
|
||||
|
||||
// we don't want things to appear in both history and suggestions
|
||||
var dupIndex = results.indexOf(term);
|
||||
if (dupIndex != -1)
|
||||
results.splice(dupIndex, 1);
|
||||
|
||||
historyResults.push(term);
|
||||
historyComments.push("");
|
||||
}
|
||||
}
|
||||
|
||||
// fill out the comment column for the suggestions
|
||||
for (var i = 0; i < results.length; ++i)
|
||||
comments.push("");
|
||||
|
||||
// if we have any suggestions, put a label at the top
|
||||
if (comments.length > 0)
|
||||
// If there are remote matches, add them.
|
||||
if (results.remote.length) {
|
||||
// "comments" column values for suggestions starts as empty strings
|
||||
let comments = new Array(results.remote.length).fill("", 1);
|
||||
comments[0] = this._suggestionLabel;
|
||||
|
||||
// now put the history results above the suggestions
|
||||
var finalResults = historyResults.concat(results);
|
||||
var finalComments = historyComments.concat(comments);
|
||||
// now put the history results above the suggestions
|
||||
finalResults = finalResults.concat(results.remote);
|
||||
finalComments = finalComments.concat(comments);
|
||||
}
|
||||
|
||||
// Notify the FE of our new results
|
||||
this.onResultsReady(searchString, finalResults, finalComments,
|
||||
this._formHistoryResult);
|
||||
|
||||
// Reset our state for next time.
|
||||
this._reset();
|
||||
this.onResultsReady(results.term, finalResults, finalComments, results.formHistoryResult);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -374,10 +95,9 @@ SuggestAutoComplete.prototype = {
|
||||
* @param comments an array of metadata corresponding to the results
|
||||
* @private
|
||||
*/
|
||||
onResultsReady: function(searchString, results, comments,
|
||||
formHistoryResult) {
|
||||
onResultsReady: function(searchString, results, comments, formHistoryResult) {
|
||||
if (this._listener) {
|
||||
var result = new FormAutoCompleteResult(
|
||||
let result = new FormAutoCompleteResult(
|
||||
searchString,
|
||||
Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
|
||||
0,
|
||||
@ -389,8 +109,7 @@ SuggestAutoComplete.prototype = {
|
||||
|
||||
this._listener.onSearchResult(this, result);
|
||||
|
||||
// Null out listener to make sure we don't notify it twice, in case our
|
||||
// timer callback still hasn't run.
|
||||
// Null out listener to make sure we don't notify it twice
|
||||
this._listener = null;
|
||||
}
|
||||
},
|
||||
@ -445,56 +164,10 @@ SuggestAutoComplete.prototype = {
|
||||
* Actual implementation of search.
|
||||
*/
|
||||
_triggerSearch: function(searchString, searchParam, listener, privacyMode) {
|
||||
// If there's an existing request, stop it. There is no smart filtering
|
||||
// here as there is when looking through history/form data because the
|
||||
// result set returned by the server is different for every typed value -
|
||||
// "ocean breathes" does not return a subset of the results returned for
|
||||
// "ocean", for example. This does nothing if there is no current request.
|
||||
this.stopSearch();
|
||||
|
||||
this._listener = listener;
|
||||
|
||||
var engine = Services.search.currentEngine;
|
||||
|
||||
this._checkForEngineSwitch(engine);
|
||||
|
||||
if (!searchString ||
|
||||
!this._suggestEnabled ||
|
||||
!engine.supportsResponseType(SEARCH_RESPONSE_SUGGESTION_JSON) ||
|
||||
!this._okToRequest()) {
|
||||
// We have an empty search string (user pressed down arrow to see
|
||||
// history), or search suggestions are disabled, or the current engine
|
||||
// has no suggest functionality, or we're in backoff mode; so just use
|
||||
// local history.
|
||||
this._sentSuggestRequest = false;
|
||||
this._startHistorySearch(searchString, searchParam);
|
||||
return;
|
||||
}
|
||||
|
||||
// Actually do the search
|
||||
this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
|
||||
createInstance(Ci.nsIXMLHttpRequest);
|
||||
var submission = engine.getSubmission(searchString,
|
||||
SEARCH_RESPONSE_SUGGESTION_JSON);
|
||||
this._suggestURI = submission.uri;
|
||||
var method = (submission.postData ? "POST" : "GET");
|
||||
this._request.open(method, this._suggestURI.spec, true);
|
||||
this._request.channel.notificationCallbacks = new AuthPromptOverride();
|
||||
if (this._request.channel instanceof Ci.nsIPrivateBrowsingChannel) {
|
||||
this._request.channel.setPrivate(privacyMode);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
function onReadyStateChange() {
|
||||
self.onReadyStateChange();
|
||||
}
|
||||
this._request.onreadystatechange = onReadyStateChange;
|
||||
this._request.send(submission.postData);
|
||||
|
||||
if (this._includeFormHistory) {
|
||||
this._sentSuggestRequest = true;
|
||||
this._startHistorySearch(searchString, searchParam);
|
||||
}
|
||||
this._suggestionController.fetch(searchString,
|
||||
privacyMode,
|
||||
Services.search.currentEngine);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -502,10 +175,7 @@ SuggestAutoComplete.prototype = {
|
||||
* implementation.
|
||||
*/
|
||||
stopSearch: function() {
|
||||
if (this._request) {
|
||||
this._request.abort();
|
||||
this._reset();
|
||||
}
|
||||
this._suggestionController.stop();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -539,31 +209,6 @@ SuggestAutoComplete.prototype = {
|
||||
Ci.nsIAutoCompleteObserver])
|
||||
};
|
||||
|
||||
function AuthPromptOverride() {
|
||||
}
|
||||
AuthPromptOverride.prototype = {
|
||||
// nsIAuthPromptProvider
|
||||
getAuthPrompt: function (reason, iid) {
|
||||
// Return a no-op nsIAuthPrompt2 implementation.
|
||||
return {
|
||||
promptAuth: function () {
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
},
|
||||
asyncPromptAuth: function () {
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
// nsIInterfaceRequestor
|
||||
getInterface: function SSLL_getInterface(iid) {
|
||||
return this.QueryInterface(iid);
|
||||
},
|
||||
|
||||
// nsISupports
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAuthPromptProvider,
|
||||
Ci.nsIInterfaceRequestor])
|
||||
};
|
||||
/**
|
||||
* SearchSuggestAutoComplete is a service implementation that handles suggest
|
||||
* results specific to web searches.
|
||||
|
@ -0,0 +1,63 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
/**
|
||||
* Dynamically create a search engine offering search suggestions via searchSuggestions.sjs.
|
||||
*
|
||||
* The engine is constructed by passing a JSON object with engine datails as the query string.
|
||||
*/
|
||||
|
||||
function handleRequest(request, response) {
|
||||
let engineData = JSON.parse(unescape(request.queryString).replace("+", " "));
|
||||
|
||||
if (!engineData.baseURL) {
|
||||
response.setStatusLine(request.httpVersion, 500, "baseURL required");
|
||||
return;
|
||||
}
|
||||
|
||||
engineData.engineType = engineData.engineType || Ci.nsISearchEngine.TYPE_OPENSEARCH;
|
||||
engineData.name = engineData.name || "Generated test engine";
|
||||
engineData.description = engineData.description || "Generated test engine description";
|
||||
engineData.method = engineData.method || "GET";
|
||||
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
|
||||
switch (engineData.engineType) {
|
||||
case Ci.nsISearchEngine.TYPE_OPENSEARCH:
|
||||
createOpenSearchEngine(response, engineData);
|
||||
break;
|
||||
default:
|
||||
response.setStatusLine(request.httpVersion, 404, "Unsupported engine type");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an OpenSearch engine for the given base URL.
|
||||
*/
|
||||
function createOpenSearchEngine(response, engineData) {
|
||||
let params = "", queryString = "";
|
||||
if (engineData.method == "POST") {
|
||||
params = "<Param name='q' value='{searchTerms}'/>";
|
||||
} else {
|
||||
queryString = "?q={searchTerms}";
|
||||
}
|
||||
|
||||
let result = "<?xml version='1.0' encoding='utf-8'?>\
|
||||
<OpenSearchDescription xmlns='http://a9.com/-/spec/opensearch/1.1/'>\
|
||||
<ShortName>" + engineData.name + "</ShortName>\
|
||||
<Description>" + engineData.description + "</Description>\
|
||||
<InputEncoding>UTF-8</InputEncoding>\
|
||||
<LongName>" + engineData.name + "</LongName>\
|
||||
<Url type='application/x-suggestions+json' method='" + engineData.method + "'\
|
||||
template='" + engineData.baseURL + "searchSuggestions.sjs" + queryString + "'>\
|
||||
" + params + "\
|
||||
</Url>\
|
||||
<Url type='text/html' method='" + engineData.method + "'\
|
||||
template='" + engineData.baseURL + queryString + "'/>\
|
||||
</OpenSearchDescription>\
|
||||
";
|
||||
response.write(result);
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Timer.jsm");
|
||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
/**
|
||||
* Provide search suggestions in the OpenSearch JSON format.
|
||||
*/
|
||||
|
||||
function handleRequest(request, response) {
|
||||
// Get the query parameters from the query string.
|
||||
let query = parseQueryString(request.queryString);
|
||||
|
||||
function writeSuggestions(query, completions = []) {
|
||||
let result = [query, completions];
|
||||
response.write(JSON.stringify(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
|
||||
let q = request.method == "GET" ? query.q : undefined;
|
||||
if (q == "no remote" || q == "no results") {
|
||||
writeSuggestions(q);
|
||||
} else if (q == "Query Mismatch") {
|
||||
writeSuggestions("This is an incorrect query string", ["some result"]);
|
||||
} else if (q == "") {
|
||||
writeSuggestions("", ["The server should never be sent an empty query"]);
|
||||
} else if (q && q.startsWith("mo")) {
|
||||
writeSuggestions(q, ["Mozilla", "modern", "mom"]);
|
||||
} else if (q && q.startsWith("I ❤️")) {
|
||||
writeSuggestions(q, ["I ❤️ Mozilla"]);
|
||||
} else if (q && q.startsWith("letter ")) {
|
||||
let letters = [];
|
||||
for (let charCode = "A".charCodeAt(); charCode <= "Z".charCodeAt(); charCode++) {
|
||||
letters.push("letter " + String.fromCharCode(charCode));
|
||||
}
|
||||
writeSuggestions(q, letters);
|
||||
} else if (q && q.startsWith("HTTP ")) {
|
||||
response.setStatusLine(request.httpVersion, q.replace("HTTP ", ""), q);
|
||||
writeSuggestions(q, [q]);
|
||||
} else if (q && q.startsWith("delay")) {
|
||||
// Delay the response by 200 milliseconds (less than the timeout but hopefully enough to abort
|
||||
// before completion).
|
||||
response.processAsync();
|
||||
writeSuggestions(q, [q]);
|
||||
setTimeout(() => response.finish(), 200);
|
||||
} else if (q && q.startsWith("slow ")) {
|
||||
// Delay the response by 10 seconds so the client timeout is reached.
|
||||
response.processAsync();
|
||||
writeSuggestions(q, [q]);
|
||||
setTimeout(() => response.finish(), 10000);
|
||||
} else if (request.method == "POST") {
|
||||
// This includes headers, not just the body
|
||||
let requestText = NetUtil.readInputStreamToString(request.bodyInputStream,
|
||||
request.bodyInputStream.available());
|
||||
// Only use the last line which contains the encoded params
|
||||
let requestLines = requestText.split("\n");
|
||||
let postParams = parseQueryString(requestLines[requestLines.length - 1]);
|
||||
writeSuggestions(postParams.q, ["Mozilla", "modern", "mom"]);
|
||||
} else {
|
||||
response.setStatusLine(request.httpVersion, 404, "Not Found");
|
||||
}
|
||||
}
|
||||
|
||||
function parseQueryString(queryString) {
|
||||
let query = {};
|
||||
queryString.split('&').forEach(function (val) {
|
||||
let [name, value] = val.split('=');
|
||||
query[name] = unescape(value).replace("+", " ");
|
||||
});
|
||||
return query;
|
||||
}
|
469
toolkit/components/search/tests/xpcshell/test_searchSuggest.js
Normal file
469
toolkit/components/search/tests/xpcshell/test_searchSuggest.js
Normal file
@ -0,0 +1,469 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Testing search suggestions from SearchSuggestionController.jsm.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/FormHistory.jsm");
|
||||
Cu.import("resource://gre/modules/SearchSuggestionController.jsm");
|
||||
Cu.import("resource://gre/modules/Timer.jsm");
|
||||
|
||||
let httpServer = new HttpServer();
|
||||
let getEngine, postEngine, unresolvableEngine;
|
||||
|
||||
function run_test() {
|
||||
removeMetadata();
|
||||
updateAppInfo();
|
||||
|
||||
let httpServer = useHttpServer();
|
||||
httpServer.registerContentType("sjs", "sjs");
|
||||
|
||||
do_register_cleanup(function* cleanup() {
|
||||
// Remove added form history entries
|
||||
yield updateSearchHistory("remove", null);
|
||||
FormHistory.shutdown();
|
||||
});
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* add_test_engines() {
|
||||
let getEngineData = {
|
||||
baseURL: gDataUrl,
|
||||
engineType: Ci.nsISearchEngine.TYPE_OPENSEARCH,
|
||||
name: "GET suggestion engine",
|
||||
method: "GET",
|
||||
};
|
||||
|
||||
let postEngineData = {
|
||||
baseURL: gDataUrl,
|
||||
engineType: Ci.nsISearchEngine.TYPE_OPENSEARCH,
|
||||
name: "POST suggestion engine",
|
||||
method: "POST",
|
||||
};
|
||||
|
||||
let unresolvableEngineData = {
|
||||
baseURL: "http://example.invalid/",
|
||||
engineType: Ci.nsISearchEngine.TYPE_OPENSEARCH,
|
||||
name: "Offline suggestion engine",
|
||||
method: "GET",
|
||||
};
|
||||
|
||||
[getEngine, postEngine, unresolvableEngine] = yield addTestEngines([
|
||||
{
|
||||
name: getEngineData.name,
|
||||
xmlFileName: "engineMaker.sjs?" + JSON.stringify(getEngineData),
|
||||
},
|
||||
{
|
||||
name: postEngineData.name,
|
||||
xmlFileName: "engineMaker.sjs?" + JSON.stringify(postEngineData),
|
||||
},
|
||||
{
|
||||
name: unresolvableEngineData.name,
|
||||
xmlFileName: "engineMaker.sjs?" + JSON.stringify(unresolvableEngineData),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
// Begin tests
|
||||
|
||||
add_task(function* simple_no_result_callback() {
|
||||
let deferred = Promise.defer();
|
||||
let controller = new SearchSuggestionController((result) => {
|
||||
do_check_eq(result.term, "no remote");
|
||||
do_check_eq(result.local.length, 0);
|
||||
do_check_eq(result.remote.length, 0);
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
controller.fetch("no remote", false, getEngine);
|
||||
yield deferred.promise;
|
||||
});
|
||||
|
||||
add_task(function* simple_no_result_callback_and_promise() {
|
||||
// Make sure both the callback and promise get results
|
||||
let deferred = Promise.defer();
|
||||
let controller = new SearchSuggestionController((result) => {
|
||||
do_check_eq(result.term, "no results");
|
||||
do_check_eq(result.local.length, 0);
|
||||
do_check_eq(result.remote.length, 0);
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
let result = yield controller.fetch("no results", false, getEngine);
|
||||
do_check_eq(result.term, "no results");
|
||||
do_check_eq(result.local.length, 0);
|
||||
do_check_eq(result.remote.length, 0);
|
||||
|
||||
yield deferred.promise;
|
||||
});
|
||||
|
||||
add_task(function* simple_no_result_promise() {
|
||||
let controller = new SearchSuggestionController();
|
||||
let result = yield controller.fetch("no remote", false, getEngine);
|
||||
do_check_eq(result.term, "no remote");
|
||||
do_check_eq(result.local.length, 0);
|
||||
do_check_eq(result.remote.length, 0);
|
||||
});
|
||||
|
||||
add_task(function* simple_remote_no_local_result() {
|
||||
let controller = new SearchSuggestionController();
|
||||
let result = yield controller.fetch("mo", false, getEngine);
|
||||
do_check_eq(result.term, "mo");
|
||||
do_check_eq(result.local.length, 0);
|
||||
do_check_eq(result.remote.length, 3);
|
||||
do_check_eq(result.remote[0], "Mozilla");
|
||||
do_check_eq(result.remote[1], "modern");
|
||||
do_check_eq(result.remote[2], "mom");
|
||||
});
|
||||
|
||||
add_task(function* simple_local_no_remote_result() {
|
||||
yield updateSearchHistory("bump", "no remote entries");
|
||||
|
||||
let controller = new SearchSuggestionController();
|
||||
let result = yield controller.fetch("no remote", false, getEngine);
|
||||
do_check_eq(result.term, "no remote");
|
||||
do_check_eq(result.local.length, 1);
|
||||
do_check_eq(result.local[0], "no remote entries");
|
||||
do_check_eq(result.remote.length, 0);
|
||||
|
||||
yield updateSearchHistory("remove", "no remote entries");
|
||||
});
|
||||
|
||||
add_task(function* simple_non_ascii() {
|
||||
yield updateSearchHistory("bump", "I ❤️ XUL");
|
||||
|
||||
let controller = new SearchSuggestionController();
|
||||
let result = yield controller.fetch("I ❤️", false, getEngine);
|
||||
do_check_eq(result.term, "I ❤️");
|
||||
do_check_eq(result.local.length, 1);
|
||||
do_check_eq(result.local[0], "I ❤️ XUL");
|
||||
do_check_eq(result.remote.length, 1);
|
||||
do_check_eq(result.remote[0], "I ❤️ Mozilla");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* both_local_remote_result_dedupe() {
|
||||
yield updateSearchHistory("bump", "Mozilla");
|
||||
|
||||
let controller = new SearchSuggestionController();
|
||||
let result = yield controller.fetch("mo", false, getEngine);
|
||||
do_check_eq(result.term, "mo");
|
||||
do_check_eq(result.local.length, 1);
|
||||
do_check_eq(result.local[0], "Mozilla");
|
||||
do_check_eq(result.remote.length, 2);
|
||||
do_check_eq(result.remote[0], "modern");
|
||||
do_check_eq(result.remote[1], "mom");
|
||||
});
|
||||
|
||||
add_task(function* POST_both_local_remote_result_dedupe() {
|
||||
let controller = new SearchSuggestionController();
|
||||
let result = yield controller.fetch("mo", false, postEngine);
|
||||
do_check_eq(result.term, "mo");
|
||||
do_check_eq(result.local.length, 1);
|
||||
do_check_eq(result.local[0], "Mozilla");
|
||||
do_check_eq(result.remote.length, 2);
|
||||
do_check_eq(result.remote[0], "modern");
|
||||
do_check_eq(result.remote[1], "mom");
|
||||
});
|
||||
|
||||
add_task(function* both_local_remote_result_dedupe2() {
|
||||
yield updateSearchHistory("bump", "mom");
|
||||
|
||||
let controller = new SearchSuggestionController();
|
||||
let result = yield controller.fetch("mo", false, getEngine);
|
||||
do_check_eq(result.term, "mo");
|
||||
do_check_eq(result.local.length, 2);
|
||||
do_check_eq(result.local[0], "mom");
|
||||
do_check_eq(result.local[1], "Mozilla");
|
||||
do_check_eq(result.remote.length, 1);
|
||||
do_check_eq(result.remote[0], "modern");
|
||||
});
|
||||
|
||||
add_task(function* both_local_remote_result_dedupe3() {
|
||||
// All of the server entries also exist locally
|
||||
yield updateSearchHistory("bump", "modern");
|
||||
|
||||
let controller = new SearchSuggestionController();
|
||||
let result = yield controller.fetch("mo", false, getEngine);
|
||||
do_check_eq(result.term, "mo");
|
||||
do_check_eq(result.local.length, 3);
|
||||
do_check_eq(result.local[0], "modern");
|
||||
do_check_eq(result.local[1], "mom");
|
||||
do_check_eq(result.local[2], "Mozilla");
|
||||
do_check_eq(result.remote.length, 0);
|
||||
});
|
||||
|
||||
add_task(function* fetch_twice_in_a_row() {
|
||||
// Two entries since the first will match the first fetch but not the second.
|
||||
yield updateSearchHistory("bump", "delay local");
|
||||
yield updateSearchHistory("bump", "delayed local");
|
||||
|
||||
let controller = new SearchSuggestionController();
|
||||
let resultPromise1 = controller.fetch("delay", false, getEngine);
|
||||
|
||||
// A second fetch while the server is still waiting to return results leads to an abort.
|
||||
let resultPromise2 = controller.fetch("delayed ", false, getEngine);
|
||||
yield resultPromise1.then((results) => do_check_null(results));
|
||||
|
||||
let result = yield resultPromise2;
|
||||
do_check_eq(result.term, "delayed ");
|
||||
do_check_eq(result.local.length, 1);
|
||||
do_check_eq(result.local[0], "delayed local");
|
||||
do_check_eq(result.remote.length, 1);
|
||||
do_check_eq(result.remote[0], "delayed ");
|
||||
});
|
||||
|
||||
add_task(function* fetch_twice_subset_reuse_formHistoryResult() {
|
||||
// This tests if we mess up re-using the cached form history result.
|
||||
// Two entries since the first will match the first fetch but not the second.
|
||||
yield updateSearchHistory("bump", "delay local");
|
||||
yield updateSearchHistory("bump", "delayed local");
|
||||
|
||||
let controller = new SearchSuggestionController();
|
||||
let result = yield controller.fetch("delay", false, getEngine);
|
||||
do_check_eq(result.term, "delay");
|
||||
do_check_eq(result.local.length, 2);
|
||||
do_check_eq(result.local[0], "delay local");
|
||||
do_check_eq(result.local[1], "delayed local");
|
||||
do_check_eq(result.remote.length, 1);
|
||||
do_check_eq(result.remote[0], "delay");
|
||||
|
||||
// Remove the entry from the DB but it should remain in the cached formHistoryResult.
|
||||
yield updateSearchHistory("remove", "delayed local");
|
||||
|
||||
let result2 = yield controller.fetch("delayed ", false, getEngine);
|
||||
do_check_eq(result2.term, "delayed ");
|
||||
do_check_eq(result2.local.length, 1);
|
||||
do_check_eq(result2.local[0], "delayed local");
|
||||
do_check_eq(result2.remote.length, 1);
|
||||
do_check_eq(result2.remote[0], "delayed ");
|
||||
});
|
||||
|
||||
add_task(function* both_identical_with_more_than_max_results() {
|
||||
// Add letters A through Z to form history which will match the server
|
||||
for (let charCode = "A".charCodeAt(); charCode <= "Z".charCodeAt(); charCode++) {
|
||||
yield updateSearchHistory("bump", "letter " + String.fromCharCode(charCode));
|
||||
}
|
||||
|
||||
let controller = new SearchSuggestionController();
|
||||
controller.maxLocalResults = 7;
|
||||
controller.maxRemoteResults = 10;
|
||||
let result = yield controller.fetch("letter ", false, getEngine);
|
||||
do_check_eq(result.term, "letter ");
|
||||
do_check_eq(result.local.length, 7);
|
||||
for (let i = 0; i < controller.maxLocalResults; i++) {
|
||||
do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i));
|
||||
}
|
||||
do_check_eq(result.remote.length, 10);
|
||||
for (let i = 0; i < controller.maxRemoteResults; i++) {
|
||||
do_check_eq(result.remote[i],
|
||||
"letter " + String.fromCharCode("A".charCodeAt() + controller.maxLocalResults + i));
|
||||
}
|
||||
});
|
||||
|
||||
add_task(function* one_of_each() {
|
||||
let controller = new SearchSuggestionController();
|
||||
controller.maxLocalResults = 1;
|
||||
controller.maxRemoteResults = 1;
|
||||
let result = yield controller.fetch("letter ", false, getEngine);
|
||||
do_check_eq(result.term, "letter ");
|
||||
do_check_eq(result.local.length, 1);
|
||||
do_check_eq(result.local[0], "letter A");
|
||||
do_check_eq(result.remote.length, 1);
|
||||
do_check_eq(result.remote[0], "letter B");
|
||||
});
|
||||
|
||||
add_task(function* one_local_zero_remote() {
|
||||
let controller = new SearchSuggestionController();
|
||||
controller.maxLocalResults = 1;
|
||||
controller.maxRemoteResults = 0;
|
||||
let result = yield controller.fetch("letter ", false, getEngine);
|
||||
do_check_eq(result.term, "letter ");
|
||||
do_check_eq(result.local.length, 1);
|
||||
do_check_eq(result.local[0], "letter A");
|
||||
do_check_eq(result.remote.length, 0);
|
||||
});
|
||||
|
||||
add_task(function* zero_local_one_remote() {
|
||||
let controller = new SearchSuggestionController();
|
||||
controller.maxLocalResults = 0;
|
||||
controller.maxRemoteResults = 1;
|
||||
let result = yield controller.fetch("letter ", false, getEngine);
|
||||
do_check_eq(result.term, "letter ");
|
||||
do_check_eq(result.local.length, 0);
|
||||
do_check_eq(result.remote.length, 1);
|
||||
do_check_eq(result.remote[0], "letter A");
|
||||
});
|
||||
|
||||
add_task(function* stop_search() {
|
||||
let controller = new SearchSuggestionController((result) => {
|
||||
do_throw("The callback shouldn't be called after stop()");
|
||||
});
|
||||
let resultPromise = controller.fetch("mo", false, getEngine);
|
||||
controller.stop();
|
||||
yield resultPromise.then((result) => {
|
||||
do_check_null(result);
|
||||
});
|
||||
});
|
||||
|
||||
add_task(function* empty_searchTerm() {
|
||||
// Empty searches don't go to the server but still get form history.
|
||||
let controller = new SearchSuggestionController();
|
||||
let result = yield controller.fetch("", false, getEngine);
|
||||
do_check_eq(result.term, "");
|
||||
do_check_true(result.local.length > 0);
|
||||
do_check_eq(result.remote.length, 0);
|
||||
});
|
||||
|
||||
add_task(function* slow_timeout() {
|
||||
let d = Promise.defer();
|
||||
function check_result(result) {
|
||||
do_check_eq(result.term, "slow ");
|
||||
do_check_eq(result.local.length, 1);
|
||||
do_check_eq(result.local[0], "slow local result");
|
||||
do_check_eq(result.remote.length, 0);
|
||||
}
|
||||
yield updateSearchHistory("bump", "slow local result");
|
||||
|
||||
let controller = new SearchSuggestionController();
|
||||
setTimeout(function check_timeout() {
|
||||
// The HTTP response takes 10 seconds so check that we already have results after 2 seconds.
|
||||
check_result(result);
|
||||
d.resolve();
|
||||
}, 2000);
|
||||
let result = yield controller.fetch("slow ", false, getEngine);
|
||||
check_result(result);
|
||||
yield d.promise;
|
||||
});
|
||||
|
||||
add_task(function* slow_stop() {
|
||||
let d = Promise.defer();
|
||||
let controller = new SearchSuggestionController();
|
||||
let resultPromise = controller.fetch("slow ", false, getEngine);
|
||||
setTimeout(function check_timeout() {
|
||||
// The HTTP response takes 10 seconds but we timeout in less than a second so just use 0.
|
||||
controller.stop();
|
||||
d.resolve();
|
||||
}, 0);
|
||||
yield resultPromise.then((result) => {
|
||||
do_check_null(result);
|
||||
});
|
||||
|
||||
yield d.promise;
|
||||
});
|
||||
|
||||
|
||||
// Error handling
|
||||
|
||||
add_task(function* remote_term_mismatch() {
|
||||
yield updateSearchHistory("bump", "Query Mismatch Entry");
|
||||
|
||||
let controller = new SearchSuggestionController();
|
||||
let result = yield controller.fetch("Query Mismatch", false, getEngine);
|
||||
do_check_eq(result.term, "Query Mismatch");
|
||||
do_check_eq(result.local.length, 1);
|
||||
do_check_eq(result.local[0], "Query Mismatch Entry");
|
||||
do_check_eq(result.remote.length, 0);
|
||||
});
|
||||
|
||||
add_task(function* http_404() {
|
||||
yield updateSearchHistory("bump", "HTTP 404 Entry");
|
||||
|
||||
let controller = new SearchSuggestionController();
|
||||
let result = yield controller.fetch("HTTP 404", false, getEngine);
|
||||
do_check_eq(result.term, "HTTP 404");
|
||||
do_check_eq(result.local.length, 1);
|
||||
do_check_eq(result.local[0], "HTTP 404 Entry");
|
||||
do_check_eq(result.remote.length, 0);
|
||||
});
|
||||
|
||||
add_task(function* http_500() {
|
||||
yield updateSearchHistory("bump", "HTTP 500 Entry");
|
||||
|
||||
let controller = new SearchSuggestionController();
|
||||
let result = yield controller.fetch("HTTP 500", false, getEngine);
|
||||
do_check_eq(result.term, "HTTP 500");
|
||||
do_check_eq(result.local.length, 1);
|
||||
do_check_eq(result.local[0], "HTTP 500 Entry");
|
||||
do_check_eq(result.remote.length, 0);
|
||||
});
|
||||
|
||||
add_task(function* unresolvable_server() {
|
||||
yield updateSearchHistory("bump", "Unresolvable Server Entry");
|
||||
|
||||
let controller = new SearchSuggestionController();
|
||||
let result = yield controller.fetch("Unresolvable Server", false, unresolvableEngine);
|
||||
do_check_eq(result.term, "Unresolvable Server");
|
||||
do_check_eq(result.local.length, 1);
|
||||
do_check_eq(result.local[0], "Unresolvable Server Entry");
|
||||
do_check_eq(result.remote.length, 0);
|
||||
});
|
||||
|
||||
|
||||
// Exception handling
|
||||
|
||||
add_task(function* missing_pb() {
|
||||
Assert.throws(() => {
|
||||
let controller = new SearchSuggestionController();
|
||||
controller.fetch("No privacy");
|
||||
}, /priva/i);
|
||||
});
|
||||
|
||||
add_task(function* missing_engine() {
|
||||
Assert.throws(() => {
|
||||
let controller = new SearchSuggestionController();
|
||||
controller.fetch("No engine", false);
|
||||
}, /engine/i);
|
||||
});
|
||||
|
||||
add_task(function* invalid_engine() {
|
||||
Assert.throws(() => {
|
||||
let controller = new SearchSuggestionController();
|
||||
controller.fetch("invalid engine", false, {});
|
||||
}, /engine/i);
|
||||
});
|
||||
|
||||
add_task(function* no_results_requested() {
|
||||
Assert.throws(() => {
|
||||
let controller = new SearchSuggestionController();
|
||||
controller.maxLocalResults = 0;
|
||||
controller.maxRemoteResults = 0;
|
||||
controller.fetch("No results requested", false, getEngine);
|
||||
}, /result/i);
|
||||
});
|
||||
|
||||
add_task(function* minus_one_results_requested() {
|
||||
Assert.throws(() => {
|
||||
let controller = new SearchSuggestionController();
|
||||
controller.maxLocalResults = -1;
|
||||
controller.fetch("-1 results requested", false, getEngine);
|
||||
}, /result/i);
|
||||
});
|
||||
|
||||
|
||||
// Helpers
|
||||
|
||||
function updateSearchHistory(operation, value) {
|
||||
let deferred = Promise.defer();
|
||||
FormHistory.update({
|
||||
op: operation,
|
||||
fieldname: "searchbar-history",
|
||||
value: value,
|
||||
},
|
||||
{
|
||||
handleError: function (error) {
|
||||
do_throw("Error occurred updating form history: " + error);
|
||||
deferred.reject(error);
|
||||
},
|
||||
handleCompletion: function (reason) {
|
||||
if (!reason)
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
@ -7,6 +7,7 @@ support-files =
|
||||
data/engine.src
|
||||
data/engine.xml
|
||||
data/engine2.xml
|
||||
data/engineMaker.sjs
|
||||
data/engine-rel-searchform.xml
|
||||
data/engine-rel-searchform-post.xml
|
||||
data/engineImages.xml
|
||||
@ -15,6 +16,7 @@ support-files =
|
||||
data/search-metadata.json
|
||||
data/search.json
|
||||
data/search.sqlite
|
||||
data/searchSuggestions.sjs
|
||||
data/searchTest.jar
|
||||
|
||||
[test_nocache.js]
|
||||
@ -35,6 +37,7 @@ support-files =
|
||||
[test_multipleIcons.js]
|
||||
[test_resultDomain.js]
|
||||
[test_serialize_file.js]
|
||||
[test_searchSuggest.js]
|
||||
[test_async.js]
|
||||
[test_sync.js]
|
||||
[test_sync_fallback.js]
|
||||
|
Loading…
Reference in New Issue
Block a user