summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/tests/unit
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /browser/components/urlbar/tests/unit
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/urlbar/tests/unit')
-rw-r--r--browser/components/urlbar/tests/unit/data/engine-suggestions.xml16
-rw-r--r--browser/components/urlbar/tests/unit/data/engine-tail-suggestions.xml14
-rw-r--r--browser/components/urlbar/tests/unit/head.js946
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarController_integration.js104
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js256
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarController_unit.js389
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarPrefs.js40
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarQueryContext.js73
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js145
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.jsm292
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarUtils_addToUrlbarHistory.js63
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js257
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarUtils_getTokenMatches.js294
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_about_urls.js100
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_bookmarked.js148
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_functional.js112
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_origins.js638
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js2408
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js74
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js90
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_search_engines.js234
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_urls.js218
-rw-r--r--browser/components/urlbar/tests/unit/test_avoid_middle_complete.js284
-rw-r--r--browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js121
-rw-r--r--browser/components/urlbar/tests/unit/test_casing.js356
-rw-r--r--browser/components/urlbar/tests/unit/test_dedupe_prefix.js116
-rw-r--r--browser/components/urlbar/tests/unit/test_dupe_urls.js63
-rw-r--r--browser/components/urlbar/tests/unit/test_encoded_urls.js97
-rw-r--r--browser/components/urlbar/tests/unit/test_heuristic_cancel.js136
-rw-r--r--browser/components/urlbar/tests/unit/test_keywords.js207
-rw-r--r--browser/components/urlbar/tests/unit/test_muxer.js246
-rw-r--r--browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js613
-rw-r--r--browser/components/urlbar/tests/unit/test_providerOmnibox.js818
-rw-r--r--browser/components/urlbar/tests/unit/test_providerOpenTabs.js45
-rw-r--r--browser/components/urlbar/tests/unit/test_providerTabToSearch.js477
-rw-r--r--browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js150
-rw-r--r--browser/components/urlbar/tests/unit/test_providerUnifiedComplete.js242
-rw-r--r--browser/components/urlbar/tests/unit/test_providerUnifiedComplete_duplicate_entries.js42
-rw-r--r--browser/components/urlbar/tests/unit/test_providersManager.js74
-rw-r--r--browser/components/urlbar/tests/unit/test_providersManager_filtering.js405
-rw-r--r--browser/components/urlbar/tests/unit/test_providersManager_maxResults.js37
-rw-r--r--browser/components/urlbar/tests/unit/test_queryScorer.js405
-rw-r--r--browser/components/urlbar/tests/unit/test_query_url.js123
-rw-r--r--browser/components/urlbar/tests/unit/test_search_engine_host.js97
-rw-r--r--browser/components/urlbar/tests/unit/test_search_suggestions.js1695
-rw-r--r--browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js359
-rw-r--r--browser/components/urlbar/tests/unit/test_search_suggestions_tail.js358
-rw-r--r--browser/components/urlbar/tests/unit/test_tokenizer.js450
-rw-r--r--browser/components/urlbar/tests/unit/test_trimming.js222
-rw-r--r--browser/components/urlbar/tests/unit/xpcshell.ini54
50 files changed, 15203 insertions, 0 deletions
diff --git a/browser/components/urlbar/tests/unit/data/engine-suggestions.xml b/browser/components/urlbar/tests/unit/data/engine-suggestions.xml
new file mode 100644
index 0000000000..2f7a6f7b09
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/data/engine-suggestions.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-suggestions.xml</ShortName>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="http://localhost:9000/suggest?{searchTerms}"/>
+<Url type="text/html"
+ method="GET"
+ template="http://localhost:9000/search"
+ rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/unit/data/engine-tail-suggestions.xml b/browser/components/urlbar/tests/unit/data/engine-tail-suggestions.xml
new file mode 100644
index 0000000000..65f208884d
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/data/engine-tail-suggestions.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-tail-suggestions.xml</ShortName>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="http://localhost:9001/suggest?{searchTerms}"/>
+<Url type="text/html"
+ method="GET"
+ template="http://localhost:9001/search"
+ rel="searchform"/>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/unit/head.js b/browser/components/urlbar/tests/unit/head.js
new file mode 100644
index 0000000000..04c5e42eb9
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/head.js
@@ -0,0 +1,946 @@
+/* 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",
+ 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"
+);
+
+add_task(async function initXPCShellDependencies() {
+ await UrlbarTestUtils.initXPCShellDependencies();
+});
+
+/**
+ * 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).
+ *
+ * @return 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.
+ * @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.addOpenSearchEngine(dataUrl + basename, null);
+ });
+}
+
+/**
+ * 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;
+}
+
+/**
+ * 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);
+ });
+
+ Services.search.setDefault(engine);
+ 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();
+}
+
+/**
+ * Returns the frecency of a url.
+ *
+ * @param {string} aURI The URI or spec to get frecency for.
+ * @returns {number} the frecency value.
+ */
+function frecencyForUrl(aURI) {
+ let url = aURI;
+ if (aURI instanceof Ci.nsIURI) {
+ url = aURI.spec;
+ } else if (aURI instanceof URL) {
+ url = aURI.href;
+ }
+ let stmt = DBConn().createStatement(
+ "SELECT frecency FROM moz_places WHERE url_hash = hash(?1) AND url = ?1"
+ );
+ stmt.bindByIndex(0, url);
+ try {
+ if (!stmt.executeStep()) {
+ throw new Error("No result for frecency.");
+ }
+ return stmt.getInt32(0);
+ } finally {
+ stmt.finalize();
+ }
+}
+
+/**
+ * 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,
+ 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 {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 {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 result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.OMNIBOX,
+ UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: [description, UrlbarUtils.HIGHLIGHT.TYPED],
+ content: [content, UrlbarUtils.HIGHLIGHT.TYPED],
+ keyword: [keyword, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: [UrlbarUtils.ICON.EXTENSION],
+ })
+ );
+ result.heuristic = heuristic;
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for a keyword search result.
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @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,
+ 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 {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 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.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.
+ * @returns {UrlbarResult}
+ */
+function makeSearchResult(
+ queryContext,
+ {
+ suggestion,
+ tailPrefix,
+ tail,
+ tailOffsetIndex,
+ engineName,
+ alias,
+ uri,
+ query,
+ engineIconUri,
+ providesSearchMode,
+ providerName,
+ inPrivateWindow,
+ isPrivateEngine,
+ heuristic = 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);
+ }
+ }
+
+ let result = new UrlbarResult(
+ type,
+ source,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
+ );
+
+ if (typeof suggestion == "string") {
+ result.payload.lowerCaseSuggestion = result.payload.suggestion.toLocaleLowerCase();
+ }
+
+ 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 {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.
+ * * @param {string} 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.
+ * @returns {UrlbarResult}
+ */
+function makeVisitResult(
+ queryContext,
+ {
+ title,
+ uri,
+ iconUri,
+ providerName,
+ tags = null,
+ heuristic = false,
+ source = UrlbarUtils.RESULT_SOURCE.HISTORY,
+ }
+) {
+ let payload = {
+ url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
+ title: [title, 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 {UrlbarQueryContext} context
+ * The context for this query.
+ * @param {string} [incompleteSearch]
+ * A search will be fired for this string and then be immediately canceled by
+ * the query in `context`.
+ * @param {string} [autofilled]
+ * The autofilled value in the first result.
+ * @param {string} [completed]
+ * The value that would be filled if the autofill result was confirmed.
+ * Has no effect if `autofilled` is not specified.
+ * @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,
+ 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();
+
+ let controller = UrlbarTestUtils.newMockController({
+ input: {
+ isPrivate: context.isPrivate,
+ onFirstResult() {
+ return false;
+ },
+ 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}`
+ );
+ if (expected.providerName) {
+ Assert.equal(
+ actual.providerName,
+ expected.providerName,
+ `result.providerName at result index ${i}`
+ );
+ }
+ 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);
+ });
+}
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js b/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js
new file mode 100644
index 0000000000..6da8255b5a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests test the UrlbarController in association with the model.
+ */
+
+"use strict";
+
+const { PromiseUtils } = ChromeUtils.import(
+ "resource://gre/modules/PromiseUtils.jsm"
+);
+
+const TEST_URL = "http://example.com";
+const match = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: TEST_URL }
+);
+let controller;
+
+/**
+ * Asserts that the query context has the expected values.
+ *
+ * @param {UrlbarQueryContext} context
+ * @param {object} expectedValues The expected values for the UrlbarQueryContext.
+ */
+function assertContextMatches(context, expectedValues) {
+ Assert.ok(
+ context instanceof UrlbarQueryContext,
+ "Should be a UrlbarQueryContext"
+ );
+
+ for (let [key, value] of Object.entries(expectedValues)) {
+ Assert.equal(
+ context[key],
+ value,
+ `Should have the expected value for ${key} in the UrlbarQueryContext`
+ );
+ }
+}
+
+add_task(async function setup() {
+ controller = UrlbarTestUtils.newMockController();
+});
+
+add_task(async function test_basic_search() {
+ let providerName = registerBasicTestProvider([match]);
+ const context = createContext(TEST_URL, { providers: [providerName] });
+
+ let startedPromise = promiseControllerNotification(
+ controller,
+ "onQueryStarted"
+ );
+ let resultsPromise = promiseControllerNotification(
+ controller,
+ "onQueryResults"
+ );
+
+ controller.startQuery(context);
+
+ let params = await startedPromise;
+
+ Assert.equal(params[0], context);
+
+ params = await resultsPromise;
+
+ Assert.deepEqual(
+ params[0].results,
+ [match],
+ "Should have the expected match"
+ );
+});
+
+add_task(async function test_cancel_search() {
+ let providerCanceledDeferred = PromiseUtils.defer();
+ let providerName = registerBasicTestProvider(
+ [match],
+ providerCanceledDeferred.resolve
+ );
+ const context = createContext(TEST_URL, { providers: [providerName] });
+
+ let startedPromise = promiseControllerNotification(
+ controller,
+ "onQueryStarted"
+ );
+ let cancelPromise = promiseControllerNotification(
+ controller,
+ "onQueryCancelled"
+ );
+
+ controller.startQuery(context);
+
+ let params = await startedPromise;
+
+ controller.cancelQuery(context);
+
+ Assert.equal(params[0], context);
+
+ info("Should tell the provider the query is canceled");
+ await providerCanceledDeferred.promise;
+
+ params = await cancelPromise;
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js b/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js
new file mode 100644
index 0000000000..d103eb10f5
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js
@@ -0,0 +1,256 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests unit test the functionality of UrlbarController by stubbing out the
+ * model and providing stubs to be called.
+ */
+
+"use strict";
+
+const TEST_URL = "http://example.com";
+const MATCH = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: TEST_URL }
+);
+const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS";
+const TELEMETRY_6_FIRST_RESULTS = "PLACES_AUTOCOMPLETE_6_FIRST_RESULTS_TIME_MS";
+
+let controller;
+let firstHistogram;
+let sixthHistogram;
+
+/**
+ * A delayed test provider, allowing the query to be delayed for an amount of time.
+ */
+class DelayedProvider extends TestProvider {
+ async startQuery(context, add) {
+ Assert.ok(context, "context is passed-in");
+ Assert.equal(typeof add, "function", "add is a callback");
+ this._add = add;
+ await new Promise(resolve => {
+ this._resultsAdded = resolve;
+ });
+ }
+ async addResults(matches, finish = true) {
+ // startQuery may have not been invoked yet, so wait for it
+ await TestUtils.waitForCondition(
+ () => !!this._add,
+ "Waiting for the _add callback"
+ );
+ for (const match of matches) {
+ this._add(this, match);
+ }
+ if (finish) {
+ this._add = null;
+ this._resultsAdded();
+ }
+ }
+}
+
+/**
+ * Returns the number of reports sent recorded within the histogram results.
+ *
+ * @param {object} results a snapshot of histogram results to check.
+ * @returns {number} The count of reports recorded in the histogram.
+ */
+function getHistogramReportsCount(results) {
+ let sum = 0;
+ for (let [, value] of Object.entries(results.values)) {
+ sum += value;
+ }
+ return sum;
+}
+
+add_task(function setup() {
+ controller = UrlbarTestUtils.newMockController();
+
+ firstHistogram = Services.telemetry.getHistogramById(TELEMETRY_1ST_RESULT);
+ sixthHistogram = Services.telemetry.getHistogramById(
+ TELEMETRY_6_FIRST_RESULTS
+ );
+});
+
+add_task(async function test_n_autocomplete_cancel() {
+ firstHistogram.clear();
+ sixthHistogram.clear();
+
+ let providerCanceledDeferred = PromiseUtils.defer();
+ let provider = new TestProvider({
+ results: [],
+ onCancel: providerCanceledDeferred.resolve,
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+ const context = createContext(TEST_URL, { providers: [provider.name] });
+
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should not have started first result stopwatch"
+ );
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should not have started first 6 results stopwatch"
+ );
+
+ controller.startQuery(context);
+
+ Assert.ok(
+ TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should have started first result stopwatch"
+ );
+ Assert.ok(
+ TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should have started first 6 results stopwatch"
+ );
+
+ controller.cancelQuery(context);
+
+ await providerCanceledDeferred.promise;
+
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should have canceled first result stopwatch"
+ );
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should have canceled first 6 results stopwatch"
+ );
+
+ let results = firstHistogram.snapshot();
+ Assert.equal(
+ results.sum,
+ 0,
+ "Should not have recorded any times (first result)"
+ );
+ results = sixthHistogram.snapshot();
+ Assert.equal(
+ results.sum,
+ 0,
+ "Should not have recorded any times (first 6 results)"
+ );
+});
+
+add_task(async function test_n_autocomplete_results() {
+ firstHistogram.clear();
+ sixthHistogram.clear();
+
+ let provider = new DelayedProvider();
+ UrlbarProvidersManager.registerProvider(provider);
+ const context = createContext(TEST_URL, { providers: [provider.name] });
+
+ let resultsPromise = promiseControllerNotification(
+ controller,
+ "onQueryResults"
+ );
+
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should not have started first result stopwatch"
+ );
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should not have started first 6 results stopwatch"
+ );
+
+ controller.startQuery(context);
+
+ Assert.ok(
+ TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should have started first result stopwatch"
+ );
+ Assert.ok(
+ TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should have started first 6 results stopwatch"
+ );
+
+ await provider.addResults([MATCH], false);
+ await resultsPromise;
+
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should have stopped the first stopwatch"
+ );
+ Assert.ok(
+ TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should have kept the first 6 results stopwatch running"
+ );
+
+ let firstResults = firstHistogram.snapshot();
+ let first6Results = sixthHistogram.snapshot();
+ Assert.equal(
+ getHistogramReportsCount(firstResults),
+ 1,
+ "Should have recorded one time for the first result"
+ );
+ Assert.equal(
+ getHistogramReportsCount(first6Results),
+ 0,
+ "Should not have recorded any times (first 6 results)"
+ );
+
+ // Now add 5 more results, so that the first 6 results is triggered.
+ for (let i = 0; i < 5; i++) {
+ resultsPromise = promiseControllerNotification(
+ controller,
+ "onQueryResults"
+ );
+ await provider.addResults(
+ [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: TEST_URL + "/i" }
+ ),
+ ],
+ false
+ );
+ await resultsPromise;
+ }
+
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should have stopped the first stopwatch"
+ );
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should have stopped the first 6 results stopwatch"
+ );
+
+ let updatedResults = firstHistogram.snapshot();
+ let updated6Results = sixthHistogram.snapshot();
+ Assert.deepEqual(
+ updatedResults,
+ firstResults,
+ "Should not have changed the histogram for the first result"
+ );
+ Assert.equal(
+ getHistogramReportsCount(updated6Results),
+ 1,
+ "Should have recorded one time for the first 6 results"
+ );
+
+ // Add one more, to check neither are updated.
+ resultsPromise = promiseControllerNotification(controller, "onQueryResults");
+ await provider.addResults([
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: TEST_URL + "/6" }
+ ),
+ ]);
+ await resultsPromise;
+
+ let secondUpdateResults = firstHistogram.snapshot();
+ let secondUpdate6Results = sixthHistogram.snapshot();
+ Assert.deepEqual(
+ secondUpdateResults,
+ firstResults,
+ "Should not have changed the histogram for the first result"
+ );
+ Assert.equal(
+ getHistogramReportsCount(secondUpdate6Results),
+ 1,
+ "Should not have changed the histogram for the first 6 results"
+ );
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarController_unit.js b/browser/components/urlbar/tests/unit/test_UrlbarController_unit.js
new file mode 100644
index 0000000000..c62df478f1
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_unit.js
@@ -0,0 +1,389 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests unit test the functionality of UrlbarController by stubbing out the
+ * model and providing stubs to be called.
+ */
+
+"use strict";
+
+// A fake ProvidersManager.
+let fPM;
+let sandbox;
+let generalListener;
+let controller;
+
+/**
+ * Asserts that the query context has the expected values.
+ *
+ * @param {UrlbarQueryContext} context
+ * @param {object} expectedValues The expected values for the UrlbarQueryContext.
+ */
+function assertContextMatches(context, expectedValues) {
+ Assert.ok(
+ context instanceof UrlbarQueryContext,
+ "Should be a UrlbarQueryContext"
+ );
+
+ for (let [key, value] of Object.entries(expectedValues)) {
+ Assert.equal(
+ context[key],
+ value,
+ `Should have the expected value for ${key} in the UrlbarQueryContext`
+ );
+ }
+}
+
+add_task(function setup() {
+ sandbox = sinon.createSandbox();
+
+ fPM = {
+ startQuery: sandbox.stub(),
+ cancelQuery: sandbox.stub(),
+ };
+
+ generalListener = {
+ onQueryStarted: sandbox.stub(),
+ onQueryResults: sandbox.stub(),
+ onQueryCancelled: sandbox.stub(),
+ };
+
+ controller = UrlbarTestUtils.newMockController({
+ manager: fPM,
+ });
+ controller.addQueryListener(generalListener);
+});
+
+add_task(function test_constructor_throws() {
+ Assert.throws(
+ () => new UrlbarController(),
+ /Missing options: input/,
+ "Should throw if the input was not supplied"
+ );
+ Assert.throws(
+ () => new UrlbarController({ input: {} }),
+ /input is missing 'window' property/,
+ "Should throw if the input is not a UrlbarInput"
+ );
+ Assert.throws(
+ () => new UrlbarController({ input: { window: {} } }),
+ /input.window should be an actual browser window/,
+ "Should throw if the input.window is not a window"
+ );
+ Assert.throws(
+ () =>
+ new UrlbarController({
+ input: {
+ window: {
+ location: "about:fake",
+ },
+ },
+ }),
+ /input.window should be an actual browser window/,
+ "Should throw if the input.window is not an object"
+ );
+ Assert.throws(
+ () =>
+ new UrlbarController({
+ input: {
+ window: {
+ location: {
+ href: "about:fake",
+ },
+ },
+ },
+ }),
+ /input.window should be an actual browser window/,
+ "Should throw if the input.window does not have the correct location"
+ );
+ Assert.throws(
+ () =>
+ new UrlbarController({
+ input: {
+ window: {
+ location: {
+ href: AppConstants.BROWSER_CHROME_URL,
+ },
+ },
+ },
+ }),
+ /input.isPrivate must be set/,
+ "Should throw if input.isPrivate is not set"
+ );
+
+ new UrlbarController({
+ input: {
+ isPrivate: false,
+ window: {
+ location: {
+ href: AppConstants.BROWSER_CHROME_URL,
+ },
+ },
+ },
+ });
+ Assert.ok(true, "Correct call should not throw");
+});
+
+add_task(function test_add_and_remove_listeners() {
+ Assert.throws(
+ () => controller.addQueryListener(null),
+ /Expected listener to be an object/,
+ "Should throw for a null listener"
+ );
+ Assert.throws(
+ () => controller.addQueryListener(123),
+ /Expected listener to be an object/,
+ "Should throw for a non-object listener"
+ );
+
+ const listener = {};
+
+ controller.addQueryListener(listener);
+
+ Assert.ok(
+ controller._listeners.has(listener),
+ "Should have added the listener to the list."
+ );
+
+ // Adding a non-existent listener shouldn't throw.
+ controller.removeQueryListener(123);
+
+ controller.removeQueryListener(listener);
+
+ Assert.ok(
+ !controller._listeners.has(listener),
+ "Should have removed the listener from the list"
+ );
+
+ sandbox.resetHistory();
+});
+
+add_task(function test__notify() {
+ const listener1 = {
+ onFake: sandbox.stub().callsFake(() => {
+ throw new Error("fake error");
+ }),
+ };
+ const listener2 = {
+ onFake: sandbox.stub(),
+ };
+
+ controller.addQueryListener(listener1);
+ controller.addQueryListener(listener2);
+
+ const param = "1234";
+
+ controller.notify("onFake", param);
+
+ Assert.equal(
+ listener1.onFake.callCount,
+ 1,
+ "Should have called the first listener method."
+ );
+ Assert.deepEqual(
+ listener1.onFake.args[0],
+ [param],
+ "Should have called the first listener with the correct argument"
+ );
+ Assert.equal(
+ listener2.onFake.callCount,
+ 1,
+ "Should have called the second listener method."
+ );
+ Assert.deepEqual(
+ listener2.onFake.args[0],
+ [param],
+ "Should have called the first listener with the correct argument"
+ );
+
+ controller.removeQueryListener(listener2);
+ controller.removeQueryListener(listener1);
+
+ // This should succeed without errors.
+ controller.notify("onNewFake");
+
+ sandbox.resetHistory();
+});
+
+add_task(function test_handle_query_starts_search() {
+ const context = createContext();
+ controller.startQuery(context);
+
+ Assert.equal(
+ fPM.startQuery.callCount,
+ 1,
+ "Should have called startQuery once"
+ );
+ Assert.equal(
+ fPM.startQuery.args[0].length,
+ 2,
+ "Should have called startQuery with two arguments"
+ );
+
+ assertContextMatches(fPM.startQuery.args[0][0], {});
+ Assert.equal(
+ fPM.startQuery.args[0][1],
+ controller,
+ "Should have passed the controller as the second argument"
+ );
+
+ Assert.equal(
+ generalListener.onQueryStarted.callCount,
+ 1,
+ "Should have called onQueryStarted for the listener"
+ );
+ Assert.deepEqual(
+ generalListener.onQueryStarted.args[0],
+ [context],
+ "Should have called onQueryStarted with the context"
+ );
+
+ sandbox.resetHistory();
+});
+
+add_task(async function test_handle_query_starts_search_sets_allowAutofill() {
+ let originalValue = Services.prefs.getBoolPref("browser.urlbar.autoFill");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", !originalValue);
+
+ await controller.startQuery(createContext());
+
+ Assert.equal(
+ fPM.startQuery.callCount,
+ 1,
+ "Should have called startQuery once"
+ );
+ Assert.equal(
+ fPM.startQuery.args[0].length,
+ 2,
+ "Should have called startQuery with two arguments"
+ );
+
+ assertContextMatches(fPM.startQuery.args[0][0], {
+ allowAutofill: !originalValue,
+ });
+ Assert.equal(
+ fPM.startQuery.args[0][1],
+ controller,
+ "Should have passed the controller as the second argument"
+ );
+
+ sandbox.resetHistory();
+
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+});
+
+add_task(function test_cancel_query() {
+ const context = createContext();
+ controller.startQuery(context);
+
+ controller.cancelQuery();
+
+ Assert.equal(
+ fPM.cancelQuery.callCount,
+ 1,
+ "Should have called cancelQuery once"
+ );
+ Assert.equal(
+ fPM.cancelQuery.args[0].length,
+ 1,
+ "Should have called cancelQuery with one argument"
+ );
+
+ Assert.equal(
+ generalListener.onQueryCancelled.callCount,
+ 1,
+ "Should have called onQueryCancelled for the listener"
+ );
+ Assert.deepEqual(
+ generalListener.onQueryCancelled.args[0],
+ [context],
+ "Should have called onQueryCancelled with the context"
+ );
+
+ sandbox.resetHistory();
+});
+
+add_task(function test_receiveResults() {
+ const context = createContext();
+ context.results = [];
+ controller.receiveResults(context);
+
+ Assert.equal(
+ generalListener.onQueryResults.callCount,
+ 1,
+ "Should have called onQueryResults for the listener"
+ );
+ Assert.deepEqual(
+ generalListener.onQueryResults.args[0],
+ [context],
+ "Should have called onQueryResults with the context"
+ );
+
+ sandbox.resetHistory();
+});
+
+add_task(async function test_notifications_order() {
+ // Clear any pending notifications.
+ const context = createContext();
+ await controller.startQuery(context);
+
+ // Check that when multiple queries are executed, the notifications arrive
+ // in the proper order.
+ let collectingListener = new Proxy(
+ {},
+ {
+ _notifications: [],
+ get(target, name) {
+ if (name == "notifications") {
+ return this._notifications;
+ }
+ return () => {
+ this._notifications.push(name);
+ };
+ },
+ }
+ );
+ controller.addQueryListener(collectingListener);
+ controller.startQuery(context);
+ Assert.deepEqual(
+ ["onQueryStarted"],
+ collectingListener.notifications,
+ "Check onQueryStarted is fired synchronously"
+ );
+ controller.startQuery(context);
+ Assert.deepEqual(
+ ["onQueryStarted", "onQueryCancelled", "onQueryFinished", "onQueryStarted"],
+ collectingListener.notifications,
+ "Check order of notifications"
+ );
+ controller.cancelQuery();
+ Assert.deepEqual(
+ [
+ "onQueryStarted",
+ "onQueryCancelled",
+ "onQueryFinished",
+ "onQueryStarted",
+ "onQueryCancelled",
+ "onQueryFinished",
+ ],
+ collectingListener.notifications,
+ "Check order of notifications"
+ );
+ await controller.startQuery(context);
+ controller.cancelQuery();
+ Assert.deepEqual(
+ [
+ "onQueryStarted",
+ "onQueryCancelled",
+ "onQueryFinished",
+ "onQueryStarted",
+ "onQueryCancelled",
+ "onQueryFinished",
+ "onQueryStarted",
+ "onQueryFinished",
+ ],
+ collectingListener.notifications,
+ "Check order of notifications"
+ );
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarPrefs.js b/browser/components/urlbar/tests/unit/test_UrlbarPrefs.js
new file mode 100644
index 0000000000..b08a5fff2a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarPrefs.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function test() {
+ Assert.throws(
+ () => UrlbarPrefs.get("browser.migration.version"),
+ /Trying to access an unknown pref/,
+ "Should throw when passing an untracked pref"
+ );
+
+ Assert.throws(
+ () => UrlbarPrefs.set("browser.migration.version", 100),
+ /Trying to access an unknown pref/,
+ "Should throw when passing an untracked pref"
+ );
+ Assert.throws(
+ () => UrlbarPrefs.set("maxRichResults", "10"),
+ /Invalid value/,
+ "Should throw when passing an invalid value type"
+ );
+
+ Assert.deepEqual(UrlbarPrefs.get("formatting.enabled"), true);
+ UrlbarPrefs.set("formatting.enabled", false);
+ Assert.deepEqual(UrlbarPrefs.get("formatting.enabled"), false);
+
+ Assert.deepEqual(UrlbarPrefs.get("maxRichResults"), 10);
+ UrlbarPrefs.set("maxRichResults", 6);
+ Assert.deepEqual(UrlbarPrefs.get("maxRichResults"), 6);
+
+ Assert.deepEqual(UrlbarPrefs.get("autoFill.stddevMultiplier"), 0.0);
+ UrlbarPrefs.set("autoFill.stddevMultiplier", 0.01);
+ // Due to rounding errors, floats are slightly imprecise, so we can't
+ // directly compare what we set to what we retrieve.
+ Assert.deepEqual(
+ parseFloat(UrlbarPrefs.get("autoFill.stddevMultiplier").toFixed(2)),
+ 0.01
+ );
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarQueryContext.js b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext.js
new file mode 100644
index 0000000000..e30e2fa0eb
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function test_constructor() {
+ Assert.throws(
+ () => new UrlbarQueryContext(),
+ /Missing or empty allowAutofill provided to UrlbarQueryContext/,
+ "Should throw with no arguments"
+ );
+
+ Assert.throws(
+ () =>
+ new UrlbarQueryContext({
+ allowAutofill: true,
+ isPrivate: false,
+ searchString: "foo",
+ }),
+ /Missing or empty maxResults provided to UrlbarQueryContext/,
+ "Should throw with a missing maxResults parameter"
+ );
+
+ Assert.throws(
+ () =>
+ new UrlbarQueryContext({
+ allowAutofill: true,
+ maxResults: 1,
+ searchString: "foo",
+ }),
+ /Missing or empty isPrivate provided to UrlbarQueryContext/,
+ "Should throw with a missing isPrivate parameter"
+ );
+
+ Assert.throws(
+ () =>
+ new UrlbarQueryContext({
+ isPrivate: false,
+ maxResults: 1,
+ searchString: "foo",
+ }),
+ /Missing or empty allowAutofill provided to UrlbarQueryContext/,
+ "Should throw with a missing allowAutofill parameter"
+ );
+
+ let qc = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: true,
+ maxResults: 1,
+ searchString: "foo",
+ });
+
+ Assert.strictEqual(
+ qc.allowAutofill,
+ false,
+ "Should have saved the correct value for allowAutofill"
+ );
+ Assert.strictEqual(
+ qc.isPrivate,
+ true,
+ "Should have saved the correct value for isPrivate"
+ );
+ Assert.equal(
+ qc.maxResults,
+ 1,
+ "Should have saved the correct value for maxResults"
+ );
+ Assert.equal(
+ qc.searchString,
+ "foo",
+ "Should have saved the correct value for searchString"
+ );
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js
new file mode 100644
index 0000000000..056110fed9
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js
@@ -0,0 +1,145 @@
+/* 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/. */
+
+/**
+ * Test for restrictions set through UrlbarQueryContext.sources.
+ */
+
+add_task(async function setup() {
+ let engine = await addTestSuggestionsEngine();
+ let oldDefaultEngine = await Services.search.getDefault();
+ Services.search.setDefault(engine);
+ registerCleanupFunction(async () =>
+ Services.search.setDefault(oldDefaultEngine)
+ );
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "unifiedComplete",
+ "@mozilla.org/autocomplete/search;1?name=unifiedcomplete",
+ "nsIAutoCompleteSearch"
+);
+
+add_task(async function test_restrictions() {
+ await PlacesTestUtils.addVisits([
+ { uri: "http://history.com/", title: "match" },
+ ]);
+ await PlacesUtils.bookmarks.insert({
+ url: "http://bookmark.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "match",
+ });
+ await UrlbarProviderOpenTabs.registerOpenTab("http://openpagematch.com/");
+
+ info("Bookmark restrict");
+ let results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.BOOKMARKS],
+ searchString: "match",
+ });
+ // Skip the heuristic result.
+ Assert.deepEqual(
+ results.filter(r => !r.heuristic).map(r => r.payload.url),
+ ["http://bookmark.com/"]
+ );
+
+ info("History restrict");
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.HISTORY],
+ searchString: "match",
+ });
+ // Skip the heuristic result.
+ Assert.deepEqual(
+ results.filter(r => !r.heuristic).map(r => r.payload.url),
+ ["http://history.com/"]
+ );
+
+ info("tabs restrict");
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.TABS],
+ searchString: "match",
+ });
+ // Skip the heuristic result.
+ Assert.deepEqual(
+ results.filter(r => !r.heuristic).map(r => r.payload.url),
+ ["http://openpagematch.com/"]
+ );
+
+ info("search restrict");
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
+ searchString: "match",
+ });
+ Assert.ok(
+ !results.some(r => r.payload.engine != "engine-suggestions.xml"),
+ "All the results should be search results"
+ );
+
+ info("search restrict should ignore restriction token");
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
+ searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARKS} match`,
+ });
+ Assert.ok(
+ !results.some(r => r.payload.engine != "engine-suggestions.xml"),
+ "All the results should be search results"
+ );
+ Assert.equal(
+ results[0].payload.query,
+ `${UrlbarTokenizer.RESTRICT.BOOKMARKS} match`,
+ "The restriction token should be ignored and not stripped"
+ );
+
+ info("search restrict with alias");
+ let aliasEngine = await Services.search.addEngineWithDetails("Test", {
+ alias: "match",
+ template: "http://example.com/?search={searchTerms}",
+ });
+ registerCleanupFunction(async function() {
+ await Services.search.removeEngine(aliasEngine);
+ });
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
+ searchString: "match this",
+ });
+ Assert.ok(
+ !results.some(r => r.payload.engine != "engine-suggestions.xml"),
+ "All the results should be search results and the alias should be ignored"
+ );
+ Assert.equal(
+ results[0].payload.query,
+ `match this`,
+ "The restriction token should be ignored and not stripped"
+ );
+
+ info("search restrict with other engine");
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
+ searchString: "match",
+ engineName: "Test",
+ });
+ Assert.ok(
+ !results.some(r => r.payload.engine != "Test"),
+ "All the results should be search results from the Test engine"
+ );
+});
+
+async function get_results(test) {
+ let controller = UrlbarTestUtils.newMockController();
+ let options = {
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ sources: test.sources,
+ };
+ if (test.engineName) {
+ options.searchMode = {
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engineName: test.engineName,
+ };
+ }
+ let queryContext = createContext(test.searchString, options);
+ await controller.startQuery(queryContext);
+ return queryContext.results;
+}
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.jsm b/browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.jsm
new file mode 100644
index 0000000000..92b2cfbb4b
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.jsm
@@ -0,0 +1,292 @@
+/* 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/. */
+
+const { UrlbarSearchUtils } = ChromeUtils.import(
+ "resource:///modules/UrlbarSearchUtils.jsm"
+);
+
+add_task(async function() {
+ await UrlbarSearchUtils.init();
+ // Tell the search service we are running in the US. This also has the
+ // desired side-effect of preventing our geoip lookup.
+ Services.prefs.setCharPref("browser.search.region", "US");
+
+ Services.search.restoreDefaultEngines();
+ Services.search.resetToOriginalDefaultEngine();
+});
+
+add_task(async function search_engine_match() {
+ let engine = await Services.search.getDefault();
+ let domain = engine.getResultDomain();
+ let token = domain.substr(0, 1);
+ let matchedEngine = (
+ await UrlbarSearchUtils.enginesForDomainPrefix(token)
+ )[0];
+ Assert.equal(matchedEngine, engine);
+});
+
+add_task(async function no_match() {
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("test")).length
+ );
+});
+
+add_task(async function hide_search_engine_nomatch() {
+ let engine = await Services.search.getDefault();
+ let domain = engine.getResultDomain();
+ let token = domain.substr(0, 1);
+ let promiseTopic = promiseSearchTopic("engine-changed");
+ await Promise.all([Services.search.removeEngine(engine), promiseTopic]);
+ Assert.ok(engine.hidden);
+ let matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix(token);
+ Assert.ok(
+ !matchedEngines.length || matchedEngines[0].getResultDomain() != domain
+ );
+ engine.hidden = false;
+ await TestUtils.waitForCondition(
+ async () => (await UrlbarSearchUtils.enginesForDomainPrefix(token)).length
+ );
+ let matchedEngine2 = (
+ await UrlbarSearchUtils.enginesForDomainPrefix(token)
+ )[0];
+ Assert.ok(matchedEngine2);
+});
+
+add_task(async function onlyEnabled_option_nomatch() {
+ let engine = await Services.search.getDefault();
+ let domain = engine.getResultDomain();
+ let token = domain.substr(0, 1);
+ Services.prefs.setCharPref("browser.search.hiddenOneOffs", engine.name);
+ let matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix(token, {
+ onlyEnabled: true,
+ });
+ Assert.ok(
+ !matchedEngines.length || matchedEngines[0].getResultDomain() != domain
+ );
+ Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
+ matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix(token, {
+ onlyEnabled: true,
+ });
+ Assert.ok(
+ matchedEngines.length && matchedEngines[0].getResultDomain() == domain
+ );
+});
+
+add_task(async function add_search_engine_match() {
+ let promiseTopic = promiseSearchTopic("engine-added");
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("bacon")).length
+ );
+ await Promise.all([
+ Services.search.addEngineWithDetails("bacon", {
+ alias: "pork",
+ description: "Search Bacon",
+ method: "GET",
+ template: "http://www.bacon.moz/?search={searchTerms}",
+ }),
+ promiseTopic,
+ ]);
+ await promiseTopic;
+ let matchedEngine = (
+ await UrlbarSearchUtils.enginesForDomainPrefix("bacon")
+ )[0];
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.searchForm, "http://www.bacon.moz");
+ Assert.equal(matchedEngine.name, "bacon");
+ Assert.equal(matchedEngine.iconURI, null);
+ info("also type part of the public suffix");
+ matchedEngine = (
+ await UrlbarSearchUtils.enginesForDomainPrefix("bacon.m")
+ )[0];
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.searchForm, "http://www.bacon.moz");
+ Assert.equal(matchedEngine.name, "bacon");
+ Assert.equal(matchedEngine.iconURI, null);
+});
+
+add_task(async function match_multiple_search_engines() {
+ let promiseTopic = promiseSearchTopic("engine-added");
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("baseball")).length
+ );
+ await Promise.all([
+ Services.search.addEngineWithDetails("baseball", {
+ description: "Search Baseball",
+ method: "GET",
+ template: "http://www.baseball.moz/?search={searchTerms}",
+ }),
+ promiseTopic,
+ ]);
+ await promiseTopic;
+ let matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix("ba");
+ Assert.equal(
+ matchedEngines.length,
+ 2,
+ "enginesForDomainPrefix returned two engines."
+ );
+ Assert.equal(matchedEngines[0].searchForm, "http://www.bacon.moz");
+ Assert.equal(matchedEngines[0].name, "bacon");
+ Assert.equal(matchedEngines[1].searchForm, "http://www.baseball.moz");
+ Assert.equal(matchedEngines[1].name, "baseball");
+});
+
+add_task(async function test_aliased_search_engine_match() {
+ Assert.equal(null, await UrlbarSearchUtils.engineForAlias("sober"));
+ // Lower case
+ let matchedEngine = await UrlbarSearchUtils.engineForAlias("pork");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "bacon");
+ Assert.ok(matchedEngine.aliases.includes("pork"));
+ Assert.equal(matchedEngine.iconURI, null);
+ // Upper case
+ matchedEngine = await UrlbarSearchUtils.engineForAlias("PORK");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "bacon");
+ Assert.ok(matchedEngine.aliases.includes("pork"));
+ Assert.equal(matchedEngine.iconURI, null);
+ // Cap case
+ matchedEngine = await UrlbarSearchUtils.engineForAlias("Pork");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "bacon");
+ Assert.ok(matchedEngine.aliases.includes("pork"));
+ Assert.equal(matchedEngine.iconURI, null);
+});
+
+add_task(async function test_aliased_search_engine_match_upper_case_alias() {
+ let promiseTopic = promiseSearchTopic("engine-added");
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("patch")).length
+ );
+ await Promise.all([
+ Services.search.addEngineWithDetails("patch", {
+ alias: "PR",
+ description: "Search Patch",
+ method: "GET",
+ template: "http://www.patch.moz/?search={searchTerms}",
+ }),
+ promiseTopic,
+ ]);
+ // lower case
+ let matchedEngine = await UrlbarSearchUtils.engineForAlias("pr");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "patch");
+ Assert.ok(matchedEngine.aliases.includes("PR"));
+ Assert.equal(matchedEngine.iconURI, null);
+ // Upper case
+ matchedEngine = await UrlbarSearchUtils.engineForAlias("PR");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "patch");
+ Assert.ok(matchedEngine.aliases.includes("PR"));
+ Assert.equal(matchedEngine.iconURI, null);
+ // Cap case
+ matchedEngine = await UrlbarSearchUtils.engineForAlias("Pr");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "patch");
+ Assert.ok(matchedEngine.aliases.includes("PR"));
+ Assert.equal(matchedEngine.iconURI, null);
+});
+
+add_task(async function remove_search_engine_nomatch() {
+ let engine = Services.search.getEngineByName("bacon");
+ let promiseTopic = promiseSearchTopic("engine-removed");
+ await Promise.all([Services.search.removeEngine(engine), promiseTopic]);
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("bacon")).length
+ );
+});
+
+add_task(async function test_builtin_aliased_search_engine_match() {
+ let engine = await UrlbarSearchUtils.engineForAlias("@google");
+ Assert.ok(engine);
+ Assert.equal(engine.name, "Google");
+ let promiseTopic = promiseSearchTopic("engine-changed");
+ await Promise.all([Services.search.removeEngine(engine), promiseTopic]);
+ let matchedEngine = await UrlbarSearchUtils.engineForAlias("@google");
+ Assert.ok(!matchedEngine);
+ engine.hidden = false;
+ await TestUtils.waitForCondition(() =>
+ UrlbarSearchUtils.engineForAlias("@google")
+ );
+ engine = await UrlbarSearchUtils.engineForAlias("@google");
+ Assert.ok(engine);
+});
+
+add_task(async function test_serps_are_equivalent() {
+ info("Subset URL has extraneous parameters.");
+ let url1 = "https://example.com/search?q=test&type=images";
+ let url2 = "https://example.com/search?q=test";
+ Assert.ok(!UrlbarSearchUtils.serpsAreEquivalent(url1, url2));
+ info("Superset URL has extraneous parameters.");
+ Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url2, url1));
+
+ info("Same keys, different values.");
+ url1 = "https://example.com/search?q=test&type=images";
+ url2 = "https://example.com/search?q=test123&type=maps";
+ Assert.ok(!UrlbarSearchUtils.serpsAreEquivalent(url1, url2));
+ Assert.ok(!UrlbarSearchUtils.serpsAreEquivalent(url2, url1));
+
+ info("Subset matching isn't strict (URL is subset of itself).");
+ Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url1, url1));
+
+ info("Origin and pathname are ignored.");
+ url1 = "https://example.com/search?q=test";
+ url2 = "https://example-1.com/maps?q=test";
+ Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url1, url2));
+ Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url2, url1));
+
+ info("Params can be optionally ignored");
+ url1 = "https://example.com/search?q=test&abc=123&foo=bar";
+ url2 = "https://example.com/search?q=test";
+ Assert.ok(!UrlbarSearchUtils.serpsAreEquivalent(url1, url2));
+ Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url1, url2, ["abc", "foo"]));
+});
+
+add_task(async function test_get_root_domain_from_engine() {
+ let engine = await Services.search.addEngineWithDetails("TestEngine2", {
+ template: "http://example.com",
+ });
+ Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "example");
+ await Services.search.removeEngine(engine);
+
+ engine = await Services.search.addEngineWithDetails("TestEngine", {
+ template: "http://www.subdomain.othersubdomain.example.com",
+ });
+ Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "example");
+ await Services.search.removeEngine(engine);
+
+ // We let engines with URL ending in .test through even though its not a valid
+ // TLD.
+ engine = await Services.search.addEngineWithDetails("TestMalformed", {
+ template: `http://mochi.test/?search={searchTerms}`,
+ });
+ Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "mochi");
+ await Services.search.removeEngine(engine);
+
+ // We return the domain for engines with a malformed URL.
+ engine = await Services.search.addEngineWithDetails("TestMalformed", {
+ template: `http://subdomain.foobar/?search={searchTerms}`,
+ });
+ Assert.equal(
+ UrlbarSearchUtils.getRootDomainFromEngine(engine),
+ "subdomain.foobar"
+ );
+ await Services.search.removeEngine(engine);
+});
+
+function promiseSearchTopic(expectedVerb) {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observe(subject, topic, verb) {
+ info("browser-search-engine-modified: " + verb);
+ if (verb == expectedVerb) {
+ Services.obs.removeObserver(observe, "browser-search-engine-modified");
+ resolve();
+ }
+ }, "browser-search-engine-modified");
+ });
+}
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarUtils_addToUrlbarHistory.js b/browser/components/urlbar/tests/unit/test_UrlbarUtils_addToUrlbarHistory.js
new file mode 100644
index 0000000000..d2d30e1516
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_addToUrlbarHistory.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests unit test the functionality of the functions in UrlbarUtils.
+ * Some functions are bigger, and split out into sepearate test_UrlbarUtils_* files.
+ */
+
+"use strict";
+
+const { PrivateBrowsingUtils } = ChromeUtils.import(
+ "resource://gre/modules/PrivateBrowsingUtils.jsm"
+);
+const { PlacesUIUtils } = ChromeUtils.import(
+ "resource:///modules/PlacesUIUtils.jsm"
+);
+
+let sandbox;
+
+add_task(function setup() {
+ sandbox = sinon.createSandbox();
+});
+
+add_task(function test_addToUrlbarHistory() {
+ sandbox.stub(PlacesUIUtils, "markPageAsTyped");
+ sandbox.stub(PrivateBrowsingUtils, "isWindowPrivate").returns(false);
+
+ UrlbarUtils.addToUrlbarHistory("http://example.com");
+ Assert.ok(
+ PlacesUIUtils.markPageAsTyped.calledOnce,
+ "Should have marked a simple URL as typed."
+ );
+ PlacesUIUtils.markPageAsTyped.resetHistory();
+
+ UrlbarUtils.addToUrlbarHistory();
+ Assert.ok(
+ PlacesUIUtils.markPageAsTyped.notCalled,
+ "Should not have attempted to mark a null URL as typed."
+ );
+ PlacesUIUtils.markPageAsTyped.resetHistory();
+
+ UrlbarUtils.addToUrlbarHistory("http://exam ple.com");
+ Assert.ok(
+ PlacesUIUtils.markPageAsTyped.notCalled,
+ "Should not have marked a URL containing a space as typed."
+ );
+ PlacesUIUtils.markPageAsTyped.resetHistory();
+
+ UrlbarUtils.addToUrlbarHistory("http://exam\x01ple.com");
+ Assert.ok(
+ PlacesUIUtils.markPageAsTyped.notCalled,
+ "Should not have marked a URL containing a control character as typed."
+ );
+ PlacesUIUtils.markPageAsTyped.resetHistory();
+
+ PrivateBrowsingUtils.isWindowPrivate.returns(true);
+ UrlbarUtils.addToUrlbarHistory("http://example.com");
+ Assert.ok(
+ PlacesUIUtils.markPageAsTyped.notCalled,
+ "Should not have marked a URL provided by a private browsing page as typed."
+ );
+ PlacesUIUtils.markPageAsTyped.resetHistory();
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js
new file mode 100644
index 0000000000..c363b4b6ec
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js
@@ -0,0 +1,257 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests unit test the functionality of UrlbarController by stubbing out the
+ * model and providing stubs to be called.
+ */
+
+"use strict";
+
+function getPostDataString(aIS) {
+ if (!aIS) {
+ return null;
+ }
+
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ sis.init(aIS);
+ let dataLines = sis.read(aIS.available()).split("\n");
+
+ // only want the last line
+ return dataLines[dataLines.length - 1];
+}
+
+function keywordResult(aURL, aPostData, aIsUnsafe) {
+ this.url = aURL;
+ this.postData = aPostData;
+ this.isUnsafe = aIsUnsafe;
+}
+
+function keyWordData() {}
+keyWordData.prototype = {
+ init(aKeyWord, aURL, aPostData, aSearchWord) {
+ this.keyword = aKeyWord;
+ this.uri = Services.io.newURI(aURL);
+ this.postData = aPostData;
+ this.searchWord = aSearchWord;
+
+ this.method = this.postData ? "POST" : "GET";
+ },
+};
+
+function bmKeywordData(aKeyWord, aURL, aPostData, aSearchWord) {
+ this.init(aKeyWord, aURL, aPostData, aSearchWord);
+}
+bmKeywordData.prototype = new keyWordData();
+
+function searchKeywordData(aKeyWord, aURL, aPostData, aSearchWord) {
+ this.init(aKeyWord, aURL, aPostData, aSearchWord);
+}
+searchKeywordData.prototype = new keyWordData();
+
+var testData = [
+ [
+ new bmKeywordData("bmget", "http://bmget/search=%s", null, "foo"),
+ new keywordResult("http://bmget/search=foo", null),
+ ],
+
+ [
+ new bmKeywordData("bmpost", "http://bmpost/", "search=%s", "foo2"),
+ new keywordResult("http://bmpost/", "search=foo2"),
+ ],
+
+ [
+ new bmKeywordData(
+ "bmpostget",
+ "http://bmpostget/search1=%s",
+ "search2=%s",
+ "foo3"
+ ),
+ new keywordResult("http://bmpostget/search1=foo3", "search2=foo3"),
+ ],
+
+ [
+ new bmKeywordData("bmget-nosearch", "http://bmget-nosearch/", null, ""),
+ new keywordResult("http://bmget-nosearch/", null),
+ ],
+
+ [
+ new searchKeywordData(
+ "searchget",
+ "http://searchget/?search={searchTerms}",
+ null,
+ "foo4"
+ ),
+ new keywordResult("http://searchget/?search=foo4", null, true),
+ ],
+
+ [
+ new searchKeywordData(
+ "searchpost",
+ "http://searchpost/",
+ "search={searchTerms}",
+ "foo5"
+ ),
+ new keywordResult("http://searchpost/", "search=foo5", true),
+ ],
+
+ [
+ new searchKeywordData(
+ "searchpostget",
+ "http://searchpostget/?search1={searchTerms}",
+ "search2={searchTerms}",
+ "foo6"
+ ),
+ new keywordResult(
+ "http://searchpostget/?search1=foo6",
+ "search2=foo6",
+ true
+ ),
+ ],
+
+ // Bookmark keywords that don't take parameters should not be activated if a
+ // parameter is passed (bug 420328).
+ [
+ new bmKeywordData("bmget-noparam", "http://bmget-noparam/", null, "foo7"),
+ new keywordResult(null, null, true),
+ ],
+ [
+ new bmKeywordData(
+ "bmpost-noparam",
+ "http://bmpost-noparam/",
+ "not_a=param",
+ "foo8"
+ ),
+ new keywordResult(null, null, true),
+ ],
+
+ // Test escaping (%s = escaped, %S = raw)
+ // UTF-8 default
+ [
+ new bmKeywordData(
+ "bmget-escaping",
+ "http://bmget/?esc=%s&raw=%S",
+ null,
+ "fo\xE9"
+ ),
+ new keywordResult("http://bmget/?esc=fo%C3%A9&raw=fo\xE9", null),
+ ],
+ // Explicitly-defined ISO-8859-1
+ [
+ new bmKeywordData(
+ "bmget-escaping2",
+ "http://bmget/?esc=%s&raw=%S&mozcharset=ISO-8859-1",
+ null,
+ "fo\xE9"
+ ),
+ new keywordResult("http://bmget/?esc=fo%E9&raw=fo\xE9", null),
+ ],
+
+ // Bug 359809: Test escaping +, /, and @
+ // UTF-8 default
+ [
+ new bmKeywordData(
+ "bmget-escaping",
+ "http://bmget/?esc=%s&raw=%S",
+ null,
+ "+/@"
+ ),
+ new keywordResult("http://bmget/?esc=%2B%2F%40&raw=+/@", null),
+ ],
+ // Explicitly-defined ISO-8859-1
+ [
+ new bmKeywordData(
+ "bmget-escaping2",
+ "http://bmget/?esc=%s&raw=%S&mozcharset=ISO-8859-1",
+ null,
+ "+/@"
+ ),
+ new keywordResult("http://bmget/?esc=%2B%2F%40&raw=+/@", null),
+ ],
+
+ // Test using a non-bmKeywordData object, to test the behavior of
+ // getShortcutOrURIAndPostData for non-keywords (setupKeywords only adds keywords for
+ // bmKeywordData objects)
+ [{ keyword: "http://gavinsharp.com" }, new keywordResult(null, null, true)],
+];
+
+add_task(async function test_getshortcutoruri() {
+ await setupKeywords();
+
+ for (let item of testData) {
+ let [data, result] = item;
+
+ let query = data.keyword;
+ if (data.searchWord) {
+ query += " " + data.searchWord;
+ }
+ let returnedData = await UrlbarUtils.getShortcutOrURIAndPostData(query);
+ // null result.url means we should expect the same query we sent in
+ let expected = result.url || query;
+ Assert.equal(
+ returnedData.url,
+ expected,
+ "got correct URL for " + data.keyword
+ );
+ Assert.equal(
+ getPostDataString(returnedData.postData),
+ result.postData,
+ "got correct postData for " + data.keyword
+ );
+ Assert.equal(
+ returnedData.mayInheritPrincipal,
+ !result.isUnsafe,
+ "got correct mayInheritPrincipal for " + data.keyword
+ );
+ }
+
+ await cleanupKeywords();
+});
+
+var folder = null;
+var gAddedEngines = [];
+
+async function setupKeywords() {
+ folder = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "keyword-test",
+ });
+ for (let item of testData) {
+ let data = item[0];
+ if (data instanceof bmKeywordData) {
+ await PlacesUtils.bookmarks.insert({
+ url: data.uri,
+ parentGuid: folder.guid,
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: data.keyword,
+ url: data.uri.spec,
+ postData: data.postData,
+ });
+ }
+
+ if (data instanceof searchKeywordData) {
+ let addedEngine = await Services.search.addEngineWithDetails(
+ data.keyword,
+ {
+ alias: data.keyword,
+ method: data.method,
+ template: data.uri.spec,
+ searchPostParams: data.postData,
+ }
+ );
+ gAddedEngines.push(addedEngine);
+ }
+ }
+}
+
+async function cleanupKeywords() {
+ await PlacesUtils.bookmarks.remove(folder);
+ for (let engine of gAddedEngines) {
+ await Services.search.removeEngine(engine);
+ }
+ gAddedEngines = [];
+}
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarUtils_getTokenMatches.js b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getTokenMatches.js
new file mode 100644
index 0000000000..bae6ffc879
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getTokenMatches.js
@@ -0,0 +1,294 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests UrlbarUtils.getTokenMatches.
+ */
+
+"use strict";
+
+add_task(function test() {
+ const tests = [
+ {
+ tokens: ["mozilla", "is", "i"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [0, 7],
+ [8, 2],
+ ],
+ },
+ {
+ tokens: ["mozilla", "is", "i"],
+ phrase: "MOZILLA IS for the Open Web",
+ expected: [
+ [0, 7],
+ [8, 2],
+ ],
+ },
+ {
+ tokens: ["mozilla", "is", "i"],
+ phrase: "MoZiLlA Is for the Open Web",
+ expected: [
+ [0, 7],
+ [8, 2],
+ ],
+ },
+ {
+ tokens: ["MOZILLA", "IS", "I"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [0, 7],
+ [8, 2],
+ ],
+ },
+ {
+ tokens: ["MoZiLlA", "Is", "I"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [0, 7],
+ [8, 2],
+ ],
+ },
+ {
+ tokens: ["mo", "b"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [0, 2],
+ [26, 1],
+ ],
+ },
+ {
+ tokens: ["mo", "b"],
+ phrase: "MOZILLA is for the OPEN WEB",
+ expected: [
+ [0, 2],
+ [26, 1],
+ ],
+ },
+ {
+ tokens: ["MO", "B"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [0, 2],
+ [26, 1],
+ ],
+ },
+ {
+ tokens: ["mo", ""],
+ phrase: "mozilla is for the Open Web",
+ expected: [[0, 2]],
+ },
+ {
+ tokens: ["mozilla"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mozilla"],
+ phrase: "MOZILLA",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mozilla"],
+ phrase: "MoZiLlA",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mozilla"],
+ phrase: "mOzIlLa",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["MOZILLA"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["MoZiLlA"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mOzIlLa"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["\u9996"],
+ phrase: "Test \u9996\u9875 Test",
+ expected: [[5, 1]],
+ },
+ {
+ tokens: ["mo", "zilla"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mo", "zilla"],
+ phrase: "MOZILLA",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mo", "zilla"],
+ phrase: "MoZiLlA",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mo", "zilla"],
+ phrase: "mOzIlLa",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["MO", "ZILLA"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["Mo", "Zilla"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["moz", "zilla"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: [""], // Should never happen in practice.
+ phrase: "mozilla",
+ expected: [],
+ },
+ {
+ tokens: ["mo", "om"],
+ phrase: "mozilla mozzarella momo",
+ expected: [
+ [0, 2],
+ [8, 2],
+ [19, 4],
+ ],
+ },
+ {
+ tokens: ["mo", "om"],
+ phrase: "MOZILLA MOZZARELLA MOMO",
+ expected: [
+ [0, 2],
+ [8, 2],
+ [19, 4],
+ ],
+ },
+ {
+ tokens: ["MO", "OM"],
+ phrase: "mozilla mozzarella momo",
+ expected: [
+ [0, 2],
+ [8, 2],
+ [19, 4],
+ ],
+ },
+ {
+ tokens: ["resume"],
+ phrase: "résumé",
+ expected: [[0, 6]],
+ },
+ {
+ // This test should succeed even in a Spanish locale where N and Ñ are
+ // considered distinct letters.
+ tokens: ["jalapeno"],
+ phrase: "jalapeño",
+ expected: [[0, 8]],
+ },
+ ];
+ for (let { tokens, phrase, expected } of tests) {
+ tokens = tokens.map(t => ({
+ value: t,
+ lowerCaseValue: t.toLocaleLowerCase(),
+ }));
+ Assert.deepEqual(
+ UrlbarUtils.getTokenMatches(tokens, phrase, UrlbarUtils.HIGHLIGHT.TYPED),
+ expected,
+ `Match "${tokens.map(t => t.value).join(", ")}" on "${phrase}"`
+ );
+ }
+});
+
+/**
+ * Tests suggestion highlighting. Note that suggestions are only highlighted if
+ * the matching token is at the beginning of a word in the matched string.
+ */
+add_task(function testSuggestions() {
+ const tests = [
+ {
+ tokens: ["mozilla", "is", "i"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [7, 1],
+ [10, 17],
+ ],
+ },
+ {
+ tokens: ["\u9996"],
+ phrase: "Test \u9996\u9875 Test",
+ expected: [
+ [0, 5],
+ [6, 6],
+ ],
+ },
+ {
+ tokens: ["mo", "zilla"],
+ phrase: "mOzIlLa",
+ expected: [[2, 5]],
+ },
+ {
+ tokens: ["MO", "ZILLA"],
+ phrase: "mozilla",
+ expected: [[2, 5]],
+ },
+ {
+ tokens: [""], // Should never happen in practice.
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mo", "om", "la"],
+ phrase: "mozilla mozzarella momo",
+ expected: [
+ [2, 6],
+ [10, 9],
+ [21, 2],
+ ],
+ },
+ {
+ tokens: ["mo", "om", "la"],
+ phrase: "MOZILLA MOZZARELLA MOMO",
+ expected: [
+ [2, 6],
+ [10, 9],
+ [21, 2],
+ ],
+ },
+ {
+ tokens: ["MO", "OM", "LA"],
+ phrase: "mozilla mozzarella momo",
+ expected: [
+ [2, 6],
+ [10, 9],
+ [21, 2],
+ ],
+ },
+ ];
+ for (let { tokens, phrase, expected } of tests) {
+ tokens = tokens.map(t => ({
+ value: t,
+ lowerCaseValue: t.toLocaleLowerCase(),
+ }));
+ Assert.deepEqual(
+ UrlbarUtils.getTokenMatches(
+ tokens,
+ phrase,
+ UrlbarUtils.HIGHLIGHT.SUGGESTED
+ ),
+ expected,
+ `Match "${tokens.map(t => t.value).join(", ")}" on "${phrase}"`
+ );
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_about_urls.js b/browser/components/urlbar/tests/unit/test_autofill_about_urls.js
new file mode 100644
index 0000000000..4a15aa5e9b
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_about_urls.js
@@ -0,0 +1,100 @@
+/* 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";
+
+const ENGINE_NAME = "engine-suggestions.xml";
+
+testEngine_setup();
+
+// "about:ab" should match "about:about"
+add_task(async function aboutAb() {
+ let context = createContext("about:ab", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "about:about",
+ completed: "about:about",
+ matches: [
+ makeVisitResult(context, {
+ uri: "about:about",
+ title: "about:about",
+ heuristic: true,
+ }),
+ ],
+ });
+});
+
+// "about:Ab" should match "about:about"
+add_task(async function aboutAb() {
+ let context = createContext("about:Ab", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "about:About",
+ completed: "about:about",
+ matches: [
+ makeVisitResult(context, {
+ uri: "about:about",
+ title: "about:about",
+ heuristic: true,
+ }),
+ ],
+ });
+});
+
+// "about:about" should match "about:about"
+add_task(async function aboutAbout() {
+ let context = createContext("about:about", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "about:about",
+ completed: "about:about",
+ matches: [
+ makeVisitResult(context, {
+ uri: "about:about",
+ title: "about:about",
+ heuristic: true,
+ }),
+ ],
+ });
+});
+
+// "about:a" should complete to "about:about" and also match "about:addons"
+add_task(async function aboutAboutAndAboutAddons() {
+ let context = createContext("about:a", { isPrivate: false });
+ await check_results({
+ context,
+ search: "about:a",
+ autofilled: "about:about",
+ completed: "about:about",
+ matches: [
+ makeVisitResult(context, {
+ uri: "about:about",
+ title: "about:about",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "about:addons",
+ title: "about:addons",
+ iconUri: "",
+ providerName: "UnifiedComplete",
+ }),
+ ],
+ });
+});
+
+// "about:" should *not* match anything
+add_task(async function aboutColonHasNoMatch() {
+ let context = createContext("about:", { isPrivate: false });
+ await check_results({
+ context,
+ search: "about:",
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ ],
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_bookmarked.js b/browser/components/urlbar/tests/unit/test_autofill_bookmarked.js
new file mode 100644
index 0000000000..39483e993a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_bookmarked.js
@@ -0,0 +1,148 @@
+/* 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/. */
+
+// This is a specific autofill test to ensure we pick the correct bookmarked
+// state of an origin. Regardless of the order of origins, we should always pick
+// the correct bookmarked status.
+
+add_task(async function() {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+
+ let host = "example.com";
+ // Add a bookmark to the http version, but ensure the https version has an
+ // higher frecency.
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ url: `http://${host}`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits(`https://${host}`);
+ }
+ // ensure both fall below the threshold.
+ for (let i = 0; i < 15; i++) {
+ await PlacesTestUtils.addVisits(`https://not-${host}`);
+ }
+
+ async function check_autofill() {
+ let threshold = await getOriginAutofillThreshold();
+ let httpOriginFrecency = await getOriginFrecency("http://", host);
+ Assert.less(
+ httpOriginFrecency,
+ threshold,
+ "Http origin frecency should be below the threshold"
+ );
+ let httpsOriginFrecency = await getOriginFrecency("https://", host);
+ Assert.less(
+ httpsOriginFrecency,
+ threshold,
+ "Https origin frecency should be below the threshold"
+ );
+ Assert.less(
+ httpOriginFrecency,
+ httpsOriginFrecency,
+ "Http origin frecency should be below the https origin frecency"
+ );
+
+ // The http version should be filled because it's bookmarked, but with the
+ // https prefix that is more frecent.
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `${host}/`,
+ completed: `https://${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `https://${host}/`,
+ title: `https://${host}`,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: `https://not-${host}/`,
+ title: `test visit for https://not-${host}/`,
+ }),
+ ],
+ });
+ }
+
+ await check_autofill();
+
+ // Now remove the bookmark, ensure to remove the orphans, then reinsert the
+ // bookmark; thus we physically invert the order of the rows in the table.
+ await checkOriginsOrder(host, ["http://", "https://"]);
+ await PlacesUtils.bookmarks.remove(bookmark);
+ await PlacesUtils.withConnectionWrapper("removeOrphans", async db => {
+ db.execute(`DELETE FROM moz_places WHERE url = :url`, {
+ url: `http://${host}/`,
+ });
+ db.execute(
+ `DELETE FROM moz_origins WHERE prefix = "http://" AND host = :host`,
+ { host }
+ );
+ });
+ bookmark = await PlacesUtils.bookmarks.insert({
+ url: `http://${host}`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+
+ await checkOriginsOrder(host, ["https://", "http://"]);
+
+ await check_autofill();
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.remove(bookmark);
+});
+
+add_task(async function test_www() {
+ // Add a bookmark to the www version
+ let host = "example.com";
+ await PlacesUtils.bookmarks.insert({
+ url: `http://www.${host}`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+
+ info("search for start of www.");
+ let context = createContext("w", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `www.${host}/`,
+ completed: `http://www.${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://www.${host}/`,
+ title: `www.${host}`,
+ heuristic: true,
+ }),
+ ],
+ });
+ info("search for full www.");
+ context = createContext("www.", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `www.${host}/`,
+ completed: `http://www.${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://www.${host}/`,
+ title: `www.${host}`,
+ heuristic: true,
+ }),
+ ],
+ });
+ info("search for host without www.");
+ context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `${host}/`,
+ completed: `http://www.${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://www.${host}/`,
+ title: `www.${host}`,
+ heuristic: true,
+ }),
+ ],
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_functional.js b/browser/components/urlbar/tests/unit/test_autofill_functional.js
new file mode 100644
index 0000000000..662cf420b8
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_functional.js
@@ -0,0 +1,112 @@
+/* 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/. */
+
+// Functional tests for inline autocomplete
+
+add_task(async function setup() {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+});
+
+add_task(async function test_urls_order() {
+ info("Add urls, check for correct order");
+ let places = [
+ { uri: Services.io.newURI("http://visit1.mozilla.org") },
+ { uri: Services.io.newURI("http://visit2.mozilla.org") },
+ ];
+ await PlacesTestUtils.addVisits(places);
+ let context = createContext("vis", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "visit2.mozilla.org/",
+ completed: "http://visit2.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://visit2.mozilla.org/",
+ title: "visit2.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://visit1.mozilla.org/",
+ title: "test visit for http://visit1.mozilla.org/",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_bookmark_first() {
+ info("With a bookmark and history, the query result should be the bookmark");
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://bookmark1.mozilla.org/"),
+ });
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("http://bookmark1.mozilla.org/foo")
+ );
+ let context = createContext("bookmark", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "bookmark1.mozilla.org/",
+ completed: "http://bookmark1.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://bookmark1.mozilla.org/",
+ title: "bookmark1.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://bookmark1.mozilla.org/foo",
+ title: "test visit for http://bookmark1.mozilla.org/foo",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_complete_querystring() {
+ info("Check to make sure we autocomplete after ?");
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("http://smokey.mozilla.org/foo?bacon=delicious")
+ );
+ let context = createContext("smokey.mozilla.org/foo?", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "smokey.mozilla.org/foo?bacon=delicious",
+ completed: "http://smokey.mozilla.org/foo?bacon=delicious",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://smokey.mozilla.org/foo?bacon=delicious",
+ title: "smokey.mozilla.org/foo?bacon=delicious",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_complete_fragment() {
+ info("Check to make sure we autocomplete after #");
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("http://smokey.mozilla.org/foo?bacon=delicious#bar")
+ );
+ let context = createContext("smokey.mozilla.org/foo?bacon=delicious#bar", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: "smokey.mozilla.org/foo?bacon=delicious#bar",
+ completed: "http://smokey.mozilla.org/foo?bacon=delicious#bar",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://smokey.mozilla.org/foo?bacon=delicious#bar",
+ title: "smokey.mozilla.org/foo?bacon=delicious#bar",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_origins.js b/browser/components/urlbar/tests/unit/test_autofill_origins.js
new file mode 100644
index 0000000000..af9685286f
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_origins.js
@@ -0,0 +1,638 @@
+/* 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";
+
+const ENGINE_NAME = "engine-suggestions.xml";
+const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback";
+
+const origin = "example.com";
+
+async function cleanup() {
+ let suggestPrefs = ["history", "bookmark", "openpage"];
+ for (let type of suggestPrefs) {
+ Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
+ }
+ await cleanupPlaces();
+}
+
+testEngine_setup();
+
+// "example.com/" should match http://example.com/. i.e., the search string
+// should be treated as if it didn't have the trailing slash.
+add_task(async function trailingSlash() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ },
+ ]);
+
+ let context = createContext(`${origin}/`, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `${origin}/`,
+ completed: `http://${origin}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://${origin}/`,
+ title: `${origin}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "example.com/" should match http://www.example.com/. i.e., the search string
+// should be treated as if it didn't have the trailing slash.
+add_task(async function trailingSlashWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www.example.com/",
+ },
+ ]);
+ let context = createContext(`${origin}/`, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "http://www.example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://www.${origin}/`,
+ title: `www.${origin}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "ex" should match http://example.com:8888/, and the port should be completed.
+add_task(async function port() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/",
+ },
+ ]);
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com:8888/",
+ completed: "http://example.com:8888/",
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://${origin}:8888/`,
+ title: `${origin}:8888`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "example.com:8" should match http://example.com:8888/, and the port should
+// be completed.
+add_task(async function portPartial() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/",
+ },
+ ]);
+ let context = createContext(`${origin}:8`, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com:8888/",
+ completed: "http://example.com:8888/",
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://${origin}:8888/`,
+ title: `${origin}:8888`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "EXaM" should match http://example.com/ and the case of the search string
+// should be preserved in the autofilled value.
+add_task(async function preserveCase() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ },
+ ]);
+ let context = createContext("EXaM", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "EXaMple.com/",
+ completed: "http://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://${origin}/`,
+ title: `${origin}`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "EXaM" should match http://example.com:8888/, the port should be completed,
+// and the case of the search string should be preserved in the autofilled
+// value.
+add_task(async function preserveCasePort() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/",
+ },
+ ]);
+ let context = createContext("EXaM", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "EXaMple.com:8888/",
+ completed: "http://example.com:8888/",
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://${origin}:8888/`,
+ title: `${origin}:8888`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "example.com:89" should *not* match http://example.com:8888/.
+add_task(async function portNoMatch1() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/",
+ },
+ ]);
+ let context = createContext(`${origin}:89`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${origin}:89/`,
+ title: `http://${origin}:89/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "example.com:9" should *not* match http://example.com:8888/.
+add_task(async function portNoMatch2() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/",
+ },
+ ]);
+ let context = createContext(`${origin}:9`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${origin}:9/`,
+ title: `http://${origin}:9/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "example/" should *not* match http://example.com/.
+add_task(async function trailingSlash_2() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ },
+ ]);
+ let context = createContext("example/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://example/",
+ title: "http://example/",
+ iconUri: "page-icon:http://example/",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// multi.dotted.domain, search up to dot.
+add_task(async function multidotted() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www.example.co.jp:8888/",
+ },
+ ]);
+ let context = createContext("www.example.co.", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.example.co.jp:8888/",
+ completed: "http://www.example.co.jp:8888/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.example.co.jp:8888/",
+ title: "www.example.co.jp:8888",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+add_task(async function test_ip() {
+ // IP addresses have complicated rules around whether they show
+ // HeuristicFallback's backup search result. Flip this pref to disable that
+ // backup search and simplify ths subtest.
+ Services.prefs.setBoolPref("keyword.enabled", false);
+ for (let str of [
+ "192.168.1.1/",
+ "255.255.255.255:8080/",
+ "[2001:db8::1428:57ab]/",
+ "[::c0a8:5909]/",
+ "[::1]/",
+ ]) {
+ info("testing " + str);
+ await PlacesTestUtils.addVisits("http://" + str);
+ for (let i = 1; i < str.length; ++i) {
+ let context = createContext(str.substring(0, i), { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: str,
+ completed: "http://" + str,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + str,
+ title: str.replace(/\/$/, ""), // strip trailing slash
+ heuristic: true,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+ }
+ Services.prefs.clearUserPref("keyword.enabled");
+});
+
+// host starting with large number.
+add_task(async function large_number_host() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://12345example.it:8888/",
+ },
+ ]);
+ let context = createContext("1234", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "12345example.it:8888/",
+ completed: "http://12345example.it:8888/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://12345example.it:8888/",
+ title: "12345example.it:8888",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// When determining which origins should be autofilled, all the origins sharing
+// a host should be added together to get their combined frecency -- i.e.,
+// prefixes should be collapsed. And then from that list, the origin with the
+// highest frecency should be chosen.
+add_task(async function groupByHost() {
+ // Add some visits to the same host, example.com. Add one http and two https
+ // so that https has a higher frecency and is therefore the origin that should
+ // be autofilled. Also add another origin that has a higher frecency than
+ // both so that alone, neither http nor https would be autofilled, but added
+ // together they should be.
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example.com/" },
+
+ { uri: "https://example.com/" },
+ { uri: "https://example.com/" },
+
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ ]);
+
+ let httpFrec = frecencyForUrl("http://example.com/");
+ let httpsFrec = frecencyForUrl("https://example.com/");
+ let otherFrec = frecencyForUrl("https://mozilla.org/");
+ Assert.less(httpFrec, httpsFrec, "Sanity check");
+ Assert.less(httpsFrec, otherFrec, "Sanity check");
+
+ // Make sure the frecencies of the three origins are as expected in relation
+ // to the threshold.
+ let threshold = await getOriginAutofillThreshold();
+ Assert.less(httpFrec, threshold, "http origin should be < threshold");
+ Assert.less(httpsFrec, threshold, "https origin should be < threshold");
+ Assert.ok(threshold <= otherFrec, "Other origin should cross threshold");
+
+ Assert.ok(
+ threshold <= httpFrec + httpsFrec,
+ "http and https origin added together should cross threshold"
+ );
+
+ // The https origin should be autofilled.
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "https://example.com",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// This is the same as the previous (groupByHost), but it changes the standard
+// deviation multiplier by setting the corresponding pref. This makes sure that
+// the pref is respected.
+add_task(async function groupByHostNonDefaultStddevMultiplier() {
+ let stddevMultiplier = 1.5;
+ Services.prefs.setCharPref(
+ "browser.urlbar.autoFill.stddevMultiplier",
+ Number(stddevMultiplier).toFixed(1)
+ );
+
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example.com/" },
+ { uri: "http://example.com/" },
+
+ { uri: "https://example.com/" },
+ { uri: "https://example.com/" },
+ { uri: "https://example.com/" },
+
+ { uri: "https://foo.com/" },
+ { uri: "https://foo.com/" },
+ { uri: "https://foo.com/" },
+
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ ]);
+
+ let httpFrec = frecencyForUrl("http://example.com/");
+ let httpsFrec = frecencyForUrl("https://example.com/");
+ let otherFrec = frecencyForUrl("https://mozilla.org/");
+ Assert.less(httpFrec, httpsFrec, "Sanity check");
+ Assert.less(httpsFrec, otherFrec, "Sanity check");
+
+ // Make sure the frecencies of the three origins are as expected in relation
+ // to the threshold.
+ let threshold = await getOriginAutofillThreshold();
+ Assert.less(httpFrec, threshold, "http origin should be < threshold");
+ Assert.less(httpsFrec, threshold, "https origin should be < threshold");
+ Assert.ok(threshold <= otherFrec, "Other origin should cross threshold");
+
+ Assert.ok(
+ threshold <= httpFrec + httpsFrec,
+ "http and https origin added together should cross threshold"
+ );
+
+ // The https origin should be autofilled.
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "https://example.com",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.stddevMultiplier");
+
+ await cleanup();
+});
+
+// This is similar to suggestHistoryFalse_bookmark_0 in test_autofill_tasks.js,
+// but it adds unbookmarked visits for multiple URLs with the same origin.
+add_task(async function suggestHistoryFalse_bookmark_multiple() {
+ // Force only bookmarked pages to be suggested and therefore only bookmarked
+ // pages to be completed.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+
+ let search = "ex";
+ let baseURL = "http://example.com/";
+ let bookmarkedURL = baseURL + "bookmarked";
+
+ // Add visits for three different URLs all sharing the same origin, and then
+ // bookmark the second one. After that, the origin should be autofilled. The
+ // reason for adding unbookmarked visits before and after adding the
+ // bookmarked visit is to make sure our aggregate SQL query for determining
+ // whether an origin is bookmarked is correct.
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: baseURL + "other1",
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: bookmarkedURL,
+ },
+ ]);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: baseURL + "other2",
+ },
+ ]);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Now bookmark the second URL. It should be suggested and completed.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: bookmarkedURL,
+ });
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: baseURL,
+ matches: [
+ makeVisitResult(context, {
+ uri: baseURL,
+ title: "example.com",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: bookmarkedURL,
+ title: "A bookmark",
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// This is similar to suggestHistoryFalse_bookmark_prefix_0 in
+// autofill_tasks.js, but it adds unbookmarked visits for multiple URLs with the
+// same origin.
+add_task(async function suggestHistoryFalse_bookmark_prefix_multiple() {
+ // Force only bookmarked pages to be suggested and therefore only bookmarked
+ // pages to be completed.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+
+ let search = "http://ex";
+ let baseURL = "http://example.com/";
+ let bookmarkedURL = baseURL + "bookmarked";
+
+ // Add visits for three different URLs all sharing the same origin, and then
+ // bookmark the second one. After that, the origin should be autofilled. The
+ // reason for adding unbookmarked visits before and after adding the
+ // bookmarked visit is to make sure our aggregate SQL query for determining
+ // whether an origin is bookmarked is correct.
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: baseURL + "other1",
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${search}/`,
+ title: `${search}/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: bookmarkedURL,
+ },
+ ]);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${search}/`,
+ title: `${search}/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: baseURL + "other2",
+ },
+ ]);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${search}/`,
+ title: `${search}/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ // Now bookmark the second URL. It should be suggested and completed.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: bookmarkedURL,
+ });
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://example.com/",
+ completed: baseURL,
+ matches: [
+ makeVisitResult(context, {
+ uri: baseURL,
+ title: "example.com",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: bookmarkedURL,
+ title: "A bookmark",
+ }),
+ ],
+ });
+
+ await cleanup();
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js b/browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js
new file mode 100644
index 0000000000..5ad440596b
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js
@@ -0,0 +1,2408 @@
+/* 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/. */
+
+const ENGINE_NAME = "engine-suggestions.xml";
+const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback";
+const UNIFIEDCOMPLETE_PROVIDERNAME = "UnifiedComplete";
+
+/**
+ * Helpful reminder of the `autofilled` and `completed` properties in the
+ * object passed to check_results:
+ * autofilled: expected input.value after autofill
+ * completed: expected input.value after autofill and enter is pressed
+ *
+ * `completed` is the URL that the controller sets to input.value, and the URL
+ * that will ultimately be loaded when you press enter.
+ **/
+
+async function cleanup() {
+ let suggestPrefs = ["history", "bookmark", "openpage"];
+ for (let type of suggestPrefs) {
+ Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
+ }
+ await cleanupPlaces();
+}
+
+testEngine_setup();
+
+let path;
+let search;
+let searchCase;
+let title;
+let url;
+const host = "example.com";
+let origins;
+
+function add_autofill_task(callback) {
+ let func = async () => {
+ info(`Running subtest with origins disabled: ${callback.name}`);
+ origins = false;
+ path = "/foo";
+ search = "example.com/f";
+ searchCase = "EXAMPLE.COM/f";
+ title = "example.com/foo";
+ url = host + path;
+ await callback();
+
+ info(`Running subtest with origins enabled: ${callback.name}`);
+ origins = true;
+ path = "/";
+ search = "ex";
+ searchCase = "EX";
+ title = "example.com";
+ url = host + path;
+ await callback();
+ };
+ Object.defineProperty(func, "name", { value: callback.name });
+ add_task(func);
+}
+
+// "ex" should match http://example.com/.
+add_autofill_task(async function basic() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "EX" should match http://example.com/.
+add_autofill_task(async function basicCase() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext(searchCase, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: searchCase + url.substr(searchCase.length),
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "ex" should match http://www.example.com/.
+add_autofill_task(async function noWWWShouldMatchWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www." + url,
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www." + url,
+ title: "www." + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "EX" should match http://www.example.com/.
+add_autofill_task(async function noWWWShouldMatchWWWCase() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www." + url,
+ },
+ ]);
+ let context = createContext(searchCase, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: searchCase + url.substr(searchCase.length),
+ completed: "http://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www." + url,
+ title: "www." + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "www.ex" should *not* match http://example.com/.
+add_autofill_task(async function wwwShouldNotMatchNoWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext("www." + search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www." + search + "/",
+ title: "http://www." + search + "/",
+ displayUrl: "http://www." + search,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www." + search,
+ title: "http://www." + search,
+ iconUri: `page-icon:http://www.${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+});
+
+// "http://ex" should match http://example.com/.
+add_autofill_task(async function prefix() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "HTTP://EX" should match http://example.com/.
+add_autofill_task(async function prefixCase() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext("HTTP://" + searchCase, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "HTTP://" + searchCase + url.substr(searchCase.length),
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "http://ex" should match http://www.example.com/.
+add_autofill_task(async function prefixNoWWWShouldMatchWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www." + url,
+ },
+ ]);
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www." + url,
+ title: "www." + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "HTTP://EX" should match http://www.example.com/.
+add_autofill_task(async function prefixNoWWWShouldMatchWWWCase() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www." + url,
+ },
+ ]);
+ let context = createContext("HTTP://" + searchCase, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "HTTP://" + searchCase + url.substr(searchCase.length),
+ completed: "http://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www." + url,
+ title: "www." + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "http://www.ex" should *not* match http://example.com/.
+add_autofill_task(async function prefixWWWShouldNotMatchNoWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext("http://www." + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://www.${search}/` : `http://www.${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://www.${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "http://ex" should *not* match https://example.com/.
+add_autofill_task(async function httpPrefixShouldNotMatchHTTPS() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://" + url,
+ },
+ ]);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "ex" should match https://example.com/.
+add_autofill_task(async function httpsBasic() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://" + url,
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "https://" + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "ex" should match https://www.example.com/.
+add_autofill_task(async function httpsNoWWWShouldMatchWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://www." + url,
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www." + url,
+ title: "https://www." + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "www.ex" should *not* match https://example.com/.
+add_autofill_task(async function httpsWWWShouldNotMatchNoWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://" + url,
+ },
+ ]);
+ let context = createContext("www." + search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www." + search + "/",
+ title: "http://www." + search + "/",
+ displayUrl: "http://www." + search,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www." + search,
+ title: "http://www." + search,
+ iconUri: `page-icon:http://www.${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+});
+
+// "https://ex" should match https://example.com/.
+add_autofill_task(async function httpsPrefix() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://" + url,
+ },
+ ]);
+ let context = createContext("https://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "https://" + url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "https://" + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "https://ex" should match https://www.example.com/.
+add_autofill_task(async function httpsPrefixNoWWWShouldMatchWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://www." + url,
+ },
+ ]);
+ let context = createContext("https://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "https://" + url,
+ completed: "https://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www." + url,
+ title: "https://www." + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "https://www.ex" should *not* match https://example.com/.
+add_autofill_task(async function httpsPrefixWWWShouldNotMatchNoWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://" + url,
+ },
+ ]);
+ let context = createContext("https://www." + search, { isPrivate: false });
+ let prefixedUrl = origins
+ ? `https://www.${search}/`
+ : `https://www.${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:https://www.${host}/`,
+ providerame: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "https://ex" should *not* match http://example.com/.
+add_autofill_task(async function httpsPrefixShouldNotMatchHTTP() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext("https://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `https://${search}/` : `https://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:https://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "test visit for http://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "https://ex" should *not* match http://example.com/, even if the latter is
+// more frecent and both could be autofilled.
+add_autofill_task(async function httpsPrefixShouldNotMatchMoreFrecentHTTP() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ {
+ uri: "http://" + url,
+ },
+ {
+ uri: "https://" + url,
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ {
+ uri: "http://otherpage",
+ },
+ ]);
+ let context = createContext("https://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "https://" + url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "https://" + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Autofill should respond to frecency changes.
+add_autofill_task(async function frecency() {
+ // Start with an http visit. It should be completed.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Add two https visits. https should now be completed.
+ for (let i = 0; i < 2; i++) {
+ await PlacesTestUtils.addVisits([{ uri: "https://" + url }]);
+ }
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "https://" + title,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Add two more http visits, three total. http should now be completed
+ // again.
+ for (let i = 0; i < 2; i++) {
+ await PlacesTestUtils.addVisits([{ uri: "http://" + url }]);
+ }
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ // Add four www https visits. www https should now be completed.
+ for (let i = 0; i < 4; i++) {
+ await PlacesTestUtils.addVisits([{ uri: "https://www." + url }]);
+ }
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www." + url,
+ title: "https://www." + title,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ // Remove the www https page.
+ await PlacesUtils.history.remove(["https://www." + url]);
+
+ // http should now be completed again.
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ // Remove the http page.
+ await PlacesUtils.history.remove(["http://" + url]);
+
+ // https should now be completed again.
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "https://" + title,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Add a visit with a different host so that "ex" doesn't autofill it.
+ // https://example.com/ should still have a higher frecency though, so it
+ // should still be autofilled.
+ await PlacesTestUtils.addVisits([{ uri: "https://not-" + url }]);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "https://" + title,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://not-" + url,
+ title: "test visit for https://not-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ // Now add 10 more visits to the different host so that the frecency of
+ // https://example.com/ falls below the autofill threshold. It should not
+ // be autofilled now.
+ for (let i = 0; i < 10; i++) {
+ await PlacesTestUtils.addVisits([{ uri: "https://not-" + url }]);
+ }
+
+ // In the `origins` case, the failure to make an autofill
+ // match means UnifiedComplete should not create a heuristic result. In the
+ // `!origins` case, autofill should still happen since there's no threshold
+ // comparison.
+ context = createContext(search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "https://not-" + url,
+ title: "test visit for https://not-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "https://" + title,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://not-" + url,
+ title: "test visit for https://not-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+
+ // Remove the visits to the different host.
+ await PlacesUtils.history.remove(["https://not-" + url]);
+
+ // https should be completed again.
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "https://" + title,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Remove the https visits.
+ await PlacesUtils.history.remove(["https://" + url]);
+
+ // Now nothing should be completed.
+ context = createContext(search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://" + search,
+ title: "http://" + search,
+ iconUri: `page-icon:http://${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+
+ await cleanup();
+});
+
+// Bookmarked places should always be autofilled, even when they don't meet
+// the threshold.
+add_autofill_task(async function bookmarkBelowThreshold() {
+ // Add some visits to a URL so that the origin autofill threshold is large.
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://not-" + url,
+ },
+ ]);
+ }
+
+ // Now bookmark another URL.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+
+ // Make sure the bookmarked origin and place frecencies are below the
+ // threshold so that the origin/URL otherwise would not be autofilled.
+ let placeFrecency = await PlacesTestUtils.fieldInDB(
+ "http://" + url,
+ "frecency"
+ );
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ Assert.ok(
+ placeFrecency < threshold,
+ `Place frecency should be below the threshold: ` +
+ `placeFrecency=${placeFrecency} threshold=${threshold}`
+ );
+ Assert.ok(
+ originFrecency < threshold,
+ `Origin frecency should be below the threshold: ` +
+ `originFrecency=${originFrecency} threshold=${threshold}`
+ );
+
+ // The bookmark should be autofilled.
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://not-" + url,
+ title: "test visit for http://not-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// Bookmarked places should be autofilled when they *do* meet the threshold.
+add_autofill_task(async function bookmarkAboveThreshold() {
+ // Bookmark a URL.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+
+ // The frecencies of the place and origin should be >= the threshold. In
+ // fact they should be the same as the threshold since the place is the only
+ // place in the database.
+ let placeFrecency = await PlacesTestUtils.fieldInDB(
+ "http://" + url,
+ "frecency"
+ );
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ Assert.equal(placeFrecency, threshold);
+ Assert.equal(originFrecency, threshold);
+
+ // The bookmark should be autofilled.
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// Bookmark a page and then clear history. The bookmarked origin/URL should
+// be autofilled even though its frecency is <= 0 since the autofill threshold
+// is 0.
+add_autofill_task(async function zeroThreshold() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+
+ await PlacesUtils.history.clear();
+
+ // Make sure the place's frecency is <= 0. (It will be reset to -1 on the
+ // history.clear() above, and then on idle it will be reset to 0. xpcshell
+ // tests disable the idle service, so in practice it should always be -1,
+ // but in order to avoid possible intermittent failures in the future, don't
+ // assume that.)
+ let placeFrecency = await PlacesTestUtils.fieldInDB(
+ "http://" + url,
+ "frecency"
+ );
+ Assert.ok(placeFrecency <= 0);
+
+ // Make sure the origin's frecency is 0.
+ let originFrecency = await getOriginFrecency("http://", host);
+ Assert.equal(originFrecency, 0);
+
+ // Make sure the autofill threshold is 0.
+ let threshold = await getOriginAutofillThreshold();
+ Assert.equal(threshold, 0);
+
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: visit
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_visit() {
+ await PlacesTestUtils.addVisits("http://" + url);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ context = createContext(search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://" + search,
+ title: "http://" + search,
+ iconUri: `page-icon:http://${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: visit
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_visit_prefix() {
+ await PlacesTestUtils.addVisits("http://" + url);
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ context = createContext(search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://" + search,
+ title: "http://" + search,
+ iconUri: `page-icon:http://${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(async function suggestHistoryFalse_bookmark_0() {
+ // Add the bookmark.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+
+ // Make the bookmark fall below the autofill frecency threshold so we ensure
+ // the bookmark is always autofilled in this case, even if it doesn't meet
+ // the threshold.
+ let meetsThreshold = true;
+ while (meetsThreshold) {
+ // Add a visit to another origin to boost the threshold.
+ await PlacesTestUtils.addVisits("http://foo-" + url);
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ meetsThreshold = threshold <= originFrecency;
+ }
+
+ // At this point, the bookmark doesn't meet the threshold, but it should
+ // still be autofilled.
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ Assert.ok(originFrecency < threshold);
+
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_bookmark_1() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://non-matching-" + url,
+ });
+ let context = createContext(search, { isPrivate: false });
+ let matches = [
+ makeBookmarkResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "A bookmark",
+ }),
+ ];
+ if (origins) {
+ matches.unshift(
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ })
+ );
+ } else {
+ matches.unshift(
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://" + search,
+ title: "http://" + search,
+ iconUri: `page-icon:http://${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ })
+ );
+ }
+ await check_results({
+ context,
+ matches,
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(async function suggestHistoryFalse_bookmark_prefix_0() {
+ // Add the bookmark.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+
+ // Make the bookmark fall below the autofill frecency threshold so we ensure
+ // the bookmark is always autofilled in this case, even if it doesn't meet
+ // the threshold.
+ let meetsThreshold = true;
+ while (meetsThreshold) {
+ // Add a visit to another origin to boost the threshold.
+ await PlacesTestUtils.addVisits("http://foo-" + url);
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ meetsThreshold = threshold <= originFrecency;
+ }
+
+ // At this point, the bookmark doesn't meet the threshold, but it should
+ // still be autofilled.
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ Assert.ok(originFrecency < threshold);
+
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_bookmark_prefix_1() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://" + url,
+ });
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ uri: "ftp://" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_bookmark_prefix_2() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://non-matching-" + url,
+ });
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_bookmark_prefix_3() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://non-matching-" + url,
+ });
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ uri: "ftp://non-matching-" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(async function suggestBookmarkFalse_visit_0() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("http://" + url);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_visit_1() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("http://non-matching-" + url);
+ let context = createContext(search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ let matches = [
+ makeVisitResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "test visit for http://non-matching-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ];
+ if (origins) {
+ matches.unshift(
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ })
+ );
+ } else {
+ matches.unshift(
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ })
+ );
+ }
+ await check_results({
+ context,
+ matches,
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(async function suggestBookmarkFalse_visit_prefix_0() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("http://" + url);
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_visit_prefix_1() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("ftp://" + url);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://" + url,
+ title: "test visit for ftp://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_visit_prefix_2() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("http://non-matching-" + url);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "test visit for http://non-matching-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_visit_prefix_3() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("ftp://non-matching-" + url);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://non-matching-" + url,
+ title: "test visit for ftp://non-matching-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: unvisited bookmark
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_unvisitedBookmark() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext(search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://" + search,
+ title: "http://" + search,
+ iconUri: `page-icon:http://${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: unvisited bookmark
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_unvisitedBookmark_prefix_0() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: unvisited bookmark
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_unvisitedBookmark_prefix_1() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: unvisited bookmark
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_unvisitedBookmark_prefix_2() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://non-matching-" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: unvisited bookmark
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_unvisitedBookmark_prefix_3() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://non-matching-" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark above autofill threshold
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(async function suggestBookmarkFalse_visitedBookmark_above() {
+ await PlacesTestUtils.addVisits("http://" + url);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark above autofill threshold
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkAbove_prefix_0() {
+ await PlacesTestUtils.addVisits("http://" + url);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark above autofill threshold
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkAbove_prefix_1() {
+ await PlacesTestUtils.addVisits("ftp://" + url);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "ftp://" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark above autofill threshold
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkAbove_prefix_2() {
+ await PlacesTestUtils.addVisits("http://non-matching-" + url);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://non-matching-" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://non-matching-" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark above autofill threshold
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkAbove_prefix_3() {
+ await PlacesTestUtils.addVisits("ftp://non-matching-" + url);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://non-matching-" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "ftp://non-matching-" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// The following suggestBookmarkFalse_visitedBookmarkBelow* tests are similar
+// to the suggestBookmarkFalse_visitedBookmarkAbove* tests, but instead of
+// checking visited bookmarks above the autofill threshold, they check visited
+// bookmarks below the threshold. These tests don't make sense for URL
+// queries (as opposed to origin queries) because URL queries don't use the
+// same autofill threshold, so we skip them when !origins.
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark below autofill threshold
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_visitedBookmarkBelow() {
+ if (!origins) {
+ // See comment above suggestBookmarkFalse_visitedBookmarkBelow.
+ return;
+ }
+ // First, make sure that `url` is below the autofill threshold.
+ await PlacesTestUtils.addVisits("http://" + url);
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits("http://some-other-" + url);
+ }
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "test visit for http://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark below autofill threshold
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkBelow_prefix_0() {
+ if (!origins) {
+ // See comment above suggestBookmarkFalse_visitedBookmarkBelow.
+ return;
+ }
+ // First, make sure that `url` is below the autofill threshold.
+ await PlacesTestUtils.addVisits("http://" + url);
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits("http://some-other-" + url);
+ }
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ title: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "test visit for http://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ title: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark below autofill threshold
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkBelow_prefix_1() {
+ if (!origins) {
+ // See comment above suggestBookmarkFalse_visitedBookmarkBelow.
+ return;
+ }
+ // First, make sure that `url` is below the autofill threshold.
+ await PlacesTestUtils.addVisits("ftp://" + url);
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits("ftp://some-other-" + url);
+ }
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ title: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://some-other-" + url,
+ title: "test visit for ftp://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://" + url,
+ title: "test visit for ftp://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ title: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://some-other-" + url,
+ title: "test visit for ftp://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://" + url,
+ title: "A bookmark",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark below autofill threshold
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkBelow_prefix_2() {
+ if (!origins) {
+ // See comment above suggestBookmarkFalse_visitedBookmarkBelow.
+ return;
+ }
+ // First, make sure that `url` is below the autofill threshold.
+ await PlacesTestUtils.addVisits("http://non-matching-" + url);
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits("http://some-other-" + url);
+ }
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ title: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "test visit for http://non-matching-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://non-matching-" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ title: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "A bookmark",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark below autofill threshold
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkBelow_prefix_3() {
+ if (!origins) {
+ // See comment above suggestBookmarkFalse_visitedBookmarkBelow.
+ return;
+ }
+ // First, make sure that `url` is below the autofill threshold.
+ await PlacesTestUtils.addVisits("ftp://non-matching-" + url);
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits("ftp://some-other-" + url);
+ }
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ title: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://some-other-" + url,
+ title: "test visit for ftp://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://non-matching-" + url,
+ title: "test visit for ftp://non-matching-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://non-matching-" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ title: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://some-other-" + url,
+ title: "test visit for ftp://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://non-matching-" + url,
+ title: "A bookmark",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
diff --git a/browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js b/browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js
new file mode 100644
index 0000000000..ef3366d8db
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js
@@ -0,0 +1,74 @@
+/* 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/. */
+
+// This tests autofill prefix fallback in case multiple origins have the same
+// exact frecency.
+// We should prefer https, or in case of other prefixes just sort by descending
+// id.
+
+add_task(async function() {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+
+ let host = "example.com";
+ let prefixes = ["https://", "https://www.", "http://", "http://www."];
+ for (let prefix of prefixes) {
+ await PlacesUtils.bookmarks.insert({
+ url: `${prefix}${host}`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ }
+ await checkOriginsOrder(host, prefixes);
+
+ // The https://www version should be filled because it's https and the www
+ // version has been added later so it has an higher id.
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `${host}/`,
+ completed: `https://www.${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `https://www.${host}/`,
+ title: `https://www.${host}`,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: `https://${host}/`,
+ title: `${host}`,
+ }),
+ ],
+ });
+
+ // Remove and reinsert bookmarks in another order.
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ prefixes = ["https://www.", "http://", "https://", "http://www."];
+ for (let prefix of prefixes) {
+ await PlacesUtils.bookmarks.insert({
+ url: `${prefix}${host}`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ }
+ await checkOriginsOrder(host, prefixes);
+
+ await check_results({
+ context,
+ autofilled: `${host}/`,
+ completed: `https://${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `https://${host}/`,
+ title: `https://${host}`,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: `https://www.${host}/`,
+ title: `www.${host}`,
+ }),
+ ],
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js b/browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js
new file mode 100644
index 0000000000..9f87947770
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js
@@ -0,0 +1,90 @@
+/* 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/. */
+
+// Tests autofilling search engine token ("@") aliases.
+
+"use strict";
+
+const TEST_ENGINE_NAME = "test autofill aliases";
+const TEST_ENGINE_ALIAS = "@autofilltest";
+
+add_task(async function init() {
+ // Add an engine with an "@" alias.
+ await Services.search.addEngineWithDetails(TEST_ENGINE_NAME, {
+ alias: TEST_ENGINE_ALIAS,
+ template: "http://example.com/?search={searchTerms}",
+ });
+ registerCleanupFunction(async () => {
+ let engine = Services.search.getEngineByName(TEST_ENGINE_NAME);
+ Assert.ok(engine);
+ await Services.search.removeEngine(engine);
+ });
+});
+
+// Searching for @autofi should autofill to @autofilltest.
+add_task(async function basic() {
+ // Add a history visit that should normally match but for the fact that the
+ // search uses an @ alias. When an @ alias is autofilled, there should be no
+ // other matches except the autofill heuristic match.
+ await PlacesTestUtils.addVisits({
+ uri: "http://example.com/",
+ title: TEST_ENGINE_ALIAS,
+ });
+
+ let search = TEST_ENGINE_ALIAS.substr(
+ 0,
+ Math.round(TEST_ENGINE_ALIAS.length / 2)
+ );
+ let autofilledValue = TEST_ENGINE_ALIAS + " ";
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: autofilledValue,
+ matches: [
+ makeSearchResult(context, {
+ engineName: TEST_ENGINE_NAME,
+ alias: TEST_ENGINE_ALIAS,
+ query: "",
+ providesSearchMode: true,
+ heuristic: false,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// Searching for @AUTOFI should autofill to @AUTOFIlltest, preserving the case
+// in the search string.
+add_task(async function preserveCase() {
+ // Add a history visit that should normally match but for the fact that the
+ // search uses an @ alias. When an @ alias is autofilled, there should be no
+ // other matches except the autofill heuristic match.
+ await PlacesTestUtils.addVisits({
+ uri: "http://example.com/",
+ title: TEST_ENGINE_ALIAS,
+ });
+
+ let search = TEST_ENGINE_ALIAS.toUpperCase().substr(
+ 0,
+ Math.round(TEST_ENGINE_ALIAS.length / 2)
+ );
+ let alias = search + TEST_ENGINE_ALIAS.substr(search.length);
+
+ let autofilledValue = alias + " ";
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: autofilledValue,
+ matches: [
+ makeSearchResult(context, {
+ engineName: TEST_ENGINE_NAME,
+ alias,
+ query: "",
+ providesSearchMode: true,
+ heuristic: false,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_search_engines.js b/browser/components/urlbar/tests/unit/test_autofill_search_engines.js
new file mode 100644
index 0000000000..63af7115f2
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_search_engines.js
@@ -0,0 +1,234 @@
+/* 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/. */
+
+// The autoFill.searchEngines pref autofills the domains of engines registered
+// with the search service. That's what this test checks. It's a different
+// path in UnifiedComplete.js from normal moz_places autofill, which is tested
+// in test_autofill_origins.js and test_autofill_urls.js.
+
+"use strict";
+
+const ENGINE_NAME = "TestEngine";
+
+add_task(async function searchEngines() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref(
+ "browser.search.separatePrivateDefault.ui.enabled",
+ false
+ );
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref(
+ "browser.search.separatePrivateDefault.ui.enabled"
+ );
+ });
+
+ let schemes = ["http", "https"];
+ for (let i = 0; i < schemes.length; i++) {
+ let scheme = schemes[i];
+ let engine = await Services.search.addEngineWithDetails(ENGINE_NAME, {
+ method: "GET",
+ template: scheme + "://www.example.com/",
+ searchGetParams: "q={searchTerms}",
+ });
+
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("example.com", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("example.com/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("www.ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("www.example.com", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("www.example.com/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: scheme + "://example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://example.com", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: scheme + "://example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://example.com/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: scheme + "://example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://www.ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: scheme + "://www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://www.example.com", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: scheme + "://www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://www.example.com/", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ search: scheme + "://www.example.com/",
+ autofilled: scheme + "://www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // We should just get a normal heuristic result from HeuristicFallback for
+ // these queries.
+ let otherScheme = schemes[(i + 1) % schemes.length];
+ context = createContext(otherScheme + "://ex", { isPrivate: false });
+ await check_results({
+ context,
+ search: otherScheme + "://ex",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: otherScheme + "://ex/",
+ title: otherScheme + "://ex/",
+ heuristic: true,
+ }),
+ ],
+ });
+ context = createContext(otherScheme + "://www.ex", { isPrivate: false });
+ await check_results({
+ context,
+ search: otherScheme + "://www.ex",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: otherScheme + "://www.ex/",
+ title: otherScheme + "://www.ex/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("example/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://example/",
+ title: "http://example/",
+ iconUri: "page-icon:http://example/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await Services.search.removeEngine(engine);
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_urls.js b/browser/components/urlbar/tests/unit/test_autofill_urls.js
new file mode 100644
index 0000000000..d0424a4b8d
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_urls.js
@@ -0,0 +1,218 @@
+/* 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";
+
+const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback";
+const UNIFIEDCOMPLETE_PROVIDERNAME = "UnifiedComplete";
+
+// "example.com/foo/" should match http://example.com/foo/.
+testEngine_setup();
+
+add_task(async function multipleSlashes() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/foo/",
+ },
+ ]);
+ let context = createContext("example.com/foo/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/foo/",
+ completed: "http://example.com/foo/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com/foo/",
+ title: "example.com/foo/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// "example.com:8888/f" should match http://example.com:8888/foo.
+add_task(async function port() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/foo",
+ },
+ ]);
+ let context = createContext("example.com:8888/f", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com:8888/foo",
+ completed: "http://example.com:8888/foo",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com:8888/foo",
+ title: "example.com:8888/foo",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// "example.com:8999/f" should *not* autofill http://example.com:8888/foo.
+add_task(async function portNoMatch() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/foo",
+ },
+ ]);
+ let context = createContext("example.com:8999/f", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://example.com:8999/f",
+ title: "http://example.com:8999/f",
+ iconUri: "page-icon:http://example.com:8999/",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// autofill to the next slash
+add_task(async function port() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/foo/bar/baz",
+ },
+ ]);
+ let context = createContext("example.com:8888/foo/b", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com:8888/foo/bar/",
+ completed: "http://example.com:8888/foo/bar/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com:8888/foo/bar/",
+ title: "example.com:8888/foo/bar/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com:8888/foo/bar/baz",
+ title: "test visit for http://example.com:8888/foo/bar/baz",
+ tags: [],
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// autofill to the next slash, end of url
+add_task(async function port() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/foo/bar/baz",
+ },
+ ]);
+ let context = createContext("example.com:8888/foo/bar/b", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: "example.com:8888/foo/bar/baz",
+ completed: "http://example.com:8888/foo/bar/baz",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com:8888/foo/bar/baz",
+ title: "example.com:8888/foo/bar/baz",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// autofill with case insensitive from history and bookmark.
+add_task(async function caseInsensitiveFromHistoryAndBookmark() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/foo",
+ },
+ ]);
+
+ await testCaseInsensitive();
+
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ await cleanupPlaces();
+});
+
+// autofill with case insensitive from history.
+add_task(async function caseInsensitiveFromHistory() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/foo",
+ },
+ ]);
+
+ await testCaseInsensitive();
+
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ await cleanupPlaces();
+});
+
+// autofill with case insensitive from bookmark.
+add_task(async function caseInsensitiveFromBookmark() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://example.com/foo",
+ });
+
+ await testCaseInsensitive();
+
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ await cleanupPlaces();
+});
+
+async function testCaseInsensitive() {
+ const testData = [
+ {
+ input: "example.com/F",
+ expectedAutofill: "example.com/Foo",
+ },
+ {
+ // Test with prefix.
+ input: "http://example.com/F",
+ expectedAutofill: "http://example.com/Foo",
+ },
+ ];
+
+ for (const { input, expectedAutofill } of testData) {
+ const context = createContext(input, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: expectedAutofill,
+ completed: "http://example.com/foo",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com/foo",
+ title: "example.com/foo",
+ heuristic: true,
+ }),
+ ],
+ });
+ }
+}
diff --git a/browser/components/urlbar/tests/unit/test_avoid_middle_complete.js b/browser/components/urlbar/tests/unit/test_avoid_middle_complete.js
new file mode 100644
index 0000000000..a6de9f3a3a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_avoid_middle_complete.js
@@ -0,0 +1,284 @@
+/* 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/. */
+
+const ENGINE_NAME = "engine-suggestions.xml";
+
+testEngine_setup();
+
+add_task(async function test_prefix_space_noautofill() {
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://moz.org/test/"),
+ });
+
+ info("Should not try to autoFill if search string contains a space");
+ let context = createContext(" mo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ query: " mo",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://moz.org/test/",
+ title: "test visit for http://moz.org/test/",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_trailing_space_noautofill() {
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://moz.org/test/"),
+ });
+
+ info("Should not try to autoFill if search string contains a space");
+ let context = createContext("mo ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ query: "mo ",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://moz.org/test/",
+ title: "test visit for http://moz.org/test/",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_autofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ let engine = await Services.search.addEngineWithDetails("CakeSearch", {
+ method: "GET",
+ template: "http://cake.search/",
+ searchGetParams: "q={searchTerms}",
+ });
+ registerCleanupFunction(async () => Services.search.removeEngine(engine));
+
+ info(
+ "Should autoFill search engine if search string does not contains a space"
+ );
+ let context = createContext("ca", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: "CakeSearch",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_prefix_space_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ let engine = await Services.search.addEngineWithDetails("CupcakeSearch", {
+ method: "GET",
+ template: "http://cupcake.search/",
+ searchGetParams: "q={searchTerms}",
+ });
+ registerCleanupFunction(async () => Services.search.removeEngine(engine));
+
+ info(
+ "Should not try to autoFill search engine if search string contains a space"
+ );
+ let context = createContext(" cu", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ query: " cu",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_trailing_space_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ let engine = await Services.search.addEngineWithDetails("BaconSearch", {
+ method: "GET",
+ template: "http://bacon.search/",
+ searchGetParams: "q={searchTerms}",
+ });
+ registerCleanupFunction(async () => Services.search.removeEngine(engine));
+
+ info(
+ "Should not try to autoFill search engine if search string contains a space"
+ );
+ let context = createContext("ba ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ query: "ba ",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_www_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ let engine = await Services.search.addEngineWithDetails("HamSearch", {
+ method: "GET",
+ template: "http://ham.search/",
+ searchGetParams: "q={searchTerms}",
+ });
+ registerCleanupFunction(async () => Services.search.removeEngine(engine));
+
+ info(
+ "Should not autoFill search engine if search string contains www. but engine doesn't"
+ );
+ let context = createContext("www.ham", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www.ham/",
+ title: "http://www.ham/",
+ displayUrl: "http://www.ham",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ query: "www.ham",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_different_scheme_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ let engine = await Services.search.addEngineWithDetails("PieSearch", {
+ method: "GET",
+ template: "https://pie.search/",
+ searchGetParams: "q={searchTerms}",
+ });
+ registerCleanupFunction(async () => Services.search.removeEngine(engine));
+
+ info(
+ "Should not autoFill search engine if search string has a different scheme."
+ );
+ let context = createContext("http://pie", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://pie/",
+ title: "http://pie/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_matching_prefix_autofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ let engine = await Services.search.addEngineWithDetails("BeanSearch", {
+ method: "GET",
+ template: "http://www.bean.search/",
+ searchGetParams: "q={searchTerms}",
+ });
+ registerCleanupFunction(async () => Services.search.removeEngine(engine));
+
+ info("Should autoFill search engine if search string has matching prefix.");
+ let context = createContext("http://www.be", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://www.bean.search/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: "BeanSearch",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Should autoFill search engine if search string has www prefix.");
+ context = createContext("www.be", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.bean.search/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: "BeanSearch",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Should autoFill search engine if search string has matching scheme.");
+ context = createContext("http://be", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://bean.search/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: "BeanSearch",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_prefix_autofill() {
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://moz.org/test/"),
+ });
+
+ info(
+ "Should not try to autoFill in-the-middle if a search is canceled immediately"
+ );
+ let context = createContext("mozi", { isPrivate: false });
+ await check_results({
+ context,
+ incompleteSearch: "moz",
+ autofilled: "mozilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ title: "mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "test visit for http://mozilla.org/test/",
+ providerName: "UnifiedComplete",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js b/browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js
new file mode 100644
index 0000000000..b9e3227874
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js
@@ -0,0 +1,121 @@
+/* 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/. */
+
+const ENGINE_NAME = "engine-suggestions.xml";
+
+testEngine_setup();
+
+add_task(async function test_protocol_trimming() {
+ for (let prot of ["http", "https", "ftp"]) {
+ let visit = {
+ // Include the protocol in the query string to ensure we get matches (see bug 1059395)
+ uri: Services.io.newURI(
+ prot +
+ "://www.mozilla.org/test/?q=" +
+ prot +
+ encodeURIComponent("://") +
+ "www.foo"
+ ),
+ title: "Test title",
+ };
+ await PlacesTestUtils.addVisits(visit);
+
+ let input = prot + "://www.";
+ info("Searching for: " + input);
+ let context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: prot + "://www.mozilla.org/",
+ completed: prot + "://www.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: prot + "://www.mozilla.org/",
+ title:
+ prot == "http" ? "www.mozilla.org" : prot + "://www.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: visit.uri.spec,
+ title: visit.title,
+ }),
+ ],
+ });
+
+ input = "www.";
+ info("Searching for: " + input);
+ context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.mozilla.org/",
+ completed: prot + "://www.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: prot + "://www.mozilla.org/",
+ title:
+ prot == "http" ? "www.mozilla.org" : prot + "://www.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: visit.uri.spec,
+ title: visit.title,
+ }),
+ ],
+ });
+
+ input = prot + "://www. ";
+ info("Searching for: " + input);
+ context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${input.trim()}/`,
+ title: `${input.trim()}/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ makeVisitResult(context, {
+ uri: visit.uri.spec,
+ title: visit.title,
+ providerName: "UnifiedComplete",
+ }),
+ ],
+ });
+
+ let inputs = [
+ prot + "://",
+ prot + ":// ",
+ prot + ":// mo",
+ prot + "://mo te",
+ prot + "://www. mo",
+ prot + "://www.mo te",
+ "www. ",
+ "www. mo",
+ "www.mo te",
+ ];
+ for (input of inputs) {
+ info("Searching for: " + input);
+ context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ query: input,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: visit.uri.spec,
+ title: visit.title,
+ providerName: "UnifiedComplete",
+ }),
+ ],
+ });
+ }
+
+ await cleanupPlaces();
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_casing.js b/browser/components/urlbar/tests/unit/test_casing.js
new file mode 100644
index 0000000000..89e58c45a9
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_casing.js
@@ -0,0 +1,356 @@
+/* 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/. */
+
+const ENGINE_NAME = "engine-suggestions.xml";
+const AUTOFILL_PROVIDERNAME = "Autofill";
+const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback";
+const UNIFIEDCOMPLETE_PROVIDERNAME = "UnifiedComplete";
+
+testEngine_setup();
+
+add_task(async function test_casing_1() {
+ info("Searching for cased entry 1");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ let context = createContext("MOZ", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "MOZilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ title: "mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "test visit for http://mozilla.org/test/",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_casing_2() {
+ info("Searching for cased entry 2");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ let context = createContext("mozilla.org/T", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/Test/",
+ completed: "http://mozilla.org/test/",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://mozilla.org/test/",
+ title: "mozilla.org/test/",
+ iconUri: "page-icon:http://mozilla.org/test/",
+ heuristic: true,
+ providerName: AUTOFILL_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_casing_3() {
+ info("Searching for cased entry 3");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("mozilla.org/T", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/Test/",
+ completed: "http://mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/Test/",
+ title: "mozilla.org/Test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_casing_4() {
+ info("Searching for cased entry 4");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("mOzilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mOzilla.org/test/",
+ completed: "http://mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://mozilla.org/Test/",
+ title: "mozilla.org/Test/",
+ iconUri: "page-icon:http://mozilla.org/Test/",
+ heuristic: true,
+ providerName: AUTOFILL_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_casing_5() {
+ info("Searching for cased entry 5");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("mOzilla.org/T", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mOzilla.org/Test/",
+ completed: "http://mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/Test/",
+ title: "mozilla.org/Test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_casing() {
+ info("Searching for untrimmed cased entry");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("http://mOz", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://mOzilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ title: "mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/Test/",
+ title: "test visit for http://mozilla.org/Test/",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_www_casing() {
+ info("Searching for untrimmed cased entry with www");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://www.mozilla.org/Test/"),
+ });
+ let context = createContext("http://www.mOz", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://www.mOzilla.org/",
+ completed: "http://www.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/",
+ title: "www.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/Test/",
+ title: "test visit for http://www.mozilla.org/Test/",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_path_casing() {
+ info("Searching for untrimmed cased entry with path");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("http://mOzilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://mOzilla.org/test/",
+ completed: "http://mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://mozilla.org/Test/",
+ title: "mozilla.org/Test/",
+ iconUri: "page-icon:http://mozilla.org/Test/",
+ heuristic: true,
+ providerName: AUTOFILL_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_path_casing_2() {
+ info("Searching for untrimmed cased entry with path 2");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("http://mOzilla.org/T", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://mOzilla.org/Test/",
+ completed: "http://mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/Test/",
+ title: "mozilla.org/Test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_path_www_casing() {
+ info("Searching for untrimmed cased entry with www and path");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://www.mozilla.org/Test/"),
+ });
+ let context = createContext("http://www.mOzilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://www.mOzilla.org/test/",
+ completed: "http://www.mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://www.mozilla.org/Test/",
+ title: "www.mozilla.org/Test/",
+ iconUri: "page-icon:http://www.mozilla.org/Test/",
+ heuristic: true,
+ providerName: AUTOFILL_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_path_www_casing_2() {
+ info("Searching for untrimmed cased entry with www and path 2");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://www.mozilla.org/Test/"),
+ });
+ let context = createContext("http://www.mOzilla.org/T", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://www.mOzilla.org/Test/",
+ completed: "http://www.mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/Test/",
+ title: "www.mozilla.org/Test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_searching() {
+ let uri1 = Services.io.newURI("http://dummy/1/");
+ let uri2 = Services.io.newURI("http://dummy/2/");
+ let uri3 = Services.io.newURI("http://dummy/3/");
+ let uri4 = Services.io.newURI("http://dummy/4/");
+ let uri5 = Services.io.newURI("http://dummy/5/");
+
+ await PlacesTestUtils.addVisits([
+ { uri: uri1, title: "uppercase lambda \u039B" },
+ { uri: uri2, title: "lowercase lambda \u03BB" },
+ { uri: uri3, title: "symbol \u212A" }, // kelvin
+ { uri: uri4, title: "uppercase K" },
+ { uri: uri5, title: "lowercase k" },
+ ]);
+
+ info("Search for lowercase lambda");
+ let context = createContext("\u03BB", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "lowercase lambda \u03BB",
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "uppercase lambda \u039B",
+ }),
+ ],
+ });
+
+ info("Search for uppercase lambda");
+ context = createContext("\u039B", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "lowercase lambda \u03BB",
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "uppercase lambda \u039B",
+ }),
+ ],
+ });
+
+ info("Search for kelvin sign");
+ context = createContext("\u212A", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeVisitResult(context, { uri: uri5.spec, title: "lowercase k" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "uppercase K" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "symbol \u212A" }),
+ ],
+ });
+
+ info("Search for lowercase k");
+ context = createContext("k", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeVisitResult(context, { uri: uri5.spec, title: "lowercase k" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "uppercase K" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "symbol \u212A" }),
+ ],
+ });
+
+ info("Search for uppercase k");
+ context = createContext("K", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeVisitResult(context, { uri: uri5.spec, title: "lowercase k" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "uppercase K" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "symbol \u212A" }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_dedupe_prefix.js b/browser/components/urlbar/tests/unit/test_dedupe_prefix.js
new file mode 100644
index 0000000000..58f223fbc4
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_dedupe_prefix.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Testing that we dedupe results that have the same URL and title as another
+// except for their prefix (e.g. http://www.).
+add_task(async function dedupe_prefix() {
+ // We need to set the title or else we won't dedupe. We only dedupe when
+ // titles match up to mitigate deduping when the www. version of a site is
+ // completely different from it's www-less counterpart and thus presumably
+ // has a different title.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/foo/",
+ title: "Example Page",
+ },
+ {
+ uri: "http://www.example.com/foo/",
+ title: "Example Page",
+ },
+ {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ },
+ {
+ uri: "https://www.example.com/foo/",
+ title: "Example Page",
+ },
+ {
+ uri: "https://www.example.com/foo/",
+ title: "Example Page",
+ },
+ ]);
+
+ // We should get https://www. as the heuristic result but https:// in the
+ // results since the latter's prefix is a higher priority.
+ let context = createContext("example.com/foo/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/foo/",
+ completed: "https://www.example.com/foo/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.example.com/foo/",
+ title: "https://www.example.com/foo/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ }),
+ ],
+ });
+
+ // Add more visits to the lowest-priority prefix. It should be the heuristic
+ // result but we should still show our highest-priority result. https://www.
+ // should not appear at all.
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www.example.com/foo/",
+ title: "Example Page",
+ },
+ ]);
+ }
+
+ context = createContext("example.com/foo/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/foo/",
+ completed: "http://www.example.com/foo/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.example.com/foo/",
+ title: "www.example.com/foo/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ }),
+ ],
+ });
+
+ // Add enough https:// vists for it to have the highest frecency. It should
+ // be the heuristic result. We should still get the https://www. result
+ // because we still show results with the same key and protocol if they differ
+ // from the heuristic result in having www.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ },
+ ]);
+ }
+
+ context = createContext("example.com/foo/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/foo/",
+ completed: "https://example.com/foo/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/foo/",
+ title: "https://example.com/foo/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://www.example.com/foo/",
+ title: "Example Page",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_dupe_urls.js b/browser/components/urlbar/tests/unit/test_dupe_urls.js
new file mode 100644
index 0000000000..9707233279
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_dupe_urls.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Ensure inline autocomplete doesn't return zero frecency pages.
+
+add_task(async function setup() {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+});
+
+add_task(async function test_dupe_urls() {
+ info("Searching for urls with dupes should only show one");
+ await PlacesTestUtils.addVisits(
+ {
+ uri: Services.io.newURI("http://mozilla.org/"),
+ },
+ {
+ uri: Services.io.newURI("http://mozilla.org/?"),
+ }
+ );
+ let context = createContext("moz", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ title: "mozilla.org",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_dupe_secure_urls() {
+ await PlacesTestUtils.addVisits(
+ {
+ uri: Services.io.newURI("https://example.org/"),
+ },
+ {
+ uri: Services.io.newURI("https://example.org/?"),
+ }
+ );
+ let context = createContext("exam", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.org/",
+ completed: "https://example.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.org/",
+ title: "https://example.org",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_encoded_urls.js b/browser/components/urlbar/tests/unit/test_encoded_urls.js
new file mode 100644
index 0000000000..5260683ddd
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_encoded_urls.js
@@ -0,0 +1,97 @@
+add_task(async function test_encoded() {
+ info("Searching for over encoded url should not break it");
+ let url = "https://www.mozilla.com/search/top/?q=%25%32%35";
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI(url),
+ title: url,
+ });
+ let context = createContext(url, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: url,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: url,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_encoded_trimmed() {
+ info("Searching for over encoded url should not break it");
+ let url = "https://www.mozilla.com/search/top/?q=%25%32%35";
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI(url),
+ title: url,
+ });
+ let context = createContext("mozilla.com/search/top/?q=%25%32%35", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: "mozilla.com/search/top/?q=%25%32%35",
+ completed: url,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: url,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_encoded_partial() {
+ info("Searching for over encoded url should not break it");
+ let url = "https://www.mozilla.com/search/top/?q=%25%32%35";
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI(url),
+ title: url,
+ });
+ let context = createContext("https://www.mozilla.com/search/top/?q=%25", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: url,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: url,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_encoded_path() {
+ info("Searching for over encoded url should not break it");
+ let url = "https://www.mozilla.com/%25%32%35/top/";
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI(url),
+ title: url,
+ });
+ let context = createContext("https://www.mozilla.com/%25%32%35/t", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: url,
+ heuristic: true,
+ }),
+ ],
+ autofilled: url,
+ completed: url,
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_heuristic_cancel.js b/browser/components/urlbar/tests/unit/test_heuristic_cancel.js
new file mode 100644
index 0000000000..8806fe0601
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_heuristic_cancel.js
@@ -0,0 +1,136 @@
+/* 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/. */
+
+/**
+ * Tests that old results from UrlbarProviderAutofill do not overwrite results
+ * from UrlbarProviderHeuristicFallback after the autofillable query is
+ * cancelled. See bug 1653436.
+ */
+
+const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
+
+/**
+ * A test provider that waits before returning results to simulate a slow DB
+ * lookup.
+ */
+class SlowHeuristicProvider extends TestProvider {
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
+ }
+
+ async startQuery(context, add) {
+ this._context = context;
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 300));
+ for (let result of this._results) {
+ add(this, result);
+ }
+ }
+}
+
+/**
+ * A fast provider that alterts the test when it has added its results.
+ */
+class FastHeuristicProvider extends TestProvider {
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
+ }
+
+ async startQuery(context, add) {
+ this._context = context;
+ for (let result of this._results) {
+ add(this, result);
+ }
+ Services.obs.notifyObservers(null, "results-added");
+ }
+}
+
+add_task(async function setup() {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+});
+
+add_task(async function() {
+ let context = createContext("m", { isPrivate: false });
+ await PlacesTestUtils.promiseAsyncUpdates();
+ info("Manually set up query and then overwrite it.");
+ // slowProvider is a stand-in for a slow UnifiedComplete returning a
+ // non-heuristic result.
+ let slowProvider = new SlowHeuristicProvider({
+ results: [
+ makeVisitResult(context, {
+ uri: `http://mozilla.org/`,
+ title: `mozilla.org/`,
+ }),
+ ],
+ });
+ UrlbarProvidersManager.registerProvider(slowProvider);
+
+ // fastProvider is a stand-in for a fast Autofill returning a heuristic
+ // result.
+ let fastProvider = new FastHeuristicProvider({
+ results: [
+ makeVisitResult(context, {
+ uri: `http://mozilla.com/`,
+ title: `mozilla.com/`,
+ heuristic: true,
+ }),
+ ],
+ });
+ UrlbarProvidersManager.registerProvider(fastProvider);
+ let firstContext = createContext("m", {
+ providers: [slowProvider.name, fastProvider.name],
+ });
+ let secondContext = createContext("ma", {
+ providers: [slowProvider.name, fastProvider.name],
+ });
+
+ let controller = UrlbarTestUtils.newMockController();
+ let queryRecieved, queryCancelled;
+ const controllerListener = {
+ onQueryResults(queryContext) {
+ console.trace(`finished query. context: ${JSON.stringify(queryContext)}`);
+ Assert.equal(
+ queryContext,
+ secondContext,
+ "Only the second query should finish."
+ );
+ queryRecieved = true;
+ },
+ onQueryCancelled(queryContext) {
+ Assert.equal(
+ queryContext,
+ firstContext,
+ "The first query should be cancelled."
+ );
+ Assert.ok(!queryCancelled, "No more than one query should be cancelled.");
+ queryCancelled = true;
+ },
+ };
+ controller.addQueryListener(controllerListener);
+
+ // Wait until FastProvider sends its results to the providers manager.
+ // Then they will be queued up in a _heuristicProvidersTimer, waiting for
+ // the results from SlowProvider.
+ let resultsAddedPromise = new Promise(resolve => {
+ let observe = async (subject, topic, data) => {
+ Services.obs.removeObserver(observe, "results-added");
+ // Fire the second query to cancel the first.
+ await controller.startQuery(secondContext);
+ resolve();
+ };
+
+ Services.obs.addObserver(observe, "results-added");
+ });
+
+ controller.startQuery(firstContext);
+ await resultsAddedPromise;
+
+ Assert.ok(queryCancelled, "At least one query was cancelled.");
+ Assert.ok(queryRecieved, "At least one query finished.");
+ controller.removeQueryListener(controllerListener);
+});
diff --git a/browser/components/urlbar/tests/unit/test_keywords.js b/browser/components/urlbar/tests/unit/test_keywords.js
new file mode 100644
index 0000000000..04984662d3
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_keywords.js
@@ -0,0 +1,207 @@
+/* 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/. */
+
+const ENGINE_NAME = "engine-suggestions.xml";
+
+testEngine_setup();
+
+add_task(async function test_non_keyword() {
+ info("Searching for non-keyworded entry should autoFill it");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ let context = createContext("moz", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ title: "mozilla.org",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_keyword() {
+ info("Searching for keyworded entry should not autoFill it");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ keyword: "moz",
+ });
+ let context = createContext("moz", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://mozilla.org/test/",
+ keyword: "moz",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_more_than_keyword() {
+ info("Searching for more than keyworded entry should autoFill it");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ keyword: "moz",
+ });
+ let context = createContext("mozi", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ title: "mozilla.org",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_less_than_keyword() {
+ info("Searching for less than keyworded entry should autoFill it");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ keyword: "moz",
+ });
+ let context = createContext("mo", { isPrivate: false });
+ await check_results({
+ context,
+ search: "mo",
+ autofilled: "mozilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ title: "mozilla.org",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_keyword_casing() {
+ info("Searching for keyworded entry is case-insensitive");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ keyword: "moz",
+ });
+ let context = createContext("MoZ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://mozilla.org/test/",
+ keyword: "MoZ",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_less_then_equal_than_keyword_bug_1124238() {
+ info("Searching for less than keyworded entry should autoFill it");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addVisits("http://mozilla.com/");
+ PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.com/"),
+ keyword: "moz",
+ });
+
+ let context = createContext("mo", { isPrivate: false });
+ await check_results({
+ context,
+ search: "mo",
+ autofilled: "mozilla.com/",
+ completed: "http://mozilla.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.com/",
+ title: "mozilla.com",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "test visit for http://mozilla.org/test/",
+ }),
+ ],
+ });
+
+ // Search with an additional character. As the input matches a keyword, the
+ // completion should equal the keyword and not the URI as before.
+ context = createContext("moz", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://mozilla.com/",
+ keyword: "moz",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Search with an additional character. The input doesn't match a keyword
+ // anymore, it should be autofilled.
+ context = createContext("mozi", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.com/",
+ completed: "http://mozilla.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.com/",
+ title: "mozilla.com",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "test visit for http://mozilla.org/test/",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_muxer.js b/browser/components/urlbar/tests/unit/test_muxer.js
new file mode 100644
index 0000000000..b714ee50c1
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_muxer.js
@@ -0,0 +1,246 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_muxer() {
+ Assert.throws(
+ () => UrlbarProvidersManager.registerMuxer(),
+ /invalid muxer/,
+ "Should throw with no arguments"
+ );
+ Assert.throws(
+ () => UrlbarProvidersManager.registerMuxer({}),
+ /invalid muxer/,
+ "Should throw with empty object"
+ );
+ Assert.throws(
+ () =>
+ UrlbarProvidersManager.registerMuxer({
+ name: "",
+ }),
+ /invalid muxer/,
+ "Should throw with empty name"
+ );
+ Assert.throws(
+ () =>
+ UrlbarProvidersManager.registerMuxer({
+ name: "test",
+ sort: "no",
+ }),
+ /invalid muxer/,
+ "Should throw with invalid sort"
+ );
+
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/tab/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ { url: "http://mozilla.org/bookmark/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/history/" }
+ ),
+ ];
+
+ let providerName = registerBasicTestProvider(matches);
+ let context = createContext(undefined, { providers: [providerName] });
+ let controller = UrlbarTestUtils.newMockController();
+ /**
+ * A test muxer.
+ */
+ class TestMuxer extends UrlbarMuxer {
+ get name() {
+ return "TestMuxer";
+ }
+ sort(queryContext) {
+ queryContext.results.sort((a, b) => {
+ if (b.source == UrlbarUtils.RESULT_SOURCE.TABS) {
+ return -1;
+ }
+ if (b.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS) {
+ return 1;
+ }
+ return a.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS ? -1 : 1;
+ });
+ }
+ }
+ let muxer = new TestMuxer();
+
+ UrlbarProvidersManager.registerMuxer(muxer);
+ context.muxer = "TestMuxer";
+
+ info("Check results, the order should be: bookmark, history, tab");
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, [matches[1], matches[2], matches[0]]);
+
+ // Sanity check, should not throw.
+ UrlbarProvidersManager.unregisterMuxer(muxer);
+ UrlbarProvidersManager.unregisterMuxer("TestMuxer"); // no-op.
+});
+
+add_task(async function test_preselectedHeuristic_singleProvider() {
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/b" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/c" }
+ ),
+ ];
+ matches[1].heuristic = true;
+
+ let providerName = registerBasicTestProvider(matches);
+ let context = createContext(undefined, {
+ providers: [providerName],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Check results, the order should be: b (heuristic), a, c");
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, [matches[1], matches[0], matches[2]]);
+});
+
+add_task(async function test_preselectedHeuristic_multiProviders() {
+ let matches1 = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/b" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/c" }
+ ),
+ ];
+
+ let matches2 = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/d" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/e" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/f" }
+ ),
+ ];
+ matches2[1].heuristic = true;
+
+ let provider1Name = registerBasicTestProvider(matches1);
+ let provider2Name = registerBasicTestProvider(matches2);
+
+ let context = createContext(undefined, {
+ providers: [provider1Name, provider2Name],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Check results, the order should be: e (heuristic), a, b, c, d, f");
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, [
+ matches2[1],
+ ...matches1,
+ matches2[0],
+ matches2[2],
+ ]);
+});
+
+add_task(async function test_suggestions() {
+ Services.prefs.setIntPref("browser.urlbar.maxHistoricalSearchSuggestions", 1);
+
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/b" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ {
+ engine: "mozSearch",
+ query: "moz",
+ suggestion: "mozzarella",
+ lowerCaseSuggestion: "mozzarella",
+ }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "mozSearch",
+ query: "moz",
+ suggestion: "mozilla",
+ lowerCaseSuggestion: "mozilla",
+ }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "mozSearch",
+ query: "moz",
+ providesSearchMode: true,
+ keyword: "@moz",
+ }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/c" }
+ ),
+ ];
+
+ let providerName = registerBasicTestProvider(matches);
+
+ let context = createContext(undefined, {
+ providers: [providerName],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Check results, the order should be: mozzarella, moz, a, b, @moz, c");
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, [
+ matches[2],
+ matches[3],
+ matches[0],
+ matches[1],
+ matches[4],
+ matches[5],
+ ]);
+
+ Services.prefs.clearUserPref("browser.urlbar.maxHistoricalSearchSuggestions");
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js b/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js
new file mode 100644
index 0000000000..ce323a5027
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js
@@ -0,0 +1,613 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that visit-url and search engine heuristic results are returned by
+ * UrlbarProviderHeuristicFallback.
+ */
+
+const ENGINE_NAME = "engine-suggestions.xml";
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+const PRIVATE_SEARCH_PREF = "browser.search.separatePrivateDefault.ui.enabled";
+
+// We make sure that restriction tokens and search terms are correctly
+// recognized when they are separated by each of these different types of spaces
+// and combinations of spaces. U+3000 is the ideographic space in CJK and is
+// commonly used by CJK speakers.
+const TEST_SPACES = [" ", "\u3000", " \u3000", "\u3000 "];
+
+add_task(async function setup() {
+ // Install a test engine so we're sure of ENGINE_NAME.
+ let engine = await addTestSuggestionsEngine();
+
+ // Install the test engine.
+ let oldDefaultEngine = await Services.search.getDefault();
+ registerCleanupFunction(async () => {
+ Services.search.setDefault(oldDefaultEngine);
+ Services.prefs.clearUserPref(SUGGEST_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF);
+ Services.prefs.clearUserPref("keyword.enabled");
+ });
+ Services.search.setDefault(engine);
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+ Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false);
+});
+
+add_task(async function() {
+ info("visit url, no protocol");
+ let query = "mozilla.org";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ title: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ }),
+ ],
+ });
+
+ info("visit url, no protocol but with 2 dots");
+ query = "www.mozilla.org";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ title: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ }),
+ ],
+ });
+
+ info("visit url, no protocol, e-mail like");
+ query = "a@b.com";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ title: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ }),
+ ],
+ });
+
+ info("visit url, with protocol but with 2 dots");
+ query = "https://www.mozilla.org";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ title: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // info("visit url, with protocol but with 3 dots");
+ query = "https://www.mozilla.org.tw";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ title: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, with protocol");
+ query = "https://mozilla.org";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ title: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, about: protocol (no host)");
+ query = "about:nonexistent";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: query,
+ title: query,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, with non-standard whitespace");
+ query = "https://mozilla.org";
+ context = createContext(`${query}\u2028`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ title: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // This is distinct because of how we predict being able to url autofill via
+ // host lookups.
+ info("visit url, host matching visited host but not visited url");
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI("http://mozilla.org/wine/"),
+ title: "Mozilla Wine",
+ transition: PlacesUtils.history.TRANSITION_TYPED,
+ },
+ ]);
+ query = "mozilla.org/rum";
+ context = createContext(`${query}\u2028`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}`,
+ title: `http://${query}`,
+ iconUri: "page-icon:http://mozilla.org/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await PlacesUtils.history.clear();
+
+ // And hosts with no dot in them are special, due to requiring safelisting.
+ info("unknown host");
+ query = "firefox";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("string with known host");
+ query = "firefox/get";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ Services.prefs.setBoolPref("browser.fixup.domainwhitelist.firefox", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.fixup.domainwhitelist.firefox");
+ });
+
+ info("known host");
+ query = "firefox";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ title: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ }),
+ ],
+ });
+
+ info("url with known host");
+ query = "firefox/get";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}`,
+ title: `http://${query}`,
+ iconUri: "page-icon:http://firefox/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, host matching visited host but not visited url, known host");
+ Services.prefs.setBoolPref("browser.fixup.domainwhitelist.mozilla", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.fixup.domainwhitelist.mozilla");
+ });
+ query = "mozilla/rum";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}`,
+ title: `http://${query}`,
+ iconUri: "page-icon:http://mozilla/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // ipv4 and ipv6 literal addresses should offer to visit.
+ info("visit url, ipv4 literal");
+ query = "127.0.0.1";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ title: `http://${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, ipv6 literal");
+ query = "[2001:db8::1]";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ title: `http://${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Setting keyword.enabled to false should always try to visit.
+ let keywordEnabled = Services.prefs.getBoolPref("keyword.enabled");
+ Services.prefs.setBoolPref("keyword.enabled", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("keyword.enabled");
+ });
+ info("visit url, keyword.enabled = false");
+ query = "bacon";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ title: `http://${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit two word query, keyword.enabled = false");
+ query = "bacon lovers";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: query,
+ title: query,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Forced search through a restriction token, keyword.enabled = false");
+ query = "?bacon";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ query: "bacon",
+ }),
+ ],
+ });
+
+ Services.prefs.setBoolPref("keyword.enabled", true);
+ info("visit two word query, keyword.enabled = true");
+ query = "bacon lovers";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ Services.prefs.setBoolPref("keyword.enabled", keywordEnabled);
+
+ info("visit url, scheme+host");
+ query = "http://example";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ title: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, scheme+host");
+ query = "ftp://example";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ title: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, host+port");
+ query = "example:8080";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ title: `http://${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("numerical operations that look like urls should search");
+ query = "123/12";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("numerical operations that look like urls should search");
+ query = "123.12/12.1";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ query = "resource:///modules";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: query,
+ title: query,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("access resource://app/modules");
+ query = "resource://app/modules";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: query,
+ title: query,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("protocol with an extra slash");
+ query = "http:///";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("change default engine");
+ let originalTestEngine = Services.search.getEngineByName(ENGINE_NAME);
+ let engine2 = await Services.search.addEngineWithDetails("AliasEngine", {
+ alias: "alias",
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+ Assert.notEqual(
+ Services.search.defaultEngine,
+ engine2,
+ "New engine shouldn't be the current engine yet"
+ );
+ await Services.search.setDefault(engine2);
+ query = "toronto";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: "AliasEngine",
+ heuristic: true,
+ }),
+ ],
+ });
+ await Services.search.setDefault(originalTestEngine);
+
+ info(
+ "Leading search-mode restriction tokens are removed from the search result."
+ );
+ for (let token of UrlbarTokenizer.SEARCH_MODE_RESTRICT) {
+ for (let spaces of TEST_SPACES) {
+ query = token + spaces + "query";
+ info("Testing: " + JSON.stringify({ query, spaces: codePoints(spaces) }));
+ let expectedQuery = query.substring(1).trimStart();
+ context = createContext(query, { isPrivate: false });
+ info(`Searching for "${query}", expecting "${expectedQuery}"`);
+ let payload = {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ query: expectedQuery,
+ alias: token,
+ };
+ if (token == UrlbarTokenizer.RESTRICT.SEARCH) {
+ payload.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
+ payload.engineName = ENGINE_NAME;
+ }
+ await check_results({
+ context,
+ matches: [makeSearchResult(context, payload)],
+ });
+ }
+ }
+
+ info(
+ "Leading search-mode restriction tokens are removed from the search result with keyword.enabled = false."
+ );
+ Services.prefs.setBoolPref("keyword.enabled", false);
+ for (let token of UrlbarTokenizer.SEARCH_MODE_RESTRICT) {
+ for (let spaces of TEST_SPACES) {
+ query = token + spaces + "query";
+ info("Testing: " + JSON.stringify({ query, spaces: codePoints(spaces) }));
+ let expectedQuery = query.substring(1).trimStart();
+ context = createContext(query, { isPrivate: false });
+ info(`Searching for "${query}", expecting "${expectedQuery}"`);
+ let payload = {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ query: expectedQuery,
+ alias: token,
+ };
+ if (token == UrlbarTokenizer.RESTRICT.SEARCH) {
+ payload.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
+ payload.engineName = ENGINE_NAME;
+ }
+ await check_results({
+ context,
+ matches: [makeSearchResult(context, payload)],
+ });
+ }
+ }
+ Services.prefs.clearUserPref("keyword.enabled");
+
+ info(
+ "Leading non-search-mode restriction tokens are not removed from the search result."
+ );
+ for (let token of Object.values(UrlbarTokenizer.RESTRICT)) {
+ if (UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(token)) {
+ continue;
+ }
+ for (let spaces of TEST_SPACES) {
+ query = token + spaces + "query";
+ info("Testing: " + JSON.stringify({ query, spaces: codePoints(spaces) }));
+ let expectedQuery = query;
+ context = createContext(query, { isPrivate: false });
+ info(`Searching for "${query}", expecting "${expectedQuery}"`);
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ query: expectedQuery,
+ engineName: ENGINE_NAME,
+ }),
+ ],
+ });
+ }
+ }
+
+ await Services.search.removeEngine(engine2);
+});
+
+/**
+ * Returns an array of code points in the given string. Each code point is
+ * returned as a hexidecimal string.
+ *
+ * @param {string} str
+ * The code points of this string will be returned.
+ * @returns {array}
+ * Array of code points in the string, where each is a hexidecimal string.
+ */
+function codePoints(str) {
+ return str.split("").map(s => s.charCodeAt(0).toString(16));
+}
diff --git a/browser/components/urlbar/tests/unit/test_providerOmnibox.js b/browser/components/urlbar/tests/unit/test_providerOmnibox.js
new file mode 100644
index 0000000000..09fdfdec05
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerOmnibox.js
@@ -0,0 +1,818 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim:set ts=2 sw=2 sts=2 et:
+ * 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/. */
+
+const { ExtensionSearchHandler } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionSearchHandler.jsm"
+);
+
+let controller = Cc["@mozilla.org/autocomplete/controller;1"].getService(
+ Ci.nsIAutoCompleteController
+);
+
+const ENGINE_NAME = "engine-suggestions.xml";
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+
+async function cleanup() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
+
+add_task(function setup() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref(SUGGEST_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ });
+});
+
+add_task(async function test_correct_errors_are_thrown() {
+ let keyword = "foo";
+ let anotherKeyword = "bar";
+ let unregisteredKeyword = "baz";
+
+ // Register a keyword.
+ ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} });
+
+ // Try registering the keyword again.
+ Assert.throws(
+ () => ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} }),
+ /The keyword provided is already registered/
+ );
+
+ // Register a different keyword.
+ ExtensionSearchHandler.registerKeyword(anotherKeyword, { emit: () => {} });
+
+ // Try calling handleSearch for an unregistered keyword.
+ let searchData = {
+ keyword: unregisteredKeyword,
+ text: `${unregisteredKeyword} `,
+ };
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData, () => {}),
+ /The keyword provided is not registered/
+ );
+
+ // Try calling handleSearch without a callback.
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData),
+ /The keyword provided is not registered/
+ );
+
+ // Try getting the description for a keyword which isn't registered.
+ Assert.throws(
+ () => ExtensionSearchHandler.getDescription(unregisteredKeyword),
+ /The keyword provided is not registered/
+ );
+
+ // Try setting the default suggestion for a keyword which isn't registered.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.setDefaultSuggestion(
+ unregisteredKeyword,
+ "suggestion"
+ ),
+ /The keyword provided is not registered/
+ );
+
+ // Try calling handleInputCancelled when there is no active input session.
+ Assert.throws(
+ () => ExtensionSearchHandler.handleInputCancelled(),
+ /There is no active input session/
+ );
+
+ // Try calling handleInputEntered when there is no active input session.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(
+ anotherKeyword,
+ `${anotherKeyword} test`,
+ "tab"
+ ),
+ /There is no active input session/
+ );
+
+ // Start a session by calling handleSearch with the registered keyword.
+ searchData = {
+ keyword,
+ text: `${keyword} test`,
+ };
+ ExtensionSearchHandler.handleSearch(searchData, () => {});
+
+ // Try providing suggestions for an unregistered keyword.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 0, []),
+ /The keyword provided is not registered/
+ );
+
+ // Try providing suggestions for an inactive keyword.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(anotherKeyword, 0, []),
+ /The keyword provided is not apart of an active input session/
+ );
+
+ // Try calling handleSearch for an inactive keyword.
+ searchData = {
+ keyword: anotherKeyword,
+ text: `${anotherKeyword} `,
+ };
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData, () => {}),
+ /A different input session is already ongoing/
+ );
+
+ // Try calling addSuggestions with an old callback ID.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(keyword, 0, []),
+ /The callback is no longer active for the keyword provided/
+ );
+
+ // Add suggestions with a valid callback ID.
+ ExtensionSearchHandler.addSuggestions(keyword, 1, []);
+
+ // Add suggestions again with a valid callback ID.
+ ExtensionSearchHandler.addSuggestions(keyword, 1, []);
+
+ // Try calling addSuggestions with a future callback ID.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(keyword, 2, []),
+ /The callback is no longer active for the keyword provided/
+ );
+
+ // End the input session by calling handleInputCancelled.
+ ExtensionSearchHandler.handleInputCancelled();
+
+ // Try calling handleInputCancelled after the session has ended.
+ Assert.throws(
+ () => ExtensionSearchHandler.handleInputCancelled(),
+ /There is no active input sessio/
+ );
+
+ // Try calling handleSearch that doesn't have a space after the keyword.
+ searchData = {
+ keyword: anotherKeyword,
+ text: `${anotherKeyword}`,
+ };
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData, () => {}),
+ /The text provided must start with/
+ );
+
+ // Try calling handleSearch with text starting with the wrong keyword.
+ searchData = {
+ keyword: anotherKeyword,
+ text: `${keyword} test`,
+ };
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData, () => {}),
+ /The text provided must start with/
+ );
+
+ // Start a new session by calling handleSearch with a different keyword
+ searchData = {
+ keyword: anotherKeyword,
+ text: `${anotherKeyword} test`,
+ };
+ ExtensionSearchHandler.handleSearch(searchData, () => {});
+
+ // Try adding suggestions again with the same callback ID now that the input session has ended.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(keyword, 1, []),
+ /The keyword provided is not apart of an active input session/
+ );
+
+ // Add suggestions with a valid callback ID.
+ ExtensionSearchHandler.addSuggestions(anotherKeyword, 2, []);
+
+ // Try adding suggestions with a valid callback ID but a different keyword.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(keyword, 2, []),
+ /The keyword provided is not apart of an active input session/
+ );
+
+ // Try adding suggestions with a valid callback ID but an unregistered keyword.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 2, []),
+ /The keyword provided is not registered/
+ );
+
+ // Set the default suggestion.
+ ExtensionSearchHandler.setDefaultSuggestion(anotherKeyword, {
+ description: "test result",
+ });
+
+ // Try ending the session using handleInputEntered with a different keyword.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(
+ keyword,
+ `${keyword} test`,
+ "tab"
+ ),
+ /A different input session is already ongoing/
+ );
+
+ // Try calling handleInputEntered with invalid text.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(anotherKeyword, ` test`, "tab"),
+ /The text provided must start with/
+ );
+
+ // Try calling handleInputEntered with an invalid disposition.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(
+ anotherKeyword,
+ `${anotherKeyword} test`,
+ "invalid"
+ ),
+ /Invalid "where" argument/
+ );
+
+ // End the session by calling handleInputEntered.
+ ExtensionSearchHandler.handleInputEntered(
+ anotherKeyword,
+ `${anotherKeyword} test`,
+ "tab"
+ );
+
+ // Try calling handleInputEntered after the session has ended.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(
+ anotherKeyword,
+ `${anotherKeyword} test`,
+ "tab"
+ ),
+ /There is no active input session/
+ );
+
+ // Unregister the keyword.
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+
+ // Try setting the default suggestion for the unregistered keyword.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.setDefaultSuggestion(keyword, {
+ description: "test",
+ }),
+ /The keyword provided is not registered/
+ );
+
+ // Try handling a search with the unregistered keyword.
+ searchData = {
+ keyword,
+ text: `${keyword} test`,
+ };
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData, () => {}),
+ /The keyword provided is not registered/
+ );
+
+ // Try unregistering the keyword again.
+ Assert.throws(
+ () => ExtensionSearchHandler.unregisterKeyword(keyword),
+ /The keyword provided is not registered/
+ );
+
+ // Unregister the other keyword.
+ ExtensionSearchHandler.unregisterKeyword(anotherKeyword);
+
+ // Try unregistering the word which was never registered.
+ Assert.throws(
+ () => ExtensionSearchHandler.unregisterKeyword(unregisteredKeyword),
+ /The keyword provided is not registered/
+ );
+
+ // Try setting the default suggestion for a word that was never registered.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.setDefaultSuggestion(unregisteredKeyword, {
+ description: "test",
+ }),
+ /The keyword provided is not registered/
+ );
+
+ await cleanup();
+});
+
+add_task(async function test_extension_private_browsing() {
+ let events = [];
+ let mockExtension = {
+ emit: message => events.push(message),
+ privateBrowsingAllowed: false,
+ };
+
+ let keyword = "foo";
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ let searchData = {
+ keyword,
+ text: `${keyword} test`,
+ inPrivateWindow: true,
+ };
+ let result = await ExtensionSearchHandler.handleSearch(searchData);
+ Assert.equal(result, false, "unable to handle search for private window");
+
+ // Try calling handleInputEntered after the session has ended.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(
+ keyword,
+ `${keyword} test`,
+ "tab"
+ ),
+ /There is no active input session/
+ );
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ await cleanup();
+});
+
+add_task(async function test_correct_events_are_emitted() {
+ let events = [];
+ function checkEvents(expectedEvents) {
+ Assert.equal(
+ events.length,
+ expectedEvents.length,
+ "The correct number of events fired"
+ );
+ expectedEvents.forEach((e, i) =>
+ Assert.equal(e, events[i], `Expected "${e}" event to fire`)
+ );
+ events = [];
+ }
+
+ let mockExtension = { emit: message => events.push(message) };
+
+ let keyword = "foo";
+ let anotherKeyword = "bar";
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+ ExtensionSearchHandler.registerKeyword(anotherKeyword, mockExtension);
+
+ let searchData = {
+ keyword,
+ text: `${keyword} `,
+ };
+ ExtensionSearchHandler.handleSearch(searchData, () => {});
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_STARTED]);
+
+ searchData.text = `${keyword} f`;
+ ExtensionSearchHandler.handleSearch(searchData, () => {});
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_CHANGED]);
+
+ ExtensionSearchHandler.handleInputEntered(keyword, searchData.text, "tab");
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]);
+
+ ExtensionSearchHandler.handleSearch(searchData, () => {});
+ checkEvents([
+ ExtensionSearchHandler.MSG_INPUT_STARTED,
+ ExtensionSearchHandler.MSG_INPUT_CHANGED,
+ ]);
+
+ ExtensionSearchHandler.handleInputCancelled();
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_CANCELLED]);
+
+ ExtensionSearchHandler.handleSearch(
+ {
+ keyword: anotherKeyword,
+ text: `${anotherKeyword} baz`,
+ },
+ () => {}
+ );
+ checkEvents([
+ ExtensionSearchHandler.MSG_INPUT_STARTED,
+ ExtensionSearchHandler.MSG_INPUT_CHANGED,
+ ]);
+
+ ExtensionSearchHandler.handleInputEntered(
+ anotherKeyword,
+ `${anotherKeyword} baz`,
+ "tab"
+ );
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]);
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+});
+
+add_task(async function test_removes_suggestion_if_its_content_is_typed_in() {
+ let keyword = "test";
+ let extensionName = "Foo Bar";
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, [
+ { content: "foo", description: "first suggestion" },
+ { content: "bar", description: "second suggestion" },
+ { content: "baz", description: "third suggestion" },
+ ]);
+ // The API doesn't have a way to notify when addition is complete.
+ do_timeout(1000, () => {
+ controller.stopSearch();
+ });
+ }
+ },
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ let query = `${keyword} unmatched`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} unmatched`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} foo`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} bar`,
+ description: "second suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} baz`,
+ description: "third suggestion",
+ }),
+ ],
+ });
+
+ query = `${keyword} foo`;
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} foo`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} bar`,
+ description: "second suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} baz`,
+ description: "third suggestion",
+ }),
+ ],
+ });
+
+ query = `${keyword} bar`;
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} bar`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} foo`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} baz`,
+ description: "third suggestion",
+ }),
+ ],
+ });
+
+ query = `${keyword} baz`;
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} baz`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} foo`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} bar`,
+ description: "second suggestion",
+ }),
+ ],
+ });
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ await cleanup();
+});
+
+add_task(async function test_extension_results_should_come_first() {
+ let keyword = "test";
+ let extensionName = "Omnibox Example";
+
+ let uri = Services.io.newURI(`http://a.com/b`);
+ await PlacesTestUtils.addVisits([{ uri, title: `${keyword} -` }]);
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, [
+ { content: "foo", description: "first suggestion" },
+ { content: "bar", description: "second suggestion" },
+ { content: "baz", description: "third suggestion" },
+ ]);
+ }
+ },
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ // Start an input session before testing MSG_INPUT_CHANGED.
+ ExtensionSearchHandler.handleSearch(
+ { keyword, text: `${keyword} ` },
+ () => {}
+ );
+
+ let query = `${keyword} -`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} -`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} foo`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} bar`,
+ description: "second suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} baz`,
+ description: "third suggestion",
+ }),
+ makeVisitResult(context, {
+ uri: `http://a.com/b`,
+ title: `${keyword} -`,
+ }),
+ ],
+ });
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ await cleanup();
+});
+
+add_task(async function test_setting_the_default_suggestion() {
+ let keyword = "test";
+ let extensionName = "Omnibox Example";
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, []);
+ // The API doesn't have a way to notify when addition is complete.
+ do_timeout(1000, () => {
+ controller.stopSearch();
+ });
+ }
+ },
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ ExtensionSearchHandler.setDefaultSuggestion(keyword, {
+ description: "hello world",
+ });
+
+ let query = `${keyword} search query`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: "hello world",
+ content: query,
+ }),
+ ],
+ });
+
+ ExtensionSearchHandler.setDefaultSuggestion(keyword, {
+ description: "foo bar",
+ });
+
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ searchParam: "enable-actions",
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: "foo bar",
+ content: query,
+ }),
+ ],
+ });
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ await cleanup();
+});
+
+add_task(async function test_maximum_number_of_suggestions_is_enforced() {
+ let keyword = "test";
+ let extensionName = "Omnibox Example";
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, [
+ { content: "a", description: "first suggestion" },
+ { content: "b", description: "second suggestion" },
+ { content: "c", description: "third suggestion" },
+ { content: "d", description: "fourth suggestion" },
+ { content: "e", description: "fifth suggestion" },
+ { content: "f", description: "sixth suggestion" },
+ { content: "g", description: "seventh suggestion" },
+ { content: "h", description: "eigth suggestion" },
+ { content: "i", description: "ninth suggestion" },
+ { content: "j", description: "tenth suggestion" },
+ ]);
+ // The API doesn't have a way to notify when addition is complete.
+ do_timeout(1000, () => {
+ controller.stopSearch();
+ });
+ }
+ },
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ // Start an input session before testing MSG_INPUT_CHANGED.
+ ExtensionSearchHandler.handleSearch(
+ { keyword, text: `${keyword} ` },
+ () => {}
+ );
+
+ let query = `${keyword} #`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} #`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} a`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} b`,
+ description: "second suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} c`,
+ description: "third suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} d`,
+ description: "fourth suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} e`,
+ description: "fifth suggestion",
+ }),
+ ],
+ });
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ await cleanup();
+});
+
+add_task(async function conflicting_alias() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ let engine = await addTestSuggestionsEngine();
+ let keyword = "test";
+ engine.alias = keyword;
+ let oldDefaultEngine = await Services.search.getDefault();
+ Services.search.setDefault(engine);
+
+ let extensionName = "Omnibox Example";
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, [
+ { content: "foo", description: "first suggestion" },
+ { content: "bar", description: "second suggestion" },
+ { content: "baz", description: "third suggestion" },
+ ]);
+ // The API doesn't have a way to notify when addition is complete.
+ do_timeout(1000, () => {
+ controller.stopSearch();
+ });
+ }
+ },
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+ let query = `${keyword} unmatched`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} unmatched`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} foo`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} bar`,
+ description: "second suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} baz`,
+ description: "third suggestion",
+ }),
+ makeSearchResult(context, {
+ query: "unmatched",
+ engineName: ENGINE_NAME,
+ alias: keyword,
+ suggestion: "unmatched",
+ }),
+ makeSearchResult(context, {
+ query: "unmatched",
+ engineName: ENGINE_NAME,
+ alias: keyword,
+ suggestion: "unmatched foo",
+ }),
+ makeSearchResult(context, {
+ query: "unmatched",
+ engineName: ENGINE_NAME,
+ alias: keyword,
+ suggestion: "unmatched bar",
+ }),
+ ],
+ });
+
+ Services.search.setDefault(oldDefaultEngine);
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+ await cleanup();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerOpenTabs.js b/browser/components/urlbar/tests/unit/test_providerOpenTabs.js
new file mode 100644
index 0000000000..898bb6885e
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerOpenTabs.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_openTabs() {
+ const userContextId = 5;
+ const url = "http://foo.mozilla.org/";
+ UrlbarProviderOpenTabs.registerOpenTab(url, userContextId);
+ UrlbarProviderOpenTabs.registerOpenTab(url, userContextId);
+ Assert.equal(
+ UrlbarProviderOpenTabs.openTabs.get(userContextId).length,
+ 2,
+ "Found all the expected tabs"
+ );
+ UrlbarProviderOpenTabs.unregisterOpenTab(url, userContextId);
+ Assert.equal(
+ UrlbarProviderOpenTabs.openTabs.get(userContextId).length,
+ 1,
+ "Found all the expected tabs"
+ );
+
+ let context = createContext();
+ let matchCount = 0;
+ let callback = function(provider, match) {
+ matchCount++;
+ Assert.ok(
+ provider instanceof UrlbarProviderOpenTabs,
+ "Got the expected provider"
+ );
+ Assert.equal(
+ match.type,
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ "Got the expected result type"
+ );
+ Assert.equal(match.payload.url, url, "Got the expected url");
+ Assert.equal(match.payload.title, undefined, "Got the expected title");
+ };
+
+ let provider = new UrlbarProviderOpenTabs();
+ await provider.startQuery(context, callback);
+ Assert.equal(matchCount, 1, "Found the expected number of matches");
+ // Sanity check that this doesn't throw.
+ provider.cancelQuery(context);
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerTabToSearch.js b/browser/components/urlbar/tests/unit/test_providerTabToSearch.js
new file mode 100644
index 0000000000..ca08493579
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerTabToSearch.js
@@ -0,0 +1,477 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests UrlbarProviderTabToSearch. See also
+ * browser/components/urlbar/tests/browser/browser_tabToSearch.js
+ */
+
+"use strict";
+
+let testEngine;
+
+add_task(async function init() {
+ // Disable search suggestions for a less verbose test.
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+ // Disable tab-to-search onboarding results. Those are covered in
+ // browser/components/urlbar/tests/browser/browser_tabToSearch.js.
+ Services.prefs.setIntPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft",
+ 0
+ );
+ testEngine = await Services.search.addEngineWithDetails("Test", {
+ template: "https://example.com/?search={searchTerms}",
+ });
+
+ registerCleanupFunction(async () => {
+ await Services.search.removeEngine(testEngine);
+ Services.prefs.clearUserPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft"
+ );
+ Services.prefs.clearUserPref("browser.search.suggest.enabled");
+ });
+});
+
+// Tests that tab-to-search results appear when the engine's result domain is
+// autofilled.
+add_task(async function basic() {
+ await PlacesTestUtils.addVisits(["https://example.com/"]);
+ let context = createContext("examp", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "https://example.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ testEngine.getResultDomain()
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+
+ info("Repeat the search but with tab-to-search disabled through pref.");
+ Services.prefs.setBoolPref("browser.urlbar.suggest.engines", false);
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "https://example.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ ],
+ });
+ Services.prefs.clearUserPref("browser.urlbar.suggest.engines");
+
+ await cleanupPlaces();
+});
+
+// Tests that tab-to-search results aren't shown when the typed string matches
+// an engine domain but there is no autofill.
+add_task(async function noAutofill() {
+ // Note we are not adding any history visits.
+ let context = createContext("examp", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: Services.search.defaultEngine.name,
+ engineIconUri: Services.search.defaultEngine.iconURI?.spec,
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ ],
+ });
+});
+
+// Tests that tab-to-search results are not shown when the typed string matches
+// an engine domain, but something else is being autofilled.
+add_task(async function autofillDoesNotMatchEngine() {
+ await PlacesTestUtils.addVisits(["https://example.test.ca/"]);
+ let context = createContext("example", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.test.ca/",
+ completed: "https://example.test.ca/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.test.ca/",
+ title: "https://example.test.ca",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+// Tests that www. is ignored for the purposes of matching autofill to
+// tab-to-search.
+add_task(async function ignoreWww() {
+ // The history result has www., the engine does not.
+ await PlacesTestUtils.addVisits(["https://www.example.com/"]);
+ let context = createContext("www.examp", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.example.com/",
+ completed: "https://www.example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.example.com/",
+ title: "https://www.example.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ testEngine.getResultDomain()
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+
+ // The engine has www., the history result does not.
+ await PlacesTestUtils.addVisits(["https://foo.bar/"]);
+ let wwwTestEngine = await Services.search.addEngineWithDetails("TestWww", {
+ template: "https://www.foo.bar/?search={searchTerms}",
+ });
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "foo.bar/",
+ completed: "https://foo.bar/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://foo.bar/",
+ title: "https://foo.bar",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: wwwTestEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ wwwTestEngine.getResultDomain()
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+
+ // Both the engine and the history result have www.
+ await PlacesTestUtils.addVisits(["https://www.foo.bar/"]);
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "foo.bar/",
+ completed: "https://www.foo.bar/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.foo.bar/",
+ title: "https://www.foo.bar",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: wwwTestEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ wwwTestEngine.getResultDomain()
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+
+ await Services.search.removeEngine(wwwTestEngine);
+});
+
+// Tests that when a user's query causes autofill to replace one engine's domain
+// with another, the correct tab-to-search results are shown.
+add_task(async function conflictingEngines() {
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([
+ "https://foobar.com/",
+ "https://foo.com/",
+ ]);
+ }
+ let fooBarTestEngine = await Services.search.addEngineWithDetails(
+ "TestFooBar",
+ { template: "https://foobar.com/?search={searchTerms}" }
+ );
+ let fooTestEngine = await Services.search.addEngineWithDetails("TestFoo", {
+ template: "https://foo.com/?search={searchTerms}",
+ });
+
+ // Search for "foo", autofilling foo.com. Observe that the foo.com
+ // tab-to-search result is shown, even though the foobar.com engine was added
+ // first (and thus enginesForDomainPrefix puts it earlier in its returned
+ // array.)
+ let context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "foo.com/",
+ completed: "https://foo.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://foo.com/",
+ title: "https://foo.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: fooTestEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ fooTestEngine.getResultDomain()
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ makeVisitResult(context, {
+ uri: "https://foobar.com/",
+ title: "test visit for https://foobar.com/",
+ providerName: "UnifiedComplete",
+ }),
+ ],
+ });
+
+ // Search for "foob", autofilling foobar.com. Observe that the foo.com
+ // tab-to-search result is replaced with the foobar.com tab-to-search result.
+ context = createContext("foob", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "foobar.com/",
+ completed: "https://foobar.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://foobar.com/",
+ title: "https://foobar.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: fooBarTestEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ fooBarTestEngine.getResultDomain()
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+ await Services.search.removeEngine(fooTestEngine);
+ await Services.search.removeEngine(fooBarTestEngine);
+});
+
+add_task(async function multipleEnginesForHostname() {
+ info(
+ "In case of multiple engines only one tab-to-search result should be returned"
+ );
+ let mapsEngine = await Services.search.addEngineWithDetails("TestMaps", {
+ template: "https://example.com/maps/?search={searchTerms}",
+ });
+ await PlacesTestUtils.addVisits(["https://example.com/"]);
+ let context = createContext("examp", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "https://example.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ testEngine.getResultDomain()
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+ await Services.search.removeEngine(mapsEngine);
+});
+
+add_task(async function test_casing() {
+ info("Tab-to-search results appear also in case of different casing.");
+ await PlacesTestUtils.addVisits(["https://example.com/"]);
+ let context = createContext("eXAm", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "eXAmple.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "https://example.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ testEngine.getResultDomain()
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_publicSuffix() {
+ info("Tab-to-search results appear also in case of partial host match.");
+ let engine = await Services.search.addEngineWithDetails("MyTest", {
+ template: "https://test.mytest.it/?search={searchTerms}",
+ });
+ await PlacesTestUtils.addVisits(["https://test.mytest.it/"]);
+ let context = createContext("my", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: Services.search.defaultEngine.name,
+ engineIconUri: Services.search.defaultEngine.iconURI?.spec,
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ makeSearchResult(context, {
+ engineName: engine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(engine.getResultDomain()),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ satisfiesAutofillThreshold: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://test.mytest.it/",
+ title: "test visit for https://test.mytest.it/",
+ providerName: "UnifiedComplete",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+ await Services.search.removeEngine(engine);
+});
+
+add_task(async function test_publicSuffixIsHost() {
+ info("Tab-to-search results does not appear in case we autofill a suffix.");
+ let suffixEngine = await Services.search.addEngineWithDetails("SuffixTest", {
+ template: "https://somesuffix.com.mx/?search={searchTerms}",
+ });
+ // The top level domain will be autofilled, not the full domain.
+ await PlacesTestUtils.addVisits(["https://com.mx/"]);
+ let context = createContext("co", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "com.mx/",
+ completed: "https://com.mx/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://com.mx/",
+ title: "https://com.mx",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+ await Services.search.removeEngine(suffixEngine);
+});
+
+add_task(async function test_disabledEngine() {
+ info("Tab-to-search results does not appear for a Pref-disabled engine.");
+ let engine = await Services.search.addEngineWithDetails("Disabled", {
+ template: "https://disabled.com/?search={searchTerms}",
+ });
+ await PlacesTestUtils.addVisits(["https://disabled.com/"]);
+ let context = createContext("dis", { isPrivate: false });
+
+ info("Sanity check that the engine would appear.");
+ await check_results({
+ context,
+ autofilled: "disabled.com/",
+ completed: "https://disabled.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://disabled.com/",
+ title: "https://disabled.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: engine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(engine.getResultDomain()),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+
+ info("Now disable the engine.");
+ Services.prefs.setCharPref("browser.search.hiddenOneOffs", engine.name);
+ await check_results({
+ context,
+ autofilled: "disabled.com/",
+ completed: "https://disabled.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://disabled.com/",
+ title: "https://disabled.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ ],
+ });
+ Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
+
+ await cleanupPlaces();
+ await Services.search.removeEngine(engine);
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js b/browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js
new file mode 100644
index 0000000000..4644117b07
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Search engine origins are autofilled normally when they get over the
+// threshold, though certain origins redirect to localized subdomains, that
+// the user is unlikely to type, for example wikipedia.org => en.wikipedia.org.
+// We should get a tab to search result also for these cases, where a normal
+// autofill wouldn't happen.
+
+"use strict";
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ // Disable tab-to-search onboarding results.
+ Services.prefs.setIntPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft",
+ 0
+ );
+ Services.prefs.setBoolPref(
+ "browser.search.separatePrivateDefault.ui.enabled",
+ false
+ );
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref(
+ "browser.search.separatePrivateDefault.ui.enabled"
+ );
+ Services.prefs.clearUserPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft"
+ );
+ });
+
+ let url = "https://en.example.com/";
+ let engine = await Services.search.addEngineWithDetails("TestEngine", {
+ method: "GET",
+ template: url,
+ searchGetParams: "q={searchTerms}",
+ });
+ let defaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(defaultEngine);
+ await Services.search.removeEngine(engine);
+ });
+ // Make sure the engine domain would be autofilled.
+ await PlacesUtils.bookmarks.insert({
+ url,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark",
+ });
+
+ info("Test matching cases");
+
+ for (let searchStr of ["ex", "example.c"]) {
+ info("Searching for " + searchStr);
+ let context = createContext(searchStr, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: Services.search.defaultEngine.name,
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: engine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: "en.example.",
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ satisfiesAutofillThreshold: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: url,
+ title: "bookmark",
+ }),
+ ],
+ });
+ }
+
+ info("Test a www engine");
+ let url2 = "https://www.it.mochi.com/";
+ let engine2 = await Services.search.addEngineWithDetails("TestEngine2", {
+ method: "GET",
+ template: url2,
+ searchGetParams: "q={searchTerms}",
+ });
+ registerCleanupFunction(async () => {
+ await Services.search.removeEngine(engine2);
+ });
+ // Make sure the engine domain would be autofilled.
+ await PlacesUtils.bookmarks.insert({
+ url: url2,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark",
+ });
+
+ for (let searchStr of ["mo", "mochi.c"]) {
+ info("Searching for " + searchStr);
+ let context = createContext(searchStr, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: Services.search.defaultEngine.name,
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: engine2.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: "www.it.mochi.",
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ satisfiesAutofillThreshold: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: url2,
+ title: "bookmark",
+ }),
+ ],
+ });
+ }
+
+ info("Test non-matching cases");
+
+ for (let searchStr of ["www.en", "www.ex", "https://ex"]) {
+ info("Searching for " + searchStr);
+ let context = createContext(searchStr, { isPrivate: false });
+ // We don't want to generate all the possible results here, just check
+ // the heuristic result is not autofill.
+ let controller = UrlbarTestUtils.newMockController();
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.ok(context.results[0].heuristic, "Check heuristic result");
+ Assert.notEqual(context.results[0].providerName, "Autofill");
+ }
+
+ info("Restricting to history should not autofill our bookmark");
+ let context = createContext("ex", {
+ isPrivate: false,
+ sources: [UrlbarUtils.RESULT_SOURCE.HISTORY],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.ok(context.results[0].heuristic, "Check heuristic result");
+ Assert.notEqual(context.results[0].providerName, "Autofill");
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerUnifiedComplete.js b/browser/components/urlbar/tests/unit/test_providerUnifiedComplete.js
new file mode 100644
index 0000000000..3b78564484
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerUnifiedComplete.js
@@ -0,0 +1,242 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This is a simple test to check the UnifiedComplete provider works, it is not
+// intended to check all the edge cases, because that component is already
+// covered by a good amount of tests.
+
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+
+add_task(async function test_unifiedComplete() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let engine = await addTestSuggestionsEngine();
+ Services.search.defaultEngine = engine;
+ let oldCurrentEngine = Services.search.defaultEngine;
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(SUGGEST_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ Services.search.defaultEngine = oldCurrentEngine;
+ });
+
+ let controller = UrlbarTestUtils.newMockController();
+ // Also check case insensitivity.
+ let searchString = "MoZ oRg";
+ let context = createContext(searchString, { isPrivate: false });
+
+ // Add entries from multiple sources.
+ await PlacesUtils.bookmarks.insert({
+ url: "https://bookmark.mozilla.org/",
+ title: "Test bookmark",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ PlacesUtils.tagging.tagURI(
+ Services.io.newURI("https://bookmark.mozilla.org/"),
+ ["mozilla", "org", "ham", "moz", "bacon"]
+ );
+ await PlacesTestUtils.addVisits([
+ { uri: "https://history.mozilla.org/", title: "Test history" },
+ { uri: "https://tab.mozilla.org/", title: "Test tab" },
+ ]);
+ UrlbarProviderOpenTabs.registerOpenTab("https://tab.mozilla.org/", 0);
+
+ await controller.startQuery(context);
+
+ info(
+ "Results:\n" +
+ context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n")
+ );
+ Assert.equal(
+ context.results.length,
+ 6,
+ "Found the expected number of matches"
+ );
+
+ Assert.deepEqual(
+ [
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_TYPE.URL,
+ ],
+ context.results.map(m => m.type),
+ "Check result types"
+ );
+
+ Assert.deepEqual(
+ [
+ searchString,
+ searchString + " foo",
+ searchString + " bar",
+ "Test bookmark",
+ "Test tab",
+ "Test history",
+ ],
+ context.results.map(m => m.title),
+ "Check match titles"
+ );
+
+ Assert.deepEqual(
+ context.results[3].payload.tags,
+ ["moz", "mozilla", "org"],
+ "Check tags"
+ );
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ UrlbarProviderOpenTabs.unregisterOpenTab("https://tab.mozilla.org/", 0);
+});
+
+add_task(async function test_bookmarkBehaviorDisabled_tagged() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+
+ // Disable the bookmark behavior in UnifiedComplete.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+
+ let controller = UrlbarTestUtils.newMockController();
+ // Also check case insensitivity.
+ let searchString = "MoZ oRg";
+ let context = createContext(searchString, { isPrivate: false });
+
+ // Add a tagged bookmark that's also visited.
+ await PlacesUtils.bookmarks.insert({
+ url: "https://bookmark.mozilla.org/",
+ title: "Test bookmark",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ PlacesUtils.tagging.tagURI(
+ Services.io.newURI("https://bookmark.mozilla.org/"),
+ ["mozilla", "org", "ham", "moz", "bacon"]
+ );
+ await PlacesTestUtils.addVisits("https://bookmark.mozilla.org/");
+
+ await controller.startQuery(context);
+
+ info(
+ "Results:\n" +
+ context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n")
+ );
+ Assert.equal(
+ context.results.length,
+ 2,
+ "Found the expected number of matches"
+ );
+
+ Assert.deepEqual(
+ [UrlbarUtils.RESULT_TYPE.SEARCH, UrlbarUtils.RESULT_TYPE.URL],
+ context.results.map(m => m.type),
+ "Check result types"
+ );
+
+ Assert.deepEqual(
+ [searchString, "Test bookmark"],
+ context.results.map(m => m.title),
+ "Check match titles"
+ );
+
+ Assert.deepEqual(context.results[1].payload.tags, [], "Check tags");
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_bookmarkBehaviorDisabled_untagged() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+
+ // Disable the bookmark behavior in UnifiedComplete.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+
+ let controller = UrlbarTestUtils.newMockController();
+ // Also check case insensitivity.
+ let searchString = "MoZ oRg";
+ let context = createContext(searchString, { isPrivate: false });
+
+ // Add an *untagged* bookmark that's also visited.
+ await PlacesUtils.bookmarks.insert({
+ url: "https://bookmark.mozilla.org/",
+ title: "Test bookmark",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ await PlacesTestUtils.addVisits("https://bookmark.mozilla.org/");
+
+ await controller.startQuery(context);
+
+ info(
+ "Results:\n" +
+ context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n")
+ );
+ Assert.equal(
+ context.results.length,
+ 2,
+ "Found the expected number of matches"
+ );
+
+ Assert.deepEqual(
+ [UrlbarUtils.RESULT_TYPE.SEARCH, UrlbarUtils.RESULT_TYPE.URL],
+ context.results.map(m => m.type),
+ "Check result types"
+ );
+
+ Assert.deepEqual(
+ [searchString, "Test bookmark"],
+ context.results.map(m => m.title),
+ "Check match titles"
+ );
+
+ Assert.deepEqual(context.results[1].payload.tags, [], "Check tags");
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_diacritics() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+
+ // Enable the bookmark behavior in UnifiedComplete.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+
+ let controller = UrlbarTestUtils.newMockController();
+ let searchString = "agui";
+ let context = createContext(searchString, { isPrivate: false });
+
+ await PlacesUtils.bookmarks.insert({
+ url: "https://bookmark.mozilla.org/%C3%A3g%CC%83u%C4%A9",
+ title: "Test bookmark with accents in path",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+
+ await controller.startQuery(context);
+
+ info(
+ "Results:\n" +
+ context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n")
+ );
+ Assert.equal(
+ context.results.length,
+ 2,
+ "Found the expected number of matches"
+ );
+
+ Assert.deepEqual(
+ [UrlbarUtils.RESULT_TYPE.SEARCH, UrlbarUtils.RESULT_TYPE.URL],
+ context.results.map(m => m.type),
+ "Check result types"
+ );
+
+ Assert.deepEqual(
+ [searchString, "Test bookmark with accents in path"],
+ context.results.map(m => m.title),
+ "Check match titles"
+ );
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerUnifiedComplete_duplicate_entries.js b/browser/components/urlbar/tests/unit/test_providerUnifiedComplete_duplicate_entries.js
new file mode 100644
index 0000000000..7533921fc6
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerUnifiedComplete_duplicate_entries.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_duplicates() {
+ const TEST_URL = "https://history.mozilla.org/";
+ await PlacesTestUtils.addVisits([
+ { uri: TEST_URL, title: "Test history" },
+ { uri: TEST_URL + "?#", title: "Test history" },
+ { uri: TEST_URL + "#", title: "Test history" },
+ ]);
+
+ let controller = UrlbarTestUtils.newMockController();
+ let searchString = "^Hist";
+ let context = createContext(searchString, { isPrivate: false });
+ await controller.startQuery(context);
+
+ // The first result will be a search heuristic, which we don't care about for
+ // this test.
+ info(
+ "Results:\n" +
+ context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n")
+ );
+ Assert.equal(
+ context.results.length,
+ 2,
+ "Found the expected number of matches"
+ );
+ Assert.equal(
+ context.results[1].type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ "Should have a history result"
+ );
+ Assert.equal(
+ context.results[1].payload.url,
+ TEST_URL + "#",
+ "Check result URL"
+ );
+
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providersManager.js b/browser/components/urlbar/tests/unit/test_providersManager.js
new file mode 100644
index 0000000000..57598448ea
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providersManager.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_providers() {
+ Assert.throws(
+ () => UrlbarProvidersManager.registerProvider(),
+ /invalid provider/,
+ "Should throw with no arguments"
+ );
+ Assert.throws(
+ () => UrlbarProvidersManager.registerProvider({}),
+ /invalid provider/,
+ "Should throw with empty object"
+ );
+ Assert.throws(
+ () =>
+ UrlbarProvidersManager.registerProvider({
+ name: "",
+ }),
+ /invalid provider/,
+ "Should throw with empty name"
+ );
+ Assert.throws(
+ () =>
+ UrlbarProvidersManager.registerProvider({
+ name: "test",
+ startQuery: "no",
+ }),
+ /invalid provider/,
+ "Should throw with invalid startQuery"
+ );
+ Assert.throws(
+ () =>
+ UrlbarProvidersManager.registerProvider({
+ name: "test",
+ startQuery: () => {},
+ cancelQuery: "no",
+ }),
+ /invalid provider/,
+ "Should throw with invalid cancelQuery"
+ );
+
+ let match = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ );
+
+ let providerName = registerBasicTestProvider([match]);
+ let context = createContext(undefined, { providers: [providerName] });
+ let controller = UrlbarTestUtils.newMockController();
+ let resultsPromise = promiseControllerNotification(
+ controller,
+ "onQueryResults"
+ );
+
+ await UrlbarProvidersManager.startQuery(context, controller);
+ // Sanity check that this doesn't throw. It should be a no-op since we await
+ // for startQuery.
+ UrlbarProvidersManager.cancelQuery(context);
+
+ let params = await resultsPromise;
+ Assert.deepEqual(params[0].results, [match]);
+});
+
+add_task(async function test_criticalSection() {
+ // Just a sanity check, this shouldn't throw.
+ await UrlbarProvidersManager.runInCriticalSection(async () => {
+ let db = await PlacesUtils.promiseLargeCacheDBConnection();
+ await db.execute(`PRAGMA page_cache`);
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_providersManager_filtering.js b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js
new file mode 100644
index 0000000000..206dd98896
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js
@@ -0,0 +1,405 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_filtering_disable_only_source() {
+ let match = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ );
+ let providerName = registerBasicTestProvider([match]);
+ let context = createContext(undefined, { providers: [providerName] });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Disable the only available source, should get no matches");
+ Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
+ let promise = Promise.race([
+ promiseControllerNotification(controller, "onQueryResults", false),
+ promiseControllerNotification(controller, "onQueryFinished"),
+ ]);
+ await controller.startQuery(context);
+ await promise;
+ Services.prefs.clearUserPref("browser.urlbar.suggest.openpage");
+ UrlbarProvidersManager.unregisterProvider({ name: providerName });
+});
+
+add_task(async function test_filtering_disable_one_source() {
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ ];
+ let providerName = registerBasicTestProvider(matches);
+ let context = createContext(undefined, { providers: [providerName] });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Disable one of the sources, should get a single match");
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ let promise = Promise.all([
+ promiseControllerNotification(controller, "onQueryResults"),
+ promiseControllerNotification(controller, "onQueryFinished"),
+ ]);
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results, matches.slice(0, 1));
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ UrlbarProvidersManager.unregisterProvider({ name: providerName });
+});
+
+add_task(async function test_filtering_restriction_token() {
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ ];
+ let providerName = registerBasicTestProvider(matches);
+ let context = createContext(`foo ${UrlbarTokenizer.RESTRICT.OPENPAGE}`, {
+ providers: [providerName],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Use a restriction character, should get a single match");
+ let promise = Promise.all([
+ promiseControllerNotification(controller, "onQueryResults"),
+ promiseControllerNotification(controller, "onQueryFinished"),
+ ]);
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results, matches.slice(0, 1));
+ UrlbarProvidersManager.unregisterProvider({ name: providerName });
+});
+
+add_task(async function test_filter_javascript() {
+ let match = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ );
+ let jsMatch = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "javascript:foo" }
+ );
+ let providerName = registerBasicTestProvider([match, jsMatch]);
+ let context = createContext(undefined, { providers: [providerName] });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("By default javascript should be filtered out");
+ let promise = promiseControllerNotification(controller, "onQueryResults");
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results, [match]);
+
+ info("Except when the user explicitly starts the search with javascript:");
+ context = createContext(`javascript: ${UrlbarTokenizer.RESTRICT.HISTORY}`, {
+ providers: [providerName],
+ });
+ promise = promiseControllerNotification(controller, "onQueryResults");
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results, [jsMatch]);
+
+ info("Disable javascript filtering");
+ Services.prefs.setBoolPref("browser.urlbar.filter.javascript", false);
+ context = createContext(undefined, { providers: [providerName] });
+ promise = promiseControllerNotification(controller, "onQueryResults");
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results, [match, jsMatch]);
+ Services.prefs.clearUserPref("browser.urlbar.filter.javascript");
+ UrlbarProvidersManager.unregisterProvider({ name: providerName });
+});
+
+add_task(async function test_filter_isActive() {
+ let goodMatches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ ];
+ let providerName = registerBasicTestProvider(goodMatches);
+
+ let badMatches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ ];
+ /**
+ * A test provider that should not be invoked.
+ */
+ class NoInvokeProvider extends UrlbarProvider {
+ get name() {
+ return "BadProvider";
+ }
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+ isActive(context) {
+ info("Acceptable sources: " + context.sources);
+ return context.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS);
+ }
+ async startQuery(context, add) {
+ Assert.ok(false, "Provider should no be invoked");
+ for (const match of badMatches) {
+ add(this, match);
+ }
+ }
+ }
+ UrlbarProvidersManager.registerProvider(new NoInvokeProvider());
+
+ let context = createContext(undefined, {
+ sources: [UrlbarUtils.RESULT_SOURCE.TABS],
+ providers: [providerName, "BadProvider"],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Only tabs should be returned");
+ let promise = promiseControllerNotification(controller, "onQueryResults");
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results.length, 1, "Should find only one match");
+ Assert.deepEqual(
+ context.results[0].source,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ "Should find only a tab match"
+ );
+ UrlbarProvidersManager.unregisterProvider({ name: providerName });
+ UrlbarProvidersManager.unregisterProvider({ name: "BadProvider" });
+});
+
+add_task(async function test_filter_queryContext() {
+ let providerName = registerBasicTestProvider();
+
+ /**
+ * A test provider that should not be invoked because of queryContext.providers.
+ */
+ class NoInvokeProvider extends UrlbarProvider {
+ get name() {
+ return "BadProvider";
+ }
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+ isActive(context) {
+ return true;
+ }
+ async startQuery(context, add) {
+ Assert.ok(false, "Provider should no be invoked");
+ }
+ }
+ UrlbarProvidersManager.registerProvider(new NoInvokeProvider());
+
+ let context = createContext(undefined, {
+ providers: [providerName],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ await controller.startQuery(context, controller);
+ UrlbarProvidersManager.unregisterProvider({ name: providerName });
+ UrlbarProvidersManager.unregisterProvider({ name: "BadProvider" });
+});
+
+add_task(async function test_nofilter_heuristic() {
+ // Checks that even if a provider returns a result that should be filtered out
+ // it will still be invoked if it's of type heuristic, and only the heuristic
+ // result is returned.
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo2/" }
+ ),
+ ];
+ matches[0].heuristic = true;
+ let providerName = registerBasicTestProvider(
+ matches,
+ undefined,
+ UrlbarUtils.PROVIDER_TYPE.HEURISTIC
+ );
+
+ let context = createContext(undefined, {
+ sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
+ providers: [providerName],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ // Disable search matches through prefs.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
+ info("Only 1 heuristic tab result should be returned");
+ let promise = promiseControllerNotification(controller, "onQueryResults");
+ await controller.startQuery(context, controller);
+ await promise;
+ Services.prefs.clearUserPref("browser.urlbar.suggest.openpage");
+ Assert.deepEqual(context.results.length, 1, "Should find only one match");
+ Assert.deepEqual(
+ context.results[0].source,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ "Should find only a tab match"
+ );
+ UrlbarProvidersManager.unregisterProvider({ name: providerName });
+});
+
+add_task(async function test_nofilter_restrict() {
+ // Checks that even if a pref is disabled, we still return results on a
+ // restriction token.
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo_tab/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ { url: "http://mozilla.org/foo_bookmark/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/foo_history/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ { engine: "noengine" }
+ ),
+ ];
+ /**
+ * A test provider.
+ */
+ class TestProvider extends UrlbarProvider {
+ get name() {
+ return "MyProvider";
+ }
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+ isActive(context) {
+ Assert.equal(context.sources.length, 1, "Check acceptable sources");
+ return true;
+ }
+ async startQuery(context, add) {
+ Assert.ok(true, "expected provider was invoked");
+ for (let match of matches) {
+ add(this, match);
+ }
+ }
+ }
+ let provider = new TestProvider();
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let typeToPropertiesMap = new Map([
+ ["HISTORY", { source: "HISTORY", pref: "history" }],
+ ["BOOKMARK", { source: "BOOKMARKS", pref: "bookmark" }],
+ ["OPENPAGE", { source: "TABS", pref: "openpage" }],
+ ["SEARCH", { source: "SEARCH", pref: "searches" }],
+ ]);
+ for (let [type, token] of Object.entries(UrlbarTokenizer.RESTRICT)) {
+ let properties = typeToPropertiesMap.get(type);
+ if (!properties) {
+ continue;
+ }
+ info("Restricting on " + type);
+ let context = createContext(token + " foo", {
+ providers: ["MyProvider"],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ // Disable the corresponding pref.
+ const pref = "browser.urlbar.suggest." + properties.pref;
+ info("Disabling " + pref);
+ Services.prefs.setBoolPref(pref, false);
+ await controller.startQuery(context, controller);
+ Assert.equal(context.results.length, 1, "Should find one result");
+ Assert.equal(
+ context.results[0].source,
+ UrlbarUtils.RESULT_SOURCE[properties.source],
+ "Check result source"
+ );
+ Services.prefs.clearUserPref(pref);
+ }
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+add_task(async function test_filter_priority() {
+ /**
+ * A test provider.
+ */
+ class TestProvider extends UrlbarTestUtils.TestProvider {
+ constructor(priority, shouldBeInvoked, namePart = "") {
+ super();
+ this._priority = priority;
+ this._name = `${priority}` + namePart;
+ this._shouldBeInvoked = shouldBeInvoked;
+ }
+ async startQuery(context, add) {
+ Assert.ok(this._shouldBeInvoked, `${this.name} was invoked`);
+ }
+ }
+
+ // Test all possible orderings of the providers to make sure the logic that
+ // finds the highest priority providers is correct.
+ let providerPerms = permute([
+ new TestProvider(0, false),
+ new TestProvider(1, false),
+ new TestProvider(2, true, "a"),
+ new TestProvider(2, true, "b"),
+ ]);
+ for (let providers of providerPerms) {
+ for (let provider of providers) {
+ UrlbarProvidersManager.registerProvider(provider);
+ }
+ let providerNames = providers.map(p => p.name);
+ let context = createContext(undefined, { providers: providerNames });
+ let controller = UrlbarTestUtils.newMockController();
+ await controller.startQuery(context, controller);
+ for (let name of providerNames) {
+ UrlbarProvidersManager.unregisterProvider({ name });
+ }
+ }
+});
+
+function permute(objects) {
+ if (objects.length <= 1) {
+ return [objects];
+ }
+ let perms = [];
+ for (let i = 0; i < objects.length; i++) {
+ let otherObjects = objects.slice();
+ otherObjects.splice(i, 1);
+ let otherPerms = permute(otherObjects);
+ for (let perm of otherPerms) {
+ perm.unshift(objects[i]);
+ }
+ perms = perms.concat(otherPerms);
+ }
+ return perms;
+}
diff --git a/browser/components/urlbar/tests/unit/test_providersManager_maxResults.js b/browser/components/urlbar/tests/unit/test_providersManager_maxResults.js
new file mode 100644
index 0000000000..c3f6e5c2e3
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providersManager_maxResults.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_maxResults() {
+ const MATCHES_LENGTH = 20;
+ let matches = [];
+ for (let i = 0; i < MATCHES_LENGTH; i++) {
+ matches.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: `http://mozilla.org/foo/${i}` }
+ )
+ );
+ }
+ let providerName = registerBasicTestProvider(matches);
+ let context = createContext(undefined, { providers: [providerName] });
+ let controller = UrlbarTestUtils.newMockController();
+
+ async function test_count(count) {
+ let promise = promiseControllerNotification(controller, "onQueryFinished");
+ context.maxResults = count;
+ await controller.startQuery(context);
+ await promise;
+ Assert.equal(
+ context.results.length,
+ Math.min(MATCHES_LENGTH, count),
+ "Check count"
+ );
+ Assert.deepEqual(context.results, matches.slice(0, count), "Check results");
+ }
+ await test_count(10);
+ await test_count(1);
+ await test_count(30);
+});
diff --git a/browser/components/urlbar/tests/unit/test_queryScorer.js b/browser/components/urlbar/tests/unit/test_queryScorer.js
new file mode 100644
index 0000000000..4af1b26112
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_queryScorer.js
@@ -0,0 +1,405 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ QueryScorer: "resource:///modules/UrlbarProviderInterventions.jsm",
+});
+
+const DISTANCE_THRESHOLD = 1;
+
+const DOCUMENTS = {
+ clear: [
+ "cache firefox",
+ "clear cache firefox",
+ "clear cache in firefox",
+ "clear cookies firefox",
+ "clear firefox cache",
+ "clear history firefox",
+ "cookies firefox",
+ "delete cookies firefox",
+ "delete history firefox",
+ "firefox cache",
+ "firefox clear cache",
+ "firefox clear cookies",
+ "firefox clear history",
+ "firefox cookie",
+ "firefox cookies",
+ "firefox delete cookies",
+ "firefox delete history",
+ "firefox history",
+ "firefox not loading pages",
+ "history firefox",
+ "how to clear cache",
+ "how to clear history",
+ ],
+ refresh: [
+ "firefox crashing",
+ "firefox keeps crashing",
+ "firefox not responding",
+ "firefox not working",
+ "firefox refresh",
+ "firefox slow",
+ "how to reset firefox",
+ "refresh firefox",
+ "reset firefox",
+ ],
+ update: [
+ "download firefox",
+ "download mozilla",
+ "firefox browser",
+ "firefox download",
+ "firefox for mac",
+ "firefox for windows",
+ "firefox free download",
+ "firefox install",
+ "firefox installer",
+ "firefox latest version",
+ "firefox mac",
+ "firefox quantum",
+ "firefox update",
+ "firefox version",
+ "firefox windows",
+ "get firefox",
+ "how to update firefox",
+ "install firefox",
+ "mozilla download",
+ "mozilla firefox 2019",
+ "mozilla firefox 2020",
+ "mozilla firefox download",
+ "mozilla firefox for mac",
+ "mozilla firefox for windows",
+ "mozilla firefox free download",
+ "mozilla firefox mac",
+ "mozilla firefox update",
+ "mozilla firefox windows",
+ "mozilla update",
+ "update firefox",
+ "update mozilla",
+ "www.firefox.com",
+ ],
+};
+
+const VARIATIONS = new Map([["firefox", ["fire fox", "fox fire", "foxfire"]]]);
+
+let tests = [
+ {
+ query: "firefox",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "bogus",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "no match",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ // clear
+ {
+ query: "firefox histo",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox histor",
+ matches: [
+ { id: "clear", score: 1 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox history",
+ matches: [
+ { id: "clear", score: 0 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox history we'll keep matching once we match",
+ matches: [
+ { id: "clear", score: 0 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ {
+ query: "firef history",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo history",
+ matches: [
+ { id: "clear", score: 1 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo histor",
+ matches: [
+ { id: "clear", score: 2 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo histor we'll keep matching once we match",
+ matches: [
+ { id: "clear", score: 2 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ {
+ query: "fire fox history",
+ matches: [
+ { id: "clear", score: 0 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "fox fire history",
+ matches: [
+ { id: "clear", score: 0 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "foxfire history",
+ matches: [
+ { id: "clear", score: 0 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ // refresh
+ {
+ query: "firefox sl",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox slo",
+ matches: [
+ { id: "refresh", score: 1 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox slow",
+ matches: [
+ { id: "refresh", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox slow we'll keep matching once we match",
+ matches: [
+ { id: "refresh", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ {
+ query: "firef slow",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo slow",
+ matches: [
+ { id: "refresh", score: 1 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo slo",
+ matches: [
+ { id: "refresh", score: 2 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo slo we'll keep matching once we match",
+ matches: [
+ { id: "refresh", score: 2 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ {
+ query: "fire fox slow",
+ matches: [
+ { id: "refresh", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "fox fire slow",
+ matches: [
+ { id: "refresh", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "foxfire slow",
+ matches: [
+ { id: "refresh", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ // update
+ {
+ query: "firefox upda",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox updat",
+ matches: [
+ { id: "update", score: 1 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox update",
+ matches: [
+ { id: "update", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox update we'll keep matching once we match",
+ matches: [
+ { id: "update", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+
+ {
+ query: "firef update",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo update",
+ matches: [
+ { id: "update", score: 1 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo updat",
+ matches: [
+ { id: "update", score: 2 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo updat we'll keep matching once we match",
+ matches: [
+ { id: "update", score: 2 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+
+ {
+ query: "fire fox update",
+ matches: [
+ { id: "update", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "fox fire update",
+ matches: [
+ { id: "update", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "foxfire update",
+ matches: [
+ { id: "update", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+];
+
+add_task(async function test() {
+ let qs = new QueryScorer({
+ distanceThreshold: DISTANCE_THRESHOLD,
+ variations: VARIATIONS,
+ });
+
+ for (let [id, phrases] of Object.entries(DOCUMENTS)) {
+ qs.addDocument({ id, phrases });
+ }
+
+ for (let { query, matches } of tests) {
+ let actual = qs
+ .score(query)
+ .map(result => ({ id: result.document.id, score: result.score }));
+ Assert.deepEqual(actual, matches, `Query: "${query}"`);
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_query_url.js b/browser/components/urlbar/tests/unit/test_query_url.js
new file mode 100644
index 0000000000..464cae1064
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_query_url.js
@@ -0,0 +1,123 @@
+/* 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/. */
+
+const ENGINE_NAME = "engine-suggestions.xml";
+const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback";
+const UNIFIEDCOMPLETE_PROVIDERNAME = "UnifiedComplete";
+
+testEngine_setup();
+
+add_task(async function test_no_slash() {
+ info("Searching for host match without slash should match host");
+ await PlacesTestUtils.addVisits([
+ { uri: "http://file.org/test/" },
+ { uri: "file:///c:/test.html" },
+ ]);
+ let context = createContext("file", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "file.org/",
+ completed: "http://file.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://file.org/",
+ title: "file.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "file:///c:/test.html",
+ title: "test visit for file:///c:/test.html",
+ iconUri: UrlbarUtils.ICON.DEFAULT,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://file.org/test/",
+ title: "test visit for http://file.org/test/",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_w_slash() {
+ info("Searching match with slash at the end should match url");
+ await PlacesTestUtils.addVisits(
+ {
+ uri: Services.io.newURI("http://file.org/test/"),
+ },
+ {
+ uri: Services.io.newURI("file:///c:/test.html"),
+ }
+ );
+ let context = createContext("file.org/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "file.org/",
+ completed: "http://file.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://file.org/",
+ title: "file.org/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://file.org/test/",
+ title: "test visit for http://file.org/test/",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_middle() {
+ info("Searching match with slash in the middle should match url");
+ await PlacesTestUtils.addVisits(
+ {
+ uri: Services.io.newURI("http://file.org/test/"),
+ },
+ {
+ uri: Services.io.newURI("file:///c:/test.html"),
+ }
+ );
+ let context = createContext("file.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "file.org/test/",
+ completed: "http://file.org/test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://file.org/test/",
+ title: "file.org/test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_nonhost() {
+ info("Searching for non-host match without slash should not match url");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("file:///c:/test.html"),
+ });
+ let context = createContext("file", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "file:///c:/test.html",
+ title: "test visit for file:///c:/test.html",
+ iconUri: UrlbarUtils.ICON.DEFAULT,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_search_engine_host.js b/browser/components/urlbar/tests/unit/test_search_engine_host.js
new file mode 100644
index 0000000000..73c455e9c4
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_engine_host.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let engine;
+
+add_task(async function test_searchEngine_autoFill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ await Services.search.addEngineWithDetails("MySearchEngine", {
+ method: "GET",
+ template: "http://my.search.com/",
+ });
+ engine = Services.search.getEngineByName("MySearchEngine");
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.search.removeEngine(engine);
+ });
+
+ // Add an uri that matches the search string with high frecency.
+ let uri = Services.io.newURI("http://www.example.com/my/");
+ let visits = [];
+ for (let i = 0; i < 100; ++i) {
+ visits.push({ uri, title: "Terms - SearchEngine Search" });
+ }
+ await PlacesTestUtils.addVisits(visits);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri,
+ title: "Example bookmark",
+ });
+ await PlacesTestUtils.promiseAsyncUpdates();
+ ok(
+ frecencyForUrl(uri) > 10000,
+ "Added URI should have expected high frecency"
+ );
+
+ info(
+ "Check search domain is autoFilled even if there's an higher frecency match"
+ );
+ let context = createContext("my", { isPrivate: false });
+ await check_results({
+ search: "my",
+ autofilled: "my.search.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: "MySearchEngine",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_noautoFill() {
+ Services.prefs.setIntPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft",
+ 0
+ );
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("http://my.search.com/samplepage/")
+ );
+
+ info("Check search domain is not autoFilled if it matches a visited domain");
+ let context = createContext("my", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "my.search.com/",
+ completed: "http://my.search.com/",
+ matches: [
+ // Note this result is a normal Autofill result and not a priority engine.
+ makeVisitResult(context, {
+ uri: "http://my.search.com/",
+ title: "my.search.com",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: engine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(engine.getResultDomain()),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ makeVisitResult(context, {
+ uri: "http://my.search.com/samplepage/",
+ title: "test visit for http://my.search.com/samplepage/",
+ providerName: "UnifiedComplete",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+ Services.prefs.clearUserPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft"
+ );
+});
diff --git a/browser/components/urlbar/tests/unit/test_search_suggestions.js b/browser/components/urlbar/tests/unit/test_search_suggestions.js
new file mode 100644
index 0000000000..0b1180959a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_suggestions.js
@@ -0,0 +1,1695 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that search engine suggestions are returned by
+ * UrlbarProviderSearchSuggestions.
+ */
+
+const { FormHistory } = ChromeUtils.import(
+ "resource://gre/modules/FormHistory.jsm"
+);
+
+const ENGINE_NAME = "engine-suggestions.xml";
+// This is fixed to match the port number in engine-suggestions.xml.
+const SERVER_PORT = 9000;
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+const PRIVATE_ENABLED_PREF = "browser.search.suggest.enabled.private";
+const PRIVATE_SEARCH_PREF = "browser.search.separatePrivateDefault.ui.enabled";
+const MAX_RICH_RESULTS_PREF = "browser.urlbar.maxRichResults";
+const MAX_FORM_HISTORY_PREF = "browser.urlbar.maxHistoricalSearchSuggestions";
+const SEARCH_STRING = "hello";
+const MATCH_BUCKETS_VALUE = "general:5,suggestion:Infinity";
+
+var suggestionsFn;
+var previousSuggestionsFn;
+
+/**
+ * Set the current suggestion funciton.
+ * @param {function} fn
+ * A function that that a search string and returns an array of strings that
+ * will be used as search suggestions.
+ * Note: `fn` should return > 0 suggestions in most cases. Otherwise, you may
+ * encounter unexpected behaviour with UrlbarProviderSuggestion's
+ * _lastLowResultsSearchSuggestion safeguard.
+ */
+function setSuggestionsFn(fn) {
+ previousSuggestionsFn = suggestionsFn;
+ suggestionsFn = fn;
+}
+
+async function cleanup() {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+ Services.prefs.clearUserPref(SUGGEST_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
+
+async function cleanUpSuggestions() {
+ await cleanup();
+ if (previousSuggestionsFn) {
+ suggestionsFn = previousSuggestionsFn;
+ previousSuggestionsFn = null;
+ }
+}
+
+function makeExpectedFormHistoryResults(context, minCount = 0) {
+ let count = Math.max(
+ minCount,
+ Services.prefs.getIntPref(MAX_FORM_HISTORY_PREF, 0)
+ );
+ let results = [];
+ for (let i = 0; i < count; i++) {
+ results.push(
+ makeFormHistoryResult(context, {
+ suggestion: `${SEARCH_STRING} world Form History ${i}`,
+ engineName: ENGINE_NAME,
+ })
+ );
+ }
+ return results;
+}
+
+function makeExpectedRemoteSuggestionResults(
+ context,
+ { suggestionPrefix = SEARCH_STRING, query = undefined } = {}
+) {
+ return [
+ makeSearchResult(context, {
+ query,
+ engineName: ENGINE_NAME,
+ suggestion: suggestionPrefix + " foo",
+ }),
+ makeSearchResult(context, {
+ query,
+ engineName: ENGINE_NAME,
+ suggestion: suggestionPrefix + " bar",
+ }),
+ ];
+}
+
+function makeExpectedSuggestionResults(
+ context,
+ { suggestionPrefix = SEARCH_STRING, query = undefined } = {}
+) {
+ return [
+ ...makeExpectedFormHistoryResults(context),
+ ...makeExpectedRemoteSuggestionResults(context, {
+ suggestionPrefix,
+ query,
+ }),
+ ];
+}
+
+add_task(async function setup() {
+ Services.prefs.setCharPref(
+ "browser.urlbar.matchBuckets",
+ MATCH_BUCKETS_VALUE
+ );
+
+ let engine = await addTestSuggestionsEngine(searchStr => {
+ return suggestionsFn(searchStr);
+ });
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["foo", "bar"];
+ return [searchStr].concat(suffixes.map(s => searchStr + " " + s));
+ });
+
+ // Install the test engine.
+ let oldDefaultEngine = await Services.search.getDefault();
+ registerCleanupFunction(async () => {
+ Services.search.setDefault(oldDefaultEngine);
+ Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF);
+ });
+ Services.search.setDefault(engine);
+ Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false);
+
+ // Add some form history.
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ let entries = makeExpectedFormHistoryResults(context, 2).map(r => ({
+ value: r.payload.suggestion,
+ source: ENGINE_NAME,
+ }));
+ await UrlbarTestUtils.formHistory.add(entries);
+});
+
+add_task(async function disabled_urlbarSuggestions() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+add_task(async function disabled_allSuggestions() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+add_task(async function disabled_privateWindow() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, false);
+ let context = createContext(SEARCH_STRING, { isPrivate: true });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+add_task(async function disabled_urlbarSuggestions_withRestrictionToken() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let context = createContext(
+ `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`,
+ { isPrivate: false }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query: SEARCH_STRING,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeExpectedSuggestionResults(context, {
+ query: SEARCH_STRING,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+add_task(
+ async function disabled_urlbarSuggestions_withRestrictionToken_private() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, false);
+ let context = createContext(
+ `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`,
+ { isPrivate: true }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query: SEARCH_STRING,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+ }
+);
+
+add_task(
+ async function disabled_urlbarSuggestions_withRestrictionToken_private_enabled() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, true);
+ let context = createContext(
+ `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`,
+ { isPrivate: true }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query: SEARCH_STRING,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeExpectedSuggestionResults(context, {
+ query: SEARCH_STRING,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+ }
+);
+
+add_task(async function enabled_by_pref_privateWindow() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, true);
+ let context = createContext(SEARCH_STRING, { isPrivate: true });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedSuggestionResults(context),
+ ],
+ });
+ await cleanUpSuggestions();
+
+ Services.prefs.clearUserPref(PRIVATE_ENABLED_PREF);
+});
+
+add_task(async function singleWordQuery() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedSuggestionResults(context),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function multiWordQuery() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ const query = `${SEARCH_STRING} world`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedSuggestionResults(context, { suggestionPrefix: query }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function suffixMatch() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ setSuggestionsFn(searchStr => {
+ let prefixes = ["baz", "quux"];
+ return prefixes.map(p => p + " " + searchStr);
+ });
+
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedFormHistoryResults(context),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "baz " + SEARCH_STRING,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "quux " + SEARCH_STRING,
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function queryIsNotASubstring() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+
+ setSuggestionsFn(searchStr => {
+ return ["aaa", "bbb"];
+ });
+
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedFormHistoryResults(context),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "aaa",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "bbb",
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function restrictToken() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ // Add a visit and a bookmark. Actually, make the bookmark visited too so
+ // that it's guaranteed, with its higher frecency, to appear above the search
+ // suggestions.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-visit`),
+ title: `${SEARCH_STRING} visit`,
+ },
+ {
+ uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-bookmark`),
+ title: `${SEARCH_STRING} bookmark`,
+ },
+ ]);
+
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-bookmark`),
+ title: `${SEARCH_STRING} bookmark`,
+ });
+
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+
+ // Do an unrestricted search to make sure everything appears in it, including
+ // the visit and bookmark.
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeBookmarkResult(context, {
+ uri: `http://example.com/${SEARCH_STRING}-bookmark`,
+ title: `${SEARCH_STRING} bookmark`,
+ }),
+ makeVisitResult(context, {
+ uri: `http://example.com/${SEARCH_STRING}-visit`,
+ title: `${SEARCH_STRING} visit`,
+ }),
+ ...makeExpectedSuggestionResults(context),
+ ],
+ });
+
+ // Now do a restricted search to make sure only suggestions appear.
+ context = createContext(
+ `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`,
+ {
+ isPrivate: false,
+ }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ query: SEARCH_STRING,
+ heuristic: true,
+ }),
+ ...makeExpectedSuggestionResults(context, {
+ suggestionPrefix: SEARCH_STRING,
+ query: SEARCH_STRING,
+ }),
+ ],
+ });
+
+ // Typing the search restriction char shows the Search Engine entry and local
+ // results.
+ context = createContext(UrlbarTokenizer.RESTRICT.SEARCH, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ query: "",
+ heuristic: true,
+ }),
+ ...makeExpectedFormHistoryResults(context),
+ ],
+ });
+
+ // Also if followed by multiple spaces.
+ context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH} `, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ query: "",
+ heuristic: true,
+ }),
+ ...makeExpectedFormHistoryResults(context),
+ ],
+ });
+
+ // If followed by any char we should fetch suggestions.
+ // Note this uses "h" to match form history.
+ context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH}h`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ query: "h",
+ heuristic: true,
+ }),
+ ...makeExpectedSuggestionResults(context, {
+ suggestionPrefix: "h",
+ query: "h",
+ }),
+ ],
+ });
+
+ // Also if followed by a space and single char.
+ context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH} h`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ query: "h",
+ heuristic: true,
+ }),
+ ...makeExpectedSuggestionResults(context, {
+ suggestionPrefix: "h",
+ query: "h",
+ }),
+ ],
+ });
+
+ // Leading search-mode restriction tokens are removed.
+ context = createContext(
+ `${UrlbarTokenizer.RESTRICT.BOOKMARK} ${SEARCH_STRING}`,
+ { isPrivate: false }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ query: SEARCH_STRING,
+ alias: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ }),
+ makeBookmarkResult(context, {
+ uri: `http://example.com/${SEARCH_STRING}-bookmark`,
+ title: `${SEARCH_STRING} bookmark`,
+ }),
+ ],
+ });
+
+ // Non-search-mode restriction tokens remain in the query and heuristic search
+ // result.
+ let token;
+ for (let t of Object.values(UrlbarTokenizer.RESTRICT)) {
+ if (!UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(t)) {
+ token = t;
+ break;
+ }
+ }
+ Assert.ok(
+ token,
+ "Non-search-mode restrict token exists -- if not, you can probably remove me!"
+ );
+ context = createContext(token, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function mixup_frecency() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ // At most, we should have 14 results in this subtest. We set this to 20 to
+ // make we're not cutting off any results and we are actually getting 12.
+ Services.prefs.setIntPref(MAX_RICH_RESULTS_PREF, 20);
+
+ // Add a visit and a bookmark. Actually, make the bookmark visited too so
+ // that it's guaranteed, with its higher frecency, to appear above the search
+ // suggestions.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI("http://example.com/lo0"),
+ title: `${SEARCH_STRING} low frecency 0`,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/lo1"),
+ title: `${SEARCH_STRING} low frecency 1`,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/lo2"),
+ title: `${SEARCH_STRING} low frecency 2`,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/lo3"),
+ title: `${SEARCH_STRING} low frecency 3`,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/lo4"),
+ title: `${SEARCH_STRING} low frecency 4`,
+ },
+ ]);
+
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI("http://example.com/hi0"),
+ title: `${SEARCH_STRING} high frecency 0`,
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/hi1"),
+ title: `${SEARCH_STRING} high frecency 1`,
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/hi2"),
+ title: `${SEARCH_STRING} high frecency 2`,
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/hi3"),
+ title: `${SEARCH_STRING} high frecency 3`,
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ ]);
+ }
+
+ for (let i = 0; i < 4; i++) {
+ let href = `http://example.com/hi${i}`;
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: href,
+ title: `${SEARCH_STRING} high frecency ${i}`,
+ });
+ }
+
+ // Do an unrestricted search to make sure everything appears in it, including
+ // the visit and bookmark.
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi3",
+ title: `${SEARCH_STRING} high frecency 3`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi2",
+ title: `${SEARCH_STRING} high frecency 2`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi1",
+ title: `${SEARCH_STRING} high frecency 1`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi0",
+ title: `${SEARCH_STRING} high frecency 0`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo4",
+ title: `${SEARCH_STRING} low frecency 4`,
+ }),
+ ...makeExpectedSuggestionResults(context),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo3",
+ title: `${SEARCH_STRING} low frecency 3`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo2",
+ title: `${SEARCH_STRING} low frecency 2`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo1",
+ title: `${SEARCH_STRING} low frecency 1`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo0",
+ title: `${SEARCH_STRING} low frecency 0`,
+ }),
+ ],
+ });
+
+ // Change the "general" context mixup.
+ Services.prefs.setCharPref(
+ "browser.urlbar.matchBuckets",
+ "suggestion:1,general:5,suggestion:1"
+ );
+
+ // Do an unrestricted search to make sure everything appears in it, including
+ // the visits and bookmarks.
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedSuggestionResults(context).slice(0, 1),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi3",
+ title: `${SEARCH_STRING} high frecency 3`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi2",
+ title: `${SEARCH_STRING} high frecency 2`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi1",
+ title: `${SEARCH_STRING} high frecency 1`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi0",
+ title: `${SEARCH_STRING} high frecency 0`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo4",
+ title: `${SEARCH_STRING} low frecency 4`,
+ }),
+ ...makeExpectedSuggestionResults(context).slice(1),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo3",
+ title: `${SEARCH_STRING} low frecency 3`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo2",
+ title: `${SEARCH_STRING} low frecency 2`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo1",
+ title: `${SEARCH_STRING} low frecency 1`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo0",
+ title: `${SEARCH_STRING} low frecency 0`,
+ }),
+ ],
+ });
+
+ // Change the "search" context mixup.
+ Services.prefs.setCharPref(
+ "browser.urlbar.matchBucketsSearch",
+ "suggestion:2,general:4"
+ );
+
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedSuggestionResults(context).slice(0, 2),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi3",
+ title: `${SEARCH_STRING} high frecency 3`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi2",
+ title: `${SEARCH_STRING} high frecency 2`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi1",
+ title: `${SEARCH_STRING} high frecency 1`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi0",
+ title: `${SEARCH_STRING} high frecency 0`,
+ }),
+ ...makeExpectedSuggestionResults(context).slice(2),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo4",
+ title: `${SEARCH_STRING} low frecency 4`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo3",
+ title: `${SEARCH_STRING} low frecency 3`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo2",
+ title: `${SEARCH_STRING} low frecency 2`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo1",
+ title: `${SEARCH_STRING} low frecency 1`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo0",
+ title: `${SEARCH_STRING} low frecency 0`,
+ }),
+ ],
+ });
+
+ Services.prefs.setCharPref(
+ "browser.urlbar.matchBuckets",
+ MATCH_BUCKETS_VALUE
+ );
+ Services.prefs.clearUserPref("browser.urlbar.matchBucketsSearch");
+ Services.prefs.clearUserPref(MAX_RICH_RESULTS_PREF);
+ await cleanUpSuggestions();
+});
+
+add_task(async function prohibit_suggestions() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(
+ `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
+ false
+ );
+
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedSuggestionResults(context),
+ ],
+ });
+
+ Services.prefs.setBoolPref(
+ `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
+ true
+ );
+ registerCleanupFunction(() => {
+ Services.prefs.setBoolPref(
+ `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
+ false
+ );
+ });
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${SEARCH_STRING}/`,
+ title: `http://${SEARCH_STRING}/`,
+ iconUri: "",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: false,
+ }),
+ ...makeExpectedFormHistoryResults(context),
+ ],
+ });
+
+ // When using multiple words, we should still get suggestions:
+ let query = `${SEARCH_STRING} world`;
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedSuggestionResults(context, { suggestionPrefix: query }),
+ ],
+ });
+
+ // Clear the whitelist for SEARCH_STRING and try preferring DNS for any single
+ // word instead:
+ Services.prefs.setBoolPref(
+ `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
+ false
+ );
+ Services.prefs.setBoolPref("browser.fixup.dns_first_for_single_words", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.fixup.dns_first_for_single_words");
+ });
+
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${SEARCH_STRING}/`,
+ title: `http://${SEARCH_STRING}/`,
+ iconUri: "",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: false,
+ }),
+ ...makeExpectedFormHistoryResults(context),
+ ],
+ });
+
+ context = createContext("somethingelse", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://somethingelse/",
+ title: "http://somethingelse/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: false,
+ }),
+ ],
+ });
+
+ // When using multiple words, we should still get suggestions:
+ query = `${SEARCH_STRING} world`;
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedSuggestionResults(context, { suggestionPrefix: query }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("browser.fixup.dns_first_for_single_words");
+
+ context = createContext("http://1.2.3.4/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://1.2.3.4/",
+ title: "http://1.2.3.4/",
+ iconUri: "page-icon:http://1.2.3.4/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("[2001::1]:30", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://[2001::1]:30/",
+ title: "http://[2001::1]:30/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("user:pass@test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://user:pass@test/",
+ title: "http://user:pass@test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("data:text/plain,Content", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "data:text/plain,Content",
+ title: "data:text/plain,Content",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("a", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function uri_like_queries() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ // We should not fetch any suggestions for an actual URL.
+ let query = "mozilla.org";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ title: `http://${query}/`,
+ uri: `http://${query}/`,
+ iconUri: "",
+ heuristic: true,
+ }),
+ makeSearchResult(context, { query, engineName: ENGINE_NAME }),
+ ],
+ });
+
+ // We should also not fetch suggestions for a partially-typed URL.
+ query = "mozilla.o";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ // Now trying queries that could be confused for URLs. They should return
+ // results.
+ const uriLikeQueries = [
+ "mozilla.org is a great website",
+ "I like mozilla.org",
+ "a/b testing",
+ "he/him",
+ "Google vs.",
+ "5.8 cm",
+ ];
+ for (query of uriLikeQueries) {
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedRemoteSuggestionResults(context, {
+ suggestionPrefix: query,
+ }),
+ ],
+ });
+ }
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function avoid_remote_url_suggestions_1() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 1);
+
+ setSuggestionsFn(searchStr => {
+ let suffixes = [".com", "/test", ":1]", "@test", ". com"];
+ return suffixes.map(s => searchStr + s);
+ });
+
+ const query = "test";
+
+ await UrlbarTestUtils.formHistory.add([`${query}.com`]);
+
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeFormHistoryResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: `${query}.com`,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: `${query}. com`,
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+ await UrlbarTestUtils.formHistory.remove([`${query}.com`]);
+ Services.prefs.clearUserPref(MAX_FORM_HISTORY_PREF);
+});
+
+add_task(async function avoid_remote_url_suggestions_2() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["ed", "eds"];
+ return suffixes.map(s => searchStr + s);
+ });
+
+ let context = createContext("htt", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "htted",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "htteds",
+ }),
+ ],
+ });
+
+ context = createContext("ftp", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "ftped",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "ftpeds",
+ }),
+ ],
+ });
+
+ context = createContext("http", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "httped",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "httpeds",
+ }),
+ ],
+ });
+
+ context = createContext("http:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("https", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "httpsed",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "httpseds",
+ }),
+ ],
+ });
+
+ context = createContext("https:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("httpd", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "httpded",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "httpdeds",
+ }),
+ ],
+ });
+
+ // Check FTP enabled
+ Services.prefs.setBoolPref("network.ftp.enabled", true);
+ registerCleanupFunction(() =>
+ Services.prefs.clearUserPref("network.ftp.enabled")
+ );
+
+ context = createContext("ftp:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("ftp:/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("ftp://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("ftp://test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "ftp://test/",
+ title: "ftp://test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Check FTP disabled
+ Services.prefs.setBoolPref("network.ftp.enabled", false);
+ context = createContext("ftp:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("ftp:/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("ftp://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("ftp://test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "ftp://test/",
+ title: "ftp://test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http:/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("https:/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("http://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("https://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("http://www", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www/",
+ title: "http://www/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("https://www", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "https://www/",
+ title: "https://www/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http://test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://test/",
+ title: "http://test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("https://test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "https://test/",
+ title: "https://test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http://www.test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www.test/",
+ title: "http://www.test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http://www.test.com", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www.test.com/",
+ title: "http://www.test.com/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("file", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "fileed",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "fileeds",
+ }),
+ ],
+ });
+
+ context = createContext("file:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("file:///Users", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "file:///Users",
+ title: "file:///Users",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("moz-test://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("moz+test://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("about", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "abouted",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "abouteds",
+ }),
+ ],
+ });
+
+ context = createContext("about:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function restrict_remote_suggestions_after_no_results() {
+ // We don't fetch remote suggestions if a query with a length over
+ // maxCharsForSearchSuggestions returns 0 results. We set it to 4 here to
+ // avoid constructing a 100+ character string.
+ Services.prefs.setIntPref("browser.urlbar.maxCharsForSearchSuggestions", 4);
+ setSuggestionsFn(searchStr => {
+ return [];
+ });
+
+ const query = SEARCH_STRING.substring(0, SEARCH_STRING.length - 1);
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedFormHistoryResults(context),
+ ],
+ });
+
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedFormHistoryResults(context),
+ // Because the previous search returned no suggestions, we will not fetch
+ // remote suggestions for this query that is just a longer version of the
+ // previous query.
+ ],
+ });
+
+ // Do one more search before resetting maxCharsForSearchSuggestions to reset
+ // the search suggestion provider's _lastLowResultsSearchSuggestion property.
+ // Otherwise it will be stuck at SEARCH_STRING, which interferes with
+ // subsequent tests.
+ context = createContext("not the search string", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("browser.urlbar.maxCharsForSearchSuggestions");
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function formHistory() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ // Setting maxHistoricalSearchSuggestions = 0 is special and indicates that
+ // the user has opted out of form history, so we should include form history
+ // neither before the expected remote results nor after, unlike the other
+ // checks below, where remaining form history is included after the expected
+ // remote results.
+ Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 0);
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedRemoteSuggestionResults(context),
+ ],
+ });
+
+ Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 1);
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedFormHistoryResults(context, 2).slice(0, 1),
+ ...makeExpectedRemoteSuggestionResults(context),
+ ...makeExpectedFormHistoryResults(context, 2).slice(1),
+ ],
+ });
+
+ Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 2);
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedFormHistoryResults(context, 2),
+ ...makeExpectedRemoteSuggestionResults(context),
+ ],
+ });
+
+ // Do a search for exactly the suggestion of the first form history result.
+ // The heuristic's query should be the suggestion; the first form history
+ // result should not be included since it dupes the heuristic; the second form
+ // history result should not be included since it doesn't match; and both
+ // remote suggestions should be included.
+ let firstSuggestion = makeExpectedFormHistoryResults(context)[0].payload
+ .suggestion;
+ context = createContext(firstSuggestion, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedRemoteSuggestionResults(context, {
+ suggestionPrefix: firstSuggestion,
+ }),
+ ],
+ });
+
+ // Add these form history strings to use below.
+ let formHistoryStrings = ["foo", "foobar", "fooquux"];
+ await UrlbarTestUtils.formHistory.add(formHistoryStrings);
+
+ // Search for "foo". "foo" shouldn't be included since it dupes the
+ // heuristic. Both "foobar" and "fooquux" should be included even though the
+ // max form history count is only two and there are three matching form
+ // history results (including "foo").
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: ENGINE_NAME,
+ }),
+ ...makeExpectedRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ // Note that the second form history result appears after the remote
+ // suggestions. This isn't ideal because it should appear right after the
+ // first form history result, but it doesn't because the actual first form
+ // history result duped the heuristic, so the muxer discarded it.
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: ENGINE_NAME,
+ }),
+ ],
+ });
+
+ // Add a visit that matches "foo" and will autofill so that the heuristic is
+ // not a search result. Now the "foo" and "foobar" form history should be
+ // included.
+ await PlacesTestUtils.addVisits("http://foo.example.com/");
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://foo.example.com/",
+ title: "foo.example.com",
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foo",
+ engineName: ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: ENGINE_NAME,
+ }),
+ ...makeExpectedRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: ENGINE_NAME,
+ }),
+ ],
+ });
+ await PlacesUtils.history.clear();
+
+ // Add SERPs for "foobar" and "food" and search for "foo". The "foo" form
+ // history should be excluded since it dupes the heuristic; the "foobar" and
+ // "fooquux" form history should be included; the "food" SERP should be
+ // included since it doesn't dupe either form history result; and the "foobar"
+ // SERP depends on the match buckets, see below.
+ let engine = await Services.search.getDefault();
+ let [serpURL1] = UrlbarUtils.getSearchQueryUrl(engine, "foobar");
+ let [serpURL2] = UrlbarUtils.getSearchQueryUrl(engine, "food");
+ await PlacesTestUtils.addVisits([serpURL1, serpURL2]);
+
+ // First, use the MATCH_BUCKETS_VALUE that the test set above. General
+ // results appear before suggestions, which means that the muxer visits the
+ // "foobar" SERP before visiting the "foobar" form history, and so it doesn't
+ // see that the SERP dupes the form history. The "foobar" SERP is therefore
+ // included.
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeVisitResult(context, {
+ uri: "http://localhost:9000/search?terms=food",
+ title: "test visit for http://localhost:9000/search?terms=food",
+ }),
+ makeVisitResult(context, {
+ uri: "http://localhost:9000/search?terms=foobar",
+ title: "test visit for http://localhost:9000/search?terms=foobar",
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: ENGINE_NAME,
+ }),
+ ...makeExpectedRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: ENGINE_NAME,
+ }),
+ ],
+ });
+
+ // Now use Firefox's default match buckets, where suggestions appear before
+ // general results. Now the muxer will see that the "foobar" SERP dupes the
+ // "foobar" form history, so it will exclude the SERP.
+ Services.prefs.setCharPref(
+ "browser.urlbar.matchBuckets",
+ "suggestion:4,general:Infinity"
+ );
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ // Note that the remote suggestions appear in between the two form history
+ // results. Ideally the form history would appear together before the
+ // remote suggestions, but they don't because the actual first form
+ // history result duped the heuristic, so the muxer discarded it.
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: ENGINE_NAME,
+ }),
+ ...makeExpectedRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: ENGINE_NAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://localhost:9000/search?terms=food",
+ title: "test visit for http://localhost:9000/search?terms=food",
+ }),
+ ],
+ });
+ Services.prefs.setCharPref(
+ "browser.urlbar.matchBuckets",
+ MATCH_BUCKETS_VALUE
+ );
+
+ await PlacesUtils.history.clear();
+
+ await UrlbarTestUtils.formHistory.remove(formHistoryStrings);
+
+ await cleanUpSuggestions();
+ await PlacesUtils.history.clear();
+ Services.prefs.clearUserPref(MAX_FORM_HISTORY_PREF);
+});
diff --git a/browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js b/browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js
new file mode 100644
index 0000000000..09a671b635
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js
@@ -0,0 +1,359 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that an engine with suggestions works with our alias autocomplete
+ * behavior.
+ */
+
+const DEFAULT_ENGINE_NAME = "TestDefaultEngine";
+const SUGGESTIONS_ENGINE_NAME = "engine-suggestions.xml";
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+const HISTORY_TITLE = "fire";
+
+// We make sure that aliases and search terms are correctly recognized when they
+// are separated by each of these different types of spaces and combinations of
+// spaces. U+3000 is the ideographic space in CJK and is commonly used by CJK
+// speakers.
+const TEST_SPACES = [" ", "\u3000", " \u3000", "\u3000 "];
+
+let engine;
+
+add_task(async function setup() {
+ engine = await addTestSuggestionsEngine();
+
+ // Set a mock engine as the default so we don't hit the network below when we
+ // do searches that return the default engine heuristic result.
+ Services.search.defaultEngine = await Services.search.addEngineWithDetails(
+ DEFAULT_ENGINE_NAME,
+ { template: "http://example.com/?s=%S" }
+ );
+
+ // History matches should not appear with @aliases, so this visit should not
+ // appear when searching with @aliases below.
+ await PlacesTestUtils.addVisits({
+ uri: engine.searchForm,
+ title: HISTORY_TITLE,
+ });
+});
+
+// A non-token alias without a trailing space shouldn't be recognized as a
+// keyword. It should be treated as part of the search string.
+add_task(async function nonTokenAlias_noTrailingSpace() {
+ Services.prefs.setBoolPref(
+ "browser.search.separatePrivateDefault.ui.enabled",
+ false
+ );
+
+ let alias = "moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ let context = createContext(alias, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: DEFAULT_ENGINE_NAME,
+ query: alias,
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.clearUserPref(
+ "browser.search.separatePrivateDefault.ui.enabled"
+ );
+});
+
+// A non-token alias with a trailing space should be recognized as a keyword,
+// and the history result should be included.
+add_task(async function nonTokenAlias_trailingSpace() {
+ let alias = "moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let isPrivate of [false, true]) {
+ for (let spaces of TEST_SPACES) {
+ info(
+ "Testing: " + JSON.stringify({ isPrivate, spaces: codePoints(spaces) })
+ );
+ let context = createContext(alias + spaces, { isPrivate });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://localhost:9000/search?terms=",
+ title: HISTORY_TITLE,
+ }),
+ ],
+ });
+ }
+ }
+});
+
+// Search for "alias HISTORY_TITLE" with a non-token alias in a non-private
+// context. The remote suggestions and history result should be shown.
+add_task(async function nonTokenAlias_history_nonPrivate() {
+ let alias = "moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + HISTORY_TITLE, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ suggestion: `${HISTORY_TITLE} foo`,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ suggestion: `${HISTORY_TITLE} bar`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://localhost:9000/search?terms=",
+ title: HISTORY_TITLE,
+ }),
+ ],
+ });
+ }
+});
+
+// Search for "alias HISTORY_TITLE" with a non-token alias in a private context.
+// The history result should be shown, but not the remote suggestions.
+add_task(async function nonTokenAlias_history_private() {
+ let alias = "moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + HISTORY_TITLE, {
+ isPrivate: true,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://localhost:9000/search?terms=",
+ title: HISTORY_TITLE,
+ }),
+ ],
+ });
+ }
+});
+
+// A token alias without a trailing space should be autofilled with a trailing
+// space and recognized as a keyword with a keyword offer.
+add_task(async function tokenAlias_noTrailingSpace() {
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let isPrivate of [false, true]) {
+ let context = createContext(alias, { isPrivate });
+ await check_results({
+ context,
+ autofilled: alias + " ",
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ providesSearchMode: true,
+ query: "",
+ heuristic: false,
+ }),
+ ],
+ });
+ }
+});
+
+// A token alias with a trailing space should be recognized as a keyword without
+// a keyword offer.
+add_task(async function tokenAlias_trailingSpace() {
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let isPrivate of [false, true]) {
+ for (let spaces of TEST_SPACES) {
+ info(
+ "Testing: " + JSON.stringify({ isPrivate, spaces: codePoints(spaces) })
+ );
+ let context = createContext(alias + spaces, { isPrivate });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "",
+ heuristic: true,
+ }),
+ ],
+ });
+ }
+ }
+});
+
+// Search for "alias HISTORY_TITLE" with a token alias in a non-private context.
+// The remote suggestions should be shown, but not the history result.
+add_task(async function tokenAlias_history_nonPrivate() {
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + HISTORY_TITLE, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ suggestion: `${HISTORY_TITLE} foo`,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ suggestion: `${HISTORY_TITLE} bar`,
+ }),
+ ],
+ });
+ }
+});
+
+// Search for "alias HISTORY_TITLE" with a token alias in a private context.
+// Neither the history result nor the remote suggestions should be shown.
+add_task(async function tokenAlias_history_private() {
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + HISTORY_TITLE, {
+ isPrivate: true,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ heuristic: true,
+ }),
+ ],
+ });
+ }
+});
+
+// Even when they're disabled, suggestions should still be returned when using a
+// token alias in a non-private context.
+add_task(async function suggestionsDisabled_nonPrivate() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + "term", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "term",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "term",
+ suggestion: "term foo",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "term",
+ suggestion: "term bar",
+ }),
+ ],
+ });
+ }
+ Services.prefs.clearUserPref(SUGGEST_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+});
+
+// Suggestions should not be returned when using a token alias in a private
+// context.
+add_task(async function suggestionsDisabled_private() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + "term", { isPrivate: true });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "term",
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.clearUserPref(SUGGEST_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ }
+});
+
+/**
+ * Returns an array of code points in the given string. Each code point is
+ * returned as a hexidecimal string.
+ *
+ * @param {string} str
+ * The code points of this string will be returned.
+ * @returns {array}
+ * Array of code points in the string, where each is a hexidecimal string.
+ */
+function codePoints(str) {
+ return str.split("").map(s => s.charCodeAt(0).toString(16));
+}
diff --git a/browser/components/urlbar/tests/unit/test_search_suggestions_tail.js b/browser/components/urlbar/tests/unit/test_search_suggestions_tail.js
new file mode 100644
index 0000000000..6d690c1b99
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_suggestions_tail.js
@@ -0,0 +1,358 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that tailed search engine suggestions are returned by
+ * UrlbarProviderSearchSuggestions when available.
+ */
+
+const { FormHistory } = ChromeUtils.import(
+ "resource://gre/modules/FormHistory.jsm"
+);
+
+const ENGINE_NAME = "engine-tail-suggestions.xml";
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+const PRIVATE_SEARCH_PREF = "browser.search.separatePrivateDefault.ui.enabled";
+const TAIL_SUGGESTIONS_PREF = "browser.urlbar.richSuggestions.tail";
+
+var suggestionsFn;
+var previousSuggestionsFn;
+
+/**
+ * Set the current suggestion funciton.
+ * @param {function} fn
+ * A function that that a search string and returns an array of strings that
+ * will be used as search suggestions.
+ * Note: `fn` should return > 1 suggestion in most cases. Otherwise, you may
+ * encounter unexceptede behaviour with UrlbarProviderSuggestion's
+ * _lastLowResultsSearchSuggestion safeguard.
+ */
+function setSuggestionsFn(fn) {
+ previousSuggestionsFn = suggestionsFn;
+ suggestionsFn = fn;
+}
+
+async function cleanup() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
+
+async function cleanUpSuggestions() {
+ await cleanup();
+ if (previousSuggestionsFn) {
+ suggestionsFn = previousSuggestionsFn;
+ previousSuggestionsFn = null;
+ }
+}
+
+add_task(async function setup() {
+ Services.prefs.setCharPref(
+ "browser.urlbar.matchBuckets",
+ "general:5,suggestion:Infinity"
+ );
+
+ let engine = await addTestTailSuggestionsEngine(searchStr => {
+ return suggestionsFn(searchStr);
+ });
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["toronto", "tunisia"];
+ return [
+ "what time is it in t",
+ suffixes.map(s => searchStr + s.slice(1)),
+ [],
+ {
+ "google:irrelevantparameter": [],
+ "google:suggestdetail": suffixes.map(s => ({
+ mp: "… ",
+ t: s,
+ })),
+ },
+ ];
+ });
+
+ // Install the test engine.
+ let oldDefaultEngine = await Services.search.getDefault();
+ registerCleanupFunction(async () => {
+ Services.search.setDefault(oldDefaultEngine);
+ Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF);
+ Services.prefs.clearUserPref(TAIL_SUGGESTIONS_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ });
+ Services.search.setDefault(engine);
+ Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false);
+ Services.prefs.setBoolPref(TAIL_SUGGESTIONS_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+});
+
+/**
+ * Tests that non-tail suggestion providers still return results correctly when
+ * the tailSuggestions pref is enabled.
+ */
+add_task(async function normal_suggestions_provider() {
+ let engine = await addTestSuggestionsEngine();
+ let tailEngine = await Services.search.getDefault();
+ Services.search.setDefault(engine);
+
+ const query = "hello world";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: "engine-suggestions.xml",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: "engine-suggestions.xml",
+ suggestion: query + " foo",
+ }),
+ makeSearchResult(context, {
+ engineName: "engine-suggestions.xml",
+ suggestion: query + " bar",
+ }),
+ ],
+ });
+
+ Services.search.setDefault(tailEngine);
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests a suggestions provider that returns only tail suggestions.
+ */
+add_task(async function basic_tail() {
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: query + "oronto",
+ tail: "toronto",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: query + "unisia",
+ tail: "tunisia",
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests a suggestions provider that returns both normal and tail suggestions.
+ * Only normal results should be shown.
+ */
+add_task(async function mixed_suggestions() {
+ // When normal suggestions are mixed with tail suggestions, they appear at the
+ // correct position in the google:suggestdetail array as empty objects.
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["toronto", "tunisia"];
+ return [
+ "what time is it in t",
+ ["what is the time today texas"].concat(
+ suffixes.map(s => searchStr + s.slice(1))
+ ),
+ [],
+ {
+ "google:irrelevantparameter": [],
+ "google:suggestdetail": [{}].concat(
+ suffixes.map(s => ({
+ mp: "… ",
+ t: s,
+ }))
+ ),
+ },
+ ];
+ });
+
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "what is the time today texas",
+ tail: undefined,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests a suggestions provider that returns both normal and tail suggestions,
+ * with tail suggestions listed before normal suggestions. In the real world
+ * we don't expect that to happen, but we should handle it by showing only the
+ * normal suggestions.
+ */
+add_task(async function mixed_suggestions_tail_first() {
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["toronto", "tunisia"];
+ return [
+ "what time is it in t",
+ suffixes
+ .map(s => searchStr + s.slice(1))
+ .concat(["what is the time today texas"]),
+ [],
+ {
+ "google:irrelevantparameter": [],
+ "google:suggestdetail": suffixes
+ .map(s => ({
+ mp: "… ",
+ t: s,
+ }))
+ .concat([{}]),
+ },
+ ];
+ });
+
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "what is the time today texas",
+ tail: undefined,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests a search that returns history results, bookmark results and tail
+ * suggestions. Only the history and bookmark results should be shown.
+ */
+add_task(async function mixed_results() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI("http://example.com/1"),
+ title: "what time is",
+ },
+ ]);
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/2",
+ title: "what time is",
+ });
+
+ // Tail suggestions should not be shown.
+ const query = "what time is";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/2",
+ title: "what time is",
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/1",
+ title: "what time is",
+ }),
+ ],
+ });
+
+ // Once we make the query specific enough to exclude the history and bookmark
+ // results, we should show tail suggestions.
+ const tQuery = "what time is it in t";
+ context = createContext(tQuery, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: tQuery + "oronto",
+ tail: "toronto",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: tQuery + "unisia",
+ tail: "tunisia",
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests that tail suggestions are deduped if their full-text form is a dupe of
+ * a local search suggestion. Remaining tail suggestions should also not be
+ * shown since we do not mix tail and non-tail suggestions.
+ */
+add_task(async function dedupe_local() {
+ Services.prefs.setIntPref("browser.urlbar.maxHistoricalSearchSuggestions", 1);
+ await UrlbarTestUtils.formHistory.add(["what time is it in toronto"]);
+
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeFormHistoryResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: query + "oronto",
+ }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("browser.urlbar.maxHistoricalSearchSuggestions");
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests that the correct number of suggestion results are displayed if
+ * maxResults is limited, even when tail suggestions are returned.
+ */
+add_task(async function limit_results() {
+ await UrlbarTestUtils.formHistory.clear();
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ context.maxResults = 2;
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: query + "oronto",
+ tail: "toronto",
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests that tail suggestions are hidden if the pref is disabled.
+ */
+add_task(async function disable_pref() {
+ let oldPrefValue = Services.prefs.getBoolPref(TAIL_SUGGESTIONS_PREF);
+ Services.prefs.setBoolPref(TAIL_SUGGESTIONS_PREF, false);
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ Services.prefs.setBoolPref(TAIL_SUGGESTIONS_PREF, oldPrefValue);
+ await cleanUpSuggestions();
+});
diff --git a/browser/components/urlbar/tests/unit/test_tokenizer.js b/browser/components/urlbar/tests/unit/test_tokenizer.js
new file mode 100644
index 0000000000..22dc629a46
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_tokenizer.js
@@ -0,0 +1,450 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_tokenizer() {
+ let testContexts = [
+ { desc: "Empty string", searchString: "", expectedTokens: [] },
+ { desc: "Spaces string", searchString: " ", expectedTokens: [] },
+ {
+ desc: "Single word string",
+ searchString: "test",
+ expectedTokens: [{ value: "test", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "Multi word string with mixed whitespace types",
+ searchString: " test1 test2\u1680test3\u2004test4\u1680",
+ expectedTokens: [
+ { value: "test1", type: UrlbarTokenizer.TYPE.TEXT },
+ { value: "test2", type: UrlbarTokenizer.TYPE.TEXT },
+ { value: "test3", type: UrlbarTokenizer.TYPE.TEXT },
+ { value: "test4", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "separate restriction char at beginning",
+ searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARK} test`,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "separate restriction char at end",
+ searchString: `test ${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ expectedTokens: [
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ ],
+ },
+ {
+ desc: "boundary restriction char at end",
+ searchString: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ expectedTokens: [
+ {
+ value: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ ],
+ },
+ {
+ desc: "boundary search restriction char at end",
+ searchString: `test${UrlbarTokenizer.RESTRICT.SEARCH}`,
+ expectedTokens: [
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ {
+ value: UrlbarTokenizer.RESTRICT.SEARCH,
+ type: UrlbarTokenizer.TYPE.RESTRICT_SEARCH,
+ },
+ ],
+ },
+ {
+ desc: "separate restriction char in the middle",
+ searchString: `test ${UrlbarTokenizer.RESTRICT.BOOKMARK} test`,
+ expectedTokens: [
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "restriction char in the middle",
+ searchString: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}test`,
+ expectedTokens: [
+ {
+ value: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}test`,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ ],
+ },
+ {
+ desc: "restriction char in the middle 2",
+ searchString: `test${UrlbarTokenizer.RESTRICT.BOOKMARK} test`,
+ expectedTokens: [
+ {
+ value: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ { value: `test`, type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "double boundary restriction char",
+ searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARK}test${UrlbarTokenizer.RESTRICT.TITLE}`,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ {
+ value: `test${UrlbarTokenizer.RESTRICT.TITLE}`,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ ],
+ },
+ {
+ desc: "double non-combinable restriction char, single char string",
+ searchString: `t${UrlbarTokenizer.RESTRICT.BOOKMARK}${UrlbarTokenizer.RESTRICT.SEARCH}`,
+ expectedTokens: [
+ {
+ value: `t${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ {
+ value: UrlbarTokenizer.RESTRICT.SEARCH,
+ type: UrlbarTokenizer.TYPE.RESTRICT_SEARCH,
+ },
+ ],
+ },
+ {
+ desc: "only boundary restriction chars",
+ searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARK}${UrlbarTokenizer.RESTRICT.TITLE}`,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ {
+ value: UrlbarTokenizer.RESTRICT.TITLE,
+ type: UrlbarTokenizer.TYPE.RESTRICT_TITLE,
+ },
+ ],
+ },
+ {
+ desc: "only the boundary restriction char",
+ searchString: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ ],
+ },
+ // Some restriction chars may be # or ?, that are also valid path parts.
+ // The next 2 tests will check we consider those as part of url paths.
+ {
+ desc: "boundary # char on path",
+ searchString: "test/#",
+ expectedTokens: [
+ { value: "test/#", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "boundary ? char on path",
+ searchString: "test/?",
+ expectedTokens: [
+ { value: "test/?", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "multiple boundary restriction chars suffix",
+ searchString: `test ${UrlbarTokenizer.RESTRICT.HISTORY} ${UrlbarTokenizer.RESTRICT.TAG}`,
+ expectedTokens: [
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ {
+ value: UrlbarTokenizer.RESTRICT.HISTORY,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ {
+ value: UrlbarTokenizer.RESTRICT.TAG,
+ type: UrlbarTokenizer.TYPE.RESTRICT_TAG,
+ },
+ ],
+ },
+ {
+ desc: "multiple boundary restriction chars prefix",
+ searchString: `${UrlbarTokenizer.RESTRICT.HISTORY} ${UrlbarTokenizer.RESTRICT.TAG} test`,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.HISTORY,
+ type: UrlbarTokenizer.TYPE.RESTRICT_HISTORY,
+ },
+ {
+ value: UrlbarTokenizer.RESTRICT.TAG,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "Math with division",
+ searchString: "3.6/1.2",
+ expectedTokens: [{ value: "3.6/1.2", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "ipv4 in bookmarks",
+ searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARK} 192.168.1.1:8`,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ { value: "192.168.1.1:8", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
+ ],
+ },
+ {
+ desc: "email",
+ searchString: "test@mozilla.com",
+ expectedTokens: [
+ { value: "test@mozilla.com", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "email2",
+ searchString: "test.test@mozilla.co.uk",
+ expectedTokens: [
+ { value: "test.test@mozilla.co.uk", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "protocol",
+ searchString: "http://test",
+ expectedTokens: [
+ { value: "http://test", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc:
+ "bogus protocol with host (we allow visits to http://///example.com)",
+ searchString: "http:///test",
+ expectedTokens: [
+ { value: "http:///test", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "file protocol with path",
+ searchString: "file:///home",
+ expectedTokens: [
+ { value: "file:///home", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "almost a protocol",
+ searchString: "http:",
+ expectedTokens: [
+ { value: "http:", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "almost a protocol 2",
+ searchString: "http:/",
+ expectedTokens: [
+ { value: "http:/", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "bogus protocol (we allow visits to http://///example.com)",
+ searchString: "http:///",
+ expectedTokens: [
+ { value: "http:///", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "file protocol",
+ searchString: "file:///",
+ expectedTokens: [
+ { value: "file:///", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "userinfo",
+ searchString: "user:pass@test",
+ expectedTokens: [
+ { value: "user:pass@test", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
+ ],
+ },
+ {
+ desc: "domain",
+ searchString: "www.mozilla.org",
+ expectedTokens: [
+ {
+ value: "www.mozilla.org",
+ type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN,
+ },
+ ],
+ },
+ {
+ desc: "data uri",
+ searchString: "data:text/plain,Content",
+ expectedTokens: [
+ {
+ value: "data:text/plain,Content",
+ type: UrlbarTokenizer.TYPE.POSSIBLE_URL,
+ },
+ ],
+ },
+ {
+ desc: "ipv6",
+ searchString: "[2001:db8::1]",
+ expectedTokens: [
+ { value: "[2001:db8::1]", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
+ ],
+ },
+ {
+ desc: "numeric domain",
+ searchString: "test1001.com",
+ expectedTokens: [
+ { value: "test1001.com", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
+ ],
+ },
+ {
+ desc: "invalid ip",
+ searchString: "192.2134.1.2",
+ expectedTokens: [
+ { value: "192.2134.1.2", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "ipv4",
+ searchString: "1.2.3.4",
+ expectedTokens: [
+ { value: "1.2.3.4", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
+ ],
+ },
+ {
+ desc: "host/path",
+ searchString: "test/test",
+ expectedTokens: [
+ { value: "test/test", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "percent encoded string",
+ searchString: "%E6%97%A5%E6%9C%AC",
+ expectedTokens: [
+ { value: "%E6%97%A5%E6%9C%AC", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "Uppercase",
+ searchString: "TEST",
+ expectedTokens: [{ value: "TEST", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "Mixed case 1",
+ searchString: "TeSt",
+ expectedTokens: [{ value: "TeSt", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "Mixed case 2",
+ searchString: "tEsT",
+ expectedTokens: [{ value: "tEsT", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "Uppercase with spaces",
+ searchString: "TEST EXAMPLE",
+ expectedTokens: [
+ { value: "TEST", type: UrlbarTokenizer.TYPE.TEXT },
+ { value: "EXAMPLE", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "Mixed case with spaces",
+ searchString: "TeSt eXaMpLe",
+ expectedTokens: [
+ { value: "TeSt", type: UrlbarTokenizer.TYPE.TEXT },
+ { value: "eXaMpLe", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "plain number",
+ searchString: "1001",
+ expectedTokens: [{ value: "1001", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "data uri with spaces",
+ searchString: "data:text/html,oh hi?",
+ expectedTokens: [
+ {
+ value: "data:text/html,oh hi?",
+ type: UrlbarTokenizer.TYPE.POSSIBLE_URL,
+ },
+ ],
+ },
+ {
+ desc: "data uri with spaces ignored with other tokens",
+ searchString: "hi data:text/html,oh hi?",
+ expectedTokens: [
+ {
+ value: "hi",
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ {
+ value: "data:text/html,oh",
+ type: UrlbarTokenizer.TYPE.POSSIBLE_URL,
+ },
+ {
+ value: "hi",
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ {
+ value: UrlbarTokenizer.RESTRICT.SEARCH,
+ type: UrlbarTokenizer.TYPE.RESTRICT_SEARCH,
+ },
+ ],
+ },
+ {
+ desc: "whitelisted host",
+ searchString: "test whitelisted",
+ expectedTokens: [
+ {
+ value: "test",
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ {
+ value: "whitelisted",
+ type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN,
+ },
+ ],
+ },
+ ];
+
+ Services.prefs.setBoolPref("browser.fixup.domainwhitelist.whitelisted", true);
+
+ for (let queryContext of testContexts) {
+ info(queryContext.desc);
+ queryContext.trimmedSearchString = queryContext.searchString.trim();
+ for (let token of queryContext.expectedTokens) {
+ token.lowerCaseValue = token.value.toLocaleLowerCase();
+ }
+ let newQueryContext = UrlbarTokenizer.tokenize(queryContext);
+ Assert.equal(
+ queryContext,
+ newQueryContext,
+ "The queryContext object is the same"
+ );
+ Assert.deepEqual(
+ queryContext.tokens,
+ queryContext.expectedTokens,
+ "Check the expected tokens"
+ );
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_trimming.js b/browser/components/urlbar/tests/unit/test_trimming.js
new file mode 100644
index 0000000000..1d275fef3a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_trimming.js
@@ -0,0 +1,222 @@
+/* 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/. */
+
+add_task(async function setup() {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+});
+
+add_task(async function test_untrimmed_secure_www() {
+ info("Searching for untrimmed https://www entry");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("https://www.mozilla.org/test/"),
+ });
+ let context = createContext("mo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "https://www.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.mozilla.org/",
+ title: "https://www.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://www.mozilla.org/test/",
+ title: "test visit for https://www.mozilla.org/test/",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_secure_www_path() {
+ info("Searching for untrimmed https://www entry with path");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("https://www.mozilla.org/test/"),
+ });
+ let context = createContext("mozilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/test/",
+ completed: "https://www.mozilla.org/test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.mozilla.org/test/",
+ title: "https://www.mozilla.org/test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_secure() {
+ info("Searching for untrimmed https:// entry");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("https://mozilla.org/test/"),
+ });
+ let context = createContext("mo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "https://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://mozilla.org/",
+ title: "https://mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://mozilla.org/test/",
+ title: "test visit for https://mozilla.org/test/",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_secure_path() {
+ info("Searching for untrimmed https:// entry with path");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("https://mozilla.org/test/"),
+ });
+ let context = createContext("mozilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/test/",
+ completed: "https://mozilla.org/test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://mozilla.org/test/",
+ title: "https://mozilla.org/test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_www() {
+ info("Searching for untrimmed http://www entry");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://www.mozilla.org/test/"),
+ });
+ let context = createContext("mo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "http://www.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/",
+ title: "www.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/test/",
+ title: "test visit for http://www.mozilla.org/test/",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_www_path() {
+ info("Searching for untrimmed http://www entry with path");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://www.mozilla.org/test/"),
+ });
+ let context = createContext("mozilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/test/",
+ completed: "http://www.mozilla.org/test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/test/",
+ title: "www.mozilla.org/test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_ftp() {
+ info("Searching for untrimmed ftp:// entry");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("ftp://mozilla.org/test/"),
+ });
+ let context = createContext("mo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "ftp://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "ftp://mozilla.org/",
+ title: "ftp://mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://mozilla.org/test/",
+ title: "test visit for ftp://mozilla.org/test/",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_ftp_path() {
+ info("Searching for untrimmed ftp:// entry with path");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("ftp://mozilla.org/test/"),
+ });
+ let context = createContext("mozilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/test/",
+ completed: "ftp://mozilla.org/test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "ftp://mozilla.org/test/",
+ title: "ftp://mozilla.org/test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_escaped_chars() {
+ info("Searching for URL with characters that are normally escaped");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("https://www.mozilla.org/啊-test"),
+ });
+ let context = createContext("https://www.mozilla.org/啊-test", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "https://www.mozilla.org/%E5%95%8A-test",
+ title: "https://www.mozilla.org/啊-test",
+ iconUri: "page-icon:https://www.mozilla.org/",
+ heuristic: true,
+ }),
+ // UnifiedComplete escapes this character.
+ makeVisitResult(context, {
+ uri: "https://www.mozilla.org/%E5%95%8A-test",
+ title: "test visit for https://www.mozilla.org/%E5%95%8A-test",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/xpcshell.ini b/browser/components/urlbar/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..555f8ac7ce
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/xpcshell.ini
@@ -0,0 +1,54 @@
+[DEFAULT]
+head = head.js
+firefox-appdir = browser
+support-files =
+ data/engine-suggestions.xml
+ data/engine-tail-suggestions.xml
+
+[test_autofill_about_urls.js]
+[test_autofill_bookmarked.js]
+[test_autofill_functional.js]
+[test_autofill_origins.js]
+[test_autofill_originsAndQueries.js]
+[test_autofill_prefix_fallback.js]
+[test_autofill_search_engines.js]
+[test_autofill_search_engine_aliases.js]
+[test_autofill_urls.js]
+[test_avoid_middle_complete.js]
+[test_avoid_stripping_to_empty_tokens.js]
+[test_casing.js]
+[test_dedupe_prefix.js]
+[test_dupe_urls.js]
+[test_encoded_urls.js]
+[test_heuristic_cancel.js]
+[test_keywords.js]
+skip-if = os == 'linux' # bug 1474616
+[test_muxer.js]
+[test_providerHeuristicFallback.js]
+[test_providerOmnibox.js]
+[test_providerOpenTabs.js]
+[test_providersManager.js]
+[test_providersManager_filtering.js]
+[test_providersManager_maxResults.js]
+[test_providerTabToSearch.js]
+[test_providerTabToSearch_partialHost.js]
+[test_providerUnifiedComplete.js]
+[test_providerUnifiedComplete_duplicate_entries.js]
+[test_query_url.js]
+[test_queryScorer.js]
+[test_search_engine_host.js]
+[test_search_suggestions.js]
+[test_search_suggestions_aliases.js]
+[test_search_suggestions_tail.js]
+[test_tokenizer.js]
+[test_trimming.js]
+[test_UrlbarController_integration.js]
+[test_UrlbarController_telemetry.js]
+[test_UrlbarController_unit.js]
+[test_UrlbarPrefs.js]
+[test_UrlbarQueryContext.js]
+[test_UrlbarQueryContext_restrictSource.js]
+[test_UrlbarSearchUtils.jsm]
+[test_UrlbarUtils_addToUrlbarHistory.js]
+[test_UrlbarUtils_getShortcutOrURIAndPostData.js]
+[test_UrlbarUtils_getTokenMatches.js]