gecko-dev/browser/components/urlbar/tests/unit/head.js

480 lines
16 KiB
JavaScript

/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
var {
UrlbarMuxer,
UrlbarProvider,
UrlbarQueryContext,
UrlbarUtils,
} = ChromeUtils.import("resource:///modules/UrlbarUtils.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
AddonTestUtils: "resource://testing-common/AddonTestUtils.jsm",
AppConstants: "resource://gre/modules/AppConstants.jsm",
HttpServer: "resource://testing-common/httpd.js",
PlacesSearchAutocompleteProvider:
"resource://gre/modules/PlacesSearchAutocompleteProvider.jsm",
PlacesTestUtils: "resource://testing-common/PlacesTestUtils.jsm",
PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
Services: "resource://gre/modules/Services.jsm",
TestUtils: "resource://testing-common/TestUtils.jsm",
UrlbarController: "resource:///modules/UrlbarController.jsm",
UrlbarInput: "resource:///modules/UrlbarInput.jsm",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.jsm",
UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
UrlbarResult: "resource:///modules/UrlbarResult.jsm",
UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm",
UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
});
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
AddonTestUtils.init(this, false);
AddonTestUtils.createAppInfo(
"xpcshell@tests.mozilla.org",
"XPCShell",
"42",
"42"
);
/**
* @param {string} searchString The search string to insert into the context.
* @param {object} properties Overrides for the default values.
* @returns {UrlbarQueryContext} Creates a dummy query context with pre-filled
* required options.
*/
function createContext(searchString = "foo", properties = {}) {
info(`Creating new queryContext with searchString: ${searchString}`);
return new UrlbarQueryContext(
Object.assign(
{
allowAutofill: UrlbarPrefs.get("autoFill"),
isPrivate: true,
maxResults: UrlbarPrefs.get("maxRichResults"),
searchString,
},
properties
)
);
}
/**
* Waits for the given notification from the supplied controller.
*
* @param {UrlbarController} controller The controller to wait for a response from.
* @param {string} notification The name of the notification to wait for.
* @param {boolean} expected Wether the notification is expected.
* @returns {Promise} A promise that is resolved with the arguments supplied to
* the notification.
*/
function promiseControllerNotification(
controller,
notification,
expected = true
) {
return new Promise((resolve, reject) => {
let proxifiedObserver = new Proxy(
{},
{
get: (target, name) => {
if (name == notification) {
return (...args) => {
controller.removeQueryListener(proxifiedObserver);
if (expected) {
resolve(args);
} else {
reject();
}
};
}
return () => false;
},
}
);
controller.addQueryListener(proxifiedObserver);
});
}
/**
* A basic test provider, returning all the provided matches.
*/
class TestProvider extends UrlbarTestUtils.TestProvider {
isActive(context) {
Assert.ok(context, "context is passed-in");
return true;
}
getPriority(context) {
Assert.ok(context, "context is passed-in");
return 0;
}
async startQuery(context, add) {
Assert.ok(context, "context is passed-in");
Assert.equal(typeof add, "function", "add is a callback");
this._context = context;
for (const result of this._results) {
add(this, result);
}
}
cancelQuery(context) {
// If the query was created but didn't run, this_context will be undefined.
if (this._context) {
Assert.equal(this._context, context, "cancelQuery: context is the same");
}
if (this._onCancel) {
this._onCancel();
}
}
}
function convertToUtf8(str) {
return String.fromCharCode(...new TextEncoder().encode(str));
}
/**
* Helper function to clear the existing providers and register a basic provider
* that returns only the results given.
*
* @param {array} results The results for the provider to return.
* @param {function} [onCancel] Optional, called when the query provider
* receives a cancel instruction.
* @param {UrlbarUtils.PROVIDER_TYPE} type The provider type.
* @returns {string} name of the registered provider
*/
function registerBasicTestProvider(results = [], onCancel, type) {
let provider = new TestProvider({ results, onCancel, type });
UrlbarProvidersManager.registerProvider(provider);
return provider.name;
}
// Creates an HTTP server for the test.
function makeTestServer(port = -1) {
let httpServer = new HttpServer();
httpServer.start(port);
registerCleanupFunction(() => httpServer.stop(() => {}));
return httpServer;
}
/**
* Adds a search engine to the Search Service.
*
* @param {string} basename
* Basename for the engine.
* @param {object} httpServer [optional] HTTP Server to use.
* @returns {Promise} Resolved once the addition is complete.
*/
async function addTestEngine(basename, httpServer = undefined) {
httpServer = httpServer || makeTestServer();
httpServer.registerDirectory("/", do_get_cwd());
let dataUrl =
"http://localhost:" + httpServer.identity.primaryPort + "/data/";
// Before initializing the search service, set the geo IP url pref to a dummy
// string. When the search service is initialized, it contacts the URI named
// in this pref, causing unnecessary error logs.
let geoPref = "browser.search.geoip.url";
Services.prefs.setCharPref(geoPref, "");
registerCleanupFunction(() => Services.prefs.clearUserPref(geoPref));
info("Adding engine: " + basename);
return new Promise(resolve => {
Services.obs.addObserver(function obs(subject, topic, data) {
let engine = subject.QueryInterface(Ci.nsISearchEngine);
info("Observed " + data + " for " + engine.name);
if (data != "engine-added" || engine.name != basename) {
return;
}
Services.obs.removeObserver(obs, "browser-search-engine-modified");
registerCleanupFunction(() => Services.search.removeEngine(engine));
resolve(engine);
}, "browser-search-engine-modified");
info("Adding engine from URL: " + dataUrl + basename);
Services.search.addEngine(dataUrl + basename, null, false);
});
}
/**
* Sets up a search engine that provides some suggestions by appending strings
* onto the search query.
*
* @param {function} suggestionsFn
* A function that returns an array of suggestion strings given a
* search string. If not given, a default function is used.
* @returns {nsISearchEngine} The new engine.
*/
async function addTestSuggestionsEngine(suggestionsFn = null) {
// This port number should match the number in engine-suggestions.xml.
let server = makeTestServer(9000);
server.registerPathHandler("/suggest", (req, resp) => {
// URL query params are x-www-form-urlencoded, which converts spaces into
// plus signs, so un-convert any plus signs back to spaces.
let searchStr = decodeURIComponent(req.queryString.replace(/\+/g, " "));
let suggestions = suggestionsFn
? suggestionsFn(searchStr)
: [searchStr].concat(["foo", "bar"].map(s => searchStr + " " + s));
let data = [searchStr, suggestions];
resp.setHeader("Content-Type", "application/json", false);
resp.write(JSON.stringify(data));
});
let engine = await addTestEngine("engine-suggestions.xml", server);
return engine;
}
/**
* Sets up a search engine that provides some tail suggestions by creating an
* array that mimics Google's tail suggestion responses.
*
* @param {function} suggestionsFn
* A function that returns an array that mimics Google's tail suggestion
* responses. See bug 1626897.
* NOTE: Consumers specifying suggestionsFn must include searchStr as a
* part of the array returned by suggestionsFn.
* @returns {nsISearchEngine} The new engine.
*/
async function addTestTailSuggestionsEngine(suggestionsFn = null) {
// This port number should match the number in engine-tail-suggestions.xml.
let server = makeTestServer(9001);
server.registerPathHandler("/suggest", (req, resp) => {
// URL query params are x-www-form-urlencoded, which converts spaces into
// plus signs, so un-convert any plus signs back to spaces.
let searchStr = decodeURIComponent(req.queryString.replace(/\+/g, " "));
let suggestions = suggestionsFn
? suggestionsFn(searchStr)
: [
"what time is it in t",
["what is the time today texas"].concat(
["toronto", "tunisia"].map(s => searchStr + s.slice(1))
),
[],
{
"google:irrelevantparameter": [],
"google:suggestdetail": [{}].concat(
["toronto", "tunisia"].map(s => ({
mp: "… ",
t: s,
}))
),
},
];
let data = suggestions;
let jsonString = JSON.stringify(data);
// This script must be evaluated as UTF-8 for this to write out the bytes of
// the string in UTF-8. If it's evaluated as Latin-1, the written bytes
// will be the result of UTF-8-encoding the result-string *twice*, which
// will break the "… " match prefixes.
let stringOfUtf8Bytes = convertToUtf8(jsonString);
resp.setHeader("Content-Type", "application/json", false);
resp.write(stringOfUtf8Bytes);
});
let engine = await addTestEngine("engine-tail-suggestions.xml", server);
return engine;
}
/**
* Creates a UrlbarResult for a search result.
* @param {UrlbarQueryContext} queryContext
* The context that this result will be displayed in.
* @param {string} [options.suggestion]
* The suggestion offered by the search engine.
* @param {string} [options.engineName]
* The name of the engine providing the suggestion. Leave blank if there
* is no suggestion.
* @param {string} [options.query]
* The query that started the search. This overrides
* `queryContext.searchString`. This is useful when the query that will show
* up in the result object will be different from what was typed. For example,
* if a leading restriction token will be used.
* @param {string} [options.alias]
* The alias for the search engine, if the search is an alias search.
* @param {string} [options.engineIconUri]
* A URI for the engine's icon.
* @param {boolean} [options.heuristic]
* True if this is a heuristic result. Defaults to false.
* @param {number} [options.keywordOffer]
* A value from UrlbarUtils.KEYWORD_OFFER.
* @returns {UrlbarResult}
*/
function makeSearchResult(
queryContext,
{
suggestion,
tail,
engineName,
alias,
query,
engineIconUri,
heuristic = false,
keywordOffer,
}
) {
UrlbarTokenizer.tokenize(queryContext);
if (!keywordOffer) {
keywordOffer = UrlbarUtils.KEYWORD_OFFER.NONE;
if (alias && !query.trim() && alias.startsWith("@")) {
keywordOffer = heuristic
? UrlbarUtils.KEYWORD_OFFER.HIDE
: UrlbarUtils.KEYWORD_OFFER.SHOW;
}
}
let result = new UrlbarResult(
UrlbarUtils.RESULT_TYPE.SEARCH,
UrlbarUtils.RESULT_SOURCE.SEARCH,
...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
engine: [engineName, UrlbarUtils.HIGHLIGHT.TYPED],
suggestion: [suggestion, UrlbarUtils.HIGHLIGHT.SUGGESTED],
tail: [tail, UrlbarUtils.HIGHLIGHT.SUGGESTED],
keyword: [alias, UrlbarUtils.HIGHLIGHT.TYPED],
// Check against undefined so consumers can pass in the empty string.
query: [
typeof query != "undefined" ? query : queryContext.searchString.trim(),
UrlbarUtils.HIGHLIGHT.TYPED,
],
isSearchHistory: false,
icon: [engineIconUri ? engineIconUri : ""],
keywordOffer,
})
);
result.heuristic = heuristic;
return result;
}
/**
* Creates a UrlbarResult for a history result.
* @param {UrlbarQueryContext} queryContext
* The context that this result will be displayed in.
* @param {string} options.title
* The page title.
* @param {string} options.uri
* The page URI.
* @param {array} [options.tags]
* An array of string tags. Defaults to an empty array.
* @param {string} [options.iconUri]
* A URI for the page's icon.
* @param {boolean} [options.heuristic]
* True if this is a heuristic result. Defaults to false.
* @returns {UrlbarResult}
*/
function makeVisitResult(
queryContext,
{ title, uri, iconUri, tags = [], heuristic = false }
) {
UrlbarTokenizer.tokenize(queryContext);
let payload = {
url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
// Check against undefined so consumers can pass in the empty string.
icon: [typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`],
title: [title, UrlbarUtils.HIGHLIGHT.TYPED],
};
if (!heuristic) {
payload.tags = [tags, UrlbarUtils.HIGHLIGHT.TYPED];
}
let result = new UrlbarResult(
UrlbarUtils.RESULT_TYPE.URL,
UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
);
result.heuristic = heuristic;
return result;
}
/**
* Creates a UrlbarResult for a bookmark result.
* @param {UrlbarQueryContext} queryContext
* The context that this result will be displayed in.
* @param {string} options.title
* The page title.
* @param {string} options.uri
* The page URI.
* @param {string} [options.iconUri]
* A URI for the page's icon.
* @param {array} [options.tags]
* An array of string tags. Defaults to an empty array.
* @param {boolean} [options.heuristic]
* True if this is a heuristic result. Defaults to false.
* @returns {UrlbarResult}
*/
function makeBookmarkResult(
queryContext,
{ title, uri, iconUri, tags = [], heuristic = false }
) {
UrlbarTokenizer.tokenize(queryContext);
let result = new UrlbarResult(
UrlbarUtils.RESULT_TYPE.URL,
UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
// Check against undefined so consumers can pass in the empty string.
icon: [typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`],
title: [title, UrlbarUtils.HIGHLIGHT.TYPED],
tags: [tags, UrlbarUtils.HIGHLIGHT.TYPED],
})
);
result.heuristic = heuristic;
return result;
}
/**
* Checks that the results returned by a UrlbarController match those in
* the param `matches`.
* @param {UrlbarQueryContext} context
* The context for this query.
* @param {array} matches
* An array of UrlbarResults.
* @param {boolean} [isPrivate]
* Set this to `true` to simulate a search in a private window.
*/
async function check_results({ context, matches = [] } = {}) {
if (!context) {
return;
}
// At this point frecency could still be updating due to latest pages
// updates.
// This is not a problem in real life, but autocomplete tests should
// return reliable resultsets, thus we have to wait.
await PlacesTestUtils.promiseAsyncUpdates();
let controller = UrlbarTestUtils.newMockController({
input: {
isPrivate: context.isPrivate,
window: {
location: {
href: AppConstants.BROWSER_CHROME_URL,
},
},
},
});
await controller.startQuery(context);
Assert.equal(
context.results.length,
matches.length,
"Found the expected number of results."
);
Assert.deepEqual(
matches.map(m => m.payload),
context.results.map(m => m.payload),
"Payloads are the same."
);
Assert.deepEqual(
matches.map(m => m.heuristic),
context.results.map(m => m.heuristic),
"Heuristic results are correctly flagged."
);
}