зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1007979 - Refactor nsSearchSuggestions into a reusable JSM. r=adw
Original JSM by mconnor.
This commit is contained in:
Родитель
93ba218ccc
Коммит
d24690ac2a
|
@ -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',
|
'toolkitsearch.manifest',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
EXTRA_JS_MODULES += [
|
||||||
|
'SearchSuggestionController.jsm',
|
||||||
|
]
|
||||||
|
|
||||||
EXTRA_PP_COMPONENTS += [
|
EXTRA_PP_COMPONENTS += [
|
||||||
'nsSearchService.js',
|
'nsSearchService.js',
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,32 +2,24 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* 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/. */
|
* 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 BROWSER_SUGGEST_PREF = "browser.search.suggest.enabled";
|
||||||
const XPCOM_SHUTDOWN_TOPIC = "xpcom-shutdown";
|
const XPCOM_SHUTDOWN_TOPIC = "xpcom-shutdown";
|
||||||
const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
|
const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
|
||||||
|
|
||||||
const Cc = Components.classes;
|
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||||
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;
|
|
||||||
|
|
||||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
Cu.import("resource://gre/modules/nsFormAutoCompleteResult.jsm");
|
Cu.import("resource://gre/modules/nsFormAutoCompleteResult.jsm");
|
||||||
Cu.import("resource://gre/modules/Services.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
|
* SuggestAutoComplete is a base class that implements nsIAutoCompleteSearch
|
||||||
* and can collect results for a given search by using the search URL supplied
|
* and can collect results for a given search by using this._suggestionController.
|
||||||
* by the subclass. We do it this way since the AutoCompleteController in
|
* We do it this way since the AutoCompleteController in Mozilla requires a
|
||||||
* Mozilla requires a unique XPCOM Service for every search provider, even if
|
* unique XPCOM Service for every search provider, even if the logic for two
|
||||||
* the logic for two providers is identical.
|
* providers is identical.
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function SuggestAutoComplete() {
|
function SuggestAutoComplete() {
|
||||||
|
@ -38,6 +30,7 @@ SuggestAutoComplete.prototype = {
|
||||||
_init: function() {
|
_init: function() {
|
||||||
this._addObservers();
|
this._addObservers();
|
||||||
this._suggestEnabled = Services.prefs.getBoolPref(BROWSER_SUGGEST_PREF);
|
this._suggestEnabled = Services.prefs.getBoolPref(BROWSER_SUGGEST_PREF);
|
||||||
|
this._suggestionController = new SearchSuggestionController(obj => this.onResultsReturned(obj));
|
||||||
},
|
},
|
||||||
|
|
||||||
get _suggestionLabel() {
|
get _suggestionLabel() {
|
||||||
|
@ -51,59 +44,6 @@ SuggestAutoComplete.prototype = {
|
||||||
*/
|
*/
|
||||||
_suggestEnabled: null,
|
_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
|
* The object implementing nsIAutoCompleteObserver that we notify when
|
||||||
* we have found results
|
* we have found results
|
||||||
|
@ -111,81 +51,6 @@ SuggestAutoComplete.prototype = {
|
||||||
*/
|
*/
|
||||||
_listener: null,
|
_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
|
* Maximum number of history items displayed. This is capped at 7
|
||||||
* because the primary consumer (Firefox search bar) displays 10 rows
|
* because the primary consumer (Firefox search bar) displays 10 rows
|
||||||
|
@ -195,176 +60,32 @@ SuggestAutoComplete.prototype = {
|
||||||
_historyLimit: 7,
|
_historyLimit: 7,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This clears all the per-request state.
|
* Callback for handling results from SearchSuggestionController.jsm
|
||||||
*/
|
|
||||||
_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.
|
|
||||||
*
|
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_okToRequest: function SAC__okToRequest() {
|
onResultsReturned: function(results) {
|
||||||
return Date.now() > this._nextRequestTime;
|
let finalResults = [];
|
||||||
},
|
let finalComments = [];
|
||||||
|
|
||||||
/**
|
// If form history has results, add them to the list.
|
||||||
* This checks to see if the new search engine is different
|
let maxHistoryItems = Math.min(results.local.length, this._historyLimit);
|
||||||
* from the previous one, and if so clears any error state that might
|
for (let i = 0; i < maxHistoryItems; ++i) {
|
||||||
* have accumulated for the old engine.
|
finalResults.push(results.local[i]);
|
||||||
*
|
finalComments.push("");
|
||||||
* @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 (this._isBackoffError(status)) {
|
// If there are remote matches, add them.
|
||||||
this._noteServerError();
|
if (results.remote.length) {
|
||||||
return;
|
// "comments" column values for suggestions starts as empty strings
|
||||||
}
|
let comments = new Array(results.remote.length).fill("", 1);
|
||||||
|
|
||||||
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)
|
|
||||||
comments[0] = this._suggestionLabel;
|
comments[0] = this._suggestionLabel;
|
||||||
|
// now put the history results above the suggestions
|
||||||
// now put the history results above the suggestions
|
finalResults = finalResults.concat(results.remote);
|
||||||
var finalResults = historyResults.concat(results);
|
finalComments = finalComments.concat(comments);
|
||||||
var finalComments = historyComments.concat(comments);
|
}
|
||||||
|
|
||||||
// Notify the FE of our new results
|
// Notify the FE of our new results
|
||||||
this.onResultsReady(searchString, finalResults, finalComments,
|
this.onResultsReady(results.term, finalResults, finalComments, results.formHistoryResult);
|
||||||
this._formHistoryResult);
|
|
||||||
|
|
||||||
// Reset our state for next time.
|
|
||||||
this._reset();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -374,10 +95,9 @@ SuggestAutoComplete.prototype = {
|
||||||
* @param comments an array of metadata corresponding to the results
|
* @param comments an array of metadata corresponding to the results
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
onResultsReady: function(searchString, results, comments,
|
onResultsReady: function(searchString, results, comments, formHistoryResult) {
|
||||||
formHistoryResult) {
|
|
||||||
if (this._listener) {
|
if (this._listener) {
|
||||||
var result = new FormAutoCompleteResult(
|
let result = new FormAutoCompleteResult(
|
||||||
searchString,
|
searchString,
|
||||||
Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
|
Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
|
||||||
0,
|
0,
|
||||||
|
@ -389,8 +109,7 @@ SuggestAutoComplete.prototype = {
|
||||||
|
|
||||||
this._listener.onSearchResult(this, result);
|
this._listener.onSearchResult(this, result);
|
||||||
|
|
||||||
// Null out listener to make sure we don't notify it twice, in case our
|
// Null out listener to make sure we don't notify it twice
|
||||||
// timer callback still hasn't run.
|
|
||||||
this._listener = null;
|
this._listener = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -445,56 +164,10 @@ SuggestAutoComplete.prototype = {
|
||||||
* Actual implementation of search.
|
* Actual implementation of search.
|
||||||
*/
|
*/
|
||||||
_triggerSearch: function(searchString, searchParam, listener, privacyMode) {
|
_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;
|
this._listener = listener;
|
||||||
|
this._suggestionController.fetch(searchString,
|
||||||
var engine = Services.search.currentEngine;
|
privacyMode,
|
||||||
|
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);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -502,10 +175,7 @@ SuggestAutoComplete.prototype = {
|
||||||
* implementation.
|
* implementation.
|
||||||
*/
|
*/
|
||||||
stopSearch: function() {
|
stopSearch: function() {
|
||||||
if (this._request) {
|
this._suggestionController.stop();
|
||||||
this._request.abort();
|
|
||||||
this._reset();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -539,31 +209,6 @@ SuggestAutoComplete.prototype = {
|
||||||
Ci.nsIAutoCompleteObserver])
|
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
|
* SearchSuggestAutoComplete is a service implementation that handles suggest
|
||||||
* results specific to web searches.
|
* 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;
|
||||||
|
}
|
|
@ -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(() => Task.spawn(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.src
|
||||||
data/engine.xml
|
data/engine.xml
|
||||||
data/engine2.xml
|
data/engine2.xml
|
||||||
|
data/engineMaker.sjs
|
||||||
data/engine-rel-searchform.xml
|
data/engine-rel-searchform.xml
|
||||||
data/engine-rel-searchform-post.xml
|
data/engine-rel-searchform-post.xml
|
||||||
data/engineImages.xml
|
data/engineImages.xml
|
||||||
|
@ -15,6 +16,7 @@ support-files =
|
||||||
data/search-metadata.json
|
data/search-metadata.json
|
||||||
data/search.json
|
data/search.json
|
||||||
data/search.sqlite
|
data/search.sqlite
|
||||||
|
data/searchSuggestions.sjs
|
||||||
data/searchTest.jar
|
data/searchTest.jar
|
||||||
|
|
||||||
[test_nocache.js]
|
[test_nocache.js]
|
||||||
|
@ -35,6 +37,7 @@ support-files =
|
||||||
[test_multipleIcons.js]
|
[test_multipleIcons.js]
|
||||||
[test_resultDomain.js]
|
[test_resultDomain.js]
|
||||||
[test_serialize_file.js]
|
[test_serialize_file.js]
|
||||||
|
[test_searchSuggest.js]
|
||||||
[test_async.js]
|
[test_async.js]
|
||||||
[test_sync.js]
|
[test_sync.js]
|
||||||
[test_sync_fallback.js]
|
[test_sync_fallback.js]
|
||||||
|
|
Загрузка…
Ссылка в новой задаче