forked from mirrors/gecko-dev
1088 lines
33 KiB
JavaScript
1088 lines
33 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||
|
||
const { XPCOMUtils } = ChromeUtils.importESModule(
|
||
"resource://gre/modules/XPCOMUtils.sys.mjs"
|
||
);
|
||
const { AppConstants } = ChromeUtils.importESModule(
|
||
"resource://gre/modules/AppConstants.sys.mjs"
|
||
);
|
||
|
||
var { UrlbarMuxer, UrlbarProvider, UrlbarQueryContext, UrlbarUtils } =
|
||
ChromeUtils.importESModule("resource:///modules/UrlbarUtils.sys.mjs");
|
||
|
||
ChromeUtils.defineESModuleGetters(this, {
|
||
AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
|
||
HttpServer: "resource://testing-common/httpd.sys.mjs",
|
||
PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
|
||
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
|
||
PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
|
||
SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs",
|
||
TestUtils: "resource://testing-common/TestUtils.sys.mjs",
|
||
UrlbarController: "resource:///modules/UrlbarController.sys.mjs",
|
||
UrlbarInput: "resource:///modules/UrlbarInput.sys.mjs",
|
||
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
|
||
UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
|
||
UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
|
||
UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
|
||
UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
|
||
sinon: "resource://testing-common/Sinon.sys.mjs",
|
||
});
|
||
|
||
ChromeUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => {
|
||
const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule(
|
||
"resource://testing-common/QuickSuggestTestUtils.sys.mjs"
|
||
);
|
||
module.init(this);
|
||
return module;
|
||
});
|
||
|
||
ChromeUtils.defineLazyGetter(this, "MerinoTestUtils", () => {
|
||
const { MerinoTestUtils: module } = ChromeUtils.importESModule(
|
||
"resource://testing-common/MerinoTestUtils.sys.mjs"
|
||
);
|
||
module.init(this);
|
||
return module;
|
||
});
|
||
|
||
ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
|
||
const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
|
||
"resource://testing-common/UrlbarTestUtils.sys.mjs"
|
||
);
|
||
module.init(this);
|
||
return module;
|
||
});
|
||
|
||
ChromeUtils.defineLazyGetter(this, "PlacesFrecencyRecalculator", () => {
|
||
return Cc["@mozilla.org/places/frecency-recalculator;1"].getService(
|
||
Ci.nsIObserver
|
||
).wrappedJSObject;
|
||
});
|
||
|
||
SearchTestUtils.init(this);
|
||
AddonTestUtils.init(this, false);
|
||
AddonTestUtils.createAppInfo(
|
||
"xpcshell@tests.mozilla.org",
|
||
"XPCShell",
|
||
"42",
|
||
"42"
|
||
);
|
||
|
||
const SUGGESTIONS_ENGINE_NAME = "Suggestions";
|
||
const TAIL_SUGGESTIONS_ENGINE_NAME = "Tail Suggestions";
|
||
|
||
/**
|
||
* Gets the database connection. If the Places connection is invalid it will
|
||
* try to create a new connection.
|
||
*
|
||
* @param [optional] aForceNewConnection
|
||
* Forces creation of a new connection to the database. When a
|
||
* connection is asyncClosed it cannot anymore schedule async statements,
|
||
* though connectionReady will keep returning true (Bug 726990).
|
||
*
|
||
* @returns The database connection or null if unable to get one.
|
||
*/
|
||
var gDBConn;
|
||
function DBConn(aForceNewConnection) {
|
||
if (!aForceNewConnection) {
|
||
let db = PlacesUtils.history.DBConnection;
|
||
if (db.connectionReady) {
|
||
return db;
|
||
}
|
||
}
|
||
|
||
// If the Places database connection has been closed, create a new connection.
|
||
if (!gDBConn || aForceNewConnection) {
|
||
let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
|
||
file.append("places.sqlite");
|
||
let dbConn = (gDBConn = Services.storage.openDatabase(file));
|
||
|
||
TestUtils.topicObserved("profile-before-change").then(() =>
|
||
dbConn.asyncClose()
|
||
);
|
||
}
|
||
|
||
return gDBConn.connectionReady ? gDBConn : null;
|
||
}
|
||
|
||
/**
|
||
* @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}`);
|
||
let context = new UrlbarQueryContext(
|
||
Object.assign(
|
||
{
|
||
allowAutofill: UrlbarPrefs.get("autoFill"),
|
||
isPrivate: true,
|
||
maxResults: UrlbarPrefs.get("maxRichResults"),
|
||
searchString,
|
||
},
|
||
properties
|
||
)
|
||
);
|
||
UrlbarTokenizer.tokenize(context);
|
||
return context;
|
||
}
|
||
|
||
/**
|
||
* 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.
|
||
* @param {string} [name] Optional, use as the provider name.
|
||
* If none, a default name is chosen.
|
||
* @returns {UrlbarProvider} The provider
|
||
*/
|
||
function registerBasicTestProvider(results = [], onCancel, type, name) {
|
||
let provider = new TestProvider({ results, onCancel, type, name });
|
||
UrlbarProvidersManager.registerProvider(provider);
|
||
registerCleanupFunction(() =>
|
||
UrlbarProvidersManager.unregisterProvider(provider)
|
||
);
|
||
return provider;
|
||
}
|
||
|
||
// Creates an HTTP server for the test.
|
||
function makeTestServer(port = -1) {
|
||
let httpServer = new HttpServer();
|
||
httpServer.start(port);
|
||
registerCleanupFunction(() => httpServer.stop(() => {}));
|
||
return httpServer;
|
||
}
|
||
|
||
/**
|
||
* 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();
|
||
server.registerPathHandler("/suggest", (req, resp) => {
|
||
let params = new URLSearchParams(req.queryString);
|
||
let searchStr = params.get("q");
|
||
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));
|
||
});
|
||
await SearchTestUtils.installSearchExtension({
|
||
name: SUGGESTIONS_ENGINE_NAME,
|
||
search_url: `http://localhost:${server.identity.primaryPort}/search`,
|
||
suggest_url: `http://localhost:${server.identity.primaryPort}/suggest`,
|
||
suggest_url_get_params: "?q={searchTerms}",
|
||
// test_search_suggestions_aliases.js uses the search form.
|
||
search_form: `http://localhost:${server.identity.primaryPort}/search?q={searchTerms}`,
|
||
});
|
||
let engine = Services.search.getEngineByName("Suggestions");
|
||
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();
|
||
server.registerPathHandler("/suggest", (req, resp) => {
|
||
let params = new URLSearchParams(req.queryString);
|
||
let searchStr = params.get("q");
|
||
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);
|
||
});
|
||
await SearchTestUtils.installSearchExtension({
|
||
name: TAIL_SUGGESTIONS_ENGINE_NAME,
|
||
search_url: `http://localhost:${server.identity.primaryPort}/search`,
|
||
suggest_url: `http://localhost:${server.identity.primaryPort}/suggest`,
|
||
suggest_url_get_params: "?q={searchTerms}",
|
||
});
|
||
let engine = Services.search.getEngineByName("Tail Suggestions");
|
||
return engine;
|
||
}
|
||
|
||
async function addOpenPages(uri, count = 1, userContextId = 0) {
|
||
for (let i = 0; i < count; i++) {
|
||
await UrlbarProviderOpenTabs.registerOpenTab(
|
||
uri.spec,
|
||
userContextId,
|
||
false
|
||
);
|
||
}
|
||
}
|
||
|
||
async function removeOpenPages(aUri, aCount = 1, aUserContextId = 0) {
|
||
for (let i = 0; i < aCount; i++) {
|
||
await UrlbarProviderOpenTabs.unregisterOpenTab(
|
||
aUri.spec,
|
||
aUserContextId,
|
||
false
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Helper for tests that generate search results but aren't interested in
|
||
* suggestions, such as autofill tests. Installs a test engine and disables
|
||
* suggestions.
|
||
*/
|
||
function testEngine_setup() {
|
||
add_task(async function setup() {
|
||
await cleanupPlaces();
|
||
let engine = await addTestSuggestionsEngine();
|
||
let oldDefaultEngine = await Services.search.getDefault();
|
||
|
||
registerCleanupFunction(async () => {
|
||
Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
|
||
Services.prefs.clearUserPref(
|
||
"browser.search.separatePrivateDefault.ui.enabled"
|
||
);
|
||
Services.search.setDefault(
|
||
oldDefaultEngine,
|
||
Ci.nsISearchService.CHANGE_REASON_UNKNOWN
|
||
);
|
||
});
|
||
|
||
Services.search.setDefault(
|
||
engine,
|
||
Ci.nsISearchService.CHANGE_REASON_UNKNOWN
|
||
);
|
||
Services.prefs.setBoolPref(
|
||
"browser.search.separatePrivateDefault.ui.enabled",
|
||
false
|
||
);
|
||
Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
|
||
});
|
||
}
|
||
|
||
async function cleanupPlaces() {
|
||
Services.prefs.clearUserPref("browser.urlbar.autoFill");
|
||
|
||
await PlacesUtils.bookmarks.eraseEverything();
|
||
await PlacesUtils.history.clear();
|
||
}
|
||
|
||
/**
|
||
* Creates a UrlbarResult for a bookmark result.
|
||
*
|
||
* @param {UrlbarQueryContext} queryContext
|
||
* The context that this result will be displayed in.
|
||
* @param {object} options
|
||
* Options for the result.
|
||
* @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.
|
||
* @param {number} [options.source]
|
||
* Where the results should be sourced from. See {@link UrlbarUtils.RESULT_SOURCE}.
|
||
* @returns {UrlbarResult}
|
||
*/
|
||
function makeBookmarkResult(
|
||
queryContext,
|
||
{
|
||
title,
|
||
uri,
|
||
iconUri,
|
||
tags = [],
|
||
heuristic = false,
|
||
source = UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
|
||
}
|
||
) {
|
||
let result = new UrlbarResult(
|
||
UrlbarUtils.RESULT_TYPE.URL,
|
||
source,
|
||
...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;
|
||
}
|
||
|
||
/**
|
||
* Creates a UrlbarResult for a form history result.
|
||
*
|
||
* @param {UrlbarQueryContext} queryContext
|
||
* The context that this result will be displayed in.
|
||
* @param {object} options
|
||
* Options for the result.
|
||
* @param {string} options.suggestion
|
||
* The form history suggestion.
|
||
* @param {string} options.engineName
|
||
* The name of the engine that will do the search when the result is picked.
|
||
* @returns {UrlbarResult}
|
||
*/
|
||
function makeFormHistoryResult(queryContext, { suggestion, engineName }) {
|
||
return new UrlbarResult(
|
||
UrlbarUtils.RESULT_TYPE.SEARCH,
|
||
UrlbarUtils.RESULT_SOURCE.HISTORY,
|
||
...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
|
||
engine: engineName,
|
||
suggestion: [suggestion, UrlbarUtils.HIGHLIGHT.SUGGESTED],
|
||
lowerCaseSuggestion: suggestion.toLocaleLowerCase(),
|
||
})
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Creates a UrlbarResult for an omnibox extension result. For more information,
|
||
* see the documentation for omnibox.SuggestResult:
|
||
* https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/omnibox/SuggestResult
|
||
*
|
||
* @param {UrlbarQueryContext} queryContext
|
||
* The context that this result will be displayed in.
|
||
* @param {object} options
|
||
* Options for the result.
|
||
* @param {string} options.content
|
||
* The string displayed when the result is highlighted.
|
||
* @param {string} options.description
|
||
* The string displayed in the address bar dropdown.
|
||
* @param {string} options.keyword
|
||
* The keyword associated with the extension returning the result.
|
||
* @param {boolean} [options.heuristic]
|
||
* True if this is a heuristic result. Defaults to false.
|
||
* @returns {UrlbarResult}
|
||
*/
|
||
function makeOmniboxResult(
|
||
queryContext,
|
||
{ content, description, keyword, heuristic = false }
|
||
) {
|
||
let payload = {
|
||
title: [description, UrlbarUtils.HIGHLIGHT.TYPED],
|
||
content: [content, UrlbarUtils.HIGHLIGHT.TYPED],
|
||
keyword: [keyword, UrlbarUtils.HIGHLIGHT.TYPED],
|
||
icon: [UrlbarUtils.ICON.EXTENSION],
|
||
};
|
||
if (!heuristic) {
|
||
payload.blockL10n = { id: "urlbar-result-menu-dismiss-firefox-suggest" };
|
||
}
|
||
let result = new UrlbarResult(
|
||
UrlbarUtils.RESULT_TYPE.OMNIBOX,
|
||
UrlbarUtils.RESULT_SOURCE.ADDON,
|
||
...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
|
||
);
|
||
result.heuristic = heuristic;
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Creates a UrlbarResult for an switch-to-tab result.
|
||
*
|
||
* @param {UrlbarQueryContext} queryContext
|
||
* The context that this result will be displayed in.
|
||
* @param {object} options
|
||
* Options for the result.
|
||
* @param {string} options.uri
|
||
* The page URI.
|
||
* @param {string} [options.title]
|
||
* The page title.
|
||
* @param {string} [options.iconUri]
|
||
* A URI for the page icon.
|
||
* @returns {UrlbarResult}
|
||
*/
|
||
function makeTabSwitchResult(queryContext, { uri, title, iconUri }) {
|
||
return new UrlbarResult(
|
||
UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
|
||
UrlbarUtils.RESULT_SOURCE.TABS,
|
||
...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
|
||
url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
|
||
title: [title, UrlbarUtils.HIGHLIGHT.TYPED],
|
||
// Check against undefined so consumers can pass in the empty string.
|
||
icon: typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`,
|
||
})
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Creates a UrlbarResult for a keyword search result.
|
||
*
|
||
* @param {UrlbarQueryContext} queryContext
|
||
* The context that this result will be displayed in.
|
||
* @param {object} options
|
||
* Options for the result.
|
||
* @param {string} options.uri
|
||
* The page URI.
|
||
* @param {string} options.keyword
|
||
* The page's search keyword.
|
||
* @param {string} [options.title]
|
||
* The title for the bookmarked keyword page.
|
||
* @param {string} [options.iconUri]
|
||
* A URI for the engine's icon.
|
||
* @param {string} [options.postData]
|
||
* The search POST data.
|
||
* @param {boolean} [options.heuristic]
|
||
* True if this is a heuristic result. Defaults to false.
|
||
* @returns {UrlbarResult}
|
||
*/
|
||
function makeKeywordSearchResult(
|
||
queryContext,
|
||
{ uri, keyword, title, iconUri, postData, heuristic = false }
|
||
) {
|
||
let result = new UrlbarResult(
|
||
UrlbarUtils.RESULT_TYPE.KEYWORD,
|
||
UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
|
||
...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
|
||
title: [title ? title : uri, UrlbarUtils.HIGHLIGHT.TYPED],
|
||
url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
|
||
keyword: [keyword, UrlbarUtils.HIGHLIGHT.TYPED],
|
||
input: [queryContext.searchString, UrlbarUtils.HIGHLIGHT.TYPED],
|
||
postData: postData || null,
|
||
icon: typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`,
|
||
})
|
||
);
|
||
|
||
if (heuristic) {
|
||
result.heuristic = heuristic;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Creates a UrlbarResult for a remote tab result.
|
||
*
|
||
* @param {UrlbarQueryContext} queryContext
|
||
* The context that this result will be displayed in.
|
||
* @param {object} options
|
||
* Options for the result.
|
||
* @param {string} options.uri
|
||
* The page URI.
|
||
* @param {string} options.device
|
||
* The name of the device that the remote tab comes from.
|
||
* @param {string} [options.title]
|
||
* The page title.
|
||
* @param {number} [options.lastUsed]
|
||
* The last time the remote tab was visited, in epoch seconds. Defaults
|
||
* to 0.
|
||
* @param {string} [options.iconUri]
|
||
* A URI for the page's icon.
|
||
* @returns {UrlbarResult}
|
||
*/
|
||
function makeRemoteTabResult(
|
||
queryContext,
|
||
{ uri, device, title, iconUri, lastUsed = 0 }
|
||
) {
|
||
let payload = {
|
||
url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
|
||
device: [device, UrlbarUtils.HIGHLIGHT.TYPED],
|
||
// Check against undefined so consumers can pass in the empty string.
|
||
icon:
|
||
typeof iconUri != "undefined"
|
||
? iconUri
|
||
: `moz-anno:favicon:page-icon:${uri}`,
|
||
lastUsed: lastUsed * 1000,
|
||
};
|
||
|
||
// Check against undefined so consumers can pass in the empty string.
|
||
if (typeof title != "undefined") {
|
||
payload.title = [title, UrlbarUtils.HIGHLIGHT.TYPED];
|
||
} else {
|
||
payload.title = [uri, UrlbarUtils.HIGHLIGHT.TYPED];
|
||
}
|
||
|
||
let result = new UrlbarResult(
|
||
UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
|
||
UrlbarUtils.RESULT_SOURCE.TABS,
|
||
...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
|
||
);
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Creates a UrlbarResult for a search result.
|
||
*
|
||
* @param {UrlbarQueryContext} queryContext
|
||
* The context that this result will be displayed in.
|
||
* @param {object} options
|
||
* Options for the result.
|
||
* @param {string} [options.suggestion]
|
||
* The suggestion offered by the search engine.
|
||
* @param {string} [options.tailPrefix]
|
||
* The characters placed at the end of a Google "tail" suggestion. See
|
||
* {@link https://firefox-source-docs.mozilla.org/browser/urlbar/nontechnical-overview.html#search-suggestions}
|
||
* @param {*} [options.tail]
|
||
* The details of the URL bar tail
|
||
* @param {number} [options.tailOffsetIndex]
|
||
* The index of the first character in the tail suggestion that should be
|
||
* @param {string} [options.engineName]
|
||
* The name of the engine providing the suggestion. Leave blank if there
|
||
* is no suggestion.
|
||
* @param {string} [options.uri]
|
||
* The URI that the search result will navigate to.
|
||
* @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 {boolean} [options.providesSearchMode]
|
||
* Whether search mode is entered when this result is selected.
|
||
* @param {string} [options.providerName]
|
||
* The name of the provider offering this result. The test suite will not
|
||
* check which provider offered a result unless this option is specified.
|
||
* @param {boolean} [options.inPrivateWindow]
|
||
* If the window to test is a private window.
|
||
* @param {boolean} [options.isPrivateEngine]
|
||
* If the engine is a private engine.
|
||
* @param {number} [options.type]
|
||
* The type of the search result. Defaults to UrlbarUtils.RESULT_TYPE.SEARCH.
|
||
* @param {number} [options.source]
|
||
* The source of the search result. Defaults to UrlbarUtils.RESULT_SOURCE.SEARCH.
|
||
* @param {boolean} [options.satisfiesAutofillThreshold]
|
||
* If this search should appear in the autofill section of the box
|
||
* @param {boolean} [options.trending]
|
||
* If the search result is a trending result. `Defaults to false`.
|
||
* @param {boolean} [options.isRichSuggestion]
|
||
* If the search result is a rich result. `Defaults to false`.
|
||
* @returns {UrlbarResult}
|
||
*/
|
||
function makeSearchResult(
|
||
queryContext,
|
||
{
|
||
suggestion,
|
||
tailPrefix,
|
||
tail,
|
||
tailOffsetIndex,
|
||
engineName,
|
||
alias,
|
||
uri,
|
||
query,
|
||
engineIconUri,
|
||
providesSearchMode,
|
||
providerName,
|
||
inPrivateWindow,
|
||
isPrivateEngine,
|
||
heuristic = false,
|
||
trending = false,
|
||
isRichSuggestion = false,
|
||
type = UrlbarUtils.RESULT_TYPE.SEARCH,
|
||
source = UrlbarUtils.RESULT_SOURCE.SEARCH,
|
||
satisfiesAutofillThreshold = false,
|
||
}
|
||
) {
|
||
// Tail suggestion common cases, handled here to reduce verbosity in tests.
|
||
if (tail) {
|
||
if (!tailPrefix) {
|
||
tailPrefix = "… ";
|
||
}
|
||
if (!tailOffsetIndex) {
|
||
tailOffsetIndex = suggestion.indexOf(tail);
|
||
}
|
||
}
|
||
|
||
let payload = {
|
||
engine: [engineName, UrlbarUtils.HIGHLIGHT.TYPED],
|
||
suggestion: [suggestion, UrlbarUtils.HIGHLIGHT.SUGGESTED],
|
||
tailPrefix,
|
||
tail: [tail, UrlbarUtils.HIGHLIGHT.SUGGESTED],
|
||
tailOffsetIndex,
|
||
keyword: [
|
||
alias,
|
||
providesSearchMode
|
||
? UrlbarUtils.HIGHLIGHT.TYPED
|
||
: UrlbarUtils.HIGHLIGHT.NONE,
|
||
],
|
||
// Check against undefined so consumers can pass in the empty string.
|
||
query: [
|
||
typeof query != "undefined" ? query : queryContext.trimmedSearchString,
|
||
UrlbarUtils.HIGHLIGHT.TYPED,
|
||
],
|
||
icon: engineIconUri,
|
||
providesSearchMode,
|
||
inPrivateWindow,
|
||
isPrivateEngine,
|
||
};
|
||
|
||
// Passing even an undefined URL in the payload creates a potentially-unwanted
|
||
// displayUrl parameter, so we add it only if specified.
|
||
if (uri) {
|
||
payload.url = uri;
|
||
}
|
||
if (providerName == "TabToSearch") {
|
||
payload.satisfiesAutofillThreshold = satisfiesAutofillThreshold;
|
||
if (payload.url.startsWith("www.")) {
|
||
payload.url = payload.url.substring(4);
|
||
}
|
||
payload.isGeneralPurposeEngine = false;
|
||
}
|
||
|
||
let result = new UrlbarResult(
|
||
type,
|
||
source,
|
||
...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
|
||
);
|
||
|
||
if (typeof suggestion == "string") {
|
||
result.payload.lowerCaseSuggestion =
|
||
result.payload.suggestion.toLocaleLowerCase();
|
||
result.payload.trending = trending;
|
||
result.isRichSuggestion = isRichSuggestion;
|
||
}
|
||
|
||
if (providerName) {
|
||
result.providerName = providerName;
|
||
}
|
||
|
||
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 {object} options Options for the result.
|
||
* @param {string} options.title
|
||
* The page title.
|
||
* @param {string} [options.fallbackTitle]
|
||
* The provider has capability to use the actual page title though,
|
||
* when the provider can’t get the page title, use this value as the fallback.
|
||
* @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.
|
||
* @param {string} options.providerName
|
||
* The name of the provider offering this result. The test suite will not
|
||
* check which provider offered a result unless this option is specified.
|
||
* @param {number} [options.source]
|
||
* The source of the result
|
||
* @returns {UrlbarResult}
|
||
*/
|
||
function makeVisitResult(
|
||
queryContext,
|
||
{
|
||
title,
|
||
fallbackTitle,
|
||
uri,
|
||
iconUri,
|
||
providerName,
|
||
tags = [],
|
||
heuristic = false,
|
||
source = UrlbarUtils.RESULT_SOURCE.HISTORY,
|
||
}
|
||
) {
|
||
let payload = {
|
||
url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
|
||
};
|
||
|
||
if (title) {
|
||
payload.title = [title, UrlbarUtils.HIGHLIGHT.TYPED];
|
||
}
|
||
|
||
if (fallbackTitle) {
|
||
payload.fallbackTitle = [fallbackTitle, UrlbarUtils.HIGHLIGHT.TYPED];
|
||
}
|
||
|
||
if (iconUri) {
|
||
payload.icon = iconUri;
|
||
} else if (
|
||
iconUri === undefined &&
|
||
source != UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL
|
||
) {
|
||
payload.icon = `page-icon:${uri}`;
|
||
}
|
||
|
||
if (!heuristic && tags) {
|
||
payload.tags = [tags, UrlbarUtils.HIGHLIGHT.TYPED];
|
||
}
|
||
|
||
let result = new UrlbarResult(
|
||
UrlbarUtils.RESULT_TYPE.URL,
|
||
source,
|
||
...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
|
||
);
|
||
|
||
if (providerName) {
|
||
result.providerName = providerName;
|
||
}
|
||
|
||
result.heuristic = heuristic;
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Checks that the results returned by a UrlbarController match those in
|
||
* the param `matches`.
|
||
*
|
||
* @param {object} options Options for the check.
|
||
* @param {UrlbarQueryContext} options.context
|
||
* The context for this query.
|
||
* @param {string} [options.incompleteSearch]
|
||
* A search will be fired for this string and then be immediately canceled by
|
||
* the query in `context`.
|
||
* @param {string} [options.autofilled]
|
||
* The autofilled value in the first result.
|
||
* @param {string} [options.completed]
|
||
* The value that would be filled if the autofill result was confirmed.
|
||
* Has no effect if `autofilled` is not specified.
|
||
* @param {Array} options.matches
|
||
* An array of UrlbarResults.
|
||
*/
|
||
async function check_results({
|
||
context,
|
||
incompleteSearch,
|
||
autofilled,
|
||
completed,
|
||
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();
|
||
|
||
const controller = UrlbarTestUtils.newMockController({
|
||
input: {
|
||
isPrivate: context.isPrivate,
|
||
onFirstResult() {
|
||
return false;
|
||
},
|
||
getSearchSource() {
|
||
return "dummy-search-source";
|
||
},
|
||
window: {
|
||
location: {
|
||
href: AppConstants.BROWSER_CHROME_URL,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
controller.setView({
|
||
get visibleResults() {
|
||
return context.results;
|
||
},
|
||
controller: {
|
||
removeResult() {},
|
||
},
|
||
acknowledgeDismissal() {},
|
||
});
|
||
|
||
if (incompleteSearch) {
|
||
let incompleteContext = createContext(incompleteSearch, {
|
||
isPrivate: context.isPrivate,
|
||
});
|
||
controller.startQuery(incompleteContext);
|
||
}
|
||
await controller.startQuery(context);
|
||
|
||
if (autofilled) {
|
||
Assert.ok(context.results[0], "There is a first result.");
|
||
Assert.ok(
|
||
context.results[0].autofill,
|
||
"The first result is an autofill result"
|
||
);
|
||
Assert.equal(
|
||
context.results[0].autofill.value,
|
||
autofilled,
|
||
"The correct value was autofilled."
|
||
);
|
||
if (completed) {
|
||
Assert.equal(
|
||
context.results[0].payload.url,
|
||
completed,
|
||
"The completed autofill value is correct."
|
||
);
|
||
}
|
||
}
|
||
if (context.results.length != matches.length) {
|
||
info("Actual results: " + JSON.stringify(context.results));
|
||
}
|
||
Assert.equal(
|
||
context.results.length,
|
||
matches.length,
|
||
"Found the expected number of results."
|
||
);
|
||
|
||
function getPayload(result) {
|
||
let payload = {};
|
||
for (let [key, value] of Object.entries(result.payload)) {
|
||
if (value !== undefined) {
|
||
payload[key] = value;
|
||
}
|
||
}
|
||
return payload;
|
||
}
|
||
|
||
for (let i = 0; i < matches.length; i++) {
|
||
let actual = context.results[i];
|
||
let expected = matches[i];
|
||
info(
|
||
`Comparing results at index ${i}:` +
|
||
" actual=" +
|
||
JSON.stringify(actual) +
|
||
" expected=" +
|
||
JSON.stringify(expected)
|
||
);
|
||
Assert.equal(
|
||
actual.type,
|
||
expected.type,
|
||
`result.type at result index ${i}`
|
||
);
|
||
Assert.equal(
|
||
actual.source,
|
||
expected.source,
|
||
`result.source at result index ${i}`
|
||
);
|
||
Assert.equal(
|
||
actual.heuristic,
|
||
expected.heuristic,
|
||
`result.heuristic at result index ${i}`
|
||
);
|
||
Assert.equal(
|
||
!!actual.isBestMatch,
|
||
!!expected.isBestMatch,
|
||
`result.isBestMatch at result index ${i}`
|
||
);
|
||
if (expected.providerName) {
|
||
Assert.equal(
|
||
actual.providerName,
|
||
expected.providerName,
|
||
`result.providerName at result index ${i}`
|
||
);
|
||
}
|
||
if (expected.hasOwnProperty("suggestedIndex")) {
|
||
Assert.equal(
|
||
actual.suggestedIndex,
|
||
expected.suggestedIndex,
|
||
`result.suggestedIndex at result index ${i}`
|
||
);
|
||
}
|
||
|
||
if (expected.payload) {
|
||
Assert.deepEqual(
|
||
getPayload(actual),
|
||
getPayload(expected),
|
||
`result.payload at result index ${i}`
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Returns the frecency of an origin.
|
||
*
|
||
* @param {string} prefix
|
||
* The origin's prefix, e.g., "http://".
|
||
* @param {string} aHost
|
||
* The origin's host.
|
||
* @returns {number} The origin's frecency.
|
||
*/
|
||
async function getOriginFrecency(prefix, aHost) {
|
||
let db = await PlacesUtils.promiseDBConnection();
|
||
let rows = await db.execute(
|
||
`
|
||
SELECT frecency
|
||
FROM moz_origins
|
||
WHERE prefix = :prefix AND host = :host
|
||
`,
|
||
{ prefix, host: aHost }
|
||
);
|
||
Assert.equal(rows.length, 1);
|
||
return rows[0].getResultByIndex(0);
|
||
}
|
||
|
||
/**
|
||
* Returns the origin frecency stats.
|
||
*
|
||
* @returns {object}
|
||
* An object { count, sum, squares }.
|
||
*/
|
||
async function getOriginFrecencyStats() {
|
||
let db = await PlacesUtils.promiseDBConnection();
|
||
let rows = await db.execute(`
|
||
SELECT
|
||
IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_count'), 0),
|
||
IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_sum'), 0),
|
||
IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_sum_of_squares'), 0)
|
||
`);
|
||
let count = rows[0].getResultByIndex(0);
|
||
let sum = rows[0].getResultByIndex(1);
|
||
let squares = rows[0].getResultByIndex(2);
|
||
return { count, sum, squares };
|
||
}
|
||
|
||
/**
|
||
* Returns the origin autofill frecency threshold.
|
||
*
|
||
* @returns {number}
|
||
* The threshold.
|
||
*/
|
||
async function getOriginAutofillThreshold() {
|
||
let { count, sum, squares } = await getOriginFrecencyStats();
|
||
if (!count) {
|
||
return 0;
|
||
}
|
||
if (count == 1) {
|
||
return sum;
|
||
}
|
||
let stddevMultiplier = UrlbarPrefs.get("autoFill.stddevMultiplier");
|
||
return (
|
||
sum / count +
|
||
stddevMultiplier * Math.sqrt((squares - (sum * sum) / count) / count)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Checks that origins appear in a given order in the database.
|
||
*
|
||
* @param {string} host The "fixed" host, without "www."
|
||
* @param {Array} prefixOrder The prefixes (scheme + www.) sorted appropriately.
|
||
*/
|
||
async function checkOriginsOrder(host, prefixOrder) {
|
||
await PlacesUtils.withConnectionWrapper("checkOriginsOrder", async db => {
|
||
let prefixes = (
|
||
await db.execute(
|
||
`SELECT prefix || iif(instr(host, "www.") = 1, "www.", "")
|
||
FROM moz_origins
|
||
WHERE host = :host OR host = "www." || :host
|
||
ORDER BY ROWID ASC
|
||
`,
|
||
{ host }
|
||
)
|
||
).map(r => r.getResultByIndex(0));
|
||
Assert.deepEqual(prefixes, prefixOrder);
|
||
});
|
||
}
|