/* 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", 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", }); XPCOMUtils.defineLazyModuleGetters(this, { HttpServer: "resource://testing-common/httpd.js", }); XPCOMUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => { const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule( "resource://testing-common/QuickSuggestTestUtils.sys.mjs" ); module.init(this); return module; }); XPCOMUtils.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; }); XPCOMUtils.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 ) ); context.view = { get visibleResults() { return context.results; }, controller: { removeResult() {}, }, acknowledgeDismissal() {}, }; 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"); Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines"); 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 priority search result. * * @param {UrlbarQueryContext} queryContext * The context that this result will be displayed in. * @param {object} options * Options for the result. * @param {string} [options.engineName] * The name of the engine providing the suggestion. Leave blank if there * is no suggestion. * @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. * @returns {UrlbarResult} */ function makePrioritySearchResult( queryContext, { engineName, engineIconUri, heuristic } ) { let result = new UrlbarResult( UrlbarUtils.RESULT_TYPE.SEARCH, UrlbarUtils.RESULT_SOURCE.SEARCH, ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, { engine: [engineName, UrlbarUtils.HIGHLIGHT.TYPED], icon: engineIconUri, }) ); 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.payload.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, }, }, }, }); 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); }); }