summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/tests/unit
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/urlbar/tests/unit')
-rw-r--r--browser/components/urlbar/tests/unit/data/engine.xml10
-rw-r--r--browser/components/urlbar/tests/unit/head.js1127
-rw-r--r--browser/components/urlbar/tests/unit/test_000_frecency.js245
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarController_integration.js83
-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.js449
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarQueryContext.js73
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js132
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.js462
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarUtils_addToUrlbarHistory.js63
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js249
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarUtils_getTokenMatches.js294
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarUtils_unEscapeURIForUI.js36
-rw-r--r--browser/components/urlbar/tests/unit/test_about_urls.js176
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_adaptiveHistory.js1441
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_bookmarked.js150
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_do_not_trim.js140
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_functional.js115
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_origins.js1041
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js2471
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_origins_alt_frecency.js243
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js76
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js85
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_search_engines.js246
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_urls.js881
-rw-r--r--browser/components/urlbar/tests/unit/test_avoid_middle_complete.js270
-rw-r--r--browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js119
-rw-r--r--browser/components/urlbar/tests/unit/test_calculator.js46
-rw-r--r--browser/components/urlbar/tests/unit/test_casing.js370
-rw-r--r--browser/components/urlbar/tests/unit/test_dedupe_prefix.js277
-rw-r--r--browser/components/urlbar/tests/unit/test_dedupe_switchTab.js34
-rw-r--r--browser/components/urlbar/tests/unit/test_download_embed_bookmarks.js137
-rw-r--r--browser/components/urlbar/tests/unit/test_empty_search.js181
-rw-r--r--browser/components/urlbar/tests/unit/test_encoded_urls.js97
-rw-r--r--browser/components/urlbar/tests/unit/test_escaping_badEscapedURI.js37
-rw-r--r--browser/components/urlbar/tests/unit/test_escaping_escapeSelf.js62
-rw-r--r--browser/components/urlbar/tests/unit/test_exposure.js195
-rw-r--r--browser/components/urlbar/tests/unit/test_frecency.js403
-rw-r--r--browser/components/urlbar/tests/unit/test_frecency_alternative_nimbus.js50
-rw-r--r--browser/components/urlbar/tests/unit/test_heuristic_cancel.js238
-rw-r--r--browser/components/urlbar/tests/unit/test_hideSponsoredHistory.js104
-rw-r--r--browser/components/urlbar/tests/unit/test_keywords.js212
-rw-r--r--browser/components/urlbar/tests/unit/test_l10nCache.js685
-rw-r--r--browser/components/urlbar/tests/unit/test_local_suggest_prefs.js126
-rw-r--r--browser/components/urlbar/tests/unit/test_match_javascript.js155
-rw-r--r--browser/components/urlbar/tests/unit/test_multi_word_search.js126
-rw-r--r--browser/components/urlbar/tests/unit/test_muxer.js731
-rw-r--r--browser/components/urlbar/tests/unit/test_protocol_ignore.js42
-rw-r--r--browser/components/urlbar/tests/unit/test_protocol_swap.js303
-rw-r--r--browser/components/urlbar/tests/unit/test_providerAliasEngines.js114
-rw-r--r--browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js691
-rw-r--r--browser/components/urlbar/tests/unit/test_providerHistoryUrlHeuristic.js197
-rw-r--r--browser/components/urlbar/tests/unit/test_providerKeywords.js360
-rw-r--r--browser/components/urlbar/tests/unit/test_providerOmnibox.js887
-rw-r--r--browser/components/urlbar/tests/unit/test_providerOpenTabs.js45
-rw-r--r--browser/components/urlbar/tests/unit/test_providerPlaces.js250
-rw-r--r--browser/components/urlbar/tests/unit/test_providerPlaces_duplicate_entries.js42
-rw-r--r--browser/components/urlbar/tests/unit/test_providerPlaces_nonEnglish.js43
-rw-r--r--browser/components/urlbar/tests/unit/test_providerPreloaded.js578
-rw-r--r--browser/components/urlbar/tests/unit/test_providerTabToSearch.js535
-rw-r--r--browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js214
-rw-r--r--browser/components/urlbar/tests/unit/test_providersManager.js74
-rw-r--r--browser/components/urlbar/tests/unit/test_providersManager_filtering.js407
-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.js121
-rw-r--r--browser/components/urlbar/tests/unit/test_quickactions.js126
-rw-r--r--browser/components/urlbar/tests/unit/test_remote_tabs.js695
-rw-r--r--browser/components/urlbar/tests/unit/test_resultGroups.js1576
-rw-r--r--browser/components/urlbar/tests/unit/test_search_engine_host.js98
-rw-r--r--browser/components/urlbar/tests/unit/test_search_engine_restyle.js124
-rw-r--r--browser/components/urlbar/tests/unit/test_search_suggestions.js2078
-rw-r--r--browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js364
-rw-r--r--browser/components/urlbar/tests/unit/test_search_suggestions_tail.js379
-rw-r--r--browser/components/urlbar/tests/unit/test_special_search.js543
-rw-r--r--browser/components/urlbar/tests/unit/test_suggestedIndex.js562
-rw-r--r--browser/components/urlbar/tests/unit/test_suggestedIndexRelativeToGroup.js596
-rw-r--r--browser/components/urlbar/tests/unit/test_tab_matches.js354
-rw-r--r--browser/components/urlbar/tests/unit/test_tags_caseInsensitivity.js137
-rw-r--r--browser/components/urlbar/tests/unit/test_tags_extendedUnicode.js66
-rw-r--r--browser/components/urlbar/tests/unit/test_tags_general.js207
-rw-r--r--browser/components/urlbar/tests/unit/test_tags_matchBookmarkTitles.js42
-rw-r--r--browser/components/urlbar/tests/unit/test_tags_returnedInSearches.js125
-rw-r--r--browser/components/urlbar/tests/unit/test_tokenizer.js449
-rw-r--r--browser/components/urlbar/tests/unit/test_trimming.js171
-rw-r--r--browser/components/urlbar/tests/unit/test_unitConversion.js504
-rw-r--r--browser/components/urlbar/tests/unit/test_word_boundary_search.js403
-rw-r--r--browser/components/urlbar/tests/unit/xpcshell.ini99
89 files changed, 30631 insertions, 0 deletions
diff --git a/browser/components/urlbar/tests/unit/data/engine.xml b/browser/components/urlbar/tests/unit/data/engine.xml
new file mode 100644
index 0000000000..61d776655f
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/data/engine.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine.xml</ShortName>
+<Description>A test search engine</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Url type="text/html" method="GET" template="http://www.example.com/">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<SearchForm>http://www.example.com/</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..8f15b0280e
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/head.js
@@ -0,0 +1,1127 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+var { UrlbarMuxer, UrlbarProvider, UrlbarQueryContext, UrlbarUtils } =
+ ChromeUtils.importESModule("resource:///modules/UrlbarUtils.sys.mjs");
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
+ SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs",
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+ UrlbarController: "resource:///modules/UrlbarController.sys.mjs",
+ UrlbarInput: "resource:///modules/UrlbarInput.sys.mjs",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
+ UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
+ UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ HttpServer: "resource://testing-common/httpd.js",
+});
+
+XPCOMUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => {
+ const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/QuickSuggestTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+XPCOMUtils.defineLazyGetter(this, "MerinoTestUtils", () => {
+ const { MerinoTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/MerinoTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
+ const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+XPCOMUtils.defineLazyGetter(this, "PlacesFrecencyRecalculator", () => {
+ return Cc["@mozilla.org/places/frecency-recalculator;1"].getService(
+ Ci.nsIObserver
+ ).wrappedJSObject;
+});
+
+SearchTestUtils.init(this);
+AddonTestUtils.init(this, false);
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+const SUGGESTIONS_ENGINE_NAME = "Suggestions";
+const TAIL_SUGGESTIONS_ENGINE_NAME = "Tail Suggestions";
+
+/**
+ * Gets the database connection. If the Places connection is invalid it will
+ * try to create a new connection.
+ *
+ * @param [optional] aForceNewConnection
+ * Forces creation of a new connection to the database. When a
+ * connection is asyncClosed it cannot anymore schedule async statements,
+ * though connectionReady will keep returning true (Bug 726990).
+ *
+ * @returns The database connection or null if unable to get one.
+ */
+var gDBConn;
+function DBConn(aForceNewConnection) {
+ if (!aForceNewConnection) {
+ let db = PlacesUtils.history.DBConnection;
+ if (db.connectionReady) {
+ return db;
+ }
+ }
+
+ // If the Places database connection has been closed, create a new connection.
+ if (!gDBConn || aForceNewConnection) {
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("places.sqlite");
+ let dbConn = (gDBConn = Services.storage.openDatabase(file));
+
+ TestUtils.topicObserved("profile-before-change").then(() =>
+ dbConn.asyncClose()
+ );
+ }
+
+ return gDBConn.connectionReady ? gDBConn : null;
+}
+
+/**
+ * @param {string} searchString The search string to insert into the context.
+ * @param {object} properties Overrides for the default values.
+ * @returns {UrlbarQueryContext} Creates a dummy query context with pre-filled
+ * required options.
+ */
+function createContext(searchString = "foo", properties = {}) {
+ info(`Creating new queryContext with searchString: ${searchString}`);
+ let context = new UrlbarQueryContext(
+ Object.assign(
+ {
+ allowAutofill: UrlbarPrefs.get("autoFill"),
+ isPrivate: true,
+ maxResults: UrlbarPrefs.get("maxRichResults"),
+ searchString,
+ },
+ properties
+ )
+ );
+ context.view = {
+ get visibleResults() {
+ return context.results;
+ },
+ controller: {
+ removeResult() {},
+ },
+ acknowledgeDismissal() {},
+ };
+ UrlbarTokenizer.tokenize(context);
+ return context;
+}
+
+/**
+ * Waits for the given notification from the supplied controller.
+ *
+ * @param {UrlbarController} controller The controller to wait for a response from.
+ * @param {string} notification The name of the notification to wait for.
+ * @param {boolean} expected Wether the notification is expected.
+ * @returns {Promise} A promise that is resolved with the arguments supplied to
+ * the notification.
+ */
+function promiseControllerNotification(
+ controller,
+ notification,
+ expected = true
+) {
+ return new Promise((resolve, reject) => {
+ let proxifiedObserver = new Proxy(
+ {},
+ {
+ get: (target, name) => {
+ if (name == notification) {
+ return (...args) => {
+ controller.removeQueryListener(proxifiedObserver);
+ if (expected) {
+ resolve(args);
+ } else {
+ reject();
+ }
+ };
+ }
+ return () => false;
+ },
+ }
+ );
+ controller.addQueryListener(proxifiedObserver);
+ });
+}
+
+/**
+ * A basic test provider, returning all the provided matches.
+ */
+class TestProvider extends UrlbarTestUtils.TestProvider {
+ isActive(context) {
+ Assert.ok(context, "context is passed-in");
+ return true;
+ }
+ getPriority(context) {
+ Assert.ok(context, "context is passed-in");
+ return 0;
+ }
+ async startQuery(context, add) {
+ Assert.ok(context, "context is passed-in");
+ Assert.equal(typeof add, "function", "add is a callback");
+ this._context = context;
+ for (const result of this._results) {
+ add(this, result);
+ }
+ }
+ cancelQuery(context) {
+ // If the query was created but didn't run, this._context will be undefined.
+ if (this._context) {
+ Assert.equal(this._context, context, "cancelQuery: context is the same");
+ }
+ if (this._onCancel) {
+ this._onCancel();
+ }
+ }
+}
+
+function convertToUtf8(str) {
+ return String.fromCharCode(...new TextEncoder().encode(str));
+}
+
+/**
+ * Helper function to clear the existing providers and register a basic provider
+ * that returns only the results given.
+ *
+ * @param {Array} results The results for the provider to return.
+ * @param {Function} [onCancel] Optional, called when the query provider
+ * receives a cancel instruction.
+ * @param {UrlbarUtils.PROVIDER_TYPE} type The provider type.
+ * @param {string} [name] Optional, use as the provider name.
+ * If none, a default name is chosen.
+ * @returns {UrlbarProvider} The provider
+ */
+function registerBasicTestProvider(results = [], onCancel, type, name) {
+ let provider = new TestProvider({ results, onCancel, type, name });
+ UrlbarProvidersManager.registerProvider(provider);
+ registerCleanupFunction(() =>
+ UrlbarProvidersManager.unregisterProvider(provider)
+ );
+ return provider;
+}
+
+// Creates an HTTP server for the test.
+function makeTestServer(port = -1) {
+ let httpServer = new HttpServer();
+ httpServer.start(port);
+ registerCleanupFunction(() => httpServer.stop(() => {}));
+ return httpServer;
+}
+
+/**
+ * Sets up a search engine that provides some suggestions by appending strings
+ * onto the search query.
+ *
+ * @param {Function} suggestionsFn
+ * A function that returns an array of suggestion strings given a
+ * search string. If not given, a default function is used.
+ * @returns {nsISearchEngine} The new engine.
+ */
+async function addTestSuggestionsEngine(suggestionsFn = null) {
+ // This port number should match the number in engine-suggestions.xml.
+ let server = makeTestServer();
+ server.registerPathHandler("/suggest", (req, resp) => {
+ let params = new URLSearchParams(req.queryString);
+ let searchStr = params.get("q");
+ let suggestions = suggestionsFn
+ ? suggestionsFn(searchStr)
+ : [searchStr].concat(["foo", "bar"].map(s => searchStr + " " + s));
+ let data = [searchStr, suggestions];
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(JSON.stringify(data));
+ });
+ await SearchTestUtils.installSearchExtension({
+ name: SUGGESTIONS_ENGINE_NAME,
+ search_url: `http://localhost:${server.identity.primaryPort}/search`,
+ suggest_url: `http://localhost:${server.identity.primaryPort}/suggest`,
+ suggest_url_get_params: "?q={searchTerms}",
+ // test_search_suggestions_aliases.js uses the search form.
+ search_form: `http://localhost:${server.identity.primaryPort}/search?q={searchTerms}`,
+ });
+ let engine = Services.search.getEngineByName("Suggestions");
+ return engine;
+}
+
+/**
+ * Sets up a search engine that provides some tail suggestions by creating an
+ * array that mimics Google's tail suggestion responses.
+ *
+ * @param {Function} suggestionsFn
+ * A function that returns an array that mimics Google's tail suggestion
+ * responses. See bug 1626897.
+ * NOTE: Consumers specifying suggestionsFn must include searchStr as a
+ * part of the array returned by suggestionsFn.
+ * @returns {nsISearchEngine} The new engine.
+ */
+async function addTestTailSuggestionsEngine(suggestionsFn = null) {
+ // This port number should match the number in engine-tail-suggestions.xml.
+ let server = makeTestServer();
+ server.registerPathHandler("/suggest", (req, resp) => {
+ let params = new URLSearchParams(req.queryString);
+ let searchStr = params.get("q");
+ let suggestions = suggestionsFn
+ ? suggestionsFn(searchStr)
+ : [
+ "what time is it in t",
+ ["what is the time today texas"].concat(
+ ["toronto", "tunisia"].map(s => searchStr + s.slice(1))
+ ),
+ [],
+ {
+ "google:irrelevantparameter": [],
+ "google:suggestdetail": [{}].concat(
+ ["toronto", "tunisia"].map(s => ({
+ mp: "… ",
+ t: s,
+ }))
+ ),
+ },
+ ];
+ let data = suggestions;
+ let jsonString = JSON.stringify(data);
+ // This script must be evaluated as UTF-8 for this to write out the bytes of
+ // the string in UTF-8. If it's evaluated as Latin-1, the written bytes
+ // will be the result of UTF-8-encoding the result-string *twice*, which
+ // will break the "… " match prefixes.
+ let stringOfUtf8Bytes = convertToUtf8(jsonString);
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(stringOfUtf8Bytes);
+ });
+ await SearchTestUtils.installSearchExtension({
+ name: TAIL_SUGGESTIONS_ENGINE_NAME,
+ search_url: `http://localhost:${server.identity.primaryPort}/search`,
+ suggest_url: `http://localhost:${server.identity.primaryPort}/suggest`,
+ suggest_url_get_params: "?q={searchTerms}",
+ });
+ let engine = Services.search.getEngineByName("Tail Suggestions");
+ return engine;
+}
+
+async function addOpenPages(uri, count = 1, userContextId = 0) {
+ for (let i = 0; i < count; i++) {
+ await UrlbarProviderOpenTabs.registerOpenTab(
+ uri.spec,
+ userContextId,
+ false
+ );
+ }
+}
+
+async function removeOpenPages(aUri, aCount = 1, aUserContextId = 0) {
+ for (let i = 0; i < aCount; i++) {
+ await UrlbarProviderOpenTabs.unregisterOpenTab(
+ aUri.spec,
+ aUserContextId,
+ false
+ );
+ }
+}
+
+/**
+ * Helper for tests that generate search results but aren't interested in
+ * suggestions, such as autofill tests. Installs a test engine and disables
+ * suggestions.
+ */
+function testEngine_setup() {
+ add_task(async function setup() {
+ await cleanupPlaces();
+ let engine = await addTestSuggestionsEngine();
+ let oldDefaultEngine = await Services.search.getDefault();
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref(
+ "browser.search.separatePrivateDefault.ui.enabled"
+ );
+ Services.search.setDefault(
+ oldDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ });
+
+ Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ Services.prefs.setBoolPref(
+ "browser.search.separatePrivateDefault.ui.enabled",
+ false
+ );
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ });
+}
+
+async function cleanupPlaces() {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
+
+/**
+ * Creates a UrlbarResult for a bookmark result.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options
+ * Options for the result.
+ * @param {string} options.title
+ * The page title.
+ * @param {string} options.uri
+ * The page URI.
+ * @param {string} [options.iconUri]
+ * A URI for the page's icon.
+ * @param {Array} [options.tags]
+ * An array of string tags. Defaults to an empty array.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * @param {number} [options.source]
+ * Where the results should be sourced from. See {@link UrlbarUtils.RESULT_SOURCE}.
+ * @returns {UrlbarResult}
+ */
+function makeBookmarkResult(
+ queryContext,
+ {
+ title,
+ uri,
+ iconUri,
+ tags = [],
+ heuristic = false,
+ source = UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ }
+) {
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ source,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
+ // Check against undefined so consumers can pass in the empty string.
+ icon: [typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`],
+ title: [title, UrlbarUtils.HIGHLIGHT.TYPED],
+ tags: [tags, UrlbarUtils.HIGHLIGHT.TYPED],
+ })
+ );
+
+ result.heuristic = heuristic;
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for a form history result.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options
+ * Options for the result.
+ * @param {string} options.suggestion
+ * The form history suggestion.
+ * @param {string} options.engineName
+ * The name of the engine that will do the search when the result is picked.
+ * @returns {UrlbarResult}
+ */
+function makeFormHistoryResult(queryContext, { suggestion, engineName }) {
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ engine: engineName,
+ suggestion: [suggestion, UrlbarUtils.HIGHLIGHT.SUGGESTED],
+ lowerCaseSuggestion: suggestion.toLocaleLowerCase(),
+ })
+ );
+}
+
+/**
+ * Creates a UrlbarResult for an omnibox extension result. For more information,
+ * see the documentation for omnibox.SuggestResult:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/omnibox/SuggestResult
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options
+ * Options for the result.
+ * @param {string} options.content
+ * The string displayed when the result is highlighted.
+ * @param {string} options.description
+ * The string displayed in the address bar dropdown.
+ * @param {string} options.keyword
+ * The keyword associated with the extension returning the result.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * @returns {UrlbarResult}
+ */
+function makeOmniboxResult(
+ queryContext,
+ { content, description, keyword, heuristic = false }
+) {
+ let payload = {
+ title: [description, UrlbarUtils.HIGHLIGHT.TYPED],
+ content: [content, UrlbarUtils.HIGHLIGHT.TYPED],
+ keyword: [keyword, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: [UrlbarUtils.ICON.EXTENSION],
+ };
+ if (!heuristic) {
+ payload.blockL10n = { id: "urlbar-result-menu-dismiss-firefox-suggest" };
+ }
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.OMNIBOX,
+ UrlbarUtils.RESULT_SOURCE.ADDON,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
+ );
+ result.heuristic = heuristic;
+
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for an switch-to-tab result.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options
+ * Options for the result.
+ * @param {string} options.uri
+ * The page URI.
+ * @param {string} [options.title]
+ * The page title.
+ * @param {string} [options.iconUri]
+ * A URI for the page icon.
+ * @returns {UrlbarResult}
+ */
+function makeTabSwitchResult(queryContext, { uri, title, iconUri }) {
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
+ title: [title, UrlbarUtils.HIGHLIGHT.TYPED],
+ // Check against undefined so consumers can pass in the empty string.
+ icon: typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`,
+ })
+ );
+}
+
+/**
+ * Creates a UrlbarResult for a keyword search result.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options
+ * Options for the result.
+ * @param {string} options.uri
+ * The page URI.
+ * @param {string} options.keyword
+ * The page's search keyword.
+ * @param {string} [options.title]
+ * The title for the bookmarked keyword page.
+ * @param {string} [options.iconUri]
+ * A URI for the engine's icon.
+ * @param {string} [options.postData]
+ * The search POST data.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * @returns {UrlbarResult}
+ */
+function makeKeywordSearchResult(
+ queryContext,
+ { uri, keyword, title, iconUri, postData, heuristic = false }
+) {
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: [title ? title : uri, UrlbarUtils.HIGHLIGHT.TYPED],
+ url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
+ keyword: [keyword, UrlbarUtils.HIGHLIGHT.TYPED],
+ input: [queryContext.searchString, UrlbarUtils.HIGHLIGHT.TYPED],
+ postData: postData || null,
+ icon: typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`,
+ })
+ );
+
+ if (heuristic) {
+ result.heuristic = heuristic;
+ }
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for a priority search result.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options
+ * Options for the result.
+ * @param {string} [options.engineName]
+ * The name of the engine providing the suggestion. Leave blank if there
+ * is no suggestion.
+ * @param {string} [options.engineIconUri]
+ * A URI for the engine's icon.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * @returns {UrlbarResult}
+ */
+function makePrioritySearchResult(
+ queryContext,
+ { engineName, engineIconUri, heuristic }
+) {
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ engine: [engineName, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: engineIconUri,
+ })
+ );
+
+ if (heuristic) {
+ result.heuristic = heuristic;
+ }
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for a remote tab result.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options
+ * Options for the result.
+ * @param {string} options.uri
+ * The page URI.
+ * @param {string} options.device
+ * The name of the device that the remote tab comes from.
+ * @param {string} [options.title]
+ * The page title.
+ * @param {number} [options.lastUsed]
+ * The last time the remote tab was visited, in epoch seconds. Defaults
+ * to 0.
+ * @param {string} [options.iconUri]
+ * A URI for the page's icon.
+ * @returns {UrlbarResult}
+ */
+function makeRemoteTabResult(
+ queryContext,
+ { uri, device, title, iconUri, lastUsed = 0 }
+) {
+ let payload = {
+ url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
+ device: [device, UrlbarUtils.HIGHLIGHT.TYPED],
+ // Check against undefined so consumers can pass in the empty string.
+ icon:
+ typeof iconUri != "undefined"
+ ? iconUri
+ : `moz-anno:favicon:page-icon:${uri}`,
+ lastUsed: lastUsed * 1000,
+ };
+
+ // Check against undefined so consumers can pass in the empty string.
+ if (typeof title != "undefined") {
+ payload.title = [title, UrlbarUtils.HIGHLIGHT.TYPED];
+ } else {
+ payload.title = [uri, UrlbarUtils.HIGHLIGHT.TYPED];
+ }
+
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
+ );
+
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for a search result.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options
+ * Options for the result.
+ * @param {string} [options.suggestion]
+ * The suggestion offered by the search engine.
+ * @param {string} [options.tailPrefix]
+ * The characters placed at the end of a Google "tail" suggestion. See
+ * {@link https://firefox-source-docs.mozilla.org/browser/urlbar/nontechnical-overview.html#search-suggestions}
+ * @param {*} [options.tail]
+ * The details of the URL bar tail
+ * @param {number} [options.tailOffsetIndex]
+ * The index of the first character in the tail suggestion that should be
+ * @param {string} [options.engineName]
+ * The name of the engine providing the suggestion. Leave blank if there
+ * is no suggestion.
+ * @param {string} [options.uri]
+ * The URI that the search result will navigate to.
+ * @param {string} [options.query]
+ * The query that started the search. This overrides
+ * `queryContext.searchString`. This is useful when the query that will show
+ * up in the result object will be different from what was typed. For example,
+ * if a leading restriction token will be used.
+ * @param {string} [options.alias]
+ * The alias for the search engine, if the search is an alias search.
+ * @param {string} [options.engineIconUri]
+ * A URI for the engine's icon.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * @param {boolean} [options.providesSearchMode]
+ * Whether search mode is entered when this result is selected.
+ * @param {string} [options.providerName]
+ * The name of the provider offering this result. The test suite will not
+ * check which provider offered a result unless this option is specified.
+ * @param {boolean} [options.inPrivateWindow]
+ * If the window to test is a private window.
+ * @param {boolean} [options.isPrivateEngine]
+ * If the engine is a private engine.
+ * @param {number} [options.type]
+ * The type of the search result. Defaults to UrlbarUtils.RESULT_TYPE.SEARCH.
+ * @param {number} [options.source]
+ * The source of the search result. Defaults to UrlbarUtils.RESULT_SOURCE.SEARCH.
+ * @param {boolean} [options.satisfiesAutofillThreshold]
+ * If this search should appear in the autofill section of the box
+ * @param {boolean} [options.trending]
+ * If the search result is a trending result. `Defaults to false`.
+ * @param {boolean} [options.isRichSuggestion]
+ * If the search result is a rich result. `Defaults to false`.
+ * @returns {UrlbarResult}
+ */
+function makeSearchResult(
+ queryContext,
+ {
+ suggestion,
+ tailPrefix,
+ tail,
+ tailOffsetIndex,
+ engineName,
+ alias,
+ uri,
+ query,
+ engineIconUri,
+ providesSearchMode,
+ providerName,
+ inPrivateWindow,
+ isPrivateEngine,
+ heuristic = false,
+ trending = false,
+ isRichSuggestion = false,
+ type = UrlbarUtils.RESULT_TYPE.SEARCH,
+ source = UrlbarUtils.RESULT_SOURCE.SEARCH,
+ satisfiesAutofillThreshold = false,
+ }
+) {
+ // Tail suggestion common cases, handled here to reduce verbosity in tests.
+ if (tail) {
+ if (!tailPrefix) {
+ tailPrefix = "… ";
+ }
+ if (!tailOffsetIndex) {
+ tailOffsetIndex = suggestion.indexOf(tail);
+ }
+ }
+
+ let payload = {
+ engine: [engineName, UrlbarUtils.HIGHLIGHT.TYPED],
+ suggestion: [suggestion, UrlbarUtils.HIGHLIGHT.SUGGESTED],
+ tailPrefix,
+ tail: [tail, UrlbarUtils.HIGHLIGHT.SUGGESTED],
+ tailOffsetIndex,
+ keyword: [
+ alias,
+ providesSearchMode
+ ? UrlbarUtils.HIGHLIGHT.TYPED
+ : UrlbarUtils.HIGHLIGHT.NONE,
+ ],
+ // Check against undefined so consumers can pass in the empty string.
+ query: [
+ typeof query != "undefined" ? query : queryContext.trimmedSearchString,
+ UrlbarUtils.HIGHLIGHT.TYPED,
+ ],
+ icon: engineIconUri,
+ providesSearchMode,
+ inPrivateWindow,
+ isPrivateEngine,
+ };
+
+ // Passing even an undefined URL in the payload creates a potentially-unwanted
+ // displayUrl parameter, so we add it only if specified.
+ if (uri) {
+ payload.url = uri;
+ }
+ if (providerName == "TabToSearch") {
+ payload.satisfiesAutofillThreshold = satisfiesAutofillThreshold;
+ if (payload.url.startsWith("www.")) {
+ payload.url = payload.url.substring(4);
+ }
+ payload.isGeneralPurposeEngine = false;
+ }
+
+ let result = new UrlbarResult(
+ type,
+ source,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
+ );
+
+ if (typeof suggestion == "string") {
+ result.payload.lowerCaseSuggestion =
+ result.payload.suggestion.toLocaleLowerCase();
+ result.payload.trending = trending;
+ result.payload.isRichSuggestion = isRichSuggestion;
+ }
+
+ if (providerName) {
+ result.providerName = providerName;
+ }
+
+ result.heuristic = heuristic;
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for a history result.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options Options for the result.
+ * @param {string} options.title
+ * The page title.
+ * @param {string} [options.fallbackTitle]
+ * The provider has capability to use the actual page title though,
+ * when the provider can’t get the page title, use this value as the fallback.
+ * @param {string} options.uri
+ * The page URI.
+ * @param {Array} [options.tags]
+ * An array of string tags. Defaults to an empty array.
+ * @param {string} [options.iconUri]
+ * A URI for the page's icon.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * @param {string} options.providerName
+ * The name of the provider offering this result. The test suite will not
+ * check which provider offered a result unless this option is specified.
+ * @param {number} [options.source]
+ * The source of the result
+ * @returns {UrlbarResult}
+ */
+function makeVisitResult(
+ queryContext,
+ {
+ title,
+ fallbackTitle,
+ uri,
+ iconUri,
+ providerName,
+ tags = [],
+ heuristic = false,
+ source = UrlbarUtils.RESULT_SOURCE.HISTORY,
+ }
+) {
+ let payload = {
+ url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
+ };
+
+ if (title) {
+ payload.title = [title, UrlbarUtils.HIGHLIGHT.TYPED];
+ }
+
+ if (fallbackTitle) {
+ payload.fallbackTitle = [fallbackTitle, UrlbarUtils.HIGHLIGHT.TYPED];
+ }
+
+ if (iconUri) {
+ payload.icon = iconUri;
+ } else if (
+ iconUri === undefined &&
+ source != UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL
+ ) {
+ payload.icon = `page-icon:${uri}`;
+ }
+
+ if (!heuristic && tags) {
+ payload.tags = [tags, UrlbarUtils.HIGHLIGHT.TYPED];
+ }
+
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ source,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
+ );
+
+ if (providerName) {
+ result.providerName = providerName;
+ }
+
+ result.heuristic = heuristic;
+ return result;
+}
+
+/**
+ * Checks that the results returned by a UrlbarController match those in
+ * the param `matches`.
+ *
+ * @param {object} options Options for the check.
+ * @param {UrlbarQueryContext} options.context
+ * The context for this query.
+ * @param {string} [options.incompleteSearch]
+ * A search will be fired for this string and then be immediately canceled by
+ * the query in `context`.
+ * @param {string} [options.autofilled]
+ * The autofilled value in the first result.
+ * @param {string} [options.completed]
+ * The value that would be filled if the autofill result was confirmed.
+ * Has no effect if `autofilled` is not specified.
+ * @param {Array} options.matches
+ * An array of UrlbarResults.
+ */
+async function check_results({
+ context,
+ incompleteSearch,
+ autofilled,
+ completed,
+ matches = [],
+} = {}) {
+ if (!context) {
+ return;
+ }
+
+ // At this point frecency could still be updating due to latest pages
+ // updates.
+ // This is not a problem in real life, but autocomplete tests should
+ // return reliable resultsets, thus we have to wait.
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ const controller = UrlbarTestUtils.newMockController({
+ input: {
+ isPrivate: context.isPrivate,
+ onFirstResult() {
+ return false;
+ },
+ getSearchSource() {
+ return "dummy-search-source";
+ },
+ window: {
+ location: {
+ href: AppConstants.BROWSER_CHROME_URL,
+ },
+ },
+ },
+ });
+
+ if (incompleteSearch) {
+ let incompleteContext = createContext(incompleteSearch, {
+ isPrivate: context.isPrivate,
+ });
+ controller.startQuery(incompleteContext);
+ }
+ await controller.startQuery(context);
+
+ if (autofilled) {
+ Assert.ok(context.results[0], "There is a first result.");
+ Assert.ok(
+ context.results[0].autofill,
+ "The first result is an autofill result"
+ );
+ Assert.equal(
+ context.results[0].autofill.value,
+ autofilled,
+ "The correct value was autofilled."
+ );
+ if (completed) {
+ Assert.equal(
+ context.results[0].payload.url,
+ completed,
+ "The completed autofill value is correct."
+ );
+ }
+ }
+ if (context.results.length != matches.length) {
+ info("Actual results: " + JSON.stringify(context.results));
+ }
+ Assert.equal(
+ context.results.length,
+ matches.length,
+ "Found the expected number of results."
+ );
+
+ function getPayload(result) {
+ let payload = {};
+ for (let [key, value] of Object.entries(result.payload)) {
+ if (value !== undefined) {
+ payload[key] = value;
+ }
+ }
+ return payload;
+ }
+
+ for (let i = 0; i < matches.length; i++) {
+ let actual = context.results[i];
+ let expected = matches[i];
+ info(
+ `Comparing results at index ${i}:` +
+ " actual=" +
+ JSON.stringify(actual) +
+ " expected=" +
+ JSON.stringify(expected)
+ );
+ Assert.equal(
+ actual.type,
+ expected.type,
+ `result.type at result index ${i}`
+ );
+ Assert.equal(
+ actual.source,
+ expected.source,
+ `result.source at result index ${i}`
+ );
+ Assert.equal(
+ actual.heuristic,
+ expected.heuristic,
+ `result.heuristic at result index ${i}`
+ );
+ Assert.equal(
+ !!actual.isBestMatch,
+ !!expected.isBestMatch,
+ `result.isBestMatch at result index ${i}`
+ );
+ if (expected.providerName) {
+ Assert.equal(
+ actual.providerName,
+ expected.providerName,
+ `result.providerName at result index ${i}`
+ );
+ }
+ if (expected.hasOwnProperty("suggestedIndex")) {
+ Assert.equal(
+ actual.suggestedIndex,
+ expected.suggestedIndex,
+ `result.suggestedIndex at result index ${i}`
+ );
+ }
+
+ if (expected.payload) {
+ Assert.deepEqual(
+ getPayload(actual),
+ getPayload(expected),
+ `result.payload at result index ${i}`
+ );
+ }
+ }
+}
+
+/**
+ * Returns the frecency of an origin.
+ *
+ * @param {string} prefix
+ * The origin's prefix, e.g., "http://".
+ * @param {string} aHost
+ * The origin's host.
+ * @returns {number} The origin's frecency.
+ */
+async function getOriginFrecency(prefix, aHost) {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(
+ `
+ SELECT frecency
+ FROM moz_origins
+ WHERE prefix = :prefix AND host = :host
+ `,
+ { prefix, host: aHost }
+ );
+ Assert.equal(rows.length, 1);
+ return rows[0].getResultByIndex(0);
+}
+
+/**
+ * Returns the origin frecency stats.
+ *
+ * @returns {object}
+ * An object { count, sum, squares }.
+ */
+async function getOriginFrecencyStats() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(`
+ SELECT
+ IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_count'), 0),
+ IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_sum'), 0),
+ IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_sum_of_squares'), 0)
+ `);
+ let count = rows[0].getResultByIndex(0);
+ let sum = rows[0].getResultByIndex(1);
+ let squares = rows[0].getResultByIndex(2);
+ return { count, sum, squares };
+}
+
+/**
+ * Returns the origin autofill frecency threshold.
+ *
+ * @returns {number}
+ * The threshold.
+ */
+async function getOriginAutofillThreshold() {
+ let { count, sum, squares } = await getOriginFrecencyStats();
+ if (!count) {
+ return 0;
+ }
+ if (count == 1) {
+ return sum;
+ }
+ let stddevMultiplier = UrlbarPrefs.get("autoFill.stddevMultiplier");
+ return (
+ sum / count +
+ stddevMultiplier * Math.sqrt((squares - (sum * sum) / count) / count)
+ );
+}
+
+/**
+ * Checks that origins appear in a given order in the database.
+ *
+ * @param {string} host The "fixed" host, without "www."
+ * @param {Array} prefixOrder The prefixes (scheme + www.) sorted appropriately.
+ */
+async function checkOriginsOrder(host, prefixOrder) {
+ await PlacesUtils.withConnectionWrapper("checkOriginsOrder", async db => {
+ let prefixes = (
+ await db.execute(
+ `SELECT prefix || iif(instr(host, "www.") = 1, "www.", "")
+ FROM moz_origins
+ WHERE host = :host OR host = "www." || :host
+ ORDER BY ROWID ASC
+ `,
+ { host }
+ )
+ ).map(r => r.getResultByIndex(0));
+ Assert.deepEqual(prefixes, prefixOrder);
+ });
+}
diff --git a/browser/components/urlbar/tests/unit/test_000_frecency.js b/browser/components/urlbar/tests/unit/test_000_frecency.js
new file mode 100644
index 0000000000..cef110963f
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_000_frecency.js
@@ -0,0 +1,245 @@
+/* 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/. */
+
+/*
+
+Autocomplete Frecency Tests
+
+- add a visit for each score permutation
+- search
+- test number of matches
+- test each item's location in results
+
+*/
+
+testEngine_setup();
+
+try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService(
+ Ci.nsINavHistoryService
+ );
+} catch (ex) {
+ do_throw("Could not get services\n");
+}
+
+var bucketPrefs = [
+ ["firstBucketCutoff", "firstBucketWeight"],
+ ["secondBucketCutoff", "secondBucketWeight"],
+ ["thirdBucketCutoff", "thirdBucketWeight"],
+ ["fourthBucketCutoff", "fourthBucketWeight"],
+ [null, "defaultBucketWeight"],
+];
+
+var bonusPrefs = {
+ embedVisitBonus: PlacesUtils.history.TRANSITION_EMBED,
+ framedLinkVisitBonus: PlacesUtils.history.TRANSITION_FRAMED_LINK,
+ linkVisitBonus: PlacesUtils.history.TRANSITION_LINK,
+ typedVisitBonus: PlacesUtils.history.TRANSITION_TYPED,
+ bookmarkVisitBonus: PlacesUtils.history.TRANSITION_BOOKMARK,
+ downloadVisitBonus: PlacesUtils.history.TRANSITION_DOWNLOAD,
+ permRedirectVisitBonus: PlacesUtils.history.TRANSITION_REDIRECT_PERMANENT,
+ tempRedirectVisitBonus: PlacesUtils.history.TRANSITION_REDIRECT_TEMPORARY,
+ reloadVisitBonus: PlacesUtils.history.TRANSITION_RELOAD,
+};
+
+// create test data
+var searchTerm = "frecency";
+var results = [];
+var now = Date.now();
+var prefPrefix = "places.frecency.";
+
+async function task_initializeBucket(bucket) {
+ let [cutoffName, weightName] = bucket;
+ // get pref values
+ let weight = Services.prefs.getIntPref(prefPrefix + weightName, 0);
+ let cutoff = Services.prefs.getIntPref(prefPrefix + cutoffName, 0);
+ if (cutoff < 1) {
+ return;
+ }
+
+ // generate a date within the cutoff period
+ let dateInPeriod = (now - (cutoff - 1) * 86400 * 1000) * 1000;
+
+ for (let [bonusName, visitType] of Object.entries(bonusPrefs)) {
+ let frecency = -1;
+ let calculatedURI = null;
+ let matchTitle = "";
+ let bonusValue = Services.prefs.getIntPref(prefPrefix + bonusName);
+ // unvisited (only for first cutoff date bucket)
+ if (
+ bonusName == "unvisitedBookmarkBonus" ||
+ bonusName == "unvisitedTypedBonus"
+ ) {
+ if (cutoffName == "firstBucketCutoff") {
+ let points = Math.ceil((bonusValue / parseFloat(100.0)) * weight);
+ let visitCount = 1; // bonusName == "unvisitedBookmarkBonus" ? 1 : 0;
+ frecency = Math.ceil(visitCount * points);
+ calculatedURI = Services.io.newURI(
+ "http://" +
+ searchTerm +
+ ".com/" +
+ bonusName +
+ ":" +
+ bonusValue +
+ "/cutoff:" +
+ cutoff +
+ "/weight:" +
+ weight +
+ "/frecency:" +
+ frecency
+ );
+ if (bonusName == "unvisitedBookmarkBonus") {
+ matchTitle = searchTerm + "UnvisitedBookmark";
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: calculatedURI,
+ title: matchTitle,
+ });
+ } else {
+ matchTitle = searchTerm + "UnvisitedTyped";
+ await PlacesTestUtils.addVisits({
+ uri: calculatedURI,
+ title: matchTitle,
+ transition: visitType,
+ visitDate: now,
+ });
+ histsvc.markPageAsTyped(calculatedURI);
+ }
+ }
+ } else {
+ // visited
+ // visited bookmarks get the visited bookmark bonus twice
+ if (visitType == Ci.nsINavHistoryService.TRANSITION_BOOKMARK) {
+ bonusValue = bonusValue * 2;
+ }
+
+ let points = Math.ceil(
+ (1 * ((bonusValue / parseFloat(100.0)).toFixed(6) * weight)) / 1
+ );
+ if (!points) {
+ if (
+ visitType == Ci.nsINavHistoryService.TRANSITION_EMBED ||
+ visitType == Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK ||
+ visitType == Ci.nsINavHistoryService.TRANSITION_DOWNLOAD ||
+ visitType == Ci.nsINavHistoryService.TRANSITION_RELOAD ||
+ bonusName == "defaultVisitBonus"
+ ) {
+ frecency = 0;
+ } else {
+ frecency = -1;
+ }
+ } else {
+ frecency = points;
+ }
+ calculatedURI = Services.io.newURI(
+ "http://" +
+ searchTerm +
+ ".com/" +
+ bonusName +
+ ":" +
+ bonusValue +
+ "/cutoff:" +
+ cutoff +
+ "/weight:" +
+ weight +
+ "/frecency:" +
+ frecency
+ );
+ if (visitType == Ci.nsINavHistoryService.TRANSITION_BOOKMARK) {
+ matchTitle = searchTerm + "Bookmarked";
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: calculatedURI,
+ title: matchTitle,
+ });
+ } else {
+ matchTitle = calculatedURI.spec.substr(
+ calculatedURI.spec.lastIndexOf("/") + 1
+ );
+ }
+ await PlacesTestUtils.addVisits({
+ uri: calculatedURI,
+ transition: visitType,
+ visitDate: dateInPeriod,
+ });
+ }
+
+ if (calculatedURI && frecency) {
+ results.push([calculatedURI, frecency, matchTitle]);
+ await PlacesTestUtils.addVisits({
+ uri: calculatedURI,
+ title: matchTitle,
+ transition: visitType,
+ visitDate: dateInPeriod,
+ });
+ }
+ }
+}
+
+add_task(async function test_frecency() {
+ // Disable autoFill for this test.
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ // always search in history + bookmarks, no matter what the default is
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmarks", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmarks");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+ for (let bucket of bucketPrefs) {
+ await task_initializeBucket(bucket);
+ }
+
+ // Sort results by frecency. Break ties by alphabetical URL.
+ results.sort((a, b) => {
+ let frecencyDiff = b[1] - a[1];
+ if (frecencyDiff == 0) {
+ return a[0].spec.localeCompare(b[0].spec);
+ }
+ return frecencyDiff;
+ });
+
+ // Make sure there's enough results returned
+ Services.prefs.setIntPref(
+ "browser.urlbar.maxRichResults",
+ // +1 for the heuristic search result.
+ results.length + 1
+ );
+
+ await PlacesTestUtils.promiseAsyncUpdates();
+ let context = createContext(searchTerm, { isPrivate: false });
+ let urlbarResults = [];
+ for (let result of results) {
+ let url = result[0].spec;
+ if (url.toLowerCase().includes("bookmark")) {
+ urlbarResults.push(
+ makeBookmarkResult(context, {
+ uri: url,
+ title: result[2],
+ })
+ );
+ } else {
+ urlbarResults.push(
+ makeVisitResult(context, {
+ uri: url,
+ title: result[2],
+ })
+ );
+ }
+ }
+
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...urlbarResults,
+ ],
+ });
+});
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..d94de4f439
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js
@@ -0,0 +1,83 @@
+/* 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.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+
+const TEST_URL = "http://example.com";
+const match = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: TEST_URL }
+);
+let controller;
+
+add_task(async function setup() {
+ controller = UrlbarTestUtils.newMockController();
+});
+
+add_task(async function test_basic_search() {
+ let provider = registerBasicTestProvider([match]);
+ const context = createContext(TEST_URL, { providers: [provider.name] });
+
+ 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 provider = registerBasicTestProvider(
+ [match],
+ providerCanceledDeferred.resolve
+ );
+ const context = createContext(TEST_URL, { providers: [provider.name] });
+
+ 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..bce6bb21ff
--- /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..6d77e6a1ac
--- /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 The query 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..c681aca387
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarPrefs.js
@@ -0,0 +1,449 @@
+/* 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
+ );
+});
+
+// Tests UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }).
+add_task(function makeResultGroups_true() {
+ Assert.deepEqual(
+ UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }),
+ {
+ children: [
+ // heuristic
+ {
+ maxResultCount: 1,
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_ENGINE_ALIAS },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_PRELOADED },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TOKEN_ALIAS_ENGINE },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_HISTORY_URL },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK },
+ ],
+ },
+ // extensions using the omnibox API
+ {
+ group: UrlbarUtils.RESULT_GROUP.OMNIBOX,
+ },
+ // main group
+ {
+ flexChildren: true,
+ children: [
+ // suggestions
+ {
+ flex: 2,
+ children: [
+ {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ {
+ flex: 4,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.TAIL_SUGGESTION,
+ },
+ ],
+ },
+ // general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ flex: 1,
+ children: [
+ {
+ availableSpan: 3,
+ group: UrlbarUtils.RESULT_GROUP.INPUT_HISTORY,
+ },
+ {
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_TAB,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.ABOUT_PAGES,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.PRELOADED,
+ },
+ ],
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.INPUT_HISTORY,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ }
+ );
+});
+
+// Tests UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: false }).
+add_task(function makeResultGroups_false() {
+ Assert.deepEqual(
+ UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: false }),
+
+ {
+ children: [
+ // heuristic
+ {
+ maxResultCount: 1,
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_ENGINE_ALIAS },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_PRELOADED },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TOKEN_ALIAS_ENGINE },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_HISTORY_URL },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK },
+ ],
+ },
+ // extensions using the omnibox API
+ {
+ group: UrlbarUtils.RESULT_GROUP.OMNIBOX,
+ },
+ // main group
+ {
+ flexChildren: true,
+ children: [
+ // general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ flex: 2,
+ children: [
+ {
+ availableSpan: 3,
+ group: UrlbarUtils.RESULT_GROUP.INPUT_HISTORY,
+ },
+ {
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_TAB,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.ABOUT_PAGES,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.PRELOADED,
+ },
+ ],
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.INPUT_HISTORY,
+ },
+ ],
+ },
+ // suggestions
+ {
+ flex: 1,
+ children: [
+ {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ {
+ flex: 4,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.TAIL_SUGGESTION,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ }
+ );
+});
+
+// Tests interaction between showSearchSuggestionsFirst and resultGroups.
+add_task(function showSearchSuggestionsFirst_resultGroups() {
+ // Check initial values.
+ Assert.equal(
+ UrlbarPrefs.get("showSearchSuggestionsFirst"),
+ true,
+ "showSearchSuggestionsFirst is true initially"
+ );
+ Assert.deepEqual(
+ UrlbarPrefs.resultGroups,
+ UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }),
+ "resultGroups is the same as the groups for which howSearchSuggestionsFirst is true"
+ );
+
+ // Set showSearchSuggestionsFirst = false.
+ UrlbarPrefs.set("showSearchSuggestionsFirst", false);
+ Assert.deepEqual(
+ UrlbarPrefs.resultGroups,
+ UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: false }),
+ "resultGroups is updated after setting showSearchSuggestionsFirst = false"
+ );
+
+ // Set showSearchSuggestionsFirst = true.
+ UrlbarPrefs.set("showSearchSuggestionsFirst", true);
+ Assert.deepEqual(
+ UrlbarPrefs.resultGroups,
+ UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }),
+ "resultGroups is updated after setting showSearchSuggestionsFirst = true"
+ );
+
+ // Set showSearchSuggestionsFirst = false again so we can clear it next.
+ UrlbarPrefs.set("showSearchSuggestionsFirst", false);
+ Assert.deepEqual(
+ UrlbarPrefs.resultGroups,
+ UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: false }),
+ "resultGroups is updated after setting showSearchSuggestionsFirst = false"
+ );
+
+ // Clear showSearchSuggestionsFirst.
+ Services.prefs.clearUserPref("browser.urlbar.showSearchSuggestionsFirst");
+ Assert.deepEqual(
+ UrlbarPrefs.resultGroups,
+ UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }),
+ "resultGroups is updated immediately after clearing showSearchSuggestionsFirst"
+ );
+ Assert.equal(
+ UrlbarPrefs.get("showSearchSuggestionsFirst"),
+ true,
+ "showSearchSuggestionsFirst defaults to true after clearing it"
+ );
+ Assert.deepEqual(
+ UrlbarPrefs.resultGroups,
+ UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }),
+ "resultGroups remains correct after getting showSearchSuggestionsFirst"
+ );
+});
+
+// Tests UrlbarPrefs.initializeShowSearchSuggestionsFirstPref() and the
+// interaction between matchGroups, showSearchSuggestionsFirst, and
+// resultGroups. It's a little complex, but the flow is:
+//
+// 1. The old matchGroups pref has some value
+// 2. UrlbarPrefs.initializeShowSearchSuggestionsFirstPref() is called to
+// translate matchGroups into the newer showSearchSuggestionsFirst pref
+// 3. The update to showSearchSuggestionsFirst causes the new resultGroups
+// pref to be set
+add_task(function initializeShowSearchSuggestionsFirstPref() {
+ // Each value in `tests`: [matchGroups, expectedShowSearchSuggestionsFirst]
+ let tests = [
+ ["suggestion:4,general:Infinity", true],
+ ["suggestion:4,general:5", true],
+ ["suggestion:1,general:5,suggestion:Infinity", true],
+ ["suggestion:Infinity", true],
+ ["suggestion:4", true],
+
+ ["foo:1,suggestion:4,general:Infinity", true],
+ ["foo:2,suggestion:4,general:5", true],
+ ["foo:3,suggestion:1,general:5,suggestion:Infinity", true],
+ ["foo:4,suggestion:Infinity", true],
+ ["foo:5,suggestion:4", true],
+
+ ["general:5,suggestion:Infinity", false],
+ ["general:5,suggestion:4", false],
+ ["general:1,suggestion:4,general:Infinity", false],
+ ["general:Infinity", false],
+ ["general:5", false],
+
+ ["foo:1,general:5,suggestion:Infinity", false],
+ ["foo:2,general:5,suggestion:4", false],
+ ["foo:3,general:1,suggestion:4,general:Infinity", false],
+ ["foo:4,general:Infinity", false],
+ ["foo:5,general:5", false],
+
+ ["", true],
+ ["bogus groups", true],
+ ];
+
+ for (let [matchGroups, expectedValue] of tests) {
+ info("Running test: " + JSON.stringify({ matchGroups, expectedValue }));
+ Services.prefs.clearUserPref("browser.urlbar.showSearchSuggestionsFirst");
+
+ // Set matchGroups.
+ Services.prefs.setCharPref("browser.urlbar.matchGroups", matchGroups);
+
+ // Call initializeShowSearchSuggestionsFirstPref.
+ UrlbarPrefs.initializeShowSearchSuggestionsFirstPref();
+
+ // Both showSearchSuggestionsFirst and resultGroups should be updated.
+ Assert.equal(
+ Services.prefs.getBoolPref("browser.urlbar.showSearchSuggestionsFirst"),
+ expectedValue,
+ "showSearchSuggestionsFirst has the expected value"
+ );
+ Assert.deepEqual(
+ UrlbarPrefs.resultGroups,
+ UrlbarPrefs.makeResultGroups({
+ showSearchSuggestionsFirst: expectedValue,
+ }),
+ "resultGroups should be updated with the appropriate default"
+ );
+ }
+
+ Services.prefs.clearUserPref("browser.urlbar.matchGroups");
+});
+
+// Tests whether observer.onNimbusChanged works.
+add_task(async function onNimbusChanged() {
+ Services.prefs.setBoolPref(
+ "browser.urlbar.autoFill.adaptiveHistory.enabled",
+ false
+ );
+
+ // Add an observer that throws an Error and an observer that does not define
+ // anything to check whether the other observers can get notifications.
+ UrlbarPrefs.addObserver({
+ onPrefChanged(pref) {
+ throw new Error("From onPrefChanged");
+ },
+ onNimbusChanged(pref) {
+ throw new Error("From onNimbusChanged");
+ },
+ });
+ UrlbarPrefs.addObserver({});
+
+ const observer = {
+ onPrefChanged(pref) {
+ this.prefChangedList.push(pref);
+ },
+ onNimbusChanged(pref) {
+ this.nimbusChangedList.push(pref);
+ },
+ };
+ observer.prefChangedList = [];
+ observer.nimbusChangedList = [];
+ UrlbarPrefs.addObserver(observer);
+
+ const doCleanup = await UrlbarTestUtils.initNimbusFeature({
+ autoFillAdaptiveHistoryEnabled: true,
+ });
+ Assert.equal(observer.prefChangedList.length, 0);
+ Assert.ok(
+ observer.nimbusChangedList.includes("autoFillAdaptiveHistoryEnabled")
+ );
+ doCleanup();
+});
+
+// Tests whether observer.onPrefChanged works.
+add_task(async function onPrefChanged() {
+ const doCleanup = await UrlbarTestUtils.initNimbusFeature({
+ autoFillAdaptiveHistoryEnabled: false,
+ });
+ Services.prefs.setBoolPref(
+ "browser.urlbar.autoFill.adaptiveHistory.enabled",
+ false
+ );
+
+ // Add an observer that throws an Error and an observer that does not define
+ // anything to check whether the other observers can get notifications.
+ UrlbarPrefs.addObserver({
+ onPrefChanged(pref) {
+ throw new Error("From onPrefChanged");
+ },
+ onNimbusChanged(pref) {
+ throw new Error("From onNimbusChanged");
+ },
+ });
+ UrlbarPrefs.addObserver({});
+
+ const deferred = PromiseUtils.defer();
+ const observer = {
+ onPrefChanged(pref) {
+ this.prefChangedList.push(pref);
+ deferred.resolve();
+ },
+ onNimbusChanged(pref) {
+ this.nimbusChangedList.push(pref);
+ deferred.resolve();
+ },
+ };
+ observer.prefChangedList = [];
+ observer.nimbusChangedList = [];
+ UrlbarPrefs.addObserver(observer);
+
+ Services.prefs.setBoolPref(
+ "browser.urlbar.autoFill.adaptiveHistory.enabled",
+ true
+ );
+ await deferred.promise;
+ Assert.equal(observer.prefChangedList.length, 1);
+ Assert.equal(observer.prefChangedList[0], "autoFill.adaptiveHistory.enabled");
+ Assert.equal(observer.nimbusChangedList.length, 0);
+
+ Services.prefs.clearUserPref(
+ "browser.urlbar.autoFill.adaptiveHistory.enabled"
+ );
+ doCleanup();
+});
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..0aadb8aef6
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js
@@ -0,0 +1,132 @@
+/* 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.
+ */
+
+testEngine_setup();
+
+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/",
+ 0,
+ false
+ );
+
+ 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 != SUGGESTIONS_ENGINE_NAME),
+ "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 != SUGGESTIONS_ENGINE_NAME),
+ "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");
+ await SearchTestUtils.installSearchExtension({
+ name: "Test",
+ keyword: "match",
+ });
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
+ searchString: "match this",
+ });
+ Assert.ok(
+ !results.some(r => r.payload.engine != SUGGESTIONS_ENGINE_NAME),
+ "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.js b/browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.js
new file mode 100644
index 0000000000..a39815937e
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.js
@@ -0,0 +1,462 @@
+/* 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.importESModule(
+ "resource:///modules/UrlbarSearchUtils.sys.mjs"
+);
+
+let baconEngineExtension;
+
+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.resetToAppDefaultEngine();
+});
+
+add_task(async function search_engine_match() {
+ let engine = await Services.search.getDefault();
+ let domain = engine.searchUrlDomain;
+ 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.searchUrlDomain;
+ 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].searchUrlDomain != domain
+ );
+ engine.hidden = false;
+ await TestUtils.waitForCondition(
+ async () => (await UrlbarSearchUtils.enginesForDomainPrefix(token)).length
+ );
+ let matchedEngine2 = (
+ await UrlbarSearchUtils.enginesForDomainPrefix(token)
+ )[0];
+ Assert.ok(matchedEngine2);
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+});
+
+add_task(async function onlyEnabled_option_nomatch() {
+ let engine = await Services.search.getDefault();
+ let domain = engine.searchUrlDomain;
+ let token = domain.substr(0, 1);
+ Services.prefs.setCharPref("browser.search.hiddenOneOffs", engine.name);
+ let matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix(token, {
+ onlyEnabled: true,
+ });
+ Assert.notEqual(matchedEngines[0].searchUrlDomain, domain);
+ Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
+ matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix(token, {
+ onlyEnabled: true,
+ });
+ Assert.equal(matchedEngines[0].searchUrlDomain, domain);
+});
+
+add_task(async function add_search_engine_match() {
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("bacon")).length
+ );
+ baconEngineExtension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "bacon",
+ keyword: "pork",
+ search_url: "https://www.bacon.moz/",
+ },
+ { skipUnload: true }
+ );
+ let matchedEngine = (
+ await UrlbarSearchUtils.enginesForDomainPrefix("bacon")
+ )[0];
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.searchForm, "https://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, "https://www.bacon.moz");
+ Assert.equal(matchedEngine.name, "bacon");
+ Assert.equal(matchedEngine.iconURI, null);
+});
+
+add_task(async function match_multiple_search_engines() {
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("baseball")).length
+ );
+ await SearchTestUtils.installSearchExtension({
+ name: "baseball",
+ search_url: "https://www.baseball.moz/",
+ });
+ let matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix("ba");
+ Assert.equal(
+ matchedEngines.length,
+ 2,
+ "enginesForDomainPrefix returned two engines."
+ );
+ Assert.equal(matchedEngines[0].searchForm, "https://www.bacon.moz");
+ Assert.equal(matchedEngines[0].name, "bacon");
+ Assert.equal(matchedEngines[1].searchForm, "https://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() {
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("patch")).length
+ );
+ await SearchTestUtils.installSearchExtension({
+ name: "patch",
+ keyword: "PR",
+ search_url: "https://www.patch.moz/",
+ });
+ // 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 promiseTopic = promiseSearchTopic("engine-removed");
+ await Promise.all([baconEngineExtension.unload(), 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 extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestEngine2",
+ search_url: "https://example.com/",
+ },
+ { skipUnload: true }
+ );
+ let engine = Services.search.getEngineByName("TestEngine2");
+ Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "example");
+ await extension.unload();
+
+ extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestEngine",
+ search_url: "https://www.subdomain.othersubdomain.example.com",
+ },
+ { skipUnload: true }
+ );
+ engine = Services.search.getEngineByName("TestEngine");
+ Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "example");
+ await extension.unload();
+
+ // We let engines with URL ending in .test through even though its not a valid
+ // TLD.
+ extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestMalformed",
+ search_url: "https://mochi.test/",
+ search_url_get_params: "search={searchTerms}",
+ },
+ { skipUnload: true }
+ );
+ engine = Services.search.getEngineByName("TestMalformed");
+ Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "mochi");
+ await extension.unload();
+
+ // We return the domain for engines with a malformed URL.
+ extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestMalformed",
+ search_url: "https://subdomain.foobar/",
+ search_url_get_params: "search={searchTerms}",
+ },
+ { skipUnload: true }
+ );
+ engine = Services.search.getEngineByName("TestMalformed");
+ Assert.equal(
+ UrlbarSearchUtils.getRootDomainFromEngine(engine),
+ "subdomain.foobar"
+ );
+ await extension.unload();
+});
+
+// Tests getSearchTermIfDefaultSerpUri() by using a variety of
+// input strings and nsIURI's.
+// Should not throw an error if the consumer passes an input
+// that when accessed, could cause an error.
+add_task(async function get_search_term_if_default_serp_uri() {
+ let testCases = [
+ {
+ url: null,
+ skipUriTest: true,
+ },
+ {
+ url: "",
+ skipUriTest: true,
+ },
+ {
+ url: "about:blank",
+ },
+ {
+ url: "about:home",
+ },
+ {
+ url: "about:newtab",
+ },
+ {
+ url: "not://a/supported/protocol",
+ },
+ {
+ url: "view-source:http://www.example.com/",
+ },
+ {
+ // Not a default engine.
+ url: "http://mochi.test:8888/?q=chocolate&pc=sample_code",
+ },
+ {
+ // Not the correct protocol.
+ url: "http://example.com/?q=chocolate&pc=sample_code",
+ },
+ {
+ // Not the same query param values.
+ url: "https://example.com/?q=chocolate&pc=sample_code2",
+ },
+ {
+ // Not the same query param values.
+ url: "https://example.com/?q=chocolate&pc=sample_code&pc2=sample_code_2",
+ },
+ {
+ url: "https://example.com/?q=chocolate&pc=sample_code",
+ expectedString: "chocolate",
+ },
+ {
+ url: "https://example.com/?q=chocolate+cakes&pc=sample_code",
+ expectedString: "chocolate cakes",
+ },
+ ];
+
+ // Create a specific engine so that the tests are matched
+ // exactly against the query params used.
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestEngine",
+ search_url: "https://example.com/",
+ search_url_get_params: "?q={searchTerms}&pc=sample_code",
+ },
+ { skipUnload: true }
+ );
+ let engine = Services.search.getEngineByName("TestEngine");
+ let originalDefaultEngine = Services.search.defaultEngine;
+ Services.search.defaultEngine = engine;
+
+ for (let testCase of testCases) {
+ let expectedString = testCase.expectedString ?? "";
+ Assert.equal(
+ UrlbarSearchUtils.getSearchTermIfDefaultSerpUri(testCase.url),
+ expectedString,
+ `Should return ${
+ expectedString == "" ? "an empty string" : "a matching search string"
+ }`
+ );
+ // Convert the string into a nsIURI and then
+ // try the test case with it.
+ if (!testCase.skipUriTest) {
+ Assert.equal(
+ UrlbarSearchUtils.getSearchTermIfDefaultSerpUri(
+ Services.io.newURI(testCase.url)
+ ),
+ expectedString,
+ `Should return ${
+ expectedString == "" ? "an empty string" : "a matching search string"
+ }`
+ );
+ }
+ }
+
+ Services.search.defaultEngine = originalDefaultEngine;
+ await extension.unload();
+});
+
+add_task(async function matchAllDomainLevels() {
+ let baseHostname = "matchalldomainlevels";
+ Assert.equal(
+ (await UrlbarSearchUtils.enginesForDomainPrefix(baseHostname)).length,
+ 0,
+ `Sanity check: No engines initially match ${baseHostname}`
+ );
+
+ // Install engines with the following domains. When we match engines below,
+ // perfectly matching domains should come before partially matching domains.
+ let baseDomain = `${baseHostname}.com`;
+ let perfectDomains = [baseDomain, `www.${baseDomain}`];
+ let partialDomains = [`foo.${baseDomain}`, `foo.bar.${baseDomain}`];
+
+ // Install engines with partially matching domains first so that the test
+ // isn't incidentally passing because engines are installed in the order it
+ // ultimately expects them in. Wait for each engine to finish installing
+ // before starting the next one to avoid intermittent out-of-order failures.
+ let extensions = [];
+ for (let list of [partialDomains, perfectDomains]) {
+ for (let domain of list) {
+ let ext = await SearchTestUtils.installSearchExtension(
+ {
+ name: domain,
+ search_url: `https://${domain}/`,
+ },
+ { skipUnload: true }
+ );
+ extensions.push(ext);
+ }
+ }
+
+ // Perfect matches come before partial matches.
+ let expectedDomains = [...perfectDomains, ...partialDomains];
+
+ // Do searches for the following strings. Each should match all the engines
+ // installed above.
+ let searchStrings = [baseHostname, baseHostname + "."];
+ for (let searchString of searchStrings) {
+ info(`Searching for "${searchString}"`);
+ let engines = await UrlbarSearchUtils.enginesForDomainPrefix(searchString, {
+ matchAllDomainLevels: true,
+ });
+ let engineData = engines.map(e => ({
+ name: e.name,
+ searchForm: e.searchForm,
+ }));
+ info("Matching engines: " + JSON.stringify(engineData));
+
+ Assert.equal(
+ engines.length,
+ expectedDomains.length,
+ "Expected number of matching engines"
+ );
+ Assert.deepEqual(
+ engineData.map(d => d.name),
+ expectedDomains,
+ "Expected matching engine names/domains in the expected order"
+ );
+ }
+
+ await Promise.all(extensions.map(e => e.unload()));
+});
+
+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..6eabc3bb52
--- /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.importESModule(
+ "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"
+);
+const { PlacesUIUtils } = ChromeUtils.importESModule(
+ "resource:///modules/PlacesUIUtils.sys.mjs"
+);
+
+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..034005b0fa
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js
@@ -0,0 +1,249 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://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", "https://bmget/search=%s", null, "foo"),
+ new keywordResult("https://bmget/search=foo", null),
+ ],
+
+ [
+ new bmKeywordData("bmpost", "https://bmpost/", "search=%s", "foo2"),
+ new keywordResult("https://bmpost/", "search=foo2"),
+ ],
+
+ [
+ new bmKeywordData(
+ "bmpostget",
+ "https://bmpostget/search1=%s",
+ "search2=%s",
+ "foo3"
+ ),
+ new keywordResult("https://bmpostget/search1=foo3", "search2=foo3"),
+ ],
+
+ [
+ new bmKeywordData("bmget-nosearch", "https://bmget-nosearch/", null, ""),
+ new keywordResult("https://bmget-nosearch/", null),
+ ],
+
+ [
+ new searchKeywordData(
+ "searchget",
+ "https://searchget/?search={searchTerms}",
+ null,
+ "foo4"
+ ),
+ new keywordResult("https://searchget/?search=foo4", null, true),
+ ],
+
+ [
+ new searchKeywordData(
+ "searchpost",
+ "https://searchpost/",
+ "search={searchTerms}",
+ "foo5"
+ ),
+ new keywordResult("https://searchpost/", "search=foo5", true),
+ ],
+
+ [
+ new searchKeywordData(
+ "searchpostget",
+ "https://searchpostget/?search1={searchTerms}",
+ "search2={searchTerms}",
+ "foo6"
+ ),
+ new keywordResult(
+ "https://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", "https://bmget-noparam/", null, "foo7"),
+ new keywordResult(null, null, true),
+ ],
+ [
+ new bmKeywordData(
+ "bmpost-noparam",
+ "https://bmpost-noparam/",
+ "not_a=param",
+ "foo8"
+ ),
+ new keywordResult(null, null, true),
+ ],
+
+ // Test escaping (%s = escaped, %S = raw)
+ // UTF-8 default
+ [
+ new bmKeywordData(
+ "bmget-escaping",
+ "https://bmget/?esc=%s&raw=%S",
+ null,
+ "fo\xE9"
+ ),
+ new keywordResult("https://bmget/?esc=fo%C3%A9&raw=fo\xE9", null),
+ ],
+ // Explicitly-defined ISO-8859-1
+ [
+ new bmKeywordData(
+ "bmget-escaping2",
+ "https://bmget/?esc=%s&raw=%S&mozcharset=ISO-8859-1",
+ null,
+ "fo\xE9"
+ ),
+ new keywordResult("https://bmget/?esc=fo%E9&raw=fo\xE9", null),
+ ],
+
+ // Bug 359809: Test escaping +, /, and @
+ // UTF-8 default
+ [
+ new bmKeywordData(
+ "bmget-escaping",
+ "https://bmget/?esc=%s&raw=%S",
+ null,
+ "+/@"
+ ),
+ new keywordResult("https://bmget/?esc=%2B%2F%40&raw=+/@", null),
+ ],
+ // Explicitly-defined ISO-8859-1
+ [
+ new bmKeywordData(
+ "bmget-escaping2",
+ "https://bmget/?esc=%s&raw=%S&mozcharset=ISO-8859-1",
+ null,
+ "+/@"
+ ),
+ new keywordResult("https://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: "https://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;
+
+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) {
+ await SearchTestUtils.installSearchExtension({
+ name: data.keyword,
+ keyword: data.keyword,
+ search_url: data.uri.spec,
+ search_url_get_params: "",
+ search_url_post_params: data.postData,
+ });
+ }
+ }
+}
+
+async function cleanupKeywords() {
+ await PlacesUtils.bookmarks.remove(folder);
+}
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_UrlbarUtils_unEscapeURIForUI.js b/browser/components/urlbar/tests/unit/test_UrlbarUtils_unEscapeURIForUI.js
new file mode 100644
index 0000000000..6efc6711c6
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_unEscapeURIForUI.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test for unEscapeURIForUI function in UrlbarUtils.
+ */
+
+"use strict";
+
+const TEST_DATA = [
+ {
+ description: "Test for characters including percent encoded chars",
+ input: "A%E3%81%82%F0%A0%AE%B7%21",
+ expected: "Aあ𠮷!",
+ testMessage: "Unescape given characters correctly",
+ },
+ {
+ description: "Test for characters over the limit",
+ input: "A%E3%81%82%F0%A0%AE%B7%21".repeat(
+ Math.ceil(UrlbarUtils.MAX_TEXT_LENGTH / 25)
+ ),
+ expected: "A%E3%81%82%F0%A0%AE%B7%21".repeat(
+ Math.ceil(UrlbarUtils.MAX_TEXT_LENGTH / 25)
+ ),
+ testMessage: "Return given characters as it is because of over the limit",
+ },
+];
+
+add_task(function () {
+ for (const { description, input, expected, testMessage } of TEST_DATA) {
+ info(description);
+
+ const result = UrlbarUtils.unEscapeURIForUI(input);
+ Assert.equal(result, expected, testMessage);
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_about_urls.js b/browser/components/urlbar/tests/unit/test_about_urls.js
new file mode 100644
index 0000000000..277ddb8ee1
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_about_urls.js
@@ -0,0 +1,176 @@
+/* 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 { AboutPagesUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/AboutPagesUtils.sys.mjs"
+);
+
+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",
+ tags: null,
+ providerName: "AboutPages",
+ }),
+ ],
+ });
+});
+
+// "about:" by itself matches a list of about: pages and nothing else
+add_task(async function aboutColonMatchesOnlyAboutPages() {
+ // We generate 9 about: page results because there are 10 results total,
+ // and the first result is the heuristic result.
+ function getFirst9AboutPages() {
+ const aboutPageNames = AboutPagesUtils.visibleAboutUrls.slice(0, 9);
+ const aboutPageResults = aboutPageNames.map(aboutPageName => {
+ return makeVisitResult(context, {
+ uri: aboutPageName,
+ title: aboutPageName,
+ tags: null,
+ providerName: "AboutPages",
+ });
+ });
+ return aboutPageResults;
+ }
+
+ let context = createContext("about:", { isPrivate: false });
+ await check_results({
+ context,
+ search: "about:",
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ ...getFirst9AboutPages(),
+ ],
+ });
+});
+
+// Results for about: pages do not match webpage titles from the user's history
+add_task(async function aboutResultsDoNotMatchTitlesInHistory() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI("http://example.com/guide/config/"),
+ title: "Guide to config in Firefox",
+ },
+ ]);
+
+ let context = createContext("about:config", { isPrivate: false });
+ await check_results({
+ context,
+ search: "about:config",
+ matches: [
+ makeVisitResult(context, {
+ uri: "about:config",
+ title: "about:config",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ ],
+ });
+});
+
+// Tests that about: pages are shown after general results.
+add_task(async function after_general() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI("http://example.com/guide/aboutaddons/"),
+ title: "Guide to about:addons in Firefox",
+ },
+ ]);
+
+ let context = createContext("about:a", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: "about:about",
+ title: "about:about",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/guide/aboutaddons/",
+ title: "Guide to about:addons in Firefox",
+ }),
+ makeVisitResult(context, {
+ uri: "about:addons",
+ title: "about:addons",
+ tags: null,
+ providerName: "AboutPages",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_adaptiveHistory.js b/browser/components/urlbar/tests/unit/test_autofill_adaptiveHistory.js
new file mode 100644
index 0000000000..94b29b913a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_adaptiveHistory.js
@@ -0,0 +1,1441 @@
+/* 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";
+
+// Test for adaptive history autofill.
+
+testEngine_setup();
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+});
+Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+
+const TEST_DATA = [
+ {
+ description: "Basic behavior for adaptive history autofill",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "URL that has www",
+ pref: true,
+ visitHistory: ["http://www.example.com/test"],
+ inputHistory: [{ uri: "http://www.example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://www.example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://www.example.com/test",
+ title: "test visit for http://www.example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "User's input starts with www",
+ pref: true,
+ visitHistory: ["http://www.example.com/test"],
+ inputHistory: [{ uri: "http://www.example.com/test", input: "www.exa" }],
+ userInput: "www.exa",
+ expected: {
+ autofilled: "www.example.com/test",
+ completed: "http://www.example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://www.example.com/test",
+ title: "test visit for http://www.example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Case differences for user's input are ignored",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "EXA" }],
+ userInput: "eXA",
+ expected: {
+ autofilled: "eXAmple.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Case differences for user's input that starts with www are ignored",
+ pref: true,
+ visitHistory: ["http://www.example.com/test"],
+ inputHistory: [{ uri: "http://www.example.com/test", input: "www.exa" }],
+ userInput: "WWW.exa",
+ expected: {
+ autofilled: "WWW.example.com/test",
+ completed: "http://www.example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://www.example.com/test",
+ title: "test visit for http://www.example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Mutiple case difference input history",
+ pref: true,
+ visitHistory: ["http://example.com/yes", "http://example.com/no"],
+ inputHistory: [
+ { uri: "http://example.com/yes", input: "exa" },
+ { uri: "http://example.com/yes", input: "EXA" },
+ { uri: "http://example.com/yes", input: "EXa" },
+ { uri: "http://example.com/yes", input: "eXa" },
+ { uri: "http://example.com/yes", input: "eXA" },
+ { uri: "http://example.com/no", input: "exa" },
+ { uri: "http://example.com/no", input: "exa" },
+ { uri: "http://example.com/no", input: "exa" },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/yes",
+ completed: "http://example.com/yes",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/yes",
+ title: "test visit for http://example.com/yes",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/no",
+ title: "test visit for http://example.com/no",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Multiple input history count",
+ pref: true,
+ visitHistory: ["http://example.com/few", "http://example.com/many"],
+ inputHistory: [
+ { uri: "http://example.com/many", input: "exa" },
+ { uri: "http://example.com/few", input: "exa" },
+ { uri: "http://example.com/many", input: "examp" },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/many",
+ completed: "http://example.com/many",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/many",
+ title: "test visit for http://example.com/many",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/few",
+ title: "test visit for http://example.com/few",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Multiple input history count with same input",
+ pref: true,
+ visitHistory: ["http://example.com/few", "http://example.com/many"],
+ inputHistory: [
+ { uri: "http://example.com/many", input: "exa" },
+ { uri: "http://example.com/few", input: "exa" },
+ { uri: "http://example.com/many", input: "exa" },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/many",
+ completed: "http://example.com/many",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/many",
+ title: "test visit for http://example.com/many",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/few",
+ title: "test visit for http://example.com/few",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Multiple input history count with same input but different frecency",
+ pref: true,
+ visitHistory: [
+ "http://example.com/few",
+ "http://example.com/many",
+ "http://example.com/many",
+ ],
+ inputHistory: [
+ { uri: "http://example.com/many", input: "exa" },
+ { uri: "http://example.com/few", input: "exa" },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/many",
+ completed: "http://example.com/many",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/many",
+ title: "test visit for http://example.com/many",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/few",
+ title: "test visit for http://example.com/few",
+ }),
+ ],
+ },
+ },
+ {
+ description: "User input is shorter than the input history",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "e",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "User input is longer than the input history",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "example",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "User input starts with input history and includes path of the url",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "example.com/te",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "User input starts with input history and but another url",
+ pref: true,
+ visitHistory: ["http://example.com/test", "http://example.org/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "example.o",
+ expected: {
+ autofilled: "example.org/",
+ completed: "http://example.org/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.org/",
+ fallbackTitle: "example.org",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.org/test",
+ title: "test visit for http://example.org/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "User input does not start with input history",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "notmatch" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "User input does not start with input history, but it includes as part of URL",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "test",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "User input does not start with visited URL",
+ pref: true,
+ visitHistory: ["http://mozilla.com/test"],
+ inputHistory: [{ uri: "http://mozilla.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://mozilla.com/test",
+ title: "test visit for http://mozilla.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Visited page is bookmarked",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ bookmarks: [{ uri: "http://example.com/test", title: "test bookmark" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test bookmark",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Visit history and no bookamrk with HISTORY source",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Visit history and no bookamrk with BOOKMARK source",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Bookmarked visit history with HISTORY source",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ visitHistory: ["http://example.com/test", "http://example.com/bookmarked"],
+ bookmarks: [
+ { uri: "http://example.com/bookmarked", title: "test bookmark" },
+ ],
+ inputHistory: [
+ {
+ uri: "http://example.com/test",
+ input: "exa",
+ },
+ {
+ uri: "http://example.com/bookmarked",
+ input: "exa",
+ },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/bookmarked",
+ completed: "http://example.com/bookmarked",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/bookmarked",
+ title: "test bookmark",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Bookmarked visit history with BOOKMARK source",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ visitHistory: ["http://example.com/test", "http://example.com/bookmarked"],
+ bookmarks: [
+ { uri: "http://example.com/bookmarked", title: "test bookmark" },
+ ],
+ inputHistory: [
+ {
+ uri: "http://example.com/test",
+ input: "exa",
+ },
+ {
+ uri: "http://example.com/bookmarked",
+ input: "exa",
+ },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/bookmarked",
+ completed: "http://example.com/bookmarked",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/bookmarked",
+ title: "test bookmark",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "No visit history with HISTORY source",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "No visit history with BOOKMARK source",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ bookmarks: [{ uri: "http://example.com/bookmarked", title: "test" }],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Match with path expression",
+ pref: true,
+ visitHistory: [
+ "http://example.com/testMany",
+ "http://example.com/testMany",
+ "http://example.com/test",
+ ],
+ inputHistory: [{ uri: "http://example.com/test", input: "example.com/te" }],
+ userInput: "example.com/te",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/testMany",
+ title: "test visit for http://example.com/testMany",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Prefixed URL for input history and the same string for user input",
+ pref: true,
+ visitHistory: [
+ "http://example.com/testMany",
+ "http://example.com/testMany",
+ "http://example.com/test",
+ ],
+ inputHistory: [
+ { uri: "http://example.com/test", input: "http://example.com/test" },
+ ],
+ userInput: "http://example.com/test",
+ expected: {
+ autofilled: "http://example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/testMany",
+ title: "test visit for http://example.com/testMany",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Prefixed URL for input history and URL expression for user input",
+ pref: true,
+ visitHistory: [
+ "http://example.com/testMany",
+ "http://example.com/testMany",
+ "http://example.com/test",
+ ],
+ inputHistory: [
+ { uri: "http://example.com/test", input: "http://example.com/te" },
+ ],
+ userInput: "http://example.com/te",
+ expected: {
+ autofilled: "http://example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/testMany",
+ title: "test visit for http://example.com/testMany",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Prefixed URL for input history and path expression for user input",
+ pref: true,
+ visitHistory: [
+ "http://example.com/testMany",
+ "http://example.com/testMany",
+ "http://example.com/test",
+ ],
+ inputHistory: [
+ { uri: "http://example.com/test", input: "http://example.com/te" },
+ ],
+ userInput: "example.com/te",
+ expected: {
+ autofilled: "example.com/testMany",
+ completed: "http://example.com/testMany",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/testMany",
+ title: "test visit for http://example.com/testMany",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Prefixed URL for input history and 'http' for user input",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "http" }],
+ userInput: "http",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Prefixed URL for input history and 'http:' for user input",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "http:" }],
+ userInput: "http:",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Prefixed URL for input history and 'http:/' for user input",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "http:/" }],
+ userInput: "http:/",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Prefixed URL for input history and 'http://' for user input",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "http://" }],
+ userInput: "http://",
+ expected: {
+ autofilled: "http://example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Prefixed URL for input history and 'http://e' for user input",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "http://e" }],
+ userInput: "http://e",
+ expected: {
+ autofilled: "http://example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Prefixed URL with www omitted for input history and 'http://e' for user input",
+ pref: true,
+ visitHistory: ["http://www.example.com/test"],
+ inputHistory: [{ uri: "http://www.example.com/test", input: "http://e" }],
+ userInput: "http://e",
+ expected: {
+ autofilled: "http://example.com/test",
+ completed: "http://www.example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://www.example.com/test",
+ title: "test visit for http://www.example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Those that match with fixed URL take precedence over those that match prefixed URL",
+ pref: true,
+ visitHistory: ["http://http.example.com/test", "http://example.com/test"],
+ inputHistory: [
+ { uri: "http://http.example.com/test", input: "http" },
+ { uri: "http://example.com/test", input: "http://example.com/test" },
+ ],
+ userInput: "http",
+ expected: {
+ autofilled: "http.example.com/test",
+ completed: "http://http.example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://http.example.com/test",
+ title: "test visit for http://http.example.com/test",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Input history is totally different string from the URL",
+ pref: true,
+ visitHistory: [
+ "http://example.com/testMany",
+ "http://example.com/testMany",
+ "http://example.com/test",
+ ],
+ inputHistory: [
+ { uri: "http://example.com/test", input: "totally-different-string" },
+ ],
+ userInput: "totally",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Input history is totally different string from the URL and there is a visit history whose URL starts with the input",
+ pref: true,
+ visitHistory: ["http://example.com/test", "http://totally.example.com"],
+ inputHistory: [
+ { uri: "http://example.com/test", input: "totally-different-string" },
+ ],
+ userInput: "totally",
+ expected: {
+ autofilled: "totally.example.com/",
+ completed: "http://totally.example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://totally.example.com/",
+ title: "test visit for http://totally.example.com/",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Use count threshold is as same as use count of input history",
+ pref: true,
+ useCountThreshold: 1 * 0.9 + 1,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [
+ { uri: "http://example.com/test", input: "exa" },
+ { uri: "http://example.com/test", input: "exa" },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Use count threshold is less than use count of input history",
+ pref: true,
+ useCountThreshold: 3,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [
+ { uri: "http://example.com/test", input: "exa" },
+ { uri: "http://example.com/test", input: "exa" },
+ { uri: "http://example.com/test", input: "exa" },
+ { uri: "http://example.com/test", input: "exa" },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Use count threshold is more than use count of input history",
+ pref: true,
+ useCountThreshold: 10,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [
+ { uri: "http://example.com/test", input: "exa" },
+ { uri: "http://example.com/test", input: "exa" },
+ { uri: "http://example.com/test", input: "exa" },
+ { uri: "http://example.com/test", input: "exa" },
+ { uri: "http://example.com/test", input: "exa" },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "minCharsThreshold pref equals to the user input length",
+ pref: true,
+ minCharsThreshold: 3,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "minCharsThreshold pref is smaller than the user input length",
+ pref: true,
+ minCharsThreshold: 2,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "minCharsThreshold pref is larger than the user input length",
+ pref: true,
+ minCharsThreshold: 4,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Prioritize path component with case-sensitive and that is visited",
+ pref: true,
+ visitHistory: [
+ "http://example.com/TEST",
+ "http://example.com/TEST",
+ "http://example.com/test",
+ ],
+ inputHistory: [
+ { uri: "http://example.com/TEST", input: "example.com/test" },
+ { uri: "http://example.com/test", input: "example.com/test" },
+ ],
+ userInput: "example.com/test",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/TEST",
+ title: "test visit for http://example.com/TEST",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Prioritize path component with case-sensitive but no visited data",
+ pref: true,
+ visitHistory: ["http://example.com/TEST"],
+ inputHistory: [
+ { uri: "http://example.com/TEST", input: "example.com/test" },
+ ],
+ userInput: "example.com/test",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ fallbackTitle: "example.com/test",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/TEST",
+ title: "test visit for http://example.com/TEST",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "With history and bookmarks sources, foreign_count == 0, frecency <= 0: No adaptive history autofill",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ frecency: 0,
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "With history source, visit_count == 0, foreign_count != 0: No adaptive history autofill",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ bookmarks: [{ uri: "http://example.com/test", title: "test bookmark" }],
+ userInput: "exa",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "With history source, visit_count > 0, foreign_count != 0, frecency <= 20: No adaptive history autofill",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ bookmarks: [{ uri: "http://example.com/test", title: "test bookmark" }],
+ frecency: 0,
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "With history source, visit_count > 0, foreign_count == 0, frecency <= 20: No adaptive history autofill",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ frecency: 0,
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Empty input string",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Turn the pref off",
+ pref: false,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+];
+
+add_task(async function inputTest() {
+ for (const {
+ description,
+ pref,
+ minCharsThreshold,
+ useCountThreshold,
+ source,
+ visitHistory,
+ inputHistory,
+ bookmarks,
+ frecency,
+ userInput,
+ expected,
+ } of TEST_DATA) {
+ info(description);
+
+ UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", pref);
+
+ if (!isNaN(minCharsThreshold)) {
+ UrlbarPrefs.set(
+ "autoFill.adaptiveHistory.minCharsThreshold",
+ minCharsThreshold
+ );
+ }
+
+ if (!isNaN(useCountThreshold)) {
+ UrlbarPrefs.set(
+ "autoFill.adaptiveHistory.useCountThreshold",
+ useCountThreshold
+ );
+ }
+
+ if (visitHistory && visitHistory.length) {
+ await PlacesTestUtils.addVisits(visitHistory);
+ }
+ for (const { uri, input } of inputHistory) {
+ await UrlbarUtils.addToInputHistory(uri, input);
+ }
+ for (const bookmark of bookmarks || []) {
+ await PlacesTestUtils.addBookmarkWithDetails(bookmark);
+ }
+
+ if (typeof frecency == "number") {
+ await PlacesUtils.withConnectionWrapper("test::setFrecency", db =>
+ db.execute(
+ `UPDATE moz_places SET frecency = :frecency WHERE url = :url`,
+ {
+ frecency,
+ url: visitHistory[0],
+ }
+ )
+ );
+ }
+
+ const sources = source
+ ? [source]
+ : [
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ ];
+
+ const context = createContext(userInput, {
+ sources,
+ isPrivate: false,
+ });
+
+ await check_results({
+ context,
+ autofilled: expected.autofilled,
+ completed: expected.completed,
+ hasAutofillTitle: expected.hasAutofillTitle,
+ matches: expected.results.map(f => f(context)),
+ });
+
+ await cleanupPlaces();
+ UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled");
+ UrlbarPrefs.clear("autoFill.adaptiveHistory.minCharsThreshold");
+ UrlbarPrefs.clear("autoFill.adaptiveHistory.useCountThreshold");
+ }
+});
+
+add_task(async function urlCase() {
+ UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", true);
+
+ const testVisitFixed = "example.com/ABC/DEF";
+ const testVisitURL = `http://${testVisitFixed}`;
+ const testInput = "example";
+ await PlacesTestUtils.addVisits([testVisitURL]);
+ await UrlbarUtils.addToInputHistory(testVisitURL, testInput);
+
+ const userInput = "example.COM/abc/def";
+ for (let i = 1; i <= userInput.length; i++) {
+ const currentUserInput = userInput.substring(0, i);
+ const context = createContext(currentUserInput, { isPrivate: false });
+
+ if (currentUserInput.length < testInput.length) {
+ // Autofill with host.
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ });
+ } else if (currentUserInput.length !== testVisitFixed.length) {
+ // Autofill using input history.
+ const autofilled = currentUserInput + testVisitFixed.substring(i);
+ await check_results({
+ context,
+ autofilled,
+ completed: "http://example.com/ABC/DEF",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ heuristic: true,
+ }),
+ ],
+ });
+ } else {
+ // Autofill using user's input.
+ await check_results({
+ context,
+ autofilled: "example.COM/abc/def",
+ completed: "http://example.com/abc/def",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com/abc/def",
+ fallbackTitle: "example.com/abc/def",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ });
+ }
+ }
+
+ await cleanupPlaces();
+ UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled");
+});
+
+add_task(async function decayTest() {
+ UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", true);
+
+ await PlacesTestUtils.addVisits(["http://example.com/test"]);
+ await UrlbarUtils.addToInputHistory("http://example.com/test", "exa");
+
+ const initContext = createContext("exa", { isPrivate: false });
+ await check_results({
+ context: initContext,
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ matches: [
+ makeVisitResult(initContext, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // The decay rate for a day is 0.975, as defined in PlacesFrecencyRecalculator
+ // Therefore, after 30 days, as use_count will be 0.975^30 = 0.468, we set the
+ // useCountThreshold 0.47 to not take the input history passed 30 days.
+ UrlbarPrefs.set("autoFill.adaptiveHistory.useCountThreshold", 0.47);
+
+ // Make 29 days later.
+ for (let i = 0; i < 29; i++) {
+ await Cc["@mozilla.org/places/frecency-recalculator;1"]
+ .getService(Ci.nsIObserver)
+ .wrappedJSObject.decay();
+ }
+ const midContext = createContext("exa", { isPrivate: false });
+ await check_results({
+ context: midContext,
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ matches: [
+ makeVisitResult(midContext, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Total 30 days later.
+ await Cc["@mozilla.org/places/frecency-recalculator;1"]
+ .getService(Ci.nsIObserver)
+ .wrappedJSObject.decay();
+ const context = createContext("exa", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+ UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled");
+ UrlbarPrefs.clear("autoFill.adaptiveHistory.useCountThreshold");
+});
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..9b8b77e82c
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_bookmarked.js
@@ -0,0 +1,150 @@
+/* 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.clearUserPref("browser.urlbar.suggest.quickactions");
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", 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: `test visit for 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}/`,
+ fallbackTitle: `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}/`,
+ fallbackTitle: `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}/`,
+ fallbackTitle: `www.${host}`,
+ heuristic: true,
+ }),
+ ],
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_do_not_trim.js b/browser/components/urlbar/tests/unit/test_autofill_do_not_trim.js
new file mode 100644
index 0000000000..5782bca210
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_do_not_trim.js
@@ -0,0 +1,140 @@
+/* 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/. */
+
+// We should not autofill when the search string contains spaces.
+
+testEngine_setup();
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/link/"),
+ });
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ await cleanupPlaces();
+ });
+});
+
+add_task(async function test_not_autofill_ws_1() {
+ info("Do not autofill whitespaced entry 1");
+ let context = createContext("mozilla.org ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ fallbackTitle: "http://mozilla.org/",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/link/",
+ title: "test visit for http://mozilla.org/link/",
+ }),
+ ],
+ });
+});
+
+add_task(async function test_not_autofill_ws_2() {
+ info("Do not autofill whitespaced entry 2");
+ let context = createContext("mozilla.org/ ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ fallbackTitle: "http://mozilla.org/",
+ iconUri: "page-icon:http://mozilla.org/",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/link/",
+ title: "test visit for http://mozilla.org/link/",
+ }),
+ ],
+ });
+});
+
+add_task(async function test_not_autofill_ws_3() {
+ info("Do not autofill whitespaced entry 3");
+ let context = createContext("mozilla.org/link ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/link",
+ fallbackTitle: "http://mozilla.org/link",
+ iconUri: "page-icon:http://mozilla.org/",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/link/",
+ title: "test visit for http://mozilla.org/link/",
+ }),
+ ],
+ });
+});
+
+add_task(async function test_not_autofill_ws_4() {
+ info(
+ "Do not autofill whitespaced entry 4, but UrlbarProviderPlaces provides heuristic result"
+ );
+ let context = createContext("mozilla.org/link/ ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/link/",
+ title: "test visit for http://mozilla.org/link/",
+ iconUri: "page-icon:http://mozilla.org/link/",
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ heuristic: true,
+ }),
+ ],
+ });
+});
+
+add_task(async function test_not_autofill_ws_5() {
+ info("Do not autofill whitespaced entry 5");
+ let context = createContext("moz illa ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ // query is made explict so makeSearchResult doesn't trim it.
+ query: "moz illa ",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/link/",
+ title: "test visit for http://mozilla.org/link/",
+ }),
+ ],
+ });
+});
+
+add_task(async function test_not_autofill_ws_6() {
+ info("Do not autofill whitespaced entry 6");
+ let context = createContext(" mozilla", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ // query is made explict so makeSearchResult doesn't trim it.
+ query: " mozilla",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/link/",
+ title: "test visit for http://mozilla.org/link/",
+ }),
+ ],
+ });
+});
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..632a0d580d
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_functional.js
@@ -0,0 +1,115 @@
+/* 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.clearUserPref("browser.urlbar.suggest.quickactions");
+ });
+
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", 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: "test visit for http://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: "A bookmark",
+ 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: "test visit for http://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:
+ "test visit for http://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..6913b1a3b9
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_origins.js
@@ -0,0 +1,1041 @@
+/* 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 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();
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+});
+Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+
+// "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: `test visit for http://${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: `test visit for http://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: `test visit for http://${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: `test visit for http://${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: `test visit for http://${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: `test visit for http://${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/`,
+ fallbackTitle: `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/`,
+ fallbackTitle: `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/",
+ fallbackTitle: "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: "test visit for http://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: `test visit for http://${str}`,
+ 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: "test visit for http://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 = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ { url: "http://example.com/" }
+ );
+ let httpsFrec = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ { url: "https://example.com/" }
+ );
+ let otherFrec = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ { url: "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: "test visit for 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 = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ {
+ url: "http://example.com/",
+ }
+ );
+ let httpsFrec = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ {
+ url: "https://example.com/",
+ }
+ );
+ let otherFrec = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ {
+ url: "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: "test visit for 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: SUGGESTIONS_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: SUGGESTIONS_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: SUGGESTIONS_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,
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: bookmarkedURL,
+ title: "A bookmark",
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// This is similar to suggestHistoryFalse_bookmark_prefix_0 in
+// autofill_test_autofill_originsAndQueries.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}/`,
+ fallbackTitle: `${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}/`,
+ fallbackTitle: `${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}/`,
+ fallbackTitle: `${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,
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: bookmarkedURL,
+ title: "A bookmark",
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// When the autofilled URL is `example.com/`, a visit for `example.com/?` should
+// not be included in the results since it dupes the autofill result.
+add_task(async function searchParams() {
+ await PlacesTestUtils.addVisits([
+ "http://example.com/",
+ "http://example.com/?",
+ "http://example.com/?foo",
+ ]);
+
+ // First, do a search with autofill disabled to make sure the visits were
+ // properly added. `example.com/?foo` has the highest frecency because it was
+ // added last; `example.com/?` has the next highest. `example.com/` dupes
+ // `example.com/?`, so it should not appear.
+ UrlbarPrefs.set("autoFill", false);
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/?foo",
+ title: "test visit for http://example.com/?foo",
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/?",
+ title: "test visit for http://example.com/?",
+ }),
+ ],
+ });
+
+ // Now do a search with autofill enabled. This time `example.com/` will be
+ // autofilled, and since `example.com/?` dupes it, `example.com/?` should not
+ // appear.
+ UrlbarPrefs.clear("autoFill");
+ context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ title: "test visit for http://example.com/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/?foo",
+ title: "test visit for http://example.com/?foo",
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// When the autofilled URL is `example.com/`, a visit for `example.com/?` should
+// not be included in the results since it dupes the autofill result. (Same as
+// the previous task but with https URLs instead of http. There shouldn't be any
+// substantive difference.)
+add_task(async function searchParams_https() {
+ await PlacesTestUtils.addVisits([
+ "https://example.com/",
+ "https://example.com/?",
+ "https://example.com/?foo",
+ ]);
+
+ // First, do a search with autofill disabled to make sure the visits were
+ // properly added. `example.com/?foo` has the highest frecency because it was
+ // added last; `example.com/?` has the next highest. `example.com/` dupes
+ // `example.com/?`, so it should not appear.
+ UrlbarPrefs.set("autoFill", false);
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/?foo",
+ title: "test visit for https://example.com/?foo",
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/?",
+ title: "test visit for https://example.com/?",
+ }),
+ ],
+ });
+
+ // Now do a search with autofill enabled. This time `example.com/` will be
+ // autofilled, and since `example.com/?` dupes it, `example.com/?` should not
+ // appear.
+ UrlbarPrefs.clear("autoFill");
+ context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "test visit for https://example.com/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/?foo",
+ title: "test visit for https://example.com/?foo",
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// Checks an origin that looks like a prefix: a scheme with no dots + a port.
+add_task(async function originLooksLikePrefix() {
+ let hostAndPort = "localhost:8888";
+ let address = `http://${hostAndPort}/`;
+ await PlacesTestUtils.addVisits([{ uri: address }]);
+
+ // addTestSuggestionsEngine adds a search engine
+ // with localhost as a server, so we have to disable the
+ // TTS result or else it will show up as a second result
+ // when searching l to localhost
+ UrlbarPrefs.set("suggest.engines", false);
+
+ for (let search of ["lo", "localhost", "localhost:", "localhost:8888"]) {
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: hostAndPort + "/",
+ completed: address,
+ matches: [
+ makeVisitResult(context, {
+ uri: address,
+ title: `test visit for http://${hostAndPort}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+});
+
+// Checks an origin whose prefix is "about:".
+add_task(async function about() {
+ const testData = [
+ {
+ uri: "about:config",
+ input: "conf",
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ context =>
+ makeBookmarkResult(context, {
+ uri: "about:config",
+ title: "A bookmark",
+ }),
+ ],
+ },
+ {
+ uri: "about:blank",
+ input: "about:blan",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "about:blan",
+ fallbackTitle: "about:blan",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ context =>
+ makeBookmarkResult(context, {
+ uri: "about:blank",
+ title: "A bookmark",
+ }),
+ ],
+ },
+ ];
+
+ for (const { uri, input, results } of testData) {
+ await PlacesTestUtils.addBookmarkWithDetails({ uri });
+
+ const context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ matches: results.map(f => f(context)),
+ });
+ await cleanup();
+ }
+});
+
+// Checks an origin whose prefix is "place:".
+add_task(async function place() {
+ const testData = [
+ {
+ uri: "place:transition=7&sort=4",
+ input: "tran",
+ },
+ {
+ uri: "place:transition=7&sort=4",
+ input: "place:tran",
+ },
+ ];
+
+ for (const { uri, input } of testData) {
+ await PlacesTestUtils.addBookmarkWithDetails({ uri });
+
+ const context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+});
+
+add_task(async function nullTitle() {
+ await doTitleTest({
+ visits: [
+ {
+ uri: "http://example.com/",
+ // Set title of visits data to an empty string causes
+ // the title to be null in the database.
+ title: "",
+ frecency: 100,
+ },
+ {
+ uri: "https://www.example.com/",
+ title: "high frecency",
+ frecency: 50,
+ },
+ {
+ uri: "http://www.example.com/",
+ title: "low frecency",
+ frecency: 1,
+ },
+ ],
+ input: "example.com",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ matches: context => [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ title: "high frecency",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://www.example.com/",
+ title: "high frecency",
+ }),
+ ],
+ },
+ });
+});
+
+add_task(async function domainTitle() {
+ await doTitleTest({
+ visits: [
+ {
+ uri: "http://example.com/",
+ title: "example.com",
+ frecency: 100,
+ },
+ {
+ uri: "https://www.example.com/",
+ title: "",
+ frecency: 50,
+ },
+ {
+ uri: "http://www.example.com/",
+ title: "lowest frecency but has title",
+ frecency: 1,
+ },
+ ],
+ input: "example.com",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ matches: context => [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ title: "lowest frecency but has title",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://www.example.com/",
+ title: "www.example.com",
+ }),
+ ],
+ },
+ });
+});
+
+add_task(async function exactMatchedTitle() {
+ await doTitleTest({
+ visits: [
+ {
+ uri: "http://example.com/",
+ title: "exact match",
+ frecency: 50,
+ },
+ {
+ uri: "https://www.example.com/",
+ title: "high frecency uri",
+ frecency: 100,
+ },
+ ],
+ input: "http://example.com/",
+ expected: {
+ autofilled: "http://example.com/",
+ completed: "http://example.com/",
+ matches: context => [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ title: "exact match",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://www.example.com/",
+ title: "high frecency uri",
+ }),
+ ],
+ },
+ });
+});
+
+async function doTitleTest({ visits, input, expected }) {
+ await PlacesTestUtils.addVisits(visits);
+ for (const { uri, frecency } of visits) {
+ // Prepare data.
+ await PlacesUtils.withConnectionWrapper("test::doTitleTest", async db => {
+ await db.execute(
+ `UPDATE moz_places SET frecency = :frecency, recalc_frecency=0 WHERE url = :url`,
+ {
+ frecency,
+ url: uri,
+ }
+ );
+ await db.executeCached("DELETE FROM moz_updateoriginsupdate_temp");
+ });
+ }
+
+ const context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: expected.autofilled,
+ completed: expected.completed,
+ matches: expected.matches(context),
+ });
+
+ 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..9b305717c0
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js
@@ -0,0 +1,2471 @@
+/* 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 HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback";
+const PLACES_PROVIDERNAME = "Places";
+
+/**
+ * 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();
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+});
+Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+
+let path;
+let search;
+let searchCase;
+let visitTitle;
+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";
+ visitTitle = (protocol, sub) =>
+ `test visit for ${protocol}://${sub}example.com/foo`;
+ url = host + path;
+ await callback();
+
+ info(`Running subtest with origins enabled: ${callback.name}`);
+ origins = true;
+ path = "/";
+ search = "ex";
+ searchCase = "EX";
+ visitTitle = (protocol, sub) =>
+ `test visit for ${protocol}://${sub}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: visitTitle("http", ""),
+ 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: visitTitle("http", ""),
+ 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: visitTitle("http", "www."),
+ 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: visitTitle("http", "www."),
+ 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 + "/",
+ fallbackTitle: "http://www." + search + "/",
+ displayUrl: "http://www." + search,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www." + search,
+ fallbackTitle: "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: visitTitle("http", ""),
+ 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: visitTitle("http", ""),
+ 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: visitTitle("http", "www."),
+ 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: visitTitle("http", "www."),
+ 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,
+ fallbackTitle: 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,
+ fallbackTitle: 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: PLACES_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: visitTitle("https", ""),
+ 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: visitTitle("https", "www."),
+ 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 + "/",
+ fallbackTitle: "http://www." + search + "/",
+ displayUrl: "http://www." + search,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www." + search,
+ fallbackTitle: "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: visitTitle("https", ""),
+ 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: visitTitle("https", "www."),
+ 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,
+ fallbackTitle: 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,
+ fallbackTitle: 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: PLACES_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: visitTitle("https", ""),
+ 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: visitTitle("http", ""),
+ 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: visitTitle("https", ""),
+ 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: visitTitle("http", ""),
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: PLACES_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: visitTitle("https", "www."),
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: PLACES_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: visitTitle("http", ""),
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: PLACES_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: visitTitle("https", ""),
+ 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: visitTitle("https", ""),
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://not-" + url,
+ title: "test visit for https://not-" + url,
+ providerName: PLACES_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
+ // HeuristicFallback 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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "https://not-" + url,
+ title: "test visit for https://not-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: visitTitle("https", ""),
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://not-" + url,
+ title: "test visit for https://not-" + url,
+ providerName: PLACES_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: visitTitle("https", ""),
+ 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: SUGGESTIONS_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,
+ fallbackTitle: "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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ // 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.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ { url: "http://" + url }
+ );
+ 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: "A bookmark",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://not-" + url,
+ title: "test visit for http://not-" + url,
+ providerName: PLACES_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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ // 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.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ { url: "http://" + url }
+ );
+ 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: "A bookmark",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// Bookmark a page and then clear history.
+// The bookmarked origin/URL should still be autofilled.
+add_autofill_task(async function zeroThreshold() {
+ const pageUrl = "http://" + url;
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: pageUrl,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.withConnectionWrapper("zeroThreshold", async db => {
+ await db.execute("UPDATE moz_places SET frecency = -1 WHERE url = :url", {
+ url: pageUrl,
+ });
+ await db.executeCached("DELETE FROM moz_updateoriginsupdate_temp");
+ });
+
+ // Make sure the place's frecency is -1.
+ let placeFrecency = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ { url: pageUrl }
+ );
+ Assert.equal(placeFrecency, -1);
+
+ // 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: "A bookmark",
+ 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: visitTitle("http", ""),
+ 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: SUGGESTIONS_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,
+ fallbackTitle: "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: visitTitle("http", ""),
+ 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: SUGGESTIONS_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,
+ fallbackTitle: "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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ // 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: "A bookmark",
+ 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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ 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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ })
+ );
+ } else {
+ matches.unshift(
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://" + search,
+ fallbackTitle: "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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ // 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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ 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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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,
+ fallbackTitle: 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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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,
+ fallbackTitle: 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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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,
+ fallbackTitle: 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: visitTitle("http", ""),
+ 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: PLACES_PROVIDERNAME,
+ }),
+ ];
+ if (origins) {
+ matches.unshift(
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ })
+ );
+ } else {
+ matches.unshift(
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: 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: visitTitle("http", ""),
+ 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,
+ fallbackTitle: 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: PLACES_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,
+ fallbackTitle: 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: PLACES_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,
+ fallbackTitle: 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: PLACES_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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ 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: SUGGESTIONS_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,
+ fallbackTitle: "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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ 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,
+ fallbackTitle: 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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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,
+ fallbackTitle: 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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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,
+ fallbackTitle: 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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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,
+ fallbackTitle: 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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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: visitTitle("http", ""),
+ 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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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: visitTitle("http", ""),
+ 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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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,
+ fallbackTitle: 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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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,
+ fallbackTitle: 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,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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,
+ fallbackTitle: 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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "test visit for http://" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ providerName: PLACES_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}/`,
+ fallbackTitle: `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: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "test visit for http://" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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}/`,
+ fallbackTitle: `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: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ providerName: PLACES_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}/`,
+ fallbackTitle: `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: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://" + url,
+ title: "test visit for ftp://" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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}/`,
+ fallbackTitle: `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: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://" + url,
+ title: "A bookmark",
+ providerName: PLACES_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}/`,
+ fallbackTitle: `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: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "test visit for http://non-matching-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://non-matching-" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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}/`,
+ fallbackTitle: `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: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "A bookmark",
+ providerName: PLACES_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}/`,
+ fallbackTitle: `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: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://non-matching-" + url,
+ title: "test visit for ftp://non-matching-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://non-matching-" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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}/`,
+ fallbackTitle: `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: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://non-matching-" + url,
+ title: "A bookmark",
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// When the heuristic is hidden, "ex" should autofill http://example.com/, and
+// there should be an additional http://example.com/ non-autofill result.
+add_autofill_task(async function hideHeuristic() {
+ UrlbarPrefs.set("experimental.hideHeuristic", true);
+ 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: visitTitle("http", ""),
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "test visit for http://" + url,
+ }),
+ ],
+ });
+ await cleanup();
+ UrlbarPrefs.set("experimental.hideHeuristic", false);
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_origins_alt_frecency.js b/browser/components/urlbar/tests/unit/test_autofill_origins_alt_frecency.js
new file mode 100644
index 0000000000..872f891f63
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_origins_alt_frecency.js
@@ -0,0 +1,243 @@
+/* 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 basic autofill test to ensure enabling the alternative frecency
+// algorithm doesn't break autofill or tab-to-search. A more comprehensive
+// testing of the algorithm itself is not included since it's something that
+// may change frequently according to experimentation results.
+// Other existing autofill tests will, of course, need to be adapted once an
+// algorithm is promoted to be the default.
+
+XPCOMUtils.defineLazyGetter(this, "PlacesFrecencyRecalculator", () => {
+ return Cc["@mozilla.org/places/frecency-recalculator;1"].getService(
+ Ci.nsIObserver
+ ).wrappedJSObject;
+});
+
+testEngine_setup();
+
+add_task(
+ {
+ pref_set: [["browser.urlbar.suggest.quickactions", false]],
+ },
+ async function test_autofill() {
+ const origin = "example.com";
+ let context = createContext(origin.substring(0, 2), { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ ],
+ });
+ // Add many visits.
+ const url = `https://${origin}/`;
+ await PlacesTestUtils.addVisits(new Array(10).fill(url));
+ Assert.equal(
+ await PlacesUtils.metadata.get("origin_alt_frecency_threshold", 0),
+ 0,
+ "Check there's no threshold initially"
+ );
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Assert.greater(
+ await PlacesUtils.metadata.get("origin_alt_frecency_threshold", 0),
+ 0,
+ "Check a threshold has been calculated"
+ );
+ await check_results({
+ context,
+ autofilled: `${origin}/`,
+ completed: url,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: `test visit for ${url}`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await PlacesUtils.history.clear();
+ }
+);
+
+add_task(
+ {
+ pref_set: [["browser.urlbar.suggest.quickactions", false]],
+ },
+ async function test_autofill_www() {
+ const origin = "example.com";
+ // Add many visits.
+ const url = `https://www.${origin}/`;
+ await PlacesTestUtils.addVisits(new Array(10).fill(url));
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ let context = createContext(origin.substring(0, 2), { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `${origin}/`,
+ completed: url,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: `test visit for ${url}`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await PlacesUtils.history.clear();
+ }
+);
+
+add_task(
+ {
+ pref_set: [
+ ["browser.urlbar.suggest.quickactions", false],
+ ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0],
+ ],
+ },
+ async function test_autofill_prefix_priority() {
+ const origin = "localhost";
+ const url = `https://${origin}/`;
+ await PlacesTestUtils.addVisits([url, `http://${origin}/`]);
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ let engine = Services.search.defaultEngine;
+ let context = createContext(origin.substring(0, 2), { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `${origin}/`,
+ completed: url,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: `test visit for ${url}`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: engine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await PlacesUtils.history.clear();
+ }
+);
+
+add_task(
+ {
+ pref_set: [["browser.urlbar.suggest.quickactions", false]],
+ },
+ async function test_autofill_threshold() {
+ async function getOriginAltFrecency(origin) {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(
+ "SELECT alt_frecency FROM moz_origins WHERE host = :origin",
+ { origin }
+ );
+ return rows?.[0].getResultByName("alt_frecency");
+ }
+ async function getThreshold() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute("SELECT avg(alt_frecency) FROM moz_origins");
+ return rows[0].getResultByIndex(0);
+ }
+
+ await PlacesTestUtils.addVisits(new Array(10).fill("https://example.com/"));
+ // Add more visits to the same origins to differenciate the frecency scores.
+ await PlacesTestUtils.addVisits([
+ "https://example.com/2",
+ "https://example.com/3",
+ ]);
+ await PlacesTestUtils.addVisits("https://somethingelse.org/");
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ let threshold = await PlacesUtils.metadata.get(
+ "origin_alt_frecency_threshold",
+ 0
+ );
+ Assert.greater(
+ threshold,
+ await getOriginAltFrecency("somethingelse.org"),
+ "Check mozilla.org has a lower frecency than the threshold"
+ );
+ Assert.equal(
+ threshold,
+ await getThreshold(),
+ "Check the threshold has been calculared correctly"
+ );
+
+ let engine = Services.search.defaultEngine;
+ let context = createContext("so", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ query: "so",
+ engineName: engine.name,
+ }),
+ makeVisitResult(context, {
+ uri: "https://somethingelse.org/",
+ title: "test visit for https://somethingelse.org/",
+ }),
+ ],
+ });
+ await PlacesUtils.history.clear();
+ }
+);
+
+add_task(
+ {
+ pref_set: [["browser.urlbar.suggest.quickactions", false]],
+ },
+ async function test_autofill_cutoff() {
+ async function getOriginAltFrecency(origin) {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(
+ "SELECT alt_frecency FROM moz_origins WHERE host = :origin",
+ { origin }
+ );
+ return rows?.[0].getResultByName("alt_frecency");
+ }
+
+ // Add many visits older than the default 90 days cutoff.
+ const visitDate = new Date(Date.now() - 120 * 86400000);
+ await PlacesTestUtils.addVisits(
+ new Array(10)
+ .fill("https://example.com/")
+ .map(url => ({ url, visitDate }))
+ );
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ Assert.strictEqual(
+ await getOriginAltFrecency("example.com"),
+ null,
+ "Check example.com has a NULL frecency"
+ );
+
+ let engine = Services.search.defaultEngine;
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ query: "ex",
+ engineName: engine.name,
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "test visit for https://example.com/",
+ }),
+ ],
+ });
+ await PlacesUtils.history.clear();
+ }
+);
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..dfb5de2149
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js
@@ -0,0 +1,76 @@
+/* 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.clearUserPref("browser.urlbar.suggest.quickactions");
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", 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}/`,
+ fallbackTitle: `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}/`,
+ fallbackTitle: `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..018a2e1681
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js
@@ -0,0 +1,85 @@
+/* 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 SearchTestUtils.installSearchExtension({
+ name: TEST_ENGINE_NAME,
+ keyword: TEST_ENGINE_ALIAS,
+ });
+});
+
+// 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..7b836a7c4d
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_search_engines.js
@@ -0,0 +1,246 @@
+/* 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 ProviderAutofill from normal moz_places autofill, which is tested
+// in test_autofill_origins.js and test_autofill_urls.js.
+
+"use strict";
+
+const ENGINE_NAME = "engine.xml";
+
+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"
+ );
+ });
+
+ // Bug 1149672: Once we drop support for http with OpenSearch engines,
+ // we should be able to drop the http part of this.
+ for (let scheme of ["https", "http"]) {
+ let extension;
+ if (scheme == "https") {
+ extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: ENGINE_NAME,
+ search_url: "https://www.example.com/",
+ },
+ { skipUnload: true }
+ );
+ } else {
+ let httpServer = makeTestServer();
+ httpServer.registerDirectory("/", do_get_cwd());
+ await Services.search.addOpenSearchEngine(
+ `http://localhost:${httpServer.identity.primaryPort}/data/engine.xml`,
+ null
+ );
+ }
+
+ 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 = scheme == "http" ? "https" : "http";
+ 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/",
+ fallbackTitle: 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/",
+ fallbackTitle: 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/",
+ fallbackTitle: "http://example/",
+ iconUri: "page-icon:http://example/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await extension?.unload();
+ }
+});
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..c816736531
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_urls.js
@@ -0,0 +1,881 @@
+/* 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 PLACES_PROVIDERNAME = "Places";
+
+// "example.com/foo/" should match http://example.com/foo/.
+testEngine_setup();
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+});
+Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+
+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: "test visit for http://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: "test visit for http://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",
+ fallbackTitle: "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/",
+ fallbackTitle: "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: PLACES_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: "test visit for http://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(true);
+
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ await cleanupPlaces();
+});
+
+// should *not* autofill if the URI fragment does not match with case-sensitive.
+add_task(async function uriFragmentCaseSensitive() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/#TEST",
+ },
+ ]);
+ const context = createContext("http://example.com/#t", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://example.com/#t",
+ fallbackTitle: "http://example.com/#t",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://example.com/#TEST",
+ title: "test visit for http://example.com/#TEST",
+ tags: [],
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+// should autofill if the URI fragment matches with case-sensitive.
+add_task(async function uriFragmentCaseSensitive() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/#TEST",
+ },
+ ]);
+ const context = createContext("http://example.com/#T", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://example.com/#TEST",
+ completed: "http://example.com/#TEST",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://example.com/#TEST",
+ title: "test visit for http://example.com/#TEST",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function uriCase() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/ABC/DEF",
+ },
+ ]);
+
+ const testData = [
+ {
+ input: "example.COM",
+ expected: {
+ autofilled: "example.COM/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "example.COM/",
+ expected: {
+ autofilled: "example.COM/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com/",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "example.COM/a",
+ expected: {
+ autofilled: "example.COM/aBC/",
+ completed: "http://example.com/ABC/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/",
+ fallbackTitle: "example.com/ABC/",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "example.com/ab",
+ expected: {
+ autofilled: "example.com/abC/",
+ completed: "http://example.com/ABC/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/",
+ fallbackTitle: "example.com/ABC/",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "example.com/abc",
+ expected: {
+ autofilled: "example.com/abc/",
+ completed: "http://example.com/ABC/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/",
+ fallbackTitle: "example.com/ABC/",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "example.com/abc/",
+ expected: {
+ autofilled: "example.com/abc/",
+ completed: "http://example.com/abc/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/abc/",
+ fallbackTitle: "example.com/abc/",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "example.com/abc/d",
+ expected: {
+ autofilled: "example.com/abc/dEF",
+ completed: "http://example.com/ABC/DEF",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ input: "example.com/abc/de",
+ expected: {
+ autofilled: "example.com/abc/deF",
+ completed: "http://example.com/ABC/DEF",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ input: "example.com/abc/def",
+ expected: {
+ autofilled: "example.com/abc/def",
+ completed: "http://example.com/abc/def",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/abc/def",
+ fallbackTitle: "example.com/abc/def",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "http://example.com/a",
+ expected: {
+ autofilled: "http://example.com/aBC/",
+ completed: "http://example.com/ABC/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/",
+ fallbackTitle: "example.com/ABC/",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "http://example.com/abc/",
+ expected: {
+ autofilled: "http://example.com/abc/",
+ completed: "http://example.com/abc/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/abc/",
+ fallbackTitle: "example.com/abc/",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "http://example.com/abc/d",
+ expected: {
+ autofilled: "http://example.com/abc/dEF",
+ completed: "http://example.com/ABC/DEF",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ input: "http://example.com/abc/def",
+ expected: {
+ autofilled: "http://example.com/abc/def",
+ completed: "http://example.com/abc/def",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/abc/def",
+ fallbackTitle: "example.com/abc/def",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "http://eXAMple.com/ABC/DEF",
+ expected: {
+ autofilled: "http://eXAMple.com/ABC/DEF",
+ completed: "http://example.com/ABC/DEF",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ input: "http://eXAMple.com/abc/def",
+ expected: {
+ autofilled: "http://eXAMple.com/abc/def",
+ completed: "http://example.com/abc/def",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/abc/def",
+ fallbackTitle: "example.com/abc/def",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ ];
+
+ for (const { input, expected } of testData) {
+ const context = createContext(input, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: expected.autofilled,
+ completed: expected.completed,
+ matches: expected.results.map(f => f(context)),
+ });
+ }
+
+ await cleanupPlaces();
+});
+
+async function testCaseInsensitive(isBookmark = false) {
+ 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: isBookmark
+ ? "A bookmark"
+ : "test visit for http://example.com/foo",
+ heuristic: true,
+ }),
+ ],
+ });
+ }
+
+ await cleanupPlaces();
+}
+
+// Checks a URL with an origin that looks like a prefix: a scheme with no dots +
+// a port.
+add_task(async function originLooksLikePrefix1() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://localhost:8888/foo",
+ },
+ ]);
+ const context = createContext("localhost:8888/f", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "localhost:8888/foo",
+ completed: "http://localhost:8888/foo",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://localhost:8888/foo",
+ title: "test visit for http://localhost:8888/foo",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// Same as previous (originLooksLikePrefix1) but uses a URL whose path has two
+// slashes, not one.
+add_task(async function originLooksLikePrefix2() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://localhost:8888/foo/bar",
+ },
+ ]);
+
+ let context = createContext("localhost:8888/f", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "localhost:8888/foo/",
+ completed: "http://localhost:8888/foo/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://localhost:8888/foo/",
+ fallbackTitle: "localhost:8888/foo/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://localhost:8888/foo/bar",
+ title: "test visit for http://localhost:8888/foo/bar",
+ providerName: PLACES_PROVIDERNAME,
+ tags: [],
+ }),
+ ],
+ });
+
+ context = createContext("localhost:8888/foo/b", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "localhost:8888/foo/bar",
+ completed: "http://localhost:8888/foo/bar",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://localhost:8888/foo/bar",
+ title: "test visit for http://localhost:8888/foo/bar",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// Checks view-source pages as a prefix
+// Uses bookmark because addVisits does not allow non-http uri's
+add_task(async function viewSourceAsPrefix() {
+ let address = "view-source:https://www.example.com/";
+ let title = "A view source bookmark";
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: address,
+ title,
+ });
+
+ let testData = [
+ {
+ input: "view-source:h",
+ completed: "view-source:https:/",
+ autofilled: "view-source:https:/",
+ },
+ {
+ input: "view-source:http",
+ completed: "view-source:https:/",
+ autofilled: "view-source:https:/",
+ },
+ {
+ input: "VIEW-SOURCE:http",
+ completed: "view-source:https:/",
+ autofilled: "VIEW-SOURCE:https:/",
+ },
+ ];
+
+ // Only autofills from view-source:h to view-source:https:/
+ for (let { input, completed, autofilled } of testData) {
+ let context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ completed,
+ autofilled,
+ matches: [
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ },
+ makeBookmarkResult(context, {
+ uri: address,
+ iconUri: "chrome://global/skin/icons/defaultFavicon.svg",
+ title,
+ }),
+ ],
+ });
+ }
+
+ await cleanupPlaces();
+});
+
+// Checks data url prefixes
+// Uses bookmark because addVisits does not allow non-http uri's
+add_task(async function dataAsPrefix() {
+ let address = "data:text/html,%3Ch1%3EHello%2C World!%3C%2Fh1%3E";
+ let title = "A data url bookmark";
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: address,
+ title,
+ });
+
+ let testData = [
+ {
+ input: "data:t",
+ completed: "data:text/",
+ autofilled: "data:text/",
+ },
+ {
+ input: "data:text",
+ completed: "data:text/",
+ autofilled: "data:text/",
+ },
+ {
+ input: "DATA:text",
+ completed: "data:text/",
+ autofilled: "DATA:text/",
+ },
+ ];
+
+ for (let { input, completed, autofilled } of testData) {
+ let context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ completed,
+ autofilled,
+ matches: [
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ },
+ makeBookmarkResult(context, {
+ uri: address,
+ iconUri: "chrome://global/skin/icons/defaultFavicon.svg",
+ title,
+ }),
+ ],
+ });
+ }
+
+ await cleanupPlaces();
+});
+
+// Checks about prefixes
+add_task(async function aboutAsPrefix() {
+ let testData = [
+ {
+ input: "about:abou",
+ completed: "about:about",
+ autofilled: "about:about",
+ },
+ {
+ input: "ABOUT:abou",
+ completed: "about:about",
+ autofilled: "ABOUT:about",
+ },
+ ];
+
+ for (let { input, completed, autofilled } of testData) {
+ let context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ completed,
+ autofilled,
+ matches: [
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ },
+ ],
+ });
+ }
+
+ await cleanupPlaces();
+});
+
+// Checks a URL that has www name in history.
+add_task(async function wwwHistory() {
+ const testData = [
+ {
+ input: "example.com/",
+ visitHistory: [{ uri: "http://www.example.com/", title: "Example" }],
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://www.example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://www.example.com/",
+ title: "Example",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ input: "https://example.com/",
+ visitHistory: [{ uri: "https://www.example.com/", title: "Example" }],
+ expected: {
+ autofilled: "https://example.com/",
+ completed: "https://www.example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "https://www.example.com/",
+ title: "Example",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ input: "https://example.com/abc",
+ visitHistory: [{ uri: "https://www.example.com/abc", title: "Example" }],
+ expected: {
+ autofilled: "https://example.com/abc",
+ completed: "https://www.example.com/abc",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "https://www.example.com/abc",
+ title: "Example",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ input: "https://example.com/ABC",
+ visitHistory: [{ uri: "https://www.example.com/abc", title: "Example" }],
+ expected: {
+ autofilled: "https://example.com/ABC",
+ completed: "https://www.example.com/ABC",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "https://www.example.com/ABC",
+ fallbackTitle: "https://www.example.com/ABC",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "https://www.example.com/abc",
+ title: "Example",
+ }),
+ ],
+ },
+ },
+ ];
+
+ for (const { input, visitHistory, expected } of testData) {
+ await PlacesTestUtils.addVisits(visitHistory);
+ const context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ completed: expected.completed,
+ autofilled: expected.autofilled,
+ matches: expected.results.map(f => f(context)),
+ });
+ await cleanupPlaces();
+ }
+});
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..5e2dba3446
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_avoid_middle_complete.js
@@ -0,0 +1,270 @@
+/* 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/. */
+
+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: SUGGESTIONS_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: SUGGESTIONS_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);
+ await SearchTestUtils.installSearchExtension({
+ name: "CakeSearch",
+ search_url: "https://cake.search",
+ });
+
+ 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);
+ await SearchTestUtils.installSearchExtension({
+ name: "CupcakeSearch",
+ search_url: "https://cupcake.search",
+ });
+
+ 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: SUGGESTIONS_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);
+ await SearchTestUtils.installSearchExtension({
+ name: "BaconSearch",
+ search_url: "https://bacon.search",
+ });
+
+ 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: SUGGESTIONS_ENGINE_NAME,
+ query: "ba ",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_www_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ await SearchTestUtils.installSearchExtension({
+ name: "HamSearch",
+ search_url: "https://ham.search",
+ });
+
+ 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/",
+ fallbackTitle: "http://www.ham/",
+ displayUrl: "http://www.ham",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ query: "www.ham",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_different_scheme_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ await SearchTestUtils.installSearchExtension({
+ name: "PieSearch",
+ search_url: "https://pie.search",
+ });
+
+ 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/",
+ fallbackTitle: "http://pie/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_matching_prefix_autofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ await SearchTestUtils.installSearchExtension({
+ name: "BeanSearch",
+ search_url: "https://www.bean.search",
+ });
+
+ info("Should autoFill search engine if search string has matching prefix.");
+ let context = createContext("https://www.be", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "https://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("https://be", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "https://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/",
+ fallbackTitle: "mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "test visit for http://mozilla.org/test/",
+ providerName: "Places",
+ }),
+ ],
+ });
+
+ 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..b4aa7e50bd
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js
@@ -0,0 +1,119 @@
+/* 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/. */
+
+testEngine_setup();
+
+add_task(async function test_protocol_trimming() {
+ for (let prot of ["http", "https"]) {
+ 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/",
+ fallbackTitle:
+ 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/",
+ fallbackTitle:
+ 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()}/`,
+ fallbackTitle: `${input.trim()}/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ makeVisitResult(context, {
+ uri: visit.uri.spec,
+ title: visit.title,
+ providerName: "Places",
+ }),
+ ],
+ });
+
+ 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: SUGGESTIONS_ENGINE_NAME,
+ query: input,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: visit.uri.spec,
+ title: visit.title,
+ providerName: "Places",
+ }),
+ ],
+ });
+ }
+
+ await cleanupPlaces();
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_calculator.js b/browser/components/urlbar/tests/unit/test_calculator.js
new file mode 100644
index 0000000000..7fa899f320
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_calculator.js
@@ -0,0 +1,46 @@
+/* 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";
+
+ChromeUtils.defineESModuleGetters(this, {
+ Calculator: "resource:///modules/UrlbarProviderCalculator.sys.mjs",
+});
+
+const FORMULAS = [
+ ["1+1", 2],
+ ["3+4*2/(1-5)", 1],
+ ["39+4*2/(1-5)", 37],
+ ["(39+4)*2/(1-5)", -21.5],
+ ["4+-5", -1],
+ ["-5*6", -30],
+ ["-5.5*6", -33],
+ ["-5.5*-6.4", 35.2],
+ ["-6-6-6", -18],
+ ["6-6-6", -6],
+ [".001 /2", 0.0005],
+ ["(0-.001)/2", -0.0005],
+ ["-.001/(0-2)", 0.0005],
+ ["1000000000000000000000000+1", 1e24],
+ ["1000000000000000000000000-1", 1e24],
+ ["1e+30+10", 1e30],
+ ["1e+30*10", 1e31],
+ ["1e+30/100", 1e28],
+ ["10/1000000000000000000000000", 1e-23],
+ ["10/-1000000000000000000000000", -1e-23],
+ ["1,500.5+2.5", 1503], // Ignore commas when using decimal seperators
+ ["1,5+2,5", 4], // Support comma seperators
+ ["1.500,5+2,5", 1503], // Ignore periods when using comma decimal seperators
+];
+
+add_task(function test() {
+ for (let [formula, result] of FORMULAS) {
+ let postfix = Calculator.infix2postfix(formula);
+ Assert.equal(
+ Calculator.evaluatePostfix(postfix),
+ result,
+ `${formula} should equal ${result}`
+ );
+ }
+});
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..91d8207c05
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_casing.js
@@ -0,0 +1,370 @@
+/* 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 AUTOFILL_PROVIDERNAME = "Autofill";
+const PLACES_PROVIDERNAME = "Places";
+
+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/",
+ fallbackTitle: "mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "test visit for http://mozilla.org/test/",
+ providerName: PLACES_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: "test visit for http://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: "test visit for http://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: "test visit for http://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: "test visit for http://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/",
+ fallbackTitle: "mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/Test/",
+ title: "test visit for http://mozilla.org/Test/",
+ providerName: PLACES_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/",
+ fallbackTitle: "www.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/Test/",
+ title: "test visit for http://www.mozilla.org/Test/",
+ providerName: PLACES_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: "test visit for http://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: "test visit for http://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: "test visit for http://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: "test visit for http://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: SUGGESTIONS_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: SUGGESTIONS_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: SUGGESTIONS_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: SUGGESTIONS_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: SUGGESTIONS_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..47a673d064
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_dedupe_prefix.js
@@ -0,0 +1,277 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// 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",
+ },
+ // Note that we add https://www.example.com/foo/ twice here.
+ {
+ uri: "https://www.example.com/foo/",
+ title: "Example Page",
+ },
+ {
+ uri: "https://www.example.com/foo/",
+ title: "Example Page",
+ },
+ ]);
+
+ // Expected results:
+ //
+ // Autofill result:
+ // https://www.example.com has the highest origin frecency since we added 2
+ // visits to https://www.example.com/foo/ and only one visit to the other
+ // URLs.
+ // Other results:
+ // https://example.com/foo/ has the highest possible prefix rank, and it
+ // does not dupe the autofill result, so only it should be included.
+ 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: "Example Page",
+ 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",
+ },
+ ]);
+ }
+
+ // Expected results:
+ //
+ // Autofill result:
+ // http://www.example.com now has the highest origin frecency since we added
+ // 4 visits to http://www.example.com/foo/
+ // Other results:
+ // Same as before
+ 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: "Example Page",
+ 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",
+ },
+ ]);
+ }
+
+ // Expected results:
+ //
+ // Autofill result:
+ // https://example.com now has the highest origin frecency since we added
+ // 6 visits to https://example.com/foo/
+ // Other results:
+ // https://example.com/foo/ has the highest possible prefix rank, but it
+ // dupes the heuristic so it should not be included.
+ // https://www.example.com/foo/ has the next highest prefix rank, and it
+ // does not dupe the heuristic, so only it should be included.
+ 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: "Example Page",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+// This is the same as the previous task but with `experimental.hideHeuristic`
+// enabled.
+add_task(async function hideHeuristic() {
+ UrlbarPrefs.set("experimental.hideHeuristic", true);
+
+ // 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",
+ },
+ // Note that we add https://www.example.com/foo/ twice here.
+ {
+ uri: "https://www.example.com/foo/",
+ title: "Example Page",
+ },
+ {
+ uri: "https://www.example.com/foo/",
+ title: "Example Page",
+ },
+ ]);
+
+ // Expected results:
+ //
+ // Autofill result:
+ // https://www.example.com has the highest origin frecency since we added 2
+ // visits to https://www.example.com/foo/ and only one visit to the other
+ // URLs.
+ // Other results:
+ // https://example.com/foo/ has the highest possible prefix rank, and it
+ // does not dupe the autofill result, so only it should be included.
+ 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: "Example Page",
+ 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",
+ },
+ ]);
+ }
+
+ // Expected results:
+ //
+ // Autofill result:
+ // http://www.example.com now has the highest origin frecency since we added
+ // 4 visits to http://www.example.com/foo/
+ // Other results:
+ // Same as before
+ 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: "Example Page",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ }),
+ ],
+ });
+
+ // Add enough https:// vists for it to have the highest frecency.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ },
+ ]);
+ }
+
+ // Expected results:
+ //
+ // Autofill result:
+ // https://example.com now has the highest origin frecency since we added
+ // 6 visits to https://example.com/foo/
+ // Other results:
+ // https://example.com/foo/ has the highest possible prefix rank. It dupes
+ // the heuristic so ordinarily it should not be included, but because the
+ // heuristic is hidden, only it should appear.
+ 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: "Example Page",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+ UrlbarPrefs.clear("experimental.hideHeuristic");
+});
diff --git a/browser/components/urlbar/tests/unit/test_dedupe_switchTab.js b/browser/components/urlbar/tests/unit/test_dedupe_switchTab.js
new file mode 100644
index 0000000000..3b49866b1e
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_dedupe_switchTab.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+testEngine_setup();
+
+add_task(async function test_deduplication_for_switch_tab() {
+ // Set up Places to think the tab is open locally.
+ let uri = Services.io.newURI("http://example.com/");
+
+ await PlacesTestUtils.addVisits({ uri, title: "An Example" });
+ await addOpenPages(uri, 1);
+ await UrlbarUtils.addToInputHistory("http://example.com/", "An");
+
+ let query = "An";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://example.com/",
+ title: "An Example",
+ }),
+ ],
+ });
+
+ await removeOpenPages(uri, 1);
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_download_embed_bookmarks.js b/browser/components/urlbar/tests/unit/test_download_embed_bookmarks.js
new file mode 100644
index 0000000000..29ce557748
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_download_embed_bookmarks.js
@@ -0,0 +1,137 @@
+/* -*- 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/. */
+
+/**
+ * Tests bug 449406 to ensure that TRANSITION_DOWNLOAD, TRANSITION_EMBED and
+ * TRANSITION_FRAMED_LINK bookmarked uri's show up in the location bar.
+ */
+
+testEngine_setup();
+
+const TRANSITION_EMBED = PlacesUtils.history.TRANSITION_EMBED;
+const TRANSITION_FRAMED_LINK = PlacesUtils.history.TRANSITION_FRAMED_LINK;
+const TRANSITION_DOWNLOAD = PlacesUtils.history.TRANSITION_DOWNLOAD;
+
+add_task(async function test_download_embed_bookmarks() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ let uri1 = Services.io.newURI("http://download/bookmarked");
+ let uri2 = Services.io.newURI("http://embed/bookmarked");
+ let uri3 = Services.io.newURI("http://framed/bookmarked");
+ let uri4 = Services.io.newURI("http://download");
+ let uri5 = Services.io.newURI("http://embed");
+ let uri6 = Services.io.newURI("http://framed");
+ await PlacesTestUtils.addVisits([
+ { uri: uri1, title: "download-bookmark", transition: TRANSITION_DOWNLOAD },
+ { uri: uri2, title: "embed-bookmark", transition: TRANSITION_EMBED },
+ { uri: uri3, title: "framed-bookmark", transition: TRANSITION_FRAMED_LINK },
+ { uri: uri4, title: "download2", transition: TRANSITION_DOWNLOAD },
+ { uri: uri5, title: "embed2", transition: TRANSITION_EMBED },
+ { uri: uri6, title: "framed2", transition: TRANSITION_FRAMED_LINK },
+ ]);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri1,
+ title: "download-bookmark",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri2,
+ title: "embed-bookmark",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri3,
+ title: "framed-bookmark",
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ info("Searching for bookmarked download uri matches");
+ let context = createContext("download-bookmark", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri1.spec,
+ title: "download-bookmark",
+ }),
+ ],
+ });
+
+ info("Searching for bookmarked embed uri matches");
+ context = createContext("embed-bookmark", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri2.spec,
+ title: "embed-bookmark",
+ }),
+ ],
+ });
+
+ info("Searching for bookmarked framed uri matches");
+ context = createContext("framed-bookmark", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri3.spec,
+ title: "framed-bookmark",
+ }),
+ ],
+ });
+
+ info("Searching for download uri does not match");
+ context = createContext("download2", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Searching for embed uri does not match");
+ context = createContext("embed2", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Searching for framed uri does not match");
+ context = createContext("framed2", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_empty_search.js b/browser/components/urlbar/tests/unit/test_empty_search.js
new file mode 100644
index 0000000000..2c6dffe8e6
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_empty_search.js
@@ -0,0 +1,181 @@
+/* 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 bug 426864 that makes sure searching a space only shows typed pages
+ * from history.
+ */
+
+testEngine_setup();
+
+add_task(async function test_empty_search() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+ });
+
+ let uri1 = Services.io.newURI("http://t.foo/1");
+ let uri2 = Services.io.newURI("http://t.foo/2");
+ let uri3 = Services.io.newURI("http://t.foo/3");
+ let uri4 = Services.io.newURI("http://t.foo/4");
+ let uri5 = Services.io.newURI("http://t.foo/5");
+ let uri6 = Services.io.newURI("http://t.foo/6");
+ let uri7 = Services.io.newURI("http://t.foo/7");
+
+ await PlacesTestUtils.addVisits([
+ { uri: uri7, title: "title" },
+ { uri: uri6, title: "title" },
+ { uri: uri4, title: "title" },
+ { uri: uri3, title: "title" },
+ { uri: uri2, title: "title" },
+ { uri: uri1, title: "title" },
+ ]);
+
+ await PlacesTestUtils.addBookmarkWithDetails({ uri: uri6, title: "title" });
+ await PlacesTestUtils.addBookmarkWithDetails({ uri: uri5, title: "title" });
+ await PlacesTestUtils.addBookmarkWithDetails({ uri: uri4, title: "title" });
+ await PlacesTestUtils.addBookmarkWithDetails({ uri: uri2, title: "title" });
+
+ await addOpenPages(uri7, 1);
+
+ // Now remove page 6 from history, so it is an unvisited bookmark.
+ await PlacesUtils.history.remove(uri6);
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ // With the changes above, the sites in descending order of frecency are:
+ // uri2
+ // uri4
+ // uri5
+ // uri6
+ // uri1
+ // uri3
+ // uri7
+
+ info("Match everything");
+ let context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri2.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri4.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri5.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri6.spec,
+ title: "title",
+ }),
+ makeVisitResult(context, { uri: uri1.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ makeTabSwitchResult(context, { uri: uri7.spec, title: "title" }),
+ ],
+ });
+
+ info("Match only history");
+ context = createContext(`foo ${UrlbarTokenizer.RESTRICT.HISTORY}`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri2.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri1.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri7.spec, title: "title" }),
+ ],
+ });
+
+ info("Drop-down empty search matches history sorted by frecency desc");
+ context = createContext(" ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ // query is made explict so makeSearchResult doesn't trim it.
+ query: " ",
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri2.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri1.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri7.spec, title: "title" }),
+ ],
+ });
+
+ info("Empty search matches only bookmarks when history is disabled");
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ context = createContext(" ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ // query is made explict so makeSearchResult doesn't trim it.
+ query: " ",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri2.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri4.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri5.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri6.spec,
+ title: "title",
+ }),
+ ],
+ });
+
+ info(
+ "Empty search matches only open tabs when bookmarks and history are disabled"
+ );
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext(" ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ // query is made explict so makeSearchResult doesn't trim it.
+ query: " ",
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, { uri: uri7.spec, title: "title" }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark");
+
+ 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..87a6015e86
--- /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,
+ autofilled: url,
+ completed: url,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: url,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_escaping_badEscapedURI.js b/browser/components/urlbar/tests/unit/test_escaping_badEscapedURI.js
new file mode 100644
index 0000000000..d330625bbb
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_escaping_badEscapedURI.js
@@ -0,0 +1,37 @@
+/* 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 bug 422277 to make sure bad escaped uris don't get escaped. This makes
+ * sure we don't hit an assertion for "not a UTF8 string".
+ */
+
+testEngine_setup();
+
+add_task(async function test() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ info("Bad escaped uri stays escaped");
+ let uri1 = Services.io.newURI("http://site/%EAid");
+ await PlacesTestUtils.addVisits([{ uri: uri1, title: "title" }]);
+ let context = createContext("site", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "title",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_escaping_escapeSelf.js b/browser/components/urlbar/tests/unit/test_escaping_escapeSelf.js
new file mode 100644
index 0000000000..73fe3940c3
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_escaping_escapeSelf.js
@@ -0,0 +1,62 @@
+/* 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 bug 422698 to make sure searches with urls from the location bar
+ * correctly match itself when it contains escaped characters.
+ */
+
+testEngine_setup();
+
+add_task(async function test_escape() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ let uri1 = Services.io.newURI("http://unescapeduri/");
+ let uri2 = Services.io.newURI("http://escapeduri/%40/");
+ await PlacesTestUtils.addVisits([
+ { uri: uri1, title: "title" },
+ { uri: uri2, title: "title" },
+ ]);
+
+ info("Unescaped location matches itself");
+ let context = createContext("http://unescapeduri/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "title",
+ iconUri: `page-icon:${uri1.spec}`,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ heuristic: true,
+ }),
+ // Note that uri2 does not appear in results.
+ ],
+ });
+
+ info("Escaped location matches itself");
+ context = createContext("http://escapeduri/%40", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://escapeduri/%40",
+ fallbackTitle: "http://escapeduri/%40",
+ iconUri: "page-icon:http://escapeduri/",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "title",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_exposure.js b/browser/components/urlbar/tests/unit/test_exposure.js
new file mode 100644
index 0000000000..837673a868
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_exposure.js
@@ -0,0 +1,195 @@
+/* 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/. */
+
+ChromeUtils.defineESModuleGetters(this, {
+ QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
+ UrlbarProviderQuickSuggest:
+ "resource:///modules/UrlbarProviderQuickSuggest.sys.mjs",
+});
+
+// Tests that registering an exposureResults pref and triggering a match causes
+// the exposure event to be recorded on the UrlbarResults.
+const REMOTE_SETTINGS_RESULTS = [
+ {
+ id: 1,
+ url: "http://test.com/q=frabbits",
+ title: "frabbits",
+ keywords: ["test"],
+ click_url: "http://click.reporting.test.com/",
+ impression_url: "http://impression.reporting.test.com/",
+ advertiser: "TestAdvertiser",
+ },
+ {
+ id: 2,
+ url: "http://test.com/q=frabbits",
+ title: "frabbits",
+ keywords: ["non_sponsored"],
+ click_url: "http://click.reporting.test.com/",
+ impression_url: "http://impression.reporting.test.com/",
+ advertiser: "wikipedia",
+ iab_category: "5 - Education",
+ },
+];
+
+const EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT = {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: "adm_sponsored",
+ qsSuggestion: "test",
+ title: "frabbits",
+ url: "http://test.com/q=frabbits",
+ originalUrl: "http://test.com/q=frabbits",
+ icon: null,
+ sponsoredImpressionUrl: "http://impression.reporting.test.com/",
+ sponsoredClickUrl: "http://click.reporting.test.com/",
+ sponsoredBlockId: 1,
+ sponsoredAdvertiser: "TestAdvertiser",
+ isSponsored: true,
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ displayUrl: "http://test.com/q=frabbits",
+ source: "remote-settings",
+ },
+};
+
+const EXPECTED_NON_SPONSORED_REMOTE_SETTINGS_RESULT = {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: "adm_nonsponsored",
+ qsSuggestion: "non_sponsored",
+ title: "frabbits",
+ url: "http://test.com/q=frabbits",
+ originalUrl: "http://test.com/q=frabbits",
+ icon: null,
+ sponsoredImpressionUrl: "http://impression.reporting.test.com/",
+ sponsoredClickUrl: "http://click.reporting.test.com/",
+ sponsoredBlockId: 2,
+ sponsoredAdvertiser: "wikipedia",
+ sponsoredIabCategory: "5 - Education",
+ isSponsored: false,
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ displayUrl: "http://test.com/q=frabbits",
+ source: "remote-settings",
+ },
+};
+
+add_setup(async function test_setup() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+
+ // FOG needs to be initialized in order for data to flow.
+ Services.fog.initializeFOG();
+
+ UrlbarPrefs.set("quicksuggest.enabled", true);
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ UrlbarPrefs.set("quicksuggest.shouldShowOnboardingDialog", false);
+
+ await MerinoTestUtils.server.start();
+
+ // Set up the remote settings client with the test data.
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: REMOTE_SETTINGS_RESULTS,
+ },
+ ],
+ });
+});
+
+add_task(async function testExposureCheck() {
+ UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", true);
+ UrlbarPrefs.set("exposureResults", "rs_adm_sponsored");
+ UrlbarPrefs.set("showExposureResults", true);
+
+ let context = createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+
+ await check_results({
+ context,
+ matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
+ });
+
+ Assert.equal(context.results[0].exposureResultType, "rs_adm_sponsored");
+ Assert.equal(context.results[0].exposureResultHidden, false);
+});
+
+add_task(async function testExposureCheckMultiple() {
+ UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", true);
+ UrlbarPrefs.set("exposureResults", "rs_adm_sponsored,rs_adm_nonsponsored");
+ UrlbarPrefs.set("showExposureResults", true);
+
+ let context = createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+
+ await check_results({
+ context,
+ matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
+ });
+
+ Assert.equal(context.results[0].exposureResultType, "rs_adm_sponsored");
+ Assert.equal(context.results[0].exposureResultHidden, false);
+
+ context = createContext("non_sponsored", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+
+ await check_results({
+ context,
+ matches: [EXPECTED_NON_SPONSORED_REMOTE_SETTINGS_RESULT],
+ });
+
+ Assert.equal(context.results[0].exposureResultType, "rs_adm_nonsponsored");
+ Assert.equal(context.results[0].exposureResultHidden, false);
+});
+
+add_task(async function exposureDisplayFiltering() {
+ UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", true);
+ UrlbarPrefs.set("exposureResults", "rs_adm_sponsored");
+ UrlbarPrefs.set("showExposureResults", false);
+
+ let context = createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+
+ await check_results({
+ context,
+ matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
+ });
+
+ Assert.equal(context.results[0].exposureResultType, "rs_adm_sponsored");
+ Assert.equal(context.results[0].exposureResultHidden, true);
+});
diff --git a/browser/components/urlbar/tests/unit/test_frecency.js b/browser/components/urlbar/tests/unit/test_frecency.js
new file mode 100644
index 0000000000..0d7a007e0d
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_frecency.js
@@ -0,0 +1,403 @@
+/* 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 bug 406358 to make sure frecency works for empty input/search, but
+ * this also tests for non-empty inputs as well. Because the interactions among
+ * DIFFERENT* visit counts and visit dates is not well defined, this test
+ * holds one of the two values constant when modifying the other.
+ *
+ * Also test bug 419068 to make sure tagged pages don't necessarily have to be
+ * first in the results.
+ *
+ * Also test bug 426166 to make sure that the results of autocomplete searches
+ * are stable. Note that failures of this test will be intermittent by nature
+ * since we are testing to make sure that the unstable sort algorithm used
+ * by SQLite is not changing the order of the results on us.
+ */
+
+testEngine_setup();
+
+async function task_setCountDate(uri, count, date) {
+ // We need visits so that frecency can be computed over multiple visits
+ let visits = [];
+ for (let i = 0; i < count; i++) {
+ visits.push({
+ uri,
+ visitDate: date,
+ transition: PlacesUtils.history.TRANSITION_TYPED,
+ });
+ }
+ await PlacesTestUtils.addVisits(visits);
+}
+
+async function setBookmark(uri) {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: uri,
+ title: "bleh",
+ });
+}
+
+async function tagURI(uri, tags) {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: uri,
+ title: "bleh",
+ });
+ PlacesUtils.tagging.tagURI(uri, tags);
+}
+
+var uri1 = Services.io.newURI("http://site.tld/1");
+var uri2 = Services.io.newURI("http://site.tld/2");
+var uri3 = Services.io.newURI("http://aaaaaaaaaa/1");
+var uri4 = Services.io.newURI("http://aaaaaaaaaa/2");
+
+// d1 is younger (should show up higher) than d2 (PRTime is in usecs not msec)
+// Make sure the dates fall into different frecency groups
+var d1 = new Date(Date.now() - 1000 * 60 * 60) * 1000;
+var d2 = new Date(Date.now() - 1000 * 60 * 60 * 24 * 10) * 1000;
+// c1 is larger (should show up higher) than c2
+var c1 = 10;
+var c2 = 1;
+
+var tests = [
+ // test things without a search term
+ async function () {
+ info("Test 0: same count, different date");
+ await task_setCountDate(uri1, c1, d1);
+ await task_setCountDate(uri2, c1, d2);
+ await tagURI(uri1, ["site"]);
+ let context = createContext(" ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ query: " ",
+ }),
+ // uri1 is a visit result despite being a tagged bookmark because we
+ // are searching for the empty string. By default, the empty string
+ // filters to history. uri1 will be displayed as a bookmark later in the
+ // test when we are searching with a non-empty string.
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "bleh",
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: `test visit for ${uri2.spec}`,
+ }),
+ ],
+ });
+ },
+ async function () {
+ info("Test 1: same count, different date");
+ await task_setCountDate(uri1, c1, d2);
+ await task_setCountDate(uri2, c1, d1);
+ await tagURI(uri1, ["site"]);
+ let context = createContext(" ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ query: " ",
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: `test visit for ${uri2.spec}`,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "bleh",
+ }),
+ ],
+ });
+ },
+ async function () {
+ info("Test 2: different count, same date");
+ await task_setCountDate(uri1, c1, d1);
+ await task_setCountDate(uri2, c2, d1);
+ await tagURI(uri1, ["site"]);
+ let context = createContext(" ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ query: " ",
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "bleh",
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: `test visit for ${uri2.spec}`,
+ }),
+ ],
+ });
+ },
+ async function () {
+ info("Test 3: different count, same date");
+ await task_setCountDate(uri1, c2, d1);
+ await task_setCountDate(uri2, c1, d1);
+ await tagURI(uri1, ["site"]);
+ let context = createContext(" ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ query: " ",
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: `test visit for ${uri2.spec}`,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "bleh",
+ }),
+ ],
+ });
+ },
+
+ // test things with a search term
+ async function () {
+ info("Test 4: same count, different date");
+ await task_setCountDate(uri1, c1, d1);
+ await task_setCountDate(uri2, c1, d2);
+ await tagURI(uri1, ["site"]);
+ let context = createContext("site", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri1.spec,
+ title: "bleh",
+ tags: ["site"],
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: `test visit for ${uri2.spec}`,
+ }),
+ ],
+ });
+ },
+ async function () {
+ info("Test 5: same count, different date");
+ await task_setCountDate(uri1, c1, d2);
+ await task_setCountDate(uri2, c1, d1);
+ await tagURI(uri1, ["site"]);
+ let context = createContext("site", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: `test visit for ${uri2.spec}`,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri1.spec,
+ title: "bleh",
+ tags: ["site"],
+ }),
+ ],
+ });
+ },
+ async function () {
+ info("Test 6: different count, same date");
+ await task_setCountDate(uri1, c1, d1);
+ await task_setCountDate(uri2, c2, d1);
+ await tagURI(uri1, ["site"]);
+ let context = createContext("site", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri1.spec,
+ title: "bleh",
+ tags: ["site"],
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: `test visit for ${uri2.spec}`,
+ }),
+ ],
+ });
+ },
+ async function () {
+ info("Test 7: different count, same date");
+ await task_setCountDate(uri1, c2, d1);
+ await task_setCountDate(uri2, c1, d1);
+ await tagURI(uri1, ["site"]);
+ let context = createContext("site", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: `test visit for ${uri2.spec}`,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri1.spec,
+ title: "bleh",
+ tags: ["site"],
+ }),
+ ],
+ });
+ },
+ // There are multiple tests for 8, hence the multiple functions
+ // Bug 426166 section
+ async function () {
+ info("Test 8.1a: same count, same date");
+ await setBookmark(uri3);
+ await setBookmark(uri4);
+ let context = createContext("a", { isPrivate: false });
+ let bookmarkResults = [
+ makeBookmarkResult(context, {
+ uri: uri4.spec,
+ title: "bleh",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri3.spec,
+ title: "bleh",
+ }),
+ ];
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...bookmarkResults,
+ ],
+ });
+
+ context = createContext("aa", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ // We need to continuously redefine the heuristic search result because it
+ // is the only one that changes with the search string.
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...bookmarkResults,
+ ],
+ });
+
+ context = createContext("aaa", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...bookmarkResults,
+ ],
+ });
+
+ context = createContext("aaaa", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...bookmarkResults,
+ ],
+ });
+
+ context = createContext("aaa", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...bookmarkResults,
+ ],
+ });
+
+ context = createContext("aa", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...bookmarkResults,
+ ],
+ });
+
+ context = createContext("a", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...bookmarkResults,
+ ],
+ });
+ },
+];
+
+add_task(async function test_frecency() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ // always search in history + bookmarks, no matter what the default is
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.engines", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+ for (let test of tests) {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+
+ await test();
+ }
+ for (let type of [
+ "history",
+ "bookmark",
+ "openpage",
+ "searches",
+ "engines",
+ "quickactions",
+ ]) {
+ Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_frecency_alternative_nimbus.js b/browser/components/urlbar/tests/unit/test_frecency_alternative_nimbus.js
new file mode 100644
index 0000000000..c2d0e4bae3
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_frecency_alternative_nimbus.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const ORIGINS_FEATUREGATE = "places.frecency.origins.alternative.featureGate";
+const ORIGINS_DAYSCUTOFF = "places.frecency.origins.alternative.daysCutOff";
+
+add_task(async function () {
+ let reset = await UrlbarTestUtils.initNimbusFeature(
+ {
+ // Empty for sanity check.
+ },
+ "frecency",
+ "config"
+ );
+ Assert.equal(Services.prefs.getBoolPref(ORIGINS_FEATUREGATE), false);
+ Assert.throws(
+ () => Services.prefs.getIntPref(ORIGINS_DAYSCUTOFF),
+ /NS_ERROR_UNEXPECTED/
+ );
+ await reset();
+
+ reset = await UrlbarTestUtils.initNimbusFeature(
+ {
+ originsAlternativeEnable: true,
+ },
+ "frecency",
+ "config"
+ );
+ Assert.equal(Services.prefs.getBoolPref(ORIGINS_FEATUREGATE), true);
+ Assert.ok(Services.prefs.prefHasUserValue(ORIGINS_FEATUREGATE));
+ Assert.throws(
+ () => Services.prefs.getIntPref(ORIGINS_DAYSCUTOFF),
+ /NS_ERROR_UNEXPECTED/
+ );
+ await reset();
+
+ reset = await UrlbarTestUtils.initNimbusFeature(
+ {
+ originsAlternativeEnable: true,
+ originsDaysCutOff: 60,
+ },
+ "frecency",
+ "config"
+ );
+ Assert.equal(Services.prefs.getBoolPref(ORIGINS_FEATUREGATE), true);
+ Assert.ok(Services.prefs.prefHasUserValue(ORIGINS_FEATUREGATE));
+ Assert.equal(Services.prefs.getIntPref(ORIGINS_DAYSCUTOFF, 90), 60);
+ Assert.ok(Services.prefs.prefHasUserValue(ORIGINS_DAYSCUTOFF));
+ await reset();
+});
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..a53786a747
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_heuristic_cancel.js
@@ -0,0 +1,238 @@
+/* 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.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderAutofill: "resource:///modules/UrlbarProviderAutofill.sys.mjs",
+});
+
+/**
+ * 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 alerts 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);
+});
+
+/**
+ * Tests that UrlbarProvidersManager._heuristicProviderTimer is cancelled when
+ * a query is cancelled.
+ */
+add_task(async function timerIsCancelled() {
+ 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 UrlbarProviderPlaces 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) {
+ 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);
+});
+
+/**
+ * Tests that old autofill results aren't displayed after a query is cancelled.
+ * See bug 1653436.
+ */
+add_task(async function autofillIsCleared() {
+ /**
+ * Steps:
+ * 1. Start query.
+ * 2. Allow UrlbarProviderAutofill to start _getAutofillResult.
+ * 3. Execute a new query with no autofill match, cancelling the first
+ * query.
+ * 4. Test that the old result from UrlbarProviderAutofill isn't displayed.
+ */
+ await PlacesTestUtils.addVisits("http://example.com");
+
+ let firstContext = createContext("e", {
+ providers: ["Autofill", "HeuristicFallback"],
+ });
+ let secondContext = createContext("em", {
+ providers: ["Autofill", "HeuristicFallback"],
+ });
+
+ info("Sanity check: The first query autofills and the second does not.");
+ await check_results({
+ firstContext,
+ autofilled: "example.com",
+ completed: "http://example.com/",
+ matches: [
+ makeVisitResult(firstContext, {
+ uri: "http://example.com/",
+ title: "example.com",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await check_results({
+ secondContext,
+ matches: [
+ makeSearchResult(secondContext, {
+ engineName: (await Services.search.getDefault()).name,
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Refresh our queries
+ firstContext = createContext("e", {
+ providers: ["Autofill", "HeuristicFallback"],
+ });
+ secondContext = createContext("em", {
+ providers: ["Autofill", "HeuristicFallback"],
+ });
+
+ // Set up controller to observe queries.
+ let controller = UrlbarTestUtils.newMockController();
+ let queryRecieved, queryCancelled;
+ const controllerListener = {
+ onQueryResults(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(
+ !UrlbarProviderAutofill._autofillData,
+ "The first result should not have populated autofill data."
+ );
+ Assert.ok(!queryCancelled, "No more than one query should be cancelled.");
+ queryCancelled = true;
+ },
+ };
+ controller.addQueryListener(controllerListener);
+
+ // Intentionally do not await this first query.
+ controller.startQuery(firstContext);
+ await controller.startQuery(secondContext);
+
+ Assert.ok(queryCancelled, "At least one query was cancelled.");
+ Assert.ok(queryRecieved, "At least one query finished.");
+ controller.removeQueryListener(controllerListener);
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_hideSponsoredHistory.js b/browser/components/urlbar/tests/unit/test_hideSponsoredHistory.js
new file mode 100644
index 0000000000..d94a655b22
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_hideSponsoredHistory.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This tests the muxer functionality that hides URLs in history that were
+// originally sponsored.
+
+"use strict";
+
+add_task(async function test() {
+ // Disable search suggestions to avoid hitting the network.
+ UrlbarPrefs.set("suggest.searches", false);
+
+ let engine = await Services.search.getDefault();
+ let pref = "browser.newtabpage.activity-stream.hideTopSitesWithSearchParam";
+
+ // This maps URL search params to objects describing whether a URL with those
+ // params is expected to appear in the search results. Each inner object maps
+ // from a value of the pref to whether the URL is expected to appear given the
+ // pref value.
+ let tests = {
+ "": {
+ "": true,
+ test: true,
+ "test=": true,
+ "test=hide": true,
+ nomatch: true,
+ "nomatch=": true,
+ "nomatch=hide": true,
+ },
+ test: {
+ "": true,
+ test: false,
+ "test=": false,
+ "test=hide": true,
+ nomatch: true,
+ "nomatch=": true,
+ "nomatch=hide": true,
+ },
+ "test=hide": {
+ "": true,
+ test: false,
+ "test=": true,
+ "test=hide": false,
+ nomatch: true,
+ "nomatch=": true,
+ "nomatch=hide": true,
+ },
+ "test=foo&test=hide": {
+ "": true,
+ test: false,
+ "test=": true,
+ "test=hide": false,
+ nomatch: true,
+ "nomatch=": true,
+ "nomatch=hide": true,
+ },
+ };
+
+ for (let [urlParams, expected] of Object.entries(tests)) {
+ for (let [prefValue, shouldAppear] of Object.entries(expected)) {
+ info(
+ "Running test: " +
+ JSON.stringify({ urlParams, prefValue, shouldAppear })
+ );
+
+ // Add a visit to a URL with search params `urlParams`.
+ let url = new URL("http://example.com/");
+ url.search = urlParams;
+ await PlacesTestUtils.addVisits(url);
+
+ // Set the pref to `prefValue`.
+ Services.prefs.setCharPref(pref, prefValue);
+
+ // Set up the context and expected results. If `shouldAppear` is true, a
+ // visit result for the URL should appear.
+ let context = createContext("ample", { isPrivate: false });
+ let expectedResults = [
+ makeSearchResult(context, {
+ heuristic: true,
+ engineName: engine.name,
+ engineIconUri: engine.iconURI?.spec,
+ }),
+ ];
+ if (shouldAppear) {
+ expectedResults.push(
+ makeVisitResult(context, {
+ uri: url.toString(),
+ title: "test visit for " + url,
+ })
+ );
+ }
+
+ // Do a search and check the results.
+ await check_results({
+ context,
+ matches: expectedResults,
+ });
+
+ await PlacesUtils.history.clear();
+ }
+ }
+
+ Services.prefs.clearUserPref(pref);
+});
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..de60742b81
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_keywords.js
@@ -0,0 +1,212 @@
+/* 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/. */
+
+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/",
+ fallbackTitle: "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/",
+ title: "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/",
+ fallbackTitle: "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/",
+ fallbackTitle: "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/",
+ title: "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: "test visit for http://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/",
+ title: "http://mozilla.com/",
+ keyword: "moz",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "test visit for http://mozilla.org/test/",
+ }),
+ ],
+ });
+
+ // 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: "A bookmark",
+ 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_l10nCache.js b/browser/components/urlbar/tests/unit/test_l10nCache.js
new file mode 100644
index 0000000000..e92c75fa01
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_l10nCache.js
@@ -0,0 +1,685 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests L10nCache in UrlbarUtils.jsm.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ L10nCache: "resource:///modules/UrlbarUtils.sys.mjs",
+});
+
+add_task(async function comprehensive() {
+ // Set up a mock localization.
+ let l10n = initL10n({
+ args0a: "Zero args value",
+ args0b: "Another zero args value",
+ args1a: "One arg value is { $arg1 }",
+ args1b: "Another one arg value is { $arg1 }",
+ args2a: "Two arg values are { $arg1 } and { $arg2 }",
+ args2b: "More two arg values are { $arg1 } and { $arg2 }",
+ args3a: "Three arg values are { $arg1 }, { $arg2 }, and { $arg3 }",
+ args3b: "More three arg values are { $arg1 }, { $arg2 }, and { $arg3 }",
+ attrs1: [".label = attrs1 label has zero args"],
+ attrs2: [
+ ".label = attrs2 label has zero args",
+ ".tooltiptext = attrs2 tooltiptext arg value is { $arg1 }",
+ ],
+ attrs3: [
+ ".label = attrs3 label has zero args",
+ ".tooltiptext = attrs3 tooltiptext arg value is { $arg1 }",
+ ".alt = attrs3 alt arg values are { $arg1 } and { $arg2 }",
+ ],
+ });
+
+ let tests = [
+ // different strings with the same number of args and also the same strings
+ // with different args
+ {
+ obj: {
+ id: "args0a",
+ },
+ expected: {
+ value: "Zero args value",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args0b",
+ },
+ expected: {
+ value: "Another zero args value",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args1a",
+ args: { arg1: "foo1" },
+ },
+ expected: {
+ value: "One arg value is foo1",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args1a",
+ args: { arg1: "foo2" },
+ },
+ expected: {
+ value: "One arg value is foo2",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args1b",
+ args: { arg1: "foo1" },
+ },
+ expected: {
+ value: "Another one arg value is foo1",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args1b",
+ args: { arg1: "foo2" },
+ },
+ expected: {
+ value: "Another one arg value is foo2",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args2a",
+ args: { arg1: "foo1", arg2: "bar1" },
+ },
+ expected: {
+ value: "Two arg values are foo1 and bar1",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args2a",
+ args: { arg1: "foo2", arg2: "bar2" },
+ },
+ expected: {
+ value: "Two arg values are foo2 and bar2",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args2b",
+ args: { arg1: "foo1", arg2: "bar1" },
+ },
+ expected: {
+ value: "More two arg values are foo1 and bar1",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args2b",
+ args: { arg1: "foo2", arg2: "bar2" },
+ },
+ expected: {
+ value: "More two arg values are foo2 and bar2",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args3a",
+ args: { arg1: "foo1", arg2: "bar1", arg3: "baz1" },
+ },
+ expected: {
+ value: "Three arg values are foo1, bar1, and baz1",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args3a",
+ args: { arg1: "foo2", arg2: "bar2", arg3: "baz2" },
+ },
+ expected: {
+ value: "Three arg values are foo2, bar2, and baz2",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args3b",
+ args: { arg1: "foo1", arg2: "bar1", arg3: "baz1" },
+ },
+ expected: {
+ value: "More three arg values are foo1, bar1, and baz1",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args3b",
+ args: { arg1: "foo2", arg2: "bar2", arg3: "baz2" },
+ },
+ expected: {
+ value: "More three arg values are foo2, bar2, and baz2",
+ attributes: null,
+ },
+ },
+
+ // two instances of the same string with their args swapped
+ {
+ obj: {
+ id: "args2a",
+ args: { arg1: "arg A", arg2: "arg B" },
+ },
+ expected: {
+ value: "Two arg values are arg A and arg B",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args2a",
+ args: { arg1: "arg B", arg2: "arg A" },
+ },
+ expected: {
+ value: "Two arg values are arg B and arg A",
+ attributes: null,
+ },
+ },
+
+ // strings with attributes
+ {
+ obj: {
+ id: "attrs1",
+ },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs1 label has zero args",
+ },
+ },
+ },
+ {
+ obj: {
+ id: "attrs2",
+ args: {
+ arg1: "arg A",
+ },
+ },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs2 label has zero args",
+ tooltiptext: "attrs2 tooltiptext arg value is arg A",
+ },
+ },
+ },
+ {
+ obj: {
+ id: "attrs3",
+ args: {
+ arg1: "arg A",
+ arg2: "arg B",
+ },
+ },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs3 label has zero args",
+ tooltiptext: "attrs3 tooltiptext arg value is arg A",
+ alt: "attrs3 alt arg values are arg A and arg B",
+ },
+ },
+ },
+ ];
+
+ let cache = new L10nCache(l10n);
+
+ // Get some non-cached strings.
+ Assert.ok(!cache.get({ id: "uncached1" }), "Uncached string 1");
+ Assert.ok(!cache.get({ id: "uncached2", args: "foo" }), "Uncached string 2");
+
+ // Add each test string and get it back.
+ for (let { obj, expected } of tests) {
+ await cache.add(obj);
+ let message = cache.get(obj);
+ Assert.deepEqual(
+ message,
+ expected,
+ "Expected message for obj: " + JSON.stringify(obj)
+ );
+ }
+
+ // Get each string again to make sure each add didn't somehow mess up the
+ // previously added strings.
+ for (let { obj, expected } of tests) {
+ Assert.deepEqual(
+ cache.get(obj),
+ expected,
+ "Expected message for obj: " + JSON.stringify(obj)
+ );
+ }
+
+ // Delete some of the strings. We'll delete every other one to mix it up.
+ for (let i = 0; i < tests.length; i++) {
+ if (i % 2 == 0) {
+ let { obj } = tests[i];
+ cache.delete(obj);
+ Assert.ok(!cache.get(obj), "Deleted from cache: " + JSON.stringify(obj));
+ }
+ }
+
+ // Get each remaining string.
+ for (let i = 0; i < tests.length; i++) {
+ if (i % 2 != 0) {
+ let { obj, expected } = tests[i];
+ Assert.deepEqual(
+ cache.get(obj),
+ expected,
+ "Expected message for obj: " + JSON.stringify(obj)
+ );
+ }
+ }
+
+ // Clear the cache.
+ cache.clear();
+ for (let { obj } of tests) {
+ Assert.ok(!cache.get(obj), "After cache clear: " + JSON.stringify(obj));
+ }
+
+ // `ensure` each test string and get it back.
+ for (let { obj, expected } of tests) {
+ await cache.ensure(obj);
+ let message = cache.get(obj);
+ Assert.deepEqual(
+ message,
+ expected,
+ "Expected message for obj: " + JSON.stringify(obj)
+ );
+
+ // Call `ensure` again. This time, `add` should not be called.
+ let originalAdd = cache.add;
+ cache.add = () => Assert.ok(false, "add erroneously called");
+ await cache.ensure(obj);
+ cache.add = originalAdd;
+ }
+
+ // Clear the cache again.
+ cache.clear();
+ for (let { obj } of tests) {
+ Assert.ok(!cache.get(obj), "After cache clear: " + JSON.stringify(obj));
+ }
+
+ // `ensureAll` the test strings and get them back.
+ let objects = tests.map(({ obj }) => obj);
+ await cache.ensureAll(objects);
+ for (let { obj, expected } of tests) {
+ let message = cache.get(obj);
+ Assert.deepEqual(
+ message,
+ expected,
+ "Expected message for obj: " + JSON.stringify(obj)
+ );
+ }
+
+ // Ensure the cache is cleared after the app locale changes
+ Assert.greater(cache.size(), 0, "The cache has messages in it.");
+ Services.obs.notifyObservers(null, "intl:app-locales-changed");
+ await l10n.ready;
+ Assert.equal(cache.size(), 0, "The cache is empty on app locale change");
+});
+
+// Tests the `excludeArgsFromCacheKey` option.
+add_task(async function excludeArgsFromCacheKey() {
+ // Set up a mock localization.
+ let l10n = initL10n({
+ args0: "Zero args value",
+ args1: "One arg value is { $arg1 }",
+ attrs0: [".label = attrs0 label has zero args"],
+ attrs1: [
+ ".label = attrs1 label has zero args",
+ ".tooltiptext = attrs1 tooltiptext arg value is { $arg1 }",
+ ],
+ });
+
+ let cache = new L10nCache(l10n);
+
+ // Test cases. For each test case, we cache a string using one or more
+ // methods, `cache.add({ excludeArgsFromCacheKey: true })` and/or
+ // `cache.ensure({ excludeArgsFromCacheKey: true })`. After calling each
+ // method, we call `cache.get()` to get the cached string.
+ //
+ // Test cases are cumulative, so when `cache.add()` is called for a string and
+ // then `cache.ensure()` is called for the same string but with different l10n
+ // argument values, the string should be re-cached with the new values.
+ //
+ // Each item in the tests array is: `{ methods, obj, gets }`
+ //
+ // {array} methods
+ // Array of cache method names, one or more of: "add", "ensure"
+ // Methods are called in the order they are listed.
+ // {object} obj
+ // An l10n object that will be passed to the cache methods:
+ // `{ id, args, excludeArgsFromCacheKey }`
+ // {array} gets
+ // An array of objects that describes a series of calls to `cache.get()` and
+ // the expected return values: `{ obj, expected }`
+ //
+ // {object} obj
+ // An l10n object that will be passed to `cache.get():`
+ // `{ id, args, excludeArgsFromCacheKey }`
+ // {object} expected
+ // The expected return value from `get()`.
+ let tests = [
+ // args0: string with no args and no attributes
+ {
+ methods: ["add", "ensure"],
+ obj: {
+ id: "args0",
+ excludeArgsFromCacheKey: true,
+ },
+ gets: [
+ {
+ obj: { id: "args0" },
+ expected: {
+ value: "Zero args value",
+ attributes: null,
+ },
+ },
+ {
+ obj: { id: "args0", excludeArgsFromCacheKey: true },
+ expected: {
+ value: "Zero args value",
+ attributes: null,
+ },
+ },
+ ],
+ },
+
+ // args1: string with one arg and no attributes
+ {
+ methods: ["add"],
+ obj: {
+ id: "args1",
+ args: { arg1: "ADD" },
+ excludeArgsFromCacheKey: true,
+ },
+ gets: [
+ {
+ obj: { id: "args1" },
+ expected: {
+ value: "One arg value is ADD",
+ attributes: null,
+ },
+ },
+ {
+ obj: { id: "args1", excludeArgsFromCacheKey: true },
+ expected: {
+ value: "One arg value is ADD",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args1",
+ args: { arg1: "some other value" },
+ excludeArgsFromCacheKey: true,
+ },
+ expected: {
+ value: "One arg value is ADD",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args1",
+ args: { arg1: "some other value" },
+ },
+ expected: undefined,
+ },
+ ],
+ },
+ {
+ methods: ["ensure"],
+ obj: {
+ id: "args1",
+ args: { arg1: "ENSURE" },
+ excludeArgsFromCacheKey: true,
+ },
+ gets: [
+ {
+ obj: { id: "args1" },
+ expected: {
+ value: "One arg value is ENSURE",
+ attributes: null,
+ },
+ },
+ {
+ obj: { id: "args1", excludeArgsFromCacheKey: true },
+ expected: {
+ value: "One arg value is ENSURE",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args1",
+ args: { arg1: "some other value" },
+ excludeArgsFromCacheKey: true,
+ },
+ expected: {
+ value: "One arg value is ENSURE",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args1",
+ args: { arg1: "some other value" },
+ },
+ expected: undefined,
+ },
+ ],
+ },
+
+ // attrs0: string with no args and one attribute
+ {
+ methods: ["add", "ensure"],
+ obj: {
+ id: "attrs0",
+ excludeArgsFromCacheKey: true,
+ },
+ gets: [
+ {
+ obj: { id: "attrs0" },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs0 label has zero args",
+ },
+ },
+ },
+ {
+ obj: { id: "attrs0", excludeArgsFromCacheKey: true },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs0 label has zero args",
+ },
+ },
+ },
+ ],
+ },
+
+ // attrs1: string with one arg and two attributes
+ {
+ methods: ["add"],
+ obj: {
+ id: "attrs1",
+ args: { arg1: "ADD" },
+ excludeArgsFromCacheKey: true,
+ },
+ gets: [
+ {
+ obj: { id: "attrs1" },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs1 label has zero args",
+ tooltiptext: "attrs1 tooltiptext arg value is ADD",
+ },
+ },
+ },
+ {
+ obj: { id: "attrs1", excludeArgsFromCacheKey: true },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs1 label has zero args",
+ tooltiptext: "attrs1 tooltiptext arg value is ADD",
+ },
+ },
+ },
+ {
+ obj: {
+ id: "attrs1",
+ args: { arg1: "some other value" },
+ excludeArgsFromCacheKey: true,
+ },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs1 label has zero args",
+ tooltiptext: "attrs1 tooltiptext arg value is ADD",
+ },
+ },
+ },
+ {
+ obj: {
+ id: "attrs1",
+ args: { arg1: "some other value" },
+ },
+ expected: undefined,
+ },
+ ],
+ },
+ {
+ methods: ["ensure"],
+ obj: {
+ id: "attrs1",
+ args: { arg1: "ENSURE" },
+ excludeArgsFromCacheKey: true,
+ },
+ gets: [
+ {
+ obj: { id: "attrs1" },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs1 label has zero args",
+ tooltiptext: "attrs1 tooltiptext arg value is ENSURE",
+ },
+ },
+ },
+ {
+ obj: { id: "attrs1", excludeArgsFromCacheKey: true },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs1 label has zero args",
+ tooltiptext: "attrs1 tooltiptext arg value is ENSURE",
+ },
+ },
+ },
+ {
+ obj: {
+ id: "attrs1",
+ args: { arg1: "some other value" },
+ excludeArgsFromCacheKey: true,
+ },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs1 label has zero args",
+ tooltiptext: "attrs1 tooltiptext arg value is ENSURE",
+ },
+ },
+ },
+ {
+ obj: {
+ id: "attrs1",
+ args: { arg1: "some other value" },
+ },
+ expected: undefined,
+ },
+ ],
+ },
+ ];
+
+ let sandbox = sinon.createSandbox();
+ let spy = sandbox.spy(cache, "add");
+
+ for (let { methods, obj, gets } of tests) {
+ for (let method of methods) {
+ info(`Calling method '${method}' with l10n obj: ` + JSON.stringify(obj));
+ await cache[method](obj);
+
+ // `add()` should always be called: We either just called it directly, or
+ // `ensure({ excludeArgsFromCacheKey: true })` called it.
+ Assert.ok(
+ spy.calledOnce,
+ "add() should have been called once: " + JSON.stringify(obj)
+ );
+ spy.resetHistory();
+
+ for (let { obj: getObj, expected } of gets) {
+ Assert.deepEqual(
+ cache.get(getObj),
+ expected,
+ "Expected message for get: " + JSON.stringify(getObj)
+ );
+ }
+ }
+ }
+
+ sandbox.restore();
+});
+
+/**
+ * Sets up a mock localization.
+ *
+ * @param {object} pairs
+ * Fluent strings as key-value pairs.
+ * @returns {Localization}
+ * The mock Localization object.
+ */
+function initL10n(pairs) {
+ let source = Object.entries(pairs)
+ .map(([key, value]) => {
+ if (Array.isArray(value)) {
+ value = value.map(s => " \n" + s).join("");
+ }
+ return `${key} = ${value}`;
+ })
+ .join("\n");
+ let registry = new L10nRegistry();
+ registry.registerSources([
+ L10nFileSource.createMock(
+ "test",
+ "app",
+ ["en-US"],
+ "/localization/{locale}",
+ [{ source, path: "/localization/en-US/test.ftl" }]
+ ),
+ ]);
+ return new Localization(["/test.ftl"], true, registry, ["en-US"]);
+}
diff --git a/browser/components/urlbar/tests/unit/test_local_suggest_prefs.js b/browser/components/urlbar/tests/unit/test_local_suggest_prefs.js
new file mode 100644
index 0000000000..2d03cc4c54
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_local_suggest_prefs.js
@@ -0,0 +1,126 @@
+/* -*- 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/. */
+
+// Test for following preferences related to local suggest.
+// * browser.urlbar.suggest.bookmark
+// * browser.urlbar.suggest.history
+// * browser.urlbar.suggest.openpage
+
+testEngine_setup();
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.engines", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+
+ const uri = Services.io.newURI("http://example.com/");
+
+ await PlacesTestUtils.addVisits([{ uri, title: "example" }]);
+ await PlacesUtils.bookmarks.insert({
+ url: uri,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+ await addOpenPages(uri);
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.engines");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.openpage");
+ await cleanupPlaces();
+ });
+});
+
+add_task(async function test_prefs() {
+ const testData = [
+ {
+ bookmark: true,
+ history: true,
+ openpage: true,
+ },
+ {
+ bookmark: false,
+ history: true,
+ openpage: true,
+ },
+ {
+ bookmark: true,
+ history: false,
+ openpage: true,
+ },
+ {
+ bookmark: true,
+ history: true,
+ openpage: false,
+ },
+ {
+ bookmark: false,
+ history: false,
+ openpage: true,
+ },
+ {
+ bookmark: false,
+ history: true,
+ openpage: false,
+ },
+ {
+ bookmark: true,
+ history: false,
+ openpage: false,
+ },
+ {
+ bookmark: false,
+ history: false,
+ openpage: false,
+ },
+ ];
+
+ for (const { bookmark, history, openpage } of testData) {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", bookmark);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", history);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", openpage);
+
+ info(`Test bookmark:${bookmark} history:${history} openpage:${openpage}`);
+
+ const context = createContext("e", { isPrivate: false });
+ const matches = [];
+
+ matches.push(
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ })
+ );
+
+ if (openpage) {
+ matches.push(
+ makeTabSwitchResult(context, {
+ uri: "http://example.com/",
+ title: "example",
+ })
+ );
+ } else if (bookmark) {
+ matches.push(
+ makeBookmarkResult(context, {
+ uri: "http://example.com/",
+ title: "example",
+ })
+ );
+ } else if (history) {
+ matches.push(
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ title: "example",
+ })
+ );
+ }
+
+ await check_results({ context, matches });
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_match_javascript.js b/browser/components/urlbar/tests/unit/test_match_javascript.js
new file mode 100644
index 0000000000..6cf7126d41
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_match_javascript.js
@@ -0,0 +1,155 @@
+/* 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 bug 417798 to make sure javascript: URIs don't show up unless the
+ * user searches for javascript: explicitly.
+ */
+
+testEngine_setup();
+
+add_task(async function test_javascript_match() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.engines", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.engines");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+ });
+
+ let uri1 = Services.io.newURI("http://abc/def");
+ let uri2 = Services.io.newURI("javascript:5");
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri2,
+ title: "Title with javascript:",
+ });
+ await PlacesTestUtils.addVisits([
+ { uri: uri1, title: "Title with javascript:" },
+ ]);
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ info("Match non-javascript: with plain search");
+ let context = createContext("a", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "Title with javascript:",
+ }),
+ ],
+ });
+
+ info("Match non-javascript: with 'javascript'");
+ context = createContext("javascript", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "Title with javascript:",
+ }),
+ ],
+ });
+
+ info("Match non-javascript with 'javascript:'");
+ context = createContext("javascript:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "Title with javascript:",
+ }),
+ ],
+ });
+
+ info("Match nothing with '5 javascript:'");
+ context = createContext("5 javascript:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Match non-javascript: with 'a javascript:'");
+ context = createContext("a javascript:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "Title with javascript:",
+ }),
+ ],
+ });
+
+ info("Match non-javascript: and javascript: with 'javascript: a'");
+ context = createContext("javascript: a", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: "javascript: a",
+ fallbackTitle: "javascript: a",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "Title with javascript:",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri2.spec,
+ iconUri: "chrome://global/skin/icons/defaultFavicon.svg",
+ title: "Title with javascript:",
+ }),
+ ],
+ });
+
+ info("Match javascript: with 'javascript: 5'");
+ context = createContext("javascript: 5", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: "javascript: 5",
+ fallbackTitle: "javascript: 5",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri2.spec,
+ iconUri: "chrome://global/skin/icons/defaultFavicon.svg",
+ title: "Title with javascript:",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_multi_word_search.js b/browser/components/urlbar/tests/unit/test_multi_word_search.js
new file mode 100644
index 0000000000..7054feb8aa
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_multi_word_search.js
@@ -0,0 +1,126 @@
+/* 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 bug 401869 to allow multiple words separated by spaces to match in
+ * the page title, page url, or bookmark title to be considered a match. All
+ * terms must match but not all terms need to be in the title, etc.
+ *
+ * Test bug 424216 by making sure bookmark titles are always shown if one is
+ * available. Also bug 425056 makes sure matches aren't found partially in the
+ * page title and partially in the bookmark.
+ */
+
+testEngine_setup();
+
+add_task(async function test_match_beginning() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ let uri1 = Services.io.newURI("http://a.b.c/d-e_f/h/t/p");
+ let uri2 = Services.io.newURI("http://d.e.f/g-h_i/h/t/p");
+ let uri3 = Services.io.newURI("http://g.h.i/j-k_l/h/t/p");
+ let uri4 = Services.io.newURI("http://j.k.l/m-n_o/h/t/p");
+ await PlacesTestUtils.addVisits([
+ { uri: uri4, title: "f(o)o b<a>r" },
+ { uri: uri3, title: "f(o)o b<a>r" },
+ { uri: uri2, title: "b(a)r b<a>z" },
+ { uri: uri1, title: "f(o)o b<a>r" },
+ ]);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri3,
+ title: "f(o)o b<a>r",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri4,
+ title: "b(a)r b<a>z",
+ });
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ info("Match 2 terms all in url");
+ let context = createContext("c d", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri1.spec, title: "f(o)o b<a>r" }),
+ ],
+ });
+
+ info("Match 1 term in url and 1 term in title");
+ context = createContext("b e", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri1.spec, title: "f(o)o b<a>r" }),
+ makeVisitResult(context, { uri: uri2.spec, title: "b(a)r b<a>z" }),
+ ],
+ });
+
+ info("Match 3 terms all in title; display bookmark title if matched");
+ context = createContext("b a z", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, { uri: uri4.spec, title: "b(a)r b<a>z" }),
+ makeVisitResult(context, { uri: uri2.spec, title: "b(a)r b<a>z" }),
+ ],
+ });
+
+ info(
+ "Match 2 terms in url and 1 in title; make sure bookmark title is used for search"
+ );
+ context = createContext("k f t", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, { uri: uri3.spec, title: "f(o)o b<a>r" }),
+ ],
+ });
+
+ info("Match 3 terms in url and 1 in title");
+ context = createContext("d i g z", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri2.spec, title: "b(a)r b<a>z" }),
+ ],
+ });
+
+ info("Match nothing");
+ context = createContext("m o z i", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ 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..dcea820835
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_muxer.js
@@ -0,0 +1,731 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+let sandbox;
+
+add_task(async function setup() {
+ sandbox = lazy.sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+});
+
+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 provider = registerBasicTestProvider(matches);
+ let context = createContext(undefined, { providers: [provider.name] });
+ let controller = UrlbarTestUtils.newMockController();
+ /**
+ * A test muxer.
+ */
+ class TestMuxer extends UrlbarMuxer {
+ get name() {
+ return "TestMuxer";
+ }
+ sort(queryContext, unsortedResults) {
+ queryContext.results = [...unsortedResults].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 provider = registerBasicTestProvider(matches);
+ let context = createContext(undefined, {
+ providers: [provider.name],
+ });
+ 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 provider1 = registerBasicTestProvider(matches1);
+ let provider2 = registerBasicTestProvider(matches2);
+
+ let context = createContext(undefined, {
+ providers: [provider1.name, provider2.name],
+ });
+ 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 provider = registerBasicTestProvider(matches);
+
+ let context = createContext(undefined, {
+ providers: [provider.name],
+ });
+ 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");
+});
+
+add_task(async function test_deduplicate_for_unitConversion() {
+ const searchSuggestion = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "Google",
+ query: "10cm to m",
+ suggestion: "= 0.1 meters",
+ }
+ );
+ const searchProvider = registerBasicTestProvider(
+ [searchSuggestion],
+ null,
+ UrlbarUtils.PROVIDER_TYPE.PROFILE
+ );
+
+ const unitConversionSuggestion = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ dynamicType: "unitConversion",
+ output: "0.1 m",
+ input: "10cm to m",
+ }
+ );
+ unitConversionSuggestion.suggestedIndex = 1;
+
+ const unitConversion = registerBasicTestProvider(
+ [unitConversionSuggestion],
+ null,
+ UrlbarUtils.PROVIDER_TYPE.PROFILE,
+ "UnitConversion"
+ );
+
+ const context = createContext(undefined, {
+ providers: [searchProvider.name, unitConversion.name],
+ });
+ const controller = UrlbarTestUtils.newMockController();
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, [unitConversionSuggestion]);
+});
+
+// These results are used in the badHeuristicGroups tests below. The order of
+// the results in the array isn't important because they all get added at the
+// same time. It's the resultGroups in each test that is important.
+const BAD_HEURISTIC_RESULTS = [
+ // heuristic
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/heuristic-0" }
+ ),
+ { heuristic: true }
+ ),
+ // heuristic
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/heuristic-1" }
+ ),
+ { heuristic: true }
+ ),
+ // non-heuristic
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/non-heuristic-0" }
+ ),
+ // non-heuristic
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/non-heuristic-1" }
+ ),
+];
+
+const BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC = BAD_HEURISTIC_RESULTS[0];
+const BAD_HEURISTIC_RESULTS_GENERAL = [
+ BAD_HEURISTIC_RESULTS[2],
+ BAD_HEURISTIC_RESULTS[3],
+];
+
+add_task(async function test_badHeuristicGroups_multiple_0() {
+ await doBadHeuristicGroupsTest(
+ [
+ // 2 heuristics with child groups
+ {
+ maxResultCount: 2,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ // infinite general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicGroups_multiple_1() {
+ await doBadHeuristicGroupsTest(
+ [
+ // infinite heuristics with child groups
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ // infinite general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicGroups_multiple_2() {
+ await doBadHeuristicGroupsTest(
+ [
+ // 2 heuristics
+ {
+ maxResultCount: 2,
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ // infinite general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicGroups_multiple_3() {
+ await doBadHeuristicGroupsTest(
+ [
+ // infinite heuristics
+ {
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ // infinite general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicGroups_multiple_4() {
+ await doBadHeuristicGroupsTest(
+ [
+ // 1 heuristic with child groups
+ {
+ maxResultCount: 1,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ // infinite general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // 1 heuristic with child groups
+ {
+ maxResultCount: 1,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ ],
+ [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicGroups_multiple_5() {
+ await doBadHeuristicGroupsTest(
+ [
+ // infinite heuristics with child groups
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ // infinite general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // infinite heuristics with child groups
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ ],
+ [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicGroups_multiple_6() {
+ await doBadHeuristicGroupsTest(
+ [
+ // 1 heuristic
+ {
+ maxResultCount: 1,
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ // infinite general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // 1 heuristic
+ {
+ maxResultCount: 1,
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ ],
+ [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicGroups_multiple_7() {
+ await doBadHeuristicGroupsTest(
+ [
+ // infinite heuristics
+ {
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ // infinite general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // infinite heuristics
+ {
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ ],
+ [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicsGroups_notFirst_0() {
+ await doBadHeuristicGroupsTest(
+ [
+ // infinite general first
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // 1 heuristic with child groups second
+ {
+ maxResultCount: 1,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ ],
+ [...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicsGroups_notFirst_1() {
+ await doBadHeuristicGroupsTest(
+ [
+ // infinite general first
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // infinite heuristics with child groups second
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ ],
+ [...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicsGroups_notFirst_2() {
+ await doBadHeuristicGroupsTest(
+ [
+ // infinite general first
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // 1 heuristic second
+ {
+ maxResultCount: 1,
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ ],
+ [...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicsGroups_notFirst_3() {
+ await doBadHeuristicGroupsTest(
+ [
+ // infinite general first
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // infinite heuristics second
+ {
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ ],
+ [...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicsGroups_notFirst_4() {
+ await doBadHeuristicGroupsTest(
+ [
+ // 1 general first
+ {
+ maxResultCount: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // infinite heuristics second
+ {
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ // infinite general third
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ [...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+/**
+ * Sets the resultGroups pref, performs a search, and then checks the results.
+ * Regardless of the groups, the muxer should include at most one heuristic in
+ * its results and it should always be the first result.
+ *
+ * @param {Array} resultGroups
+ * The result groups.
+ * @param {Array} expectedResults
+ * The expected results.
+ */
+async function doBadHeuristicGroupsTest(resultGroups, expectedResults) {
+ sandbox.stub(UrlbarPrefs, "resultGroups").get(() => {
+ return { children: resultGroups };
+ });
+
+ let provider = registerBasicTestProvider(BAD_HEURISTIC_RESULTS);
+ let context = createContext("foo", { providers: [provider.name] });
+ let controller = UrlbarTestUtils.newMockController();
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, expectedResults);
+
+ sandbox.restore();
+}
+
+// When `maxRichResults` is positive and taken up by suggested-index result(s),
+// both the heuristic and suggested-index results should be included because we
+// (a) make room for the heuristic and (b) assume all suggested-index results
+// should be included even if it means exceeding `maxRichResults`. The specified
+// `maxRichResults` span will be exceeded in this case.
+add_task(async function roomForHeuristic_suggestedIndex() {
+ let results = [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://example.com/heuristic" }
+ ),
+ { heuristic: true }
+ ),
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://example.com/suggestedIndex" }
+ ),
+ { suggestedIndex: 1 }
+ ),
+ ];
+
+ UrlbarPrefs.set("maxRichResults", 1);
+
+ let provider = registerBasicTestProvider(results);
+ let context = createContext(undefined, { providers: [provider.name] });
+ await check_results({
+ context,
+ matches: results,
+ });
+
+ UrlbarPrefs.clear("maxRichResults");
+});
+
+// When `maxRichResults` is positive but less than the heuristic's result span,
+// the heuristic should be included because we make room for it even if it means
+// exceeding `maxRichResults`. The specified `maxRichResults` span will be
+// exceeded in this case.
+add_task(async function roomForHeuristic_largeResultSpan() {
+ let results = [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://example.com/heuristic" }
+ ),
+ { heuristic: true, resultSpan: 2 }
+ ),
+ ];
+
+ UrlbarPrefs.set("maxRichResults", 1);
+
+ let provider = registerBasicTestProvider(results);
+ let context = createContext(undefined, { providers: [provider.name] });
+ await check_results({
+ context,
+ matches: results,
+ });
+
+ UrlbarPrefs.clear("maxRichResults");
+});
+
+// When `maxRichResults` is zero and there are no suggested-index results, the
+// heuristic should not be included.
+add_task(async function roomForHeuristic_maxRichResultsZero() {
+ let results = [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://example.com/heuristic" }
+ ),
+ { heuristic: true }
+ ),
+ ];
+
+ UrlbarPrefs.set("maxRichResults", 0);
+
+ let provider = registerBasicTestProvider(results);
+ let context = createContext(undefined, { providers: [provider.name] });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ UrlbarPrefs.clear("maxRichResults");
+});
+
+// When `maxRichResults` is zero and suggested-index results are present,
+// neither the heuristic nor the suggested-index results should be included.
+add_task(async function roomForHeuristic_maxRichResultsZero_suggestedIndex() {
+ let results = [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://example.com/heuristic" }
+ ),
+ { heuristic: true }
+ ),
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://example.com/suggestedIndex" }
+ ),
+ { suggestedIndex: 1 }
+ ),
+ ];
+
+ UrlbarPrefs.set("maxRichResults", 0);
+
+ let provider = registerBasicTestProvider(results);
+ let context = createContext(undefined, { providers: [provider.name] });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ UrlbarPrefs.clear("maxRichResults");
+});
diff --git a/browser/components/urlbar/tests/unit/test_protocol_ignore.js b/browser/components/urlbar/tests/unit/test_protocol_ignore.js
new file mode 100644
index 0000000000..2e5096cb46
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_protocol_ignore.js
@@ -0,0 +1,42 @@
+/* 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 bug 424509 to make sure searching for "h" doesn't match "http" of urls.
+ */
+
+testEngine_setup();
+
+add_task(async function test_escape() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ let uri1 = Services.io.newURI("http://site/");
+ let uri2 = Services.io.newURI("http://happytimes/");
+ await PlacesTestUtils.addVisits([
+ { uri: uri1, title: "title" },
+ { uri: uri2, title: "title" },
+ ]);
+
+ info("Searching for h matches site and not http://");
+ let context = createContext("h", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "title",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_protocol_swap.js b/browser/components/urlbar/tests/unit/test_protocol_swap.js
new file mode 100644
index 0000000000..980904001c
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_protocol_swap.js
@@ -0,0 +1,303 @@
+/* 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 bug 424717 to make sure searching with an existing location like
+ * http://site/ also matches https://site/ or ftp://site/. Same thing for
+ * ftp://site/ and https://site/.
+ *
+ * Test bug 461483 to make sure a search for "w" doesn't match the "www." from
+ * site subdomains.
+ */
+
+testEngine_setup();
+
+add_task(async function test_swap_protocol() {
+ let uri1 = Services.io.newURI("http://www.site/");
+ let uri2 = Services.io.newURI("http://site/");
+ let uri3 = Services.io.newURI("ftp://ftp.site/");
+ let uri4 = Services.io.newURI("ftp://site/");
+ let uri5 = Services.io.newURI("https://www.site/");
+ let uri6 = Services.io.newURI("https://site/");
+ let uri7 = Services.io.newURI("http://woohoo/");
+ let uri8 = Services.io.newURI("http://wwwwwwacko/");
+ await PlacesTestUtils.addVisits([
+ { uri: uri8, title: "title" },
+ { uri: uri7, title: "title" },
+ { uri: uri6, title: "title" },
+ { uri: uri5, title: "title" },
+ { uri: uri4, title: "title" },
+ { uri: uri3, title: "title" },
+ { uri: uri2, title: "title" },
+ { uri: uri1, title: "title" },
+ ]);
+
+ // Disable autoFill to avoid handling the first result.
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ info("http://www.site matches 'www.site' pages");
+ let searchString = "http://www.site";
+ let context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ title: "title",
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ ],
+ });
+
+ info("http://site matches all sites");
+ searchString = "http://site";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ title: "title",
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri6.spec, title: "title" }),
+ ],
+ });
+
+ info("ftp://ftp.site matches itself");
+ searchString = "ftp://ftp.site";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ ],
+ });
+
+ info("ftp://site matches all sites");
+ searchString = "ftp://site";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri6.spec, title: "title" }),
+ ],
+ });
+
+ info("https://www.site matches all sites");
+ searchString = "https://www.sit";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ ],
+ });
+
+ info("https://site matches all sites");
+ searchString = "https://sit";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri6.spec, title: "title" }),
+ ],
+ });
+
+ info("www.site matches 'www.site' pages");
+ searchString = "www.site";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://${searchString}/`,
+ title: "title",
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ ],
+ });
+
+ info("w matches 'w' pages, including 'www'");
+ context = createContext("w", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri7.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri8.spec, title: "title" }),
+ ],
+ });
+
+ info("http://w matches 'w' pages, including 'www'");
+ searchString = "http://w";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri7.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri8.spec, title: "title" }),
+ ],
+ });
+
+ info("http://www.w matches nothing");
+ searchString = "http://www.w";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("ww matches no 'ww' pages, including 'www'");
+ context = createContext("ww", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri8.spec, title: "title" }),
+ ],
+ });
+
+ info("http://ww matches no 'ww' pages, including 'www'");
+ searchString = "http://ww";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri8.spec, title: "title" }),
+ ],
+ });
+
+ info("http://www.ww matches nothing");
+ searchString = "http://www.ww";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("www matches 'www' pages");
+ context = createContext("www", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri8.spec, title: "title" }),
+ ],
+ });
+
+ info("http://www matches 'www' pages");
+ searchString = "http://www";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri8.spec, title: "title" }),
+ ],
+ });
+
+ info("http://www.www matches nothing");
+ searchString = "http://www.www";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerAliasEngines.js b/browser/components/urlbar/tests/unit/test_providerAliasEngines.js
new file mode 100644
index 0000000000..14ba368f0d
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerAliasEngines.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests search engine aliases. See
+ * browser/components/urlbar/tests/browser/browser_tokenAlias.js for tests of
+ * the token alias list (i.e. showing all aliased engines on a "@" query).
+ */
+
+testEngine_setup();
+
+// Basic test that uses two engines, a GET engine and a POST engine, neither
+// providing search suggestions.
+add_task(async function basicGetAndPost() {
+ await SearchTestUtils.installSearchExtension({
+ name: "AliasedGETMozSearch",
+ keyword: "get",
+ search_url: "https://s.example.com/search",
+ });
+ await SearchTestUtils.installSearchExtension({
+ name: "AliasedPOSTMozSearch",
+ keyword: "post",
+ search_url: "https://s.example.com/search",
+ search_url_post_params: "q={searchTerms}",
+ });
+
+ for (let alias of ["get", "post"]) {
+ let context = createContext(alias, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ ],
+ });
+
+ context = createContext(`${alias} `, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+ query: "",
+ alias,
+ heuristic: true,
+ providerName: "AliasEngines",
+ }),
+ ],
+ });
+
+ context = createContext(`${alias} fire`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+ query: "fire",
+ alias,
+ heuristic: true,
+ providerName: "AliasEngines",
+ }),
+ ],
+ });
+
+ context = createContext(`${alias} mozilla`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+ query: "mozilla",
+ alias,
+ heuristic: true,
+ providerName: "AliasEngines",
+ }),
+ ],
+ });
+
+ context = createContext(`${alias} MoZiLlA`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+ query: "MoZiLlA",
+ alias,
+ heuristic: true,
+ providerName: "AliasEngines",
+ }),
+ ],
+ });
+
+ context = createContext(`${alias} mozzarella mozilla`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+ query: "mozzarella mozilla",
+ alias,
+ heuristic: true,
+ providerName: "AliasEngines",
+ }),
+ ],
+ });
+ }
+
+ await cleanupPlaces();
+});
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..101f6fb21d
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js
@@ -0,0 +1,691 @@
+/* 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 QUICKACTIONS_PREF = "browser.urlbar.suggest.quickactions";
+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 "];
+
+testEngine_setup();
+
+add_task(async function setup() {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref(QUICKACTIONS_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF);
+ Services.prefs.clearUserPref("keyword.enabled");
+ });
+ Services.prefs.setBoolPref(QUICKACTIONS_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}/`,
+ fallbackTitle: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_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}/`,
+ fallbackTitle: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_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}/`,
+ fallbackTitle: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_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}/`,
+ fallbackTitle: `${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}/`,
+ fallbackTitle: `${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}/`,
+ fallbackTitle: `${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,
+ fallbackTitle: 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}/`,
+ fallbackTitle: `${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}`,
+ fallbackTitle: `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: SUGGESTIONS_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: SUGGESTIONS_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}/`,
+ fallbackTitle: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_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}`,
+ fallbackTitle: `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}`,
+ fallbackTitle: `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}/`,
+ fallbackTitle: `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}/`,
+ fallbackTitle: `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}/`,
+ fallbackTitle: `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,
+ fallbackTitle: 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: SUGGESTIONS_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: SUGGESTIONS_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}/`,
+ fallbackTitle: `${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}/`,
+ fallbackTitle: `${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}/`,
+ fallbackTitle: `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: SUGGESTIONS_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: SUGGESTIONS_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,
+ fallbackTitle: 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,
+ fallbackTitle: query,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("protocol with an extra slash");
+ query = "http:///";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("change default engine");
+ let originalTestEngine = Services.search.getEngineByName(
+ SUGGESTIONS_ENGINE_NAME
+ );
+ await SearchTestUtils.installSearchExtension({
+ name: "AliasEngine",
+ keyword: "alias",
+ });
+ let engine2 = Services.search.getEngineByName("AliasEngine");
+ Assert.notEqual(
+ Services.search.defaultEngine,
+ engine2,
+ "New engine shouldn't be the current engine yet"
+ );
+ await Services.search.setDefault(
+ engine2,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ query = "toronto";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: "AliasEngine",
+ heuristic: true,
+ }),
+ ],
+ });
+ await Services.search.setDefault(
+ originalTestEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ 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 = SUGGESTIONS_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 = SUGGESTIONS_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: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ],
+ });
+ }
+ }
+
+ info(
+ "Test the format inputed is user@host, and the host is in domainwhitelist"
+ );
+ Services.prefs.setBoolPref("browser.fixup.domainwhitelist.test-host", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.fixup.domainwhitelist.test-host");
+ });
+
+ query = "any@test-host";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ fallbackTitle: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ],
+ });
+
+ info(
+ "Test the format inputed is user@host, but the host is not in domainwhitelist"
+ );
+ query = "any@not-host";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ query,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ],
+ });
+
+ info(
+ "Test if the format of user:pass@host is handled as visit even if the host is not in domainwhitelist"
+ );
+ query = "user:pass@not-host";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://user:pass@not-host/",
+ fallbackTitle: "http://user:pass@not-host/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Test if the format of user@ipaddress is handled as visit");
+ query = "user@192.168.0.1";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://user@192.168.0.1/",
+ fallbackTitle: "http://user@192.168.0.1/",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ heuristic: false,
+ query,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ],
+ });
+});
+
+/**
+ * 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_providerHistoryUrlHeuristic.js b/browser/components/urlbar/tests/unit/test_providerHistoryUrlHeuristic.js
new file mode 100644
index 0000000000..6f26fee7cb
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerHistoryUrlHeuristic.js
@@ -0,0 +1,197 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the behavior of UrlbarProviderHistoryUrlHeuristic.
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ });
+});
+
+add_task(async function test_basic() {
+ await PlacesTestUtils.addVisits([
+ { uri: "https://example.com/", title: "Example COM" },
+ ]);
+
+ const testCases = [
+ {
+ input: "https://example.com/",
+ expected: context => [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "Example COM",
+ iconUri: "page-icon:https://example.com/",
+ heuristic: true,
+ providerName: "HistoryUrlHeuristic",
+ }),
+ ],
+ },
+ {
+ input: "https://www.example.com/",
+ expected: context => [
+ makeVisitResult(context, {
+ uri: "https://www.example.com/",
+ title: "Example COM",
+ iconUri: "page-icon:https://example.com/",
+ heuristic: true,
+ providerName: "HistoryUrlHeuristic",
+ }),
+ ],
+ },
+ {
+ input: "http://example.com/",
+ expected: context => [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ title: "Example COM",
+ iconUri: "page-icon:https://example.com/",
+ heuristic: true,
+ providerName: "HistoryUrlHeuristic",
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "Example COM",
+ iconUri: "page-icon:https://example.com/",
+ providerName: "Places",
+ }),
+ ],
+ },
+ {
+ input: "example.com",
+ expected: context => [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ title: "Example COM",
+ iconUri: "page-icon:https://example.com/",
+ heuristic: true,
+ providerName: "HistoryUrlHeuristic",
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "Example COM",
+ iconUri: "page-icon:https://example.com/",
+ providerName: "Places",
+ }),
+ ],
+ },
+ {
+ input: "www.example.com",
+ expected: context => [
+ makeVisitResult(context, {
+ uri: "http://www.example.com/",
+ title: "Example COM",
+ iconUri: "page-icon:https://example.com/",
+ heuristic: true,
+ providerName: "HistoryUrlHeuristic",
+ }),
+ ],
+ },
+ {
+ input: "htp:example.com",
+ expected: context => [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ title: "Example COM",
+ iconUri: "page-icon:https://example.com/",
+ heuristic: true,
+ providerName: "HistoryUrlHeuristic",
+ }),
+ ],
+ },
+ ];
+
+ for (const { input, expected } of testCases) {
+ info(`Test with "${input}"`);
+ const context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ matches: expected(context),
+ });
+ }
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_null_title() {
+ await PlacesTestUtils.addVisits([{ uri: "https://example.com/", title: "" }]);
+
+ const context = createContext("https://example.com/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "https://example.com/",
+ fallbackTitle: "https://example.com/",
+ iconUri: "page-icon:https://example.com/",
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ ],
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_over_max_length_text() {
+ let uri = "https://example.com/";
+ for (; uri.length < UrlbarUtils.MAX_TEXT_LENGTH; ) {
+ uri += "0123456789";
+ }
+
+ await PlacesTestUtils.addVisits([{ uri, title: "Example MAX" }]);
+
+ const context = createContext(uri, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri,
+ fallbackTitle: uri,
+ iconUri: "page-icon:https://example.com/",
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ ],
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_unsupported_protocol() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "about:robots",
+ title: "Robots!",
+ });
+
+ const context = createContext("about:robots", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "about:robots",
+ fallbackTitle: "about:robots",
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ makeBookmarkResult(context, {
+ uri: "about:robots",
+ title: "Robots!",
+ }),
+ makeVisitResult(context, {
+ uri: "about:robots",
+ title: "about:robots",
+ tags: null,
+ providerName: "AboutPages",
+ }),
+ ],
+ });
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerKeywords.js b/browser/components/urlbar/tests/unit/test_providerKeywords.js
new file mode 100644
index 0000000000..3be5fe30c0
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerKeywords.js
@@ -0,0 +1,360 @@
+/* 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 bug 392143 that puts keyword results into the autocomplete. Makes
+ * sure that multiple parameter queries get spaces converted to +, + converted
+ * to %2B, non-ascii become escaped, and pages in history that match the
+ * keyword uses the page's title.
+ *
+ * Also test for bug 249468 by making sure multiple keyword bookmarks with the
+ * same keyword appear in the list.
+ */
+
+testEngine_setup();
+
+add_task(async function test_keyword_search() {
+ let uri1 = "http://abc/?search=%s";
+ let uri2 = "http://abc/?search=ThisPageIsInHistory";
+ let uri3 = "http://abc/?search=%s&raw=%S";
+ let uri4 = "http://abc/?search=%s&raw=%S&mozcharset=ISO-8859-1";
+ let uri5 = "http://def/?search=%s";
+ let uri6 = "http://ghi/?search=%s&raw=%S";
+ let uri7 = "http://somedomain.example/key2";
+ await PlacesTestUtils.addVisits([
+ { uri: uri1 },
+ { uri: uri2 },
+ { uri: uri3 },
+ { uri: uri6 },
+ { uri: uri7 },
+ ]);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri1,
+ title: "Keyword",
+ keyword: "key",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri1,
+ title: "Post",
+ keyword: "post",
+ postData: "post_search=%s",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri3,
+ title: "Encoded",
+ keyword: "encoded",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri4,
+ title: "Charset",
+ keyword: "charset",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri2,
+ title: "Noparam",
+ keyword: "noparam",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri2,
+ title: "Noparam-Post",
+ keyword: "post_noparam",
+ postData: "noparam=1",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri5,
+ title: "Keyword",
+ keyword: "key2",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri6,
+ title: "Charset-history",
+ keyword: "charset_history",
+ });
+
+ await PlacesUtils.history.update({
+ url: uri6,
+ annotations: new Map([[PlacesUtils.CHARSET_ANNO, "ISO-8859-1"]]),
+ });
+
+ info("Plain keyword query");
+ let context = createContext("key term", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=term",
+ keyword: "key",
+ title: "abc: term",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Plain keyword UC");
+ context = createContext("key TERM", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=TERM",
+ keyword: "key",
+ title: "abc: TERM",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Multi-word keyword query");
+ context = createContext("key multi word", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=multi%20word",
+ keyword: "key",
+ title: "abc: multi word",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Keyword query with +");
+ context = createContext("key blocking+", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=blocking%2B",
+ keyword: "key",
+ title: "abc: blocking+",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Unescaped term in query");
+ // ... but note that we call encodeURIComponent() on the query string when we
+ // build the URL, so the expected result will have the ユニコード substring
+ // encoded in the URL.
+ context = createContext("key ユニコード", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=" + encodeURIComponent("ユニコード"),
+ keyword: "key",
+ title: "abc: ユニコード",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Keyword that happens to match a page");
+ context = createContext("key ThisPageIsInHistory", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=ThisPageIsInHistory",
+ keyword: "key",
+ title: "abc: ThisPageIsInHistory",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Keyword with partial page match");
+ context = createContext("key ThisPage", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=ThisPage",
+ keyword: "key",
+ title: "abc: ThisPage",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ // Only the most recent bookmark for the URL:
+ makeBookmarkResult(context, {
+ uri: "http://abc/?search=ThisPageIsInHistory",
+ title: "Noparam-Post",
+ }),
+ ],
+ });
+
+ // For the keyword with no query terms (with or without space after), the
+ // domain is different from the other tests because otherwise all the other
+ // test bookmarks and history entries would be matches.
+ info("Keyword without query (without space)");
+ context = createContext("key2", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://def/?search=",
+ fallbackTitle: "http://def/?search=",
+ keyword: "key2",
+ iconUri: "page-icon:http://def/?search=%s",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri5,
+ title: "Keyword",
+ }),
+ ],
+ });
+
+ info("Keyword without query (with space)");
+ context = createContext("key2 ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://def/?search=",
+ fallbackTitle: "http://def/?search=",
+ keyword: "key2",
+ iconUri: "page-icon:http://def/?search=%s",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri5,
+ title: "Keyword",
+ }),
+ ],
+ });
+
+ info("POST Keyword");
+ context = createContext("post foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=foo",
+ keyword: "post",
+ title: "abc: foo",
+ postData: "post_search=foo",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("escaping with default UTF-8 charset");
+ context = createContext("encoded foé", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=fo%C3%A9&raw=foé",
+ keyword: "encoded",
+ title: "abc: foé",
+ iconUri: "page-icon:http://abc/?search=%s&raw=%S",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("escaping with forced ISO-8859-1 charset");
+ context = createContext("charset foé", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=fo%E9&raw=foé",
+ keyword: "charset",
+ title: "abc: foé",
+ iconUri: "page-icon:http://abc/?search=%s&raw=%S&mozcharset=ISO-8859-1",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("escaping with ISO-8859-1 charset annotated in history");
+ context = createContext("charset_history foé", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://ghi/?search=fo%E9&raw=foé",
+ keyword: "charset_history",
+ title: "ghi: foé",
+ iconUri: "page-icon:http://ghi/?search=%s&raw=%S",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Bug 359809: escaping +, / and @ with default UTF-8 charset");
+ context = createContext("encoded +/@", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=%2B%2F%40&raw=+/@",
+ keyword: "encoded",
+ title: "abc: +/@",
+ iconUri: "page-icon:http://abc/?search=%s&raw=%S",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Bug 359809: escaping +, / and @ with forced ISO-8859-1 charset");
+ context = createContext("charset +/@", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=%2B%2F%40&raw=+/@",
+ keyword: "charset",
+ title: "abc: +/@",
+ iconUri: "page-icon:http://abc/?search=%s&raw=%S&mozcharset=ISO-8859-1",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Bug 1228111 - Keyword with a space in front");
+ context = createContext(" key test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=test",
+ keyword: "key",
+ title: "abc: test",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Bug 1481319 - Keyword with a prefix in front");
+ context = createContext("http://key2", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://key2/",
+ fallbackTitle: "http://key2/",
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ makeVisitResult(context, {
+ uri: uri7,
+ title: "test visit for http://somedomain.example/key2",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
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..134158b5b1
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerOmnibox.js
@@ -0,0 +1,887 @@
+/* -*- 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.importESModule(
+ "resource://gre/modules/ExtensionSearchHandler.sys.mjs"
+);
+
+let controller = Cc["@mozilla.org/autocomplete/controller;1"].getService(
+ Ci.nsIAutoCompleteController
+);
+
+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_extension_private_browsing_allowed() {
+ 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: "foobar", description: "second suggestion" },
+ ]);
+ // The API doesn't have a way to notify when addition is complete.
+ do_timeout(1000, () => {
+ controller.stopSearch();
+ });
+ }
+ },
+ privateBrowsingAllowed: true,
+ };
+
+ let keyword = "foo";
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ let query = `${keyword} foo`;
+ let context = createContext(query, { isPrivate: true });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: query,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} foobar`,
+ description: "second suggestion",
+ }),
+ ],
+ });
+
+ 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" },
+ { content: "k", description: "eleventh suggestion" },
+ { content: "l", description: "twelfth 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",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} f`,
+ description: "sixth suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} g`,
+ description: "seventh suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} h`,
+ description: "eigth suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} i`,
+ description: "ninth 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, Ci.nsISearchService.CHANGE_REASON_UNKNOWN);
+
+ 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: SUGGESTIONS_ENGINE_NAME,
+ alias: keyword,
+ suggestion: "unmatched",
+ }),
+ makeSearchResult(context, {
+ query: "unmatched",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias: keyword,
+ suggestion: "unmatched foo",
+ }),
+ makeSearchResult(context, {
+ query: "unmatched",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias: keyword,
+ suggestion: "unmatched bar",
+ }),
+ ],
+ });
+
+ Services.search.setDefault(
+ oldDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ 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..6cdf632b42
--- /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, false);
+ UrlbarProviderOpenTabs.registerOpenTab(url, userContextId, false);
+ Assert.equal(
+ UrlbarProviderOpenTabs._openTabs.get(userContextId).length,
+ 2,
+ "Found all the expected tabs"
+ );
+ UrlbarProviderOpenTabs.unregisterOpenTab(url, userContextId, false);
+ 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_providerPlaces.js b/browser/components/urlbar/tests/unit/test_providerPlaces.js
new file mode 100644
index 0000000000..c64f3345e1
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerPlaces.js
@@ -0,0 +1,250 @@
+/* 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 Places 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_places() {
+ 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, false);
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ 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,
+ false
+ );
+});
+
+add_task(async function test_bookmarkBehaviorDisabled_tagged() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+
+ // Disable the bookmark behavior.
+ 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ 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.
+ 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ 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.
+ 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ 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_providerPlaces_duplicate_entries.js b/browser/components/urlbar/tests/unit/test_providerPlaces_duplicate_entries.js
new file mode 100644
index 0000000000..7533921fc6
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerPlaces_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_providerPlaces_nonEnglish.js b/browser/components/urlbar/tests/unit/test_providerPlaces_nonEnglish.js
new file mode 100644
index 0000000000..2cb5f5797a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerPlaces_nonEnglish.js
@@ -0,0 +1,43 @@
+/* -*- 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/. */
+
+/*
+
+Test autocomplete for non-English URLs
+
+- add a visit for a page with a non-English URL
+- search
+- test number of matches (should be exactly one)
+
+*/
+
+testEngine_setup();
+
+add_task(async function test_autocomplete_non_english() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ let searchTerm = "ユニコード";
+ let unescaped = "http://www.foobar.com/" + searchTerm + "/";
+ let uri = Services.io.newURI(unescaped);
+ await PlacesTestUtils.addVisits(uri);
+ let context = createContext(searchTerm, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri.spec,
+ title: `test visit for ${uri.spec}`,
+ }),
+ ],
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerPreloaded.js b/browser/components/urlbar/tests/unit/test_providerPreloaded.js
new file mode 100644
index 0000000000..0785e3afba
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerPreloaded.js
@@ -0,0 +1,578 @@
+/**
+ * Test for bug 1211726 - preload list of top web sites for better
+ * autocompletion on empty profiles.
+ */
+
+testEngine_setup();
+
+const PREF_FEATURE_ENABLED = "browser.urlbar.usepreloadedtopurls.enabled";
+const PREF_FEATURE_EXPIRE_DAYS =
+ "browser.urlbar.usepreloadedtopurls.expire_days";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderPreloadedSites:
+ "resource:///modules/UrlbarProviderPreloadedSites.sys.mjs",
+});
+
+Cu.importGlobalProperties(["fetch"]);
+
+let yahoooURI = "https://yahooo.com/";
+let gooogleURI = "https://gooogle.com/";
+
+UrlbarProviderPreloadedSites.populatePreloadedSiteStorage([
+ [yahoooURI, "Yahooo"],
+ [gooogleURI, "Gooogle"],
+]);
+
+async function assert_feature_works(condition) {
+ info("List Results do appear " + condition);
+ let context = createContext("ooo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: "HeuristicFallback",
+ }),
+ makeVisitResult(context, {
+ uri: yahoooURI,
+ title: "Yahooo",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: `page-icon:${yahoooURI}`,
+ providerName: "PreloadedSites",
+ }),
+ makeVisitResult(context, {
+ uri: gooogleURI,
+ title: "Gooogle",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: `page-icon:${gooogleURI}`,
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+
+ info("Autofill does appear " + condition);
+ context = createContext("gooo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "gooogle.com/",
+ completed: gooogleURI,
+ matches: [
+ makeVisitResult(context, {
+ uri: gooogleURI,
+ fallbackTitle: gooogleURI.slice(0, -1), // Trim trailing slash.
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: `page-icon:${gooogleURI}`,
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ ],
+ });
+}
+
+async function assert_feature_does_not_appear(condition) {
+ info("List Results don't appear " + condition);
+ let context = createContext("ooo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: "HeuristicFallback",
+ }),
+ ],
+ });
+
+ info("Autofill doesn't appear " + condition);
+ context = createContext("gooo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: "HeuristicFallback",
+ }),
+ ],
+ });
+}
+
+add_task(async function test_it_works() {
+ // Not expired but OFF
+ Services.prefs.setIntPref(PREF_FEATURE_EXPIRE_DAYS, 14);
+ Services.prefs.setBoolPref(PREF_FEATURE_ENABLED, false);
+ await assert_feature_does_not_appear("when OFF by prefs");
+
+ // Now turn it ON
+ Services.prefs.setBoolPref(PREF_FEATURE_ENABLED, true);
+ await assert_feature_works("when ON by prefs");
+
+ // And expire
+ Services.prefs.setIntPref(PREF_FEATURE_EXPIRE_DAYS, 0);
+ await assert_feature_does_not_appear("when expired");
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_sorting_against_bookmark() {
+ let boookmarkURI = "https://boookmark.com/";
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: boookmarkURI,
+ title: "Boookmark",
+ });
+
+ Services.prefs.setBoolPref(PREF_FEATURE_ENABLED, true);
+ Services.prefs.setIntPref(PREF_FEATURE_EXPIRE_DAYS, 14);
+
+ info("Preloaded Top Sites are placed lower than Bookmarks");
+ let context = createContext("ooo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: boookmarkURI,
+ title: "Boookmark",
+ }),
+ makeVisitResult(context, {
+ uri: yahoooURI,
+ title: "Yahooo",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: `page-icon:${yahoooURI}`,
+ providerName: "PreloadedSites",
+ }),
+ makeVisitResult(context, {
+ uri: gooogleURI,
+ title: "Gooogle",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: `page-icon:${gooogleURI}`,
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_sorting_against_history() {
+ let histoooryURI = "https://histooory.com/";
+ await PlacesTestUtils.addVisits({ uri: histoooryURI, title: "Histooory" });
+
+ Services.prefs.setBoolPref(PREF_FEATURE_ENABLED, true);
+ Services.prefs.setIntPref(PREF_FEATURE_EXPIRE_DAYS, 14);
+
+ info("Preloaded Top Sites are placed lower than History entries");
+ let context = createContext("ooo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: histoooryURI,
+ title: "Histooory",
+ }),
+ makeVisitResult(context, {
+ uri: yahoooURI,
+ title: "Yahooo",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: `page-icon:${yahoooURI}`,
+ providerName: "PreloadedSites",
+ }),
+ makeVisitResult(context, {
+ uri: gooogleURI,
+ title: "Gooogle",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: `page-icon:${gooogleURI}`,
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_scheme_and_www() {
+ // Order is important to check sorting
+ let sites = [
+ ["https://www.ooops-https-www.com/", "Ooops"],
+ ["https://ooops-https.com/", "Ooops"],
+ ["HTTP://ooops-HTTP.com/", "Ooops"],
+ ["HTTP://www.ooops-HTTP-www.com/", "Ooops"],
+ ["https://foo.com/", "Title with www"],
+ ["https://www.bar.com/", "Tile"],
+ ];
+
+ let titlesMap = new Map(sites);
+
+ UrlbarProviderPreloadedSites.populatePreloadedSiteStorage(sites);
+
+ // No matches when just typing the protocol.
+ let context = createContext("https://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("www.", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.ooops-https-www.com/",
+ completed: "https://www.ooops-https-www.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.ooops-https-www.com/",
+ fallbackTitle: "https://www.ooops-https-www.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: "page-icon:https://www.ooops-https-www.com/",
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://www.ooops-http-www.com/",
+ title: titlesMap.get("HTTP://www.ooops-HTTP-www.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:http://www.ooops-http-www.com/",
+ providerName: "PreloadedSites",
+ }),
+ makeVisitResult(context, {
+ uri: "https://www.bar.com/",
+ title: titlesMap.get("https://www.bar.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:https://www.bar.com/",
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+
+ context = createContext("http://www.", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://www.ooops-http-www.com/",
+ completed: "http://www.ooops-http-www.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.ooops-http-www.com/",
+ fallbackTitle: "www.ooops-http-www.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: "page-icon:http://www.ooops-http-www.com/",
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("ftp://ooops", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "ftp://ooops/",
+ fallbackTitle: "ftp://ooops/",
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("ww", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.ooops-https-www.com/",
+ completed: "https://www.ooops-https-www.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.ooops-https-www.com/",
+ fallbackTitle: "https://www.ooops-https-www.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: "page-icon:https://www.ooops-https-www.com/",
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://www.ooops-http-www.com/",
+ title: titlesMap.get("HTTP://www.ooops-HTTP-www.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:http://www.ooops-http-www.com/",
+ providerName: "PreloadedSites",
+ }),
+ makeVisitResult(context, {
+ uri: "https://foo.com/",
+ title: titlesMap.get("https://foo.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:https://foo.com/",
+ providerName: "PreloadedSites",
+ }),
+ makeVisitResult(context, {
+ uri: "https://www.bar.com/",
+ title: titlesMap.get("https://www.bar.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:https://www.bar.com/",
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+
+ context = createContext("ooops", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "ooops-https-www.com/",
+ completed: "https://www.ooops-https-www.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.ooops-https-www.com/",
+ fallbackTitle: "https://www.ooops-https-www.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: "page-icon:https://www.ooops-https-www.com/",
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://ooops-https.com/",
+ title: titlesMap.get("https://ooops-https.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:https://ooops-https.com/",
+ providerName: "PreloadedSites",
+ }),
+ makeVisitResult(context, {
+ uri: "http://ooops-http.com/",
+ title: titlesMap.get("HTTP://ooops-HTTP.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:http://ooops-http.com/",
+ providerName: "PreloadedSites",
+ }),
+ makeVisitResult(context, {
+ uri: "http://www.ooops-http-www.com/",
+ title: titlesMap.get("HTTP://www.ooops-HTTP-www.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:http://www.ooops-http-www.com/",
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+
+ context = createContext("www.ooops", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.ooops-https-www.com/",
+ completed: "https://www.ooops-https-www.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.ooops-https-www.com/",
+ fallbackTitle: "https://www.ooops-https-www.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: "page-icon:https://www.ooops-https-www.com/",
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://www.ooops-http-www.com/",
+ title: titlesMap.get("HTTP://www.ooops-HTTP-www.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:http://www.ooops-http-www.com/",
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+
+ context = createContext("ooops-https-www", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "ooops-https-www.com/",
+ completed: "https://www.ooops-https-www.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.ooops-https-www.com/",
+ fallbackTitle: "https://www.ooops-https-www.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: "page-icon:https://www.ooops-https-www.com/",
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("www.ooops-https.", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www.ooops-https./",
+ fallbackTitle: "http://www.ooops-https./",
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: "HeuristicFallback",
+ }),
+ ],
+ });
+
+ context = createContext("https://ooops", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "https://ooops-https-www.com/",
+ completed: "https://www.ooops-https-www.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.ooops-https-www.com/",
+ fallbackTitle: "https://www.ooops-https-www.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: "page-icon:https://www.ooops-https-www.com/",
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://ooops-https.com/",
+ title: titlesMap.get("https://ooops-https.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:https://ooops-https.com/",
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+
+ context = createContext("https://www.ooops", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "https://www.ooops-https-www.com/",
+ completed: "https://www.ooops-https-www.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.ooops-https-www.com/",
+ fallbackTitle: "https://www.ooops-https-www.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: "page-icon:https://www.ooops-https-www.com/",
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http://www.ooops-http.", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www.ooops-http./",
+ fallbackTitle: "http://www.ooops-http./",
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http://ooops-https", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://ooops-https/",
+ fallbackTitle: "http://ooops-https/",
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_data_file() {
+ let response = await fetch(
+ "chrome://browser/content/urlbar/preloaded-top-urls.json"
+ );
+
+ info("Source file is supplied and fetched OK");
+ Assert.ok(response.ok);
+
+ info("The JSON is parsed");
+ let sites = await response.json();
+
+ // Add test site so this test doesn't depend on the contents of the data file.
+ sites.push(["https://www.example.com/", "Example"]);
+
+ info("Storage is populated");
+ UrlbarProviderPreloadedSites.populatePreloadedSiteStorage(sites);
+
+ let lastSite = sites.pop();
+ let uri = Services.io.newURI(lastSite[0]);
+
+ info("Storage is populated from JSON correctly");
+ let context = createContext(uri.host, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: uri.host + "/",
+ completed: uri.spec,
+ matches: [
+ makeVisitResult(context, {
+ uri: uri.spec,
+ fallbackTitle: uri.spec.slice(0, -1), // Trim trailing slash.
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: `page-icon:${uri.spec}`,
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_partial_scheme() {
+ // "tt" should not result in a match of "ttps://whatever.com/".
+ let testUrl = "http://www.ttt.com/";
+ UrlbarProviderPreloadedSites.populatePreloadedSiteStorage([
+ [testUrl, "Test"],
+ ]);
+ let context = createContext("tt", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "ttt.com/",
+ completed: testUrl,
+ matches: [
+ makeVisitResult(context, {
+ uri: testUrl,
+ fallbackTitle: "www.ttt.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: `page-icon:${testUrl}`,
+ heuristic: true,
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
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..fe625f7bb9
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerTabToSearch.js
@@ -0,0 +1,535 @@
+/* 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
+ );
+ await SearchTestUtils.installSearchExtension({ name: "Test" });
+ testEngine = await Services.search.getEngineByName("Test");
+
+ registerCleanupFunction(async () => {
+ 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: "test visit for https://example.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain),
+ 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: "test visit for https://example.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ ],
+ });
+ Services.prefs.clearUserPref("browser.urlbar.suggest.engines");
+
+ await cleanupPlaces();
+});
+
+// Tests that tab-to-search results are shown when the typed string matches an
+// engine domain even when 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",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+});
+
+// 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: "test visit for 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: "test visit for https://www.example.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+
+ // The engine has www., the history result does not.
+ await PlacesTestUtils.addVisits(["https://foo.bar/"]);
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestWww",
+ search_url: "https://www.foo.bar/",
+ },
+ { skipUnload: true }
+ );
+ let wwwTestEngine = Services.search.getEngineByName("TestWww");
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "foo.bar/",
+ completed: "https://foo.bar/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://foo.bar/",
+ title: "test visit for https://foo.bar/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: wwwTestEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ wwwTestEngine.searchUrlDomain
+ ),
+ 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: "test visit for https://www.foo.bar/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: wwwTestEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ wwwTestEngine.searchUrlDomain
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+
+ await extension.unload();
+});
+
+// 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 extension1 = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestFooBar",
+ search_url: "https://foobar.com/",
+ },
+ { skipUnload: true }
+ );
+ let extension2 = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestFoo",
+ search_url: "https://foo.com/",
+ },
+ { skipUnload: true }
+ );
+ let fooBarTestEngine = Services.search.getEngineByName("TestFooBar");
+ let fooTestEngine = Services.search.getEngineByName("TestFoo");
+
+ // 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: "test visit for https://foo.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: fooTestEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ fooTestEngine.searchUrlDomain
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ makeVisitResult(context, {
+ uri: "https://foobar.com/",
+ title: "test visit for https://foobar.com/",
+ providerName: "Places",
+ }),
+ ],
+ });
+
+ // 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: "test visit for https://foobar.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: fooBarTestEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ fooBarTestEngine.searchUrlDomain
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+ await extension1.unload();
+ await extension2.unload();
+});
+
+add_task(async function multipleEnginesForHostname() {
+ info(
+ "In case of multiple engines only one tab-to-search result should be returned"
+ );
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestMaps",
+ search_url: "https://example.com/maps/",
+ },
+ { skipUnload: true }
+ );
+
+ let context = createContext("examp", { isPrivate: false });
+ let maxResultCount = UrlbarPrefs.get("maxRichResults");
+
+ // Add enough visits to autofill example.com.
+ for (let i = 0; i < maxResultCount; i++) {
+ await PlacesTestUtils.addVisits("https://example.com/");
+ }
+
+ // Add enough visits to other URLs matching our query to fill up the list of
+ // results.
+ let otherVisitResults = [];
+ for (let i = 0; i < maxResultCount; i++) {
+ let url = "https://mochi.test:8888/example/" + i;
+ await PlacesTestUtils.addVisits(url);
+ otherVisitResults.unshift(
+ makeVisitResult(context, {
+ uri: url,
+ title: "test visit for " + url,
+ })
+ );
+ }
+
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "test visit for https://example.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ // There should be `maxResultCount` - 2 other visit results. If this fails
+ // because there are actually `maxResultCount` - 3 other results, then the
+ // muxer is improperly including both TabToSearch results in its
+ // calculation of the total available result span instead of only one, so
+ // one fewer visit result appears than expected.
+ ...otherVisitResults.slice(0, maxResultCount - 2),
+ ],
+ });
+ await cleanupPlaces();
+ await extension.unload();
+});
+
+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: "test visit for https://example.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain),
+ 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 extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "MyTest",
+ search_url: "https://test.mytest.it/",
+ },
+ { skipUnload: true }
+ );
+ let engine = Services.search.getEngineByName("MyTest");
+ 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,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ satisfiesAutofillThreshold: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://test.mytest.it/",
+ title: "test visit for https://test.mytest.it/",
+ providerName: "Places",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+ await extension.unload();
+});
+
+add_task(async function test_publicSuffixIsHost() {
+ info("Tab-to-search results does not appear in case we autofill a suffix.");
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "SuffixTest",
+ search_url: "https://somesuffix.com.mx/",
+ },
+ { skipUnload: true }
+ );
+
+ // 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: "test visit for https://com.mx/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+ await extension.unload();
+});
+
+add_task(async function test_disabledEngine() {
+ info("Tab-to-search results does not appear for a Pref-disabled engine.");
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "Disabled",
+ search_url: "https://disabled.com/",
+ },
+ { skipUnload: true }
+ );
+ let engine = Services.search.getEngineByName("Disabled");
+ 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: "test visit for https://disabled.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: engine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain),
+ 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: "test visit for https://disabled.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ ],
+ });
+ Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
+
+ await cleanupPlaces();
+ await extension.unload();
+});
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..d2391c0f43
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js
@@ -0,0 +1,214 @@
+/* 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";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderAutofill: "resource:///modules/UrlbarProviderAutofill.sys.mjs",
+});
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", 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.urlbar.suggest.quickactions");
+ Services.prefs.clearUserPref(
+ "browser.search.separatePrivateDefault.ui.enabled"
+ );
+ Services.prefs.clearUserPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft"
+ );
+ });
+});
+
+add_task(async function test() {
+ let url = "https://en.example.com/";
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestEngine",
+ search_url: url,
+ },
+ { setAsDefault: true }
+ );
+
+ // 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: "TestEngine",
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ 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/";
+ await SearchTestUtils.installSearchExtension({
+ name: "TestEngine2",
+ search_url: url2,
+ });
+
+ let engine2 = Services.search.getEngineByName("TestEngine2");
+ // 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,
+ 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("Tab-to-search is not shown when an unrelated site is autofilled.");
+ let wikiUrl = "https://wikipedia.org/";
+ await SearchTestUtils.installSearchExtension({
+ name: "FakeWikipedia",
+ search_url: url,
+ });
+ let wikiEngine = Services.search.getEngineByName("TestEngine");
+
+ // Make sure that wikiUrl will pass getTopHostOverThreshold.
+ await PlacesUtils.bookmarks.insert({
+ url: wikiUrl,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "Wikipedia",
+ });
+
+ // Make sure an unrelated www site is autofilled.
+ let wwwUrl = "https://www.example.com";
+ await PlacesUtils.bookmarks.insert({
+ url: wwwUrl,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "Example",
+ });
+
+ let searchStr = "w";
+ let context = createContext(searchStr, {
+ isPrivate: false,
+ sources: [UrlbarUtils.RESULT_SOURCE.BOOKMARKS],
+ });
+ let host = await UrlbarProviderAutofill.getTopHostOverThreshold(context, [
+ wikiEngine.searchUrlDomain,
+ ]);
+ Assert.equal(
+ host,
+ wikiEngine.searchUrlDomain,
+ "The search satisfies the autofill threshold requirement."
+ );
+ await check_results({
+ context,
+ autofilled: "www.example.com/",
+ completed: "https://www.example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: `${wwwUrl}/`,
+ title: "Example",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ // Note that tab-to-search is not shown.
+ makeBookmarkResult(context, {
+ uri: wikiUrl,
+ title: "Wikipedia",
+ }),
+ makeBookmarkResult(context, {
+ uri: url2,
+ title: "bookmark",
+ }),
+ ],
+ });
+
+ info("Restricting to history should not autofill our bookmark");
+ 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");
+
+ await cleanupPlaces();
+});
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..8446ed0675
--- /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 provider = registerBasicTestProvider([match]);
+ let context = createContext(undefined, { providers: [provider.name] });
+ 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..a631bfa204
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js
@@ -0,0 +1,407 @@
+/* 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 provider = registerBasicTestProvider([match]);
+ let context = createContext(undefined, { providers: [provider.name] });
+ 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: provider.name });
+});
+
+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 provider = registerBasicTestProvider(matches);
+ let context = createContext(undefined, { providers: [provider.name] });
+ 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(provider);
+});
+
+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 provider = registerBasicTestProvider(matches);
+ let context = createContext(`foo ${UrlbarTokenizer.RESTRICT.OPENPAGE}`, {
+ providers: [provider.name],
+ });
+ 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(provider);
+});
+
+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 provider = registerBasicTestProvider([match, jsMatch]);
+ let context = createContext(undefined, { providers: [provider.name] });
+ 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: [provider.name],
+ });
+ 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: [provider.name] });
+ 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(provider);
+});
+
+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 provider = 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);
+ }
+ }
+ }
+ let badProvider = new NoInvokeProvider();
+ UrlbarProvidersManager.registerProvider(badProvider);
+
+ let context = createContext(undefined, {
+ sources: [UrlbarUtils.RESULT_SOURCE.TABS],
+ providers: [provider.name, "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(provider);
+ UrlbarProvidersManager.unregisterProvider(badProvider);
+});
+
+add_task(async function test_filter_queryContext() {
+ let provider = 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");
+ }
+ }
+ let badProvider = new NoInvokeProvider();
+ UrlbarProvidersManager.registerProvider(badProvider);
+
+ let context = createContext(undefined, {
+ providers: [provider.name],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ await controller.startQuery(context, controller);
+ UrlbarProvidersManager.unregisterProvider(provider);
+ UrlbarProvidersManager.unregisterProvider(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 provider = registerBasicTestProvider(
+ matches,
+ undefined,
+ UrlbarUtils.PROVIDER_TYPE.HEURISTIC
+ );
+
+ let context = createContext(undefined, {
+ sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
+ providers: [provider.name],
+ });
+ 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(provider);
+});
+
+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..b30b9352cd
--- /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 provider = registerBasicTestProvider(matches);
+ let context = createContext(undefined, { providers: [provider.name] });
+ 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..1d6171eac4
--- /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";
+
+ChromeUtils.defineESModuleGetters(this, {
+ QueryScorer: "resource:///modules/UrlbarProviderInterventions.sys.mjs",
+});
+
+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..e49c966d76
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_query_url.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 PLACES_PROVIDERNAME = "Places";
+
+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/",
+ fallbackTitle: "file.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "file:///c:/test.html",
+ title: "test visit for file:///c:/test.html",
+ iconUri: UrlbarUtils.ICON.DEFAULT,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://file.org/test/",
+ title: "test visit for http://file.org/test/",
+ providerName: PLACES_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/",
+ fallbackTitle: "file.org/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://file.org/test/",
+ title: "test visit for http://file.org/test/",
+ providerName: PLACES_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: "test visit for http://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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "file:///c:/test.html",
+ title: "test visit for file:///c:/test.html",
+ iconUri: UrlbarUtils.ICON.DEFAULT,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_quickactions.js b/browser/components/urlbar/tests/unit/test_quickactions.js
new file mode 100644
index 0000000000..ddeb5a7561
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_quickactions.js
@@ -0,0 +1,126 @@
+/* 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";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderQuickActions:
+ "resource:///modules/UrlbarProviderQuickActions.sys.mjs",
+});
+
+let expectedMatch = (key, inputLength) => ({
+ type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ source: UrlbarUtils.RESULT_SOURCE.ACTIONS,
+ heuristic: false,
+ payload: {
+ results: [{ key }],
+ dynamicType: "quickactions",
+ helpUrl: UrlbarProviderQuickActions.helpUrl,
+ inputLength,
+ },
+});
+
+testEngine_setup();
+
+add_task(async function init() {
+ UrlbarPrefs.set("quickactions.enabled", true);
+ UrlbarPrefs.set("suggest.quickactions", true);
+
+ UrlbarProviderQuickActions.addAction("newaction", {
+ commands: ["newaction"],
+ });
+
+ registerCleanupFunction(async () => {
+ UrlbarPrefs.clear("quickactions.enabled");
+ UrlbarPrefs.clear("suggest.quickactions");
+ UrlbarProviderQuickActions.removeAction("newaction");
+ });
+});
+
+add_task(async function nomatch() {
+ let context = createContext("this doesnt match", {
+ providers: [UrlbarProviderQuickActions.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+});
+
+add_task(async function quickactions_disabled() {
+ UrlbarPrefs.set("suggest.quickactions", false);
+ let context = createContext("new", {
+ providers: [UrlbarProviderQuickActions.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+});
+
+add_task(async function quickactions_match() {
+ UrlbarPrefs.set("suggest.quickactions", true);
+ let context = createContext("new", {
+ providers: [UrlbarProviderQuickActions.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [expectedMatch("newaction", 3)],
+ });
+});
+
+add_task(async function duplicate_matches() {
+ UrlbarProviderQuickActions.addAction("testaction", {
+ commands: ["testaction", "test"],
+ });
+
+ let context = createContext("testaction", {
+ providers: [UrlbarProviderQuickActions.name],
+ isPrivate: false,
+ });
+
+ await check_results({
+ context,
+ matches: [expectedMatch("testaction", 10)],
+ });
+
+ UrlbarProviderQuickActions.removeAction("testaction");
+});
+
+add_task(async function remove_action() {
+ UrlbarProviderQuickActions.addAction("testaction", {
+ commands: ["testaction"],
+ });
+ UrlbarProviderQuickActions.removeAction("testaction");
+
+ let context = createContext("test", {
+ providers: [UrlbarProviderQuickActions.name],
+ isPrivate: false,
+ });
+
+ await check_results({
+ context,
+ matches: [],
+ });
+});
+
+add_task(async function minimum_search_string() {
+ let searchString = "newa";
+ for (let minimumSearchString of [0, 3]) {
+ UrlbarPrefs.set("quickactions.minimumSearchString", minimumSearchString);
+ for (let i = 1; i < 4; i++) {
+ let context = createContext(searchString.substring(0, i), {
+ providers: [UrlbarProviderQuickActions.name],
+ isPrivate: false,
+ });
+ let matches =
+ i >= minimumSearchString ? [expectedMatch("newaction", i)] : [];
+ await check_results({ context, matches });
+ }
+ }
+ UrlbarPrefs.clear("quickactions.minimumSearchString");
+});
diff --git a/browser/components/urlbar/tests/unit/test_remote_tabs.js b/browser/components/urlbar/tests/unit/test_remote_tabs.js
new file mode 100644
index 0000000000..5b834c876e
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_remote_tabs.js
@@ -0,0 +1,695 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim:set ts=2 sw=2 sts=2 et:
+ */
+"use strict";
+
+const { Weave } = ChromeUtils.importESModule(
+ "resource://services-sync/main.sys.mjs"
+);
+
+// A mock "Tabs" engine which autocomplete will use instead of the real
+// engine. We pass a constructor that Sync creates.
+function MockTabsEngine() {
+ this.clients = null; // We'll set this dynamically
+}
+
+MockTabsEngine.prototype = {
+ name: "tabs",
+
+ startTracking() {},
+ getAllClients() {
+ return this.clients;
+ },
+};
+
+// A clients engine that doesn't need to be a constructor.
+let MockClientsEngine = {
+ getClientType(guid) {
+ Assert.ok(guid.endsWith("desktop") || guid.endsWith("mobile"));
+ return guid.endsWith("mobile") ? "phone" : "desktop";
+ },
+ remoteClientExists(id) {
+ return true;
+ },
+ getClientName(id) {
+ return id.endsWith("mobile") ? "My Phone" : "My Desktop";
+ },
+};
+
+// Configure the singleton engine for a test.
+function configureEngine(clients) {
+ // Configure the instance Sync created.
+ let engine = Weave.Service.engineManager.get("tabs");
+ engine.clients = clients;
+ Weave.Service.clientsEngine = MockClientsEngine;
+ // Send an observer that pretends the engine just finished a sync.
+ Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs");
+}
+
+testEngine_setup();
+
+add_task(async function setup() {
+ // Tell Sync about the mocks.
+ Weave.Service.engineManager.register(MockTabsEngine);
+
+ // Tell the Sync XPCOM service it is initialized.
+ let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject;
+ weaveXPCService.ready = true;
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("services.sync.username");
+ Services.prefs.clearUserPref("services.sync.registerEngines");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+ await cleanupPlaces();
+ });
+
+ Services.prefs.setCharPref("services.sync.username", "someone@somewhere.com");
+ Services.prefs.setCharPref("services.sync.registerEngines", "");
+ // Avoid hitting the network.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+});
+
+add_task(async function test_minimal() {
+ // The minimal client and tabs info we can get away with.
+ configureEngine([
+ {
+ id: "desktop",
+ tabs: [
+ {
+ urlHistory: ["http://example.com/"],
+ },
+ ],
+ },
+ ]);
+
+ let query = "ex";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://example.com/",
+ device: "My Desktop",
+ }),
+ ],
+ });
+});
+
+add_task(async function test_maximal() {
+ // Every field that could possibly exist on a remote record.
+ configureEngine([
+ {
+ id: "mobile",
+ tabs: [
+ {
+ urlHistory: ["http://example.com/"],
+ title: "An Example",
+ icon: "http://favicon",
+ },
+ ],
+ },
+ ]);
+
+ let query = "ex";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://example.com/",
+ device: "My Phone",
+ title: "An Example",
+ iconUri: "moz-anno:favicon:http://favicon/",
+ }),
+ ],
+ });
+});
+
+add_task(async function test_noShowIcons() {
+ Services.prefs.setBoolPref("services.sync.syncedTabs.showRemoteIcons", false);
+ configureEngine([
+ {
+ id: "mobile",
+ tabs: [
+ {
+ urlHistory: ["http://example.com/"],
+ title: "An Example",
+ icon: "http://favicon",
+ },
+ ],
+ },
+ ]);
+
+ let query = "ex";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://example.com/",
+ device: "My Phone",
+ title: "An Example",
+ // expecting the default favicon due to that pref.
+ iconUri: "",
+ }),
+ ],
+ });
+ Services.prefs.clearUserPref("services.sync.syncedTabs.showRemoteIcons");
+});
+
+add_task(async function test_dontMatchSyncedTabs() {
+ Services.prefs.setBoolPref("services.sync.syncedTabs.showRemoteTabs", false);
+ configureEngine([
+ {
+ id: "mobile",
+ tabs: [
+ {
+ urlHistory: ["http://example.com/"],
+ title: "An Example",
+ icon: "http://favicon",
+ },
+ ],
+ },
+ ]);
+
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("services.sync.syncedTabs.showRemoteTabs");
+});
+
+add_task(async function test_tabsDisabledInUrlbar() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.remotetab", false);
+ configureEngine([
+ {
+ id: "mobile",
+ tabs: [
+ {
+ urlHistory: ["http://example.com/"],
+ title: "An Example",
+ icon: "http://favicon",
+ },
+ ],
+ },
+ ]);
+
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("browser.urlbar.suggest.remotetab");
+});
+
+add_task(async function test_matches_title() {
+ // URL doesn't match search expression, should still match the title.
+ configureEngine([
+ {
+ id: "mobile",
+ tabs: [
+ {
+ urlHistory: ["http://foo.com/"],
+ title: "An Example",
+ },
+ ],
+ },
+ ]);
+
+ let query = "ex";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.com/",
+ device: "My Phone",
+ title: "An Example",
+ }),
+ ],
+ });
+});
+
+add_task(async function test_localtab_matches_override() {
+ // We have an open tab to the same page on a remote device, only "switch to
+ // tab" should appear as duplicate detection removed the remote one.
+
+ // First set up Sync to have the page as a remote tab.
+ configureEngine([
+ {
+ id: "mobile",
+ tabs: [
+ {
+ urlHistory: ["http://foo.com/"],
+ title: "An Example",
+ },
+ ],
+ },
+ ]);
+
+ // Set up Places to think the tab is open locally.
+ let uri = Services.io.newURI("http://foo.com/");
+ await PlacesTestUtils.addVisits([{ uri, title: "An Example" }]);
+ await addOpenPages(uri, 1);
+
+ let query = "ex";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://foo.com/",
+ title: "An Example",
+ }),
+ ],
+ });
+
+ await removeOpenPages(uri, 1);
+ await cleanupPlaces();
+});
+
+add_task(async function test_remotetab_matches_override() {
+ // If we have an history result to the same page, we should only get the
+ // remote tab match.
+ let url = "http://foo.remote.com/";
+ // First set up Sync to have the page as a remote tab.
+ configureEngine([
+ {
+ id: "mobile",
+ tabs: [
+ {
+ urlHistory: [url],
+ title: "An Example",
+ },
+ ],
+ },
+ ]);
+
+ // Set up Places to think the tab is in history.
+ await PlacesTestUtils.addVisits(url);
+
+ let query = "ex";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/",
+ device: "My Phone",
+ title: "An Example",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_mixed_result_types() {
+ // In case we have many results, non-remote results should flex to the bottom.
+ let url = "http://foo.remote.com/";
+ let tabs = Array(6)
+ .fill(0)
+ .map((e, i) => ({
+ urlHistory: [`${url}${i}`],
+ title: "A title",
+ lastUsed: Math.floor(Date.now() / 1000) - i * 86400, // i days ago.
+ }));
+ // First set up Sync to have the page as a remote tab.
+ configureEngine([{ id: "mobile", tabs }]);
+
+ // Register the page as an open tab.
+ let openTabUrl = url + "openpage/";
+ let uri = Services.io.newURI(openTabUrl);
+ await PlacesTestUtils.addVisits([{ uri, title: "An Example" }]);
+ await addOpenPages(uri, 1);
+
+ // Also add a local history result.
+ let historyUrl = url + "history/";
+ await PlacesTestUtils.addVisits(historyUrl);
+
+ let query = "rem";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/0",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[0].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/1",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[1].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/2",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[2].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/3",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[3].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/4",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[4].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/5",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[5].lastUsed,
+ }),
+ makeVisitResult(context, {
+ uri: historyUrl,
+ title: "test visit for " + historyUrl,
+ }),
+ makeTabSwitchResult(context, {
+ uri: openTabUrl,
+ title: "An Example",
+ }),
+ ],
+ });
+ await removeOpenPages(uri, 1);
+ await cleanupPlaces();
+});
+
+add_task(async function test_many_remotetab_results() {
+ let url = "http://foo.remote.com/";
+ let tabs = Array(8)
+ .fill(0)
+ .map((e, i) => ({
+ urlHistory: [`${url}${i}`],
+ title: "A title",
+ lastUsed: Math.floor(Date.now() / 1000) - i * 86400, // i days old.
+ }));
+
+ // First set up Sync to have the page as a remote tab.
+ configureEngine([
+ {
+ id: "mobile",
+ tabs,
+ },
+ ]);
+
+ let query = "rem";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/0",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[0].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/1",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[1].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/2",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[2].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/3",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[3].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/4",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[4].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/5",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[5].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/6",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[6].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/7",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[7].lastUsed,
+ }),
+ ],
+ });
+});
+
+add_task(async function multiple_clients() {
+ let url = "http://foo.remote.com/";
+ let mobileTabs = Array(2)
+ .fill(0)
+ .map((e, i) => ({
+ urlHistory: [`${url}mobile/${i}`],
+ lastUsed: Date.now() / 1000 - 4 * 86400, // 4 days old (past threshold)
+ }));
+
+ let desktopTabs = Array(3)
+ .fill(0)
+ .map((e, i) => ({
+ urlHistory: [`${url}desktop/${i}`],
+ lastUsed: Date.now() / 1000 - 1, // Fresh tabs
+ }));
+
+ // mobileTabs has the most recent tab, making it the most recent client. The
+ // rest of its tabs are stale. The tabs in desktopTabs are fresh, but not
+ // as fresh as the most recent tab in mobileTab.
+ mobileTabs.push({
+ urlHistory: [`${url}mobile/fresh`],
+ lastUsed: Date.now() / 1000,
+ });
+
+ configureEngine([
+ {
+ id: "mobile",
+ tabs: mobileTabs,
+ },
+ {
+ id: "desktop",
+ tabs: desktopTabs,
+ },
+ ]);
+
+ // We expect that we will show the recent tab from mobileTabs, then all the
+ // tabs from desktopTabs, then the remaining tabs from mobileTabs.
+ let query = "rem";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/mobile/fresh",
+ device: "My Phone",
+ lastUsed: mobileTabs[2].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/desktop/0",
+ device: "My Desktop",
+ lastUsed: desktopTabs[0].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/desktop/1",
+ device: "My Desktop",
+ lastUsed: desktopTabs[1].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/desktop/2",
+ device: "My Desktop",
+ lastUsed: desktopTabs[2].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/mobile/0",
+ device: "My Phone",
+ lastUsed: mobileTabs[0].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/mobile/1",
+ device: "My Phone",
+ lastUsed: mobileTabs[1].lastUsed,
+ }),
+ ],
+ });
+});
+
+add_task(async function test_restrictionCharacter() {
+ let url = "http://foo.remote.com/";
+ let tabs = Array(5)
+ .fill(0)
+ .map((e, i) => ({
+ urlHistory: [`${url}${i}`],
+ title: "A title",
+ lastUsed: Math.floor(Date.now() / 1000) - i,
+ }));
+ configureEngine([
+ {
+ id: "mobile",
+ tabs,
+ },
+ ]);
+
+ // Also add an open page.
+ let openTabUrl = url + "openpage/";
+ let uri = Services.io.newURI(openTabUrl);
+ await PlacesTestUtils.addVisits([{ uri, title: "An Example" }]);
+ await addOpenPages(uri, 1);
+
+ // We expect the open tab to flex to the bottom.
+ let query = UrlbarTokenizer.RESTRICT.OPENPAGE;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/0",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[0].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/1",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[1].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/2",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[2].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/3",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[3].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/4",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[4].lastUsed,
+ }),
+ makeTabSwitchResult(context, {
+ uri: openTabUrl,
+ title: "An Example",
+ }),
+ ],
+ });
+ await removeOpenPages(uri, 1);
+ await cleanupPlaces();
+});
+
+add_task(async function test_duplicate_remote_tabs() {
+ let url = "http://foo.remote.com/";
+ let tabs = Array(3)
+ .fill(0)
+ .map((e, i) => ({
+ urlHistory: [url],
+ title: "A title",
+ lastUsed: Math.floor(Date.now() / 1000),
+ }));
+ configureEngine([
+ {
+ id: "mobile",
+ tabs,
+ },
+ ]);
+
+ // We expect the duplicate tabs to be deduped.
+ let query = UrlbarTokenizer.RESTRICT.OPENPAGE;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[0].lastUsed,
+ }),
+ ],
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_resultGroups.js b/browser/components/urlbar/tests/unit/test_resultGroups.js
new file mode 100644
index 0000000000..e385f7d2fb
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_resultGroups.js
@@ -0,0 +1,1576 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the muxer's result groups composition logic: child groups,
+// `availableSpan`, `maxResultCount`, flex, etc. The purpose of this test is to
+// check the composition logic, not every possible result type or group.
+
+"use strict";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+// The possible limit-related properties in result groups.
+const LIMIT_KEYS = ["availableSpan", "maxResultCount"];
+
+// Most of this test adds tasks using `add_resultGroupsLimit_tasks`. It works
+// like this. Instead of defining `maxResultCount` or `availableSpan` in their
+// result groups, tasks define a `limit` property. The value of this property is
+// a number just like any of the values for the limit-related properties. At
+// runtime, `add_resultGroupsLimit_tasks` adds multiple tasks, one for each key
+// in `LIMIT_KEYS`. In each of these tasks, the `limit` property is replaced
+// with the actual limit key. This allows us to run checks against each of the
+// limit keys using essentially the same task.
+
+const MAX_RICH_RESULTS_PREF = "browser.urlbar.maxRichResults";
+
+// For simplicity, most of the flex tests below assume that this is 10, so
+// you'll need to update them if you change this.
+const MAX_RESULTS = 10;
+
+let sandbox;
+
+add_task(async function setup() {
+ // Set a specific maxRichResults for sanity's sake.
+ Services.prefs.setIntPref(MAX_RICH_RESULTS_PREF, MAX_RESULTS);
+
+ sandbox = lazy.sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "empty root",
+ resultGroups: {},
+ providerResults: [...makeHistoryResults(1)],
+ expectedResultIndexes: [],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "root with empty children",
+ resultGroups: {
+ children: [],
+ },
+ providerResults: [...makeHistoryResults(1)],
+ expectedResultIndexes: [],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "root no match",
+ resultGroups: {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ providerResults: [...makeHistoryResults(1)],
+ expectedResultIndexes: [],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "children no match",
+ resultGroups: {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }],
+ },
+ providerResults: [...makeHistoryResults(1)],
+ expectedResultIndexes: [],
+});
+
+add_resultGroupsLimit_tasks({
+ // The actual max result count on the root is always context.maxResults and
+ // limit is ignored, so we expect the result in this case.
+ testName: "root limit: 0",
+ resultGroups: {
+ limit: 0,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ providerResults: [...makeHistoryResults(1)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroupsLimit_tasks({
+ // The actual max result count on the root is always context.maxResults and
+ // limit is ignored, so we expect the result in this case.
+ testName: "root limit: 0 with children",
+ resultGroups: {
+ limit: 0,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ providerResults: [...makeHistoryResults(1)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "child limit: 0",
+ resultGroups: {
+ children: [
+ {
+ limit: 0,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(1)],
+ expectedResultIndexes: [],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "root group",
+ resultGroups: {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ providerResults: [...makeHistoryResults(1)],
+ expectedResultIndexes: [...makeIndexRange(0, 1)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "root group multiple",
+ resultGroups: {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [...makeIndexRange(0, 2)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "child group multiple",
+ resultGroups: {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [0, 1],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "simple limit",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "limit siblings",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ ],
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [0, 1],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "limit nested",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "limit nested siblings",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ ],
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "limit nested uncle",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ ],
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [0, 1],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "limit nested override bad",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ children: [
+ {
+ limit: 99,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "limit nested override good",
+ resultGroups: {
+ children: [
+ {
+ limit: 99,
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups",
+ resultGroups: {
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 2)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups limit 1",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 1), ...makeIndexRange(0, 2)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups limit 2",
+ resultGroups: {
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 1)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups limit 3",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [
+ ...makeIndexRange(2, 1),
+ ...makeIndexRange(0, 2),
+ ...makeIndexRange(3, 1),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups limit 4",
+ resultGroups: {
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 1)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups nested 1",
+ resultGroups: {
+ children: [
+ {
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 2)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups nested 2",
+ resultGroups: {
+ children: [
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 2)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups nested limit 1",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 1)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups nested limit 2",
+ resultGroups: {
+ children: [
+ {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 1), ...makeIndexRange(0, 2)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups nested limit 3",
+ resultGroups: {
+ children: [
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 1)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups nested limit 4",
+ resultGroups: {
+ children: [
+ {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }],
+ },
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [
+ ...makeIndexRange(2, 1),
+ ...makeIndexRange(0, 2),
+ ...makeIndexRange(3, 1),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups nested limit 5",
+ resultGroups: {
+ children: [
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 1)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups nested limit 6",
+ resultGroups: {
+ children: [
+ {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [
+ ...makeIndexRange(2, 1),
+ ...makeIndexRange(0, 2),
+ ...makeIndexRange(3, 1),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex 1",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (1 / (1 + 1))) = 5
+ ...makeIndexRange(MAX_RESULTS, 5),
+ // remote suggestions: round(10 * (1 / (1 + 1))) = 5
+ ...makeIndexRange(0, 5),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex 2",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (2 / 3)) = 7
+ ...makeIndexRange(MAX_RESULTS, 7),
+ // remote suggestions: round(10 * (1 / 3)) = 3
+ ...makeIndexRange(0, 3),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex 3",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (1 / 3)) = 3
+ ...makeIndexRange(MAX_RESULTS, 3),
+ // remote suggestions: round(10 * (2 / 3)) = 7
+ ...makeIndexRange(0, 7),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex 4",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (1 / 3)) = 3, and then incremented to 4 so
+ // that the total result span is 10 instead of 9. This group is incremented
+ // because the fractional part of its unrounded ideal max result count is
+ // 0.33 (since 10 * (1 / 3) = 3.33), the same as the other two groups, and
+ // this group is first.
+ ...makeIndexRange(2 * MAX_RESULTS, 4),
+ // remote suggestions: round(10 * (1 / 3)) = 3
+ ...makeIndexRange(MAX_RESULTS, 3),
+ // form history: round(10 * (1 / 3)) = 3
+ // The first three form history results dupe the three remote suggestions,
+ // so they should not be included.
+ ...makeIndexRange(3, 3),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex 5",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (2 / 4)) = 5
+ ...makeIndexRange(2 * MAX_RESULTS, 5),
+ // remote suggestions: round(10 * (1 / 4)) = 3
+ ...makeIndexRange(MAX_RESULTS, 3),
+ // form history: round(10 * (1 / 4)) = 3, but context.maxResults is 10, so 2
+ // The first three form history results dupe the three remote suggestions,
+ // so they should not be included.
+ ...makeIndexRange(3, 2),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex 6",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (1 / 4)) = 3
+ ...makeIndexRange(2 * MAX_RESULTS, 3),
+ // remote suggestions: round(10 * (2 / 4)) = 5
+ ...makeIndexRange(MAX_RESULTS, 5),
+ // form history: round(10 * (1 / 4)) = 3, but context.maxResults is 10, so 2
+ // The first five form history results dupe the five remote suggestions, so
+ // they should not be included.
+ ...makeIndexRange(5, 2),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex 7",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (1 / 4)) = 3
+ ...makeIndexRange(2 * MAX_RESULTS, 3),
+ // remote suggestions: round(10 * (1 / 4)) = 3, and then decremented to 2 so
+ // that the total result span is 10 instead of 11. This group is decremented
+ // because the fractional part of its unrounded ideal max result count is
+ // 0.5 (since 10 * (1 / 4) = 2.5), the same as the previous group, and the
+ // next group's fractional part is zero.
+ ...makeIndexRange(MAX_RESULTS, 2),
+ // form history: round(10 * (2 / 4)) = 5
+ // The first 2 form history results dupe the three remote suggestions, so
+ // they should not be included.
+ ...makeIndexRange(2, 5),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex overfill 1",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (2 / (2 + 0 + 1))) = 7
+ ...makeIndexRange(MAX_RESULTS, 7),
+ // form history: round(10 * (1 / (2 + 0 + 1))) = 3
+ ...makeIndexRange(0, 3),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex overfill 2",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeRemoteSuggestionResults(1),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(9 * (2 / (2 + 0 + 1))) = 6
+ ...makeIndexRange(MAX_RESULTS + 1, 6),
+ // remote suggestions
+ ...makeIndexRange(MAX_RESULTS, 1),
+ // form history: round(9 * (1 / (2 + 0 + 1))) = 3
+ // The first form history result dupes the remote suggestion, so it should
+ // not be included.
+ ...makeIndexRange(1, 3),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested limit 1",
+ resultGroups: {
+ children: [
+ {
+ limit: 5,
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(5 * (2 / (2 + 1))) = 3
+ ...makeIndexRange(MAX_RESULTS, 3),
+ // remote suggestions: round(5 * (1 / (2 + 1))) = 2
+ ...makeIndexRange(0, 2),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested limit 2",
+ resultGroups: {
+ children: [
+ {
+ limit: 7,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general: round(7 * (1 / (2 + 1))) = 2
+ ...makeIndexRange(MAX_RESULTS, 2),
+ // remote suggestions: round(7 * (2 / (2 + 1))) = 5
+ ...makeIndexRange(0, 5),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested limit 3",
+ resultGroups: {
+ children: [
+ {
+ limit: 7,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ limit: 3,
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general: round(7 * (1 / (2 + 1))) = 2
+ ...makeIndexRange(2 * MAX_RESULTS, 2),
+ // remote suggestions: round(7 * (2 / (2 + 1))) = 5
+ ...makeIndexRange(0, 5),
+ // form history: round(3 * (2 / (2 + 1))) = 2
+ // The first five form history results dupe the five remote suggestions, so
+ // they should not be included.
+ ...makeIndexRange(MAX_RESULTS + 5, 2),
+ // general: round(3 * (1 / (2 + 1))) = 1
+ ...makeIndexRange(2 * MAX_RESULTS + 2, 1),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested limit 4",
+ resultGroups: {
+ children: [
+ {
+ limit: 7,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ limit: 3,
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY },
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general: round(7 * (1 / (2 + 1))) = 2
+ ...makeIndexRange(2 * MAX_RESULTS, 2),
+ // remote suggestions: round(7 * (2 / (2 + 1))) = 5
+ ...makeIndexRange(0, 5),
+ // form history: round(3 * (2 / (2 + 1))) = 2
+ // The first five form history results dupe the five remote suggestions, so
+ // they should not be included.
+ ...makeIndexRange(MAX_RESULTS + 5, 3),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested limit 5",
+ resultGroups: {
+ children: [
+ {
+ limit: 7,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ limit: 3,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general: round(7 * (1 / (2 + 1))) = 2
+ ...makeIndexRange(2 * MAX_RESULTS, 2),
+ // remote suggestions: round(7 * (2 / (2 + 1))) = 5
+ ...makeIndexRange(0, 5),
+ // form history: round(3 * (2 / (2 + 1))) = 2
+ // The first five form history results dupe the five remote suggestions, so
+ // they should not be included.
+ ...makeIndexRange(MAX_RESULTS + 5, 3),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ flex: 1,
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // outer 1: general & remote suggestions: round(10 * (2 / (2 + 1))) = 7
+ // inner 1: general: round(7 * (1 / (2 + 1))) = 2
+ ...makeIndexRange(2 * MAX_RESULTS, 2),
+ // inner 2: remote suggestions: round(7 * (2 / (2 + 1))) = 5
+ ...makeIndexRange(0, 5),
+
+ // outer 2: form history & general: round(10 * (1 / (2 + 1))) = 3
+ // inner 1: form history: round(3 * (2 / (2 + 1))) = 2
+ // The first five form history results dupe the five remote suggestions, so
+ // they should not be included.
+ ...makeIndexRange(MAX_RESULTS + 5, 2),
+ // inner 2: general: round(3 * (1 / (2 + 1))) = 1
+ ...makeIndexRange(2 * MAX_RESULTS + 2, 1),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested overfill 1",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ flex: 1,
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // outer 1: general & remote suggestions: round(10 * (2 / (2 + 1))) = 7
+ // inner 1: general: no results
+ // inner 2: remote suggestions: round(7 * (2 / (2 + 0))) = 7
+ ...makeIndexRange(0, 7),
+
+ // outer 2: form history & general: round(10 * (1 / (2 + 1))) = 3
+ // inner 1: form history: round(3 * (2 / (2 + 0))) = 3
+ // The first seven form history results dupe the seven remote suggestions,
+ // so they should not be included.
+ ...makeIndexRange(MAX_RESULTS + 7, 3),
+ // inner 2: general: no results
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested overfill 2",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ flex: 1,
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [...makeFormHistoryResults(MAX_RESULTS)],
+ expectedResultIndexes: [
+ // outer 1: general & remote suggestions: round(10 * (2 / (2 + 1))) = 7
+ // inner 1: general: no results
+ // inner 2: remote suggestions: no results
+
+ // outer 2: form history & general: round(10 * (1 / (0 + 1))) = 10
+ // inner 1: form history: round(10 * (2 / (2 + 0))) = 10
+ ...makeIndexRange(0, MAX_RESULTS),
+ // inner 2: general: no results
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested overfill 3",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ flex: 1,
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [...makeRemoteSuggestionResults(MAX_RESULTS)],
+ expectedResultIndexes: [
+ // outer 1: general & remote suggestions: round(10 * (2 / (2 + 0))) = 10
+ // inner 1: general: no results
+ // inner 2: remote suggestions: round(10 * (2 / (2 + 0))) = 10
+ ...makeIndexRange(0, MAX_RESULTS),
+
+ // outer 2: form history & general: round(10 * (1 / (2 + 1))) = 3
+ // inner 1: form history: no results
+ // inner 2: general: no results
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "limit ignored with flex",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ limit: 1,
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (2 / (2 + 1))) = 7 -- limit ignored
+ ...makeIndexRange(MAX_RESULTS, 7),
+ // remote suggestions: round(10 * (1 / (2 + 1))) = 3
+ ...makeIndexRange(0, 3),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "resultSpan = 3 followed by others",
+ resultGroups: {
+ children: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ providerResults: [
+ // max results remote suggestions
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ // 1 history with resultSpan = 3
+ Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 }),
+ ],
+ expectedResultIndexes: [
+ // general/history: 1
+ ...makeIndexRange(MAX_RESULTS, 1),
+ // remote suggestions: maxResults - resultSpan of 3 = 10 - 3 = 7
+ ...makeIndexRange(0, 7),
+ ],
+});
+
+add_resultGroups_task({
+ testName: "maxResultCount: 1, availableSpan: 3",
+ resultGroups: {
+ children: [
+ {
+ maxResultCount: 1,
+ availableSpan: 3,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(MAX_RESULTS)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroups_task({
+ testName: "maxResultCount: 1, availableSpan: 3, resultSpan = 3",
+ resultGroups: {
+ children: [
+ {
+ maxResultCount: 1,
+ availableSpan: 3,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [
+ Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 }),
+ Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 }),
+ Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 }),
+ ],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroups_task({
+ testName: "maxResultCount: 3, availableSpan: 1",
+ resultGroups: {
+ children: [
+ {
+ maxResultCount: 3,
+ availableSpan: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(MAX_RESULTS)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroups_task({
+ testName: "maxResultCount: 3, availableSpan: 1, resultSpan = 3",
+ resultGroups: {
+ children: [
+ {
+ maxResultCount: 3,
+ availableSpan: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 })],
+ expectedResultIndexes: [],
+});
+
+add_resultGroups_task({
+ testName: "availableSpan: 1",
+ resultGroups: {
+ children: [
+ {
+ availableSpan: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(MAX_RESULTS)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroups_task({
+ testName: "availableSpan: 1, resultSpan = 3",
+ resultGroups: {
+ children: [
+ {
+ availableSpan: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 })],
+ expectedResultIndexes: [],
+});
+
+add_resultGroups_task({
+ testName: "availableSpan: 3, resultSpan = 2 and resultSpan = 1",
+ resultGroups: {
+ children: [
+ {
+ availableSpan: 3,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [
+ makeHistoryResults(1)[0],
+ Object.assign(makeHistoryResults(1)[0], { resultSpan: 2 }),
+ makeHistoryResults(1)[0],
+ ],
+ expectedResultIndexes: [0, 1],
+});
+
+add_resultGroups_task({
+ testName: "availableSpan: 3, resultSpan = 1 and resultSpan = 2",
+ resultGroups: {
+ children: [
+ {
+ availableSpan: 3,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [
+ Object.assign(makeHistoryResults(1)[0], { resultSpan: 2 }),
+ makeHistoryResults(1)[0],
+ makeHistoryResults(1)[0],
+ ],
+ expectedResultIndexes: [0, 1],
+});
+
+/**
+ * Adds a single test task.
+ *
+ * @param {object} options
+ * The options for the test
+ * @param {string} options.testName
+ * This name is logged with `info` as the task starts.
+ * @param {object} options.resultGroups
+ * browser.urlbar.resultGroups is set to this value as the task starts.
+ * @param {Array} options.providerResults
+ * Array of result objects that the test provider will add.
+ * @param {Array} options.expectedResultIndexes
+ * Array of indexes in `providerResults` of the expected final results.
+ */
+function add_resultGroups_task({
+ testName,
+ resultGroups,
+ providerResults,
+ expectedResultIndexes,
+}) {
+ let func = async () => {
+ info(`Running resultGroups test: ${testName}`);
+ info(`Setting result groups: ` + JSON.stringify(resultGroups));
+ setResultGroups(resultGroups);
+ let provider = registerBasicTestProvider(providerResults);
+ let context = createContext("foo", { providers: [provider.name] });
+ let controller = UrlbarTestUtils.newMockController();
+ await UrlbarProvidersManager.startQuery(context, controller);
+ UrlbarProvidersManager.unregisterProvider(provider);
+ let expectedResults = expectedResultIndexes.map(i => providerResults[i]);
+ Assert.deepEqual(context.results, expectedResults);
+ setResultGroups(null);
+ };
+ Object.defineProperty(func, "name", { value: testName });
+ add_task(func);
+}
+
+/**
+ * Adds test tasks for each of the keys in `LIMIT_KEYS`.
+ *
+ * @param {object} options
+ * The options for the test
+ * @param {string} options.testName
+ * The name of the test.
+ * @param {object} options.resultGroups
+ * The resultGroups object to set.
+ * @param {Array} options.providerResults
+ * The results to return from the test
+ * @param {Array} options.expectedResultIndexes
+ * Indexes of the expected results within {@link providerResults}
+ */
+function add_resultGroupsLimit_tasks({
+ testName,
+ resultGroups,
+ providerResults,
+ expectedResultIndexes,
+}) {
+ for (let key of LIMIT_KEYS) {
+ add_resultGroups_task({
+ testName: `${testName} (limit: ${key})`,
+ resultGroups: replaceLimitWithKey(resultGroups, key),
+ providerResults,
+ expectedResultIndexes,
+ });
+ }
+}
+
+function replaceLimitWithKey(group, key) {
+ group = { ...group };
+ if ("limit" in group) {
+ group[key] = group.limit;
+ delete group.limit;
+ }
+ for (let i = 0; i < group.children?.length; i++) {
+ group.children[i] = replaceLimitWithKey(group.children[i], key);
+ }
+ return group;
+}
+
+function makeHistoryResults(count) {
+ let results = [];
+ for (let i = 0; i < count; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://example.com/" + i }
+ )
+ );
+ }
+ return results;
+}
+
+function makeRemoteSuggestionResults(count) {
+ let results = [];
+ for (let i = 0; i < count; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "test",
+ query: "test",
+ suggestion: "test " + i,
+ lowerCaseSuggestion: "test " + i,
+ }
+ )
+ );
+ }
+ return results;
+}
+
+function makeFormHistoryResults(count) {
+ let results = [];
+ for (let i = 0; i < count; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ {
+ engine: "test",
+ suggestion: "test " + i,
+ lowerCaseSuggestion: "test " + i,
+ }
+ )
+ );
+ }
+ return results;
+}
+
+function makeIndexRange(startIndex, count) {
+ let indexes = [];
+ for (let i = startIndex; i < startIndex + count; i++) {
+ indexes.push(i);
+ }
+ return indexes;
+}
+
+function setResultGroups(resultGroups) {
+ sandbox.restore();
+ if (resultGroups) {
+ sandbox.stub(UrlbarPrefs, "resultGroups").get(() => resultGroups);
+ }
+}
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..8135566547
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_engine_host.js
@@ -0,0 +1,98 @@
+/* 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 SearchTestUtils.installSearchExtension({
+ name: "MySearchEngine",
+ search_url: "https://my.search.com/",
+ });
+ engine = Services.search.getEngineByName("MySearchEngine");
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ // 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ ok(
+ (await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: 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/",
+ fallbackTitle: "my.search.com",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: engine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ makeVisitResult(context, {
+ uri: "http://my.search.com/samplepage/",
+ title: "test visit for http://my.search.com/samplepage/",
+ providerName: "Places",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+ Services.prefs.clearUserPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft"
+ );
+});
diff --git a/browser/components/urlbar/tests/unit/test_search_engine_restyle.js b/browser/components/urlbar/tests/unit/test_search_engine_restyle.js
new file mode 100644
index 0000000000..5bfdd66416
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_engine_restyle.js
@@ -0,0 +1,124 @@
+/* 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/. */
+
+testEngine_setup();
+
+const engineDomain = "s.example.com";
+add_task(async function setup() {
+ Services.prefs.setBoolPref("browser.urlbar.restyleSearches", true);
+ await SearchTestUtils.installSearchExtension({
+ name: "MozSearch",
+ search_url: `https://${engineDomain}/search`,
+ });
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.restyleSearches");
+ });
+});
+
+add_task(async function test_searchEngine() {
+ let uri = Services.io.newURI(`https://${engineDomain}/search?q=Terms`);
+ await PlacesTestUtils.addVisits({
+ uri,
+ title: "Terms - SearchEngine Search",
+ });
+
+ info("Past search terms should be styled.");
+ let context = createContext("term", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ engineName: "MozSearch",
+ suggestion: "Terms",
+ }),
+ ],
+ });
+
+ info(
+ "Searching for a superset of the search string in history should not restyle."
+ );
+ context = createContext("Terms Foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Bookmarked past searches should not be restyled");
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri,
+ title: "Terms - SearchEngine Search",
+ });
+
+ context = createContext("term", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri.spec,
+ title: "Terms - SearchEngine Search",
+ }),
+ ],
+ });
+
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ info("Past search terms should not be styled if restyling is disabled");
+ Services.prefs.setBoolPref("browser.urlbar.restyleSearches", false);
+ context = createContext("term", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri.spec,
+ title: "Terms - SearchEngine Search",
+ }),
+ ],
+ });
+ Services.prefs.setBoolPref("browser.urlbar.restyleSearches", true);
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_extraneousParameters() {
+ info("SERPs in history with extraneous parameters should not be restyled.");
+ let uri = Services.io.newURI(
+ `https://${engineDomain}/search?q=Terms&p=2&type=img`
+ );
+ await PlacesTestUtils.addVisits({
+ uri,
+ title: "Terms - SearchEngine Search",
+ });
+
+ let context = createContext("term", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri.spec,
+ title: "Terms - SearchEngine Search",
+ }),
+ ],
+ });
+});
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..1a49ff2e7d
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_suggestions.js
@@ -0,0 +1,2078 @@
+/* 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 lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+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 TAB_TO_SEARCH_PREF = "browser.urlbar.suggest.engines";
+const TRENDING_PREF = "browser.urlbar.trending.featureGate";
+const QUICKACTIONS_PREF = "browser.urlbar.suggest.quickactions";
+const MAX_RICH_RESULTS_PREF = "browser.urlbar.maxRichResults";
+const MAX_FORM_HISTORY_PREF = "browser.urlbar.maxHistoricalSearchSuggestions";
+const SHOW_SEARCH_SUGGESTIONS_FIRST_PREF =
+ "browser.urlbar.showSearchSuggestionsFirst";
+const SEARCH_STRING = "hello";
+
+const MAX_RESULTS = Services.prefs.getIntPref(MAX_RICH_RESULTS_PREF, 10);
+
+var suggestionsFn;
+var previousSuggestionsFn;
+let port;
+let sandbox;
+
+/**
+ * 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();
+ sandbox.restore();
+}
+
+async function cleanUpSuggestions() {
+ await cleanup();
+ if (previousSuggestionsFn) {
+ suggestionsFn = previousSuggestionsFn;
+ previousSuggestionsFn = null;
+ }
+}
+
+function makeFormHistoryResults(context, count) {
+ let results = [];
+ for (let i = 0; i < count; i++) {
+ results.push(
+ makeFormHistoryResult(context, {
+ suggestion: `${SEARCH_STRING} world Form History ${i}`,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ })
+ );
+ }
+ return results;
+}
+
+function makeRemoteSuggestionResults(
+ context,
+ { suggestionPrefix = SEARCH_STRING, query = undefined } = {}
+) {
+ // The suggestions function in `setup` returns:
+ // [searchString, searchString + "foo", searchString + "bar"]
+ // But when the heuristic is a search result, the muxer discards suggestion
+ // results that match the search string, and therefore we expect only two
+ // remote suggestion results, the "foo" and "bar" ones.
+ return [
+ makeSearchResult(context, {
+ query,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: suggestionPrefix + " foo",
+ }),
+ makeSearchResult(context, {
+ query,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: suggestionPrefix + " bar",
+ }),
+ ];
+}
+
+function setResultGroups(groups) {
+ sandbox.restore();
+ sandbox.stub(UrlbarPrefs, "resultGroups").get(() => {
+ return {
+ children: [
+ // heuristic
+ {
+ maxResultCount: 1,
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TOKEN_ALIAS_ENGINE },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK },
+ ],
+ },
+ // extensions using the omnibox API
+ {
+ group: UrlbarUtils.RESULT_GROUP.OMNIBOX,
+ },
+ ...groups,
+ ],
+ };
+ });
+}
+
+add_task(async function setup() {
+ sandbox = lazy.sinon.createSandbox();
+
+ let engine = await addTestSuggestionsEngine(searchStr => {
+ return suggestionsFn(searchStr);
+ });
+ port = engine.getSubmission("").uri.port;
+
+ 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,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF);
+ Services.prefs.clearUserPref(TRENDING_PREF);
+ Services.prefs.clearUserPref(QUICKACTIONS_PREF);
+ Services.prefs.clearUserPref(TAB_TO_SEARCH_PREF);
+ sandbox.restore();
+ });
+ Services.search.setDefault(engine, Ci.nsISearchService.CHANGE_REASON_UNKNOWN);
+ Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false);
+ Services.prefs.setBoolPref(TRENDING_PREF, false);
+ Services.prefs.setBoolPref(QUICKACTIONS_PREF, false);
+ // Tab-to-search engines can introduce unexpected results, espescially because
+ // they depend on real en-US engines.
+ Services.prefs.setBoolPref(TAB_TO_SEARCH_PREF, false);
+
+ // Add MAX_RESULTS form history.
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ let entries = makeFormHistoryResults(context, MAX_RESULTS).map(r => ({
+ value: r.payload.suggestion,
+ source: SUGGESTIONS_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: SUGGESTIONS_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: SUGGESTIONS_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: SUGGESTIONS_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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(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: SUGGESTIONS_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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "baz " + SEARCH_STRING,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "quux " + SEARCH_STRING,
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function remoteSuggestionsDupeSearchString() {
+ Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 0);
+
+ // Return remote suggestions with the trimmed search string, the uppercased
+ // search string, and the search string with a trailing space, plus the usual
+ // "foo" and "bar" suggestions.
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["foo", "bar"];
+ return [searchStr.trim(), searchStr.toUpperCase(), searchStr + " "].concat(
+ suffixes.map(s => searchStr + " " + s)
+ );
+ });
+
+ // Do a search with a trailing space. All the variations of the search string
+ // with regard to spaces and case should be discarded from the remote
+ // suggestions, leaving only the usual "foo" and "bar" suggestions.
+ let query = SEARCH_STRING + " ";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeRemoteSuggestionResults(context),
+ ],
+ });
+
+ await cleanUpSuggestions();
+ Services.prefs.clearUserPref(MAX_FORM_HISTORY_PREF);
+});
+
+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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "aaa",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 5),
+ ...makeRemoteSuggestionResults(context),
+ 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`,
+ }),
+ ],
+ });
+
+ // 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: SUGGESTIONS_ENGINE_NAME,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ query: SEARCH_STRING,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(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: SUGGESTIONS_ENGINE_NAME,
+ query: "",
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 1),
+ ],
+ });
+
+ // Also if followed by multiple spaces.
+ context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH} `, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ query: "",
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 1),
+ ],
+ });
+
+ // 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: SUGGESTIONS_ENGINE_NAME,
+ query: "h",
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(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: SUGGESTIONS_ENGINE_NAME,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ query: "h",
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function mixup_frecency() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+
+ // At most, we should have 22 results in this subtest. We set this to 30 to
+ // make we're not cutting off any results and we are actually getting 22.
+ Services.prefs.setIntPref(MAX_RICH_RESULTS_PREF, 30);
+
+ // 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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS),
+ ...makeRemoteSuggestionResults(context),
+ 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`,
+ }),
+ 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 mixup.
+ setResultGroups([
+ // 1 suggestion
+ {
+ maxResultCount: 1,
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ ],
+ },
+ // 5 general
+ {
+ maxResultCount: 5,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // 1 suggestion
+ {
+ maxResultCount: 1,
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ ],
+ },
+ // remaining general
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ // remaining suggestions
+ { group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ ]);
+
+ // 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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, 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`,
+ }),
+ ...makeFormHistoryResults(context, 2).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`,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS).slice(2),
+ ...makeRemoteSuggestionResults(context),
+ ],
+ });
+
+ 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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(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}/`,
+ fallbackTitle: `http://${SEARCH_STRING}/`,
+ iconUri: "",
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 2),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: false,
+ }),
+ ],
+ });
+
+ // 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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(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}/`,
+ fallbackTitle: `http://${SEARCH_STRING}/`,
+ iconUri: "",
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 2),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: false,
+ }),
+ ],
+ });
+
+ context = createContext("somethingelse", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://somethingelse/",
+ fallbackTitle: "http://somethingelse/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(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/",
+ fallbackTitle: "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/",
+ fallbackTitle: "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/",
+ fallbackTitle: "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",
+ fallbackTitle: "data:text/plain,Content",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("a", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_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,
+ fallbackTitle: `http://${query}/`,
+ uri: `http://${query}/`,
+ iconUri: "",
+ heuristic: true,
+ }),
+ makeSearchResult(context, { query, engineName: SUGGESTIONS_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: SUGGESTIONS_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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: query,
+ }),
+ ],
+ });
+ }
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function avoid_remote_url_suggestions_1() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ 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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: `${query}.com`,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: `${query}. com`,
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+ await UrlbarTestUtils.formHistory.remove([`${query}.com`]);
+});
+
+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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "htted",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "htteds",
+ }),
+ ],
+ });
+
+ context = createContext("ftp", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "ftped",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "ftpeds",
+ }),
+ ],
+ });
+
+ context = createContext("http", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "httped",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "httpeds",
+ }),
+ ],
+ });
+
+ context = createContext("http:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("https", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "httpsed",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "httpseds",
+ }),
+ ],
+ });
+
+ context = createContext("https:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("httpd", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "httpded",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "httpdeds",
+ }),
+ ],
+ });
+
+ // Check FTP disabled
+ context = createContext("ftp:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("ftp:/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("ftp://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_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/",
+ fallbackTitle: "ftp://test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http:/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("https:/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("https://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_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/",
+ fallbackTitle: "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/",
+ fallbackTitle: "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/",
+ fallbackTitle: "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/",
+ fallbackTitle: "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/",
+ fallbackTitle: "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/",
+ fallbackTitle: "http://www.test.com/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("file", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "fileed",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "fileeds",
+ }),
+ ],
+ });
+
+ context = createContext("file:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_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",
+ fallbackTitle: "file:///Users",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("moz-test://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("moz+test://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("about", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "abouted",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "abouteds",
+ }),
+ ],
+ });
+
+ 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: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 1),
+ ],
+ });
+
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 1),
+ // 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: SUGGESTIONS_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);
+
+ // `maxHistoricalSearchSuggestions` is no longer treated as a max count but as
+ // a boolean: If it's zero, then the user has opted out of form history so we
+ // shouldn't include any at all; if it's non-zero, then we include form
+ // history according to the limits specified in the muxer's result groups.
+
+ // zero => no form history
+ Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 0);
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeRemoteSuggestionResults(context),
+ ],
+ });
+
+ // non-zero => allow form history
+ Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 1);
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(context),
+ ],
+ });
+
+ // non-zero => allow form history
+ Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 2);
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(context),
+ ],
+ });
+
+ Services.prefs.clearUserPref(MAX_FORM_HISTORY_PREF);
+
+ // 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 other form
+ // history results should not be included since they don't match; and both
+ // remote suggestions should be included.
+ let firstSuggestion = makeFormHistoryResults(context, 1)[0].payload
+ .suggestion;
+ context = createContext(firstSuggestion, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: firstSuggestion,
+ }),
+ ],
+ });
+
+ // Do the same search but in uppercase with a trailing space. We should get
+ // the same results, i.e., the form history result dupes the trimmed search
+ // string so it shouldn't be included.
+ let query = firstSuggestion.toUpperCase() + " ";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: firstSuggestion.toUpperCase(),
+ }),
+ ],
+ });
+
+ // Add a form history entry that dupes the first remote suggestion and do a
+ // search that triggers both. The form history should be included but the
+ // remote suggestion should not since it dupes the form history.
+ let suggestionPrefix = "dupe";
+ let dupeSuggestion = makeRemoteSuggestionResults(context, {
+ suggestionPrefix,
+ })[0].payload.suggestion;
+ Assert.ok(dupeSuggestion, "Sanity check: dupeSuggestion is defined");
+ await UrlbarTestUtils.formHistory.add([dupeSuggestion]);
+
+ context = createContext(suggestionPrefix, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: dupeSuggestion,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ...makeRemoteSuggestionResults(context, { suggestionPrefix }).slice(1),
+ ],
+ });
+
+ await UrlbarTestUtils.formHistory.remove([dupeSuggestion]);
+
+ // Add these form history strings to use below.
+ let formHistoryStrings = ["foo", "FOO ", "foobar", "fooquux"];
+ await UrlbarTestUtils.formHistory.add(formHistoryStrings);
+
+ // Search for "foo". "foo" and "FOO " shouldn't be included since they dupe
+ // the heuristic. Both "foobar" and "fooquux" should be included even though
+ // the max form history count is only two and there are four matching form
+ // history results (including the discarded "foo" and "FOO ").
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ ],
+ });
+
+ // 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. The "foo" remote suggestion should not be included since it
+ // dupes the "foo" form history.
+ 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: "test visit for http://foo.example.com/",
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foo",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ ],
+ });
+ await PlacesUtils.history.clear();
+
+ // Add SERPs for "foobar", "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" and "fooBAR " SERPs depend on the result groups, see below.
+ let engine = await Services.search.getDefault();
+ let serpURLs = ["foobar", "fooBAR ", "food"].map(
+ term => UrlbarUtils.getSearchQueryUrl(engine, term)[0]
+ );
+ await PlacesTestUtils.addVisits(serpURLs);
+
+ // First set showSearchSuggestionsFirst = false so that general results appear
+ // before suggestions, which means that the muxer visits the "foobar" and
+ // "fooBAR " SERPs before visiting the "foobar" form history, and so it
+ // doesn't see that these two SERPs dupe the form history. They are therefore
+ // included.
+ Services.prefs.setBoolPref(SHOW_SEARCH_SUGGESTIONS_FIRST_PREF, false);
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: `http://localhost:${port}/search?q=food`,
+ title: `test visit for http://localhost:${port}/search?q=food`,
+ }),
+ makeVisitResult(context, {
+ uri: `http://localhost:${port}/search?q=fooBAR+`,
+ title: `test visit for http://localhost:${port}/search?q=fooBAR+`,
+ }),
+ makeVisitResult(context, {
+ uri: `http://localhost:${port}/search?q=foobar`,
+ title: `test visit for http://localhost:${port}/search?q=foobar`,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ ],
+ });
+
+ // Now clear showSearchSuggestionsFirst so that suggestions appear before
+ // general results. Now the muxer will see that the "foobar" and "fooBAR "
+ // SERPs dupe the "foobar" form history, so it will exclude them.
+ Services.prefs.clearUserPref(SHOW_SEARCH_SUGGESTIONS_FIRST_PREF);
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ makeVisitResult(context, {
+ uri: `http://localhost:${port}/search?q=food`,
+ title: `test visit for http://localhost:${port}/search?q=food`,
+ }),
+ ],
+ });
+
+ await UrlbarTestUtils.formHistory.remove(formHistoryStrings);
+
+ await cleanUpSuggestions();
+ await PlacesUtils.history.clear();
+});
+
+// When the heuristic is hidden, search results that match the heuristic should
+// be included and not deduped.
+add_task(async function hideHeuristic() {
+ UrlbarPrefs.set("experimental.hideHeuristic", true);
+ UrlbarPrefs.set("browser.search.suggest.enabled", true);
+ UrlbarPrefs.set("suggest.searches", true);
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ makeSearchResult(context, {
+ query: SEARCH_STRING,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: SEARCH_STRING,
+ }),
+ ...makeRemoteSuggestionResults(context),
+ ],
+ });
+ await cleanUpSuggestions();
+ UrlbarPrefs.clear("experimental.hideHeuristic");
+});
+
+// When the heuristic is hidden, form history results that match the heuristic
+// should be included and not deduped.
+add_task(async function hideHeuristic_formHistory() {
+ UrlbarPrefs.set("experimental.hideHeuristic", true);
+ UrlbarPrefs.set("browser.search.suggest.enabled", true);
+ UrlbarPrefs.set("suggest.searches", true);
+
+ // Search for exactly the suggestion of the first form history result.
+ // Expected results:
+ //
+ // * First form history should be included even though it dupes the heuristic
+ // * Other form history should not be included because they don't match the
+ // search string
+ // * The first remote suggestion that just echoes the search string should not
+ // be included because it dupes the first form history
+ // * The remaining remote suggestions should be included because they don't
+ // dupe anything
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ let firstFormHistory = makeFormHistoryResults(context, 1)[0];
+ context = createContext(firstFormHistory.payload.suggestion, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ firstFormHistory,
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: firstFormHistory.payload.suggestion,
+ }),
+ ],
+ });
+
+ // Add these form history strings to use below.
+ let formHistoryStrings = ["foo", "FOO ", "foobar", "fooquux"];
+ await UrlbarTestUtils.formHistory.add(formHistoryStrings);
+
+ // Search for "foo". Expected results:
+ //
+ // * "foo" form history should be included even though it dupes the heuristic
+ // * "FOO " form history should not be included because it dupes the "foo"
+ // form history
+ // * "foobar" and "fooqux" form history should be included because they don't
+ // dupe anything
+ // * "foo" remote suggestion should not be included because it dupes the "foo"
+ // form history
+ // * "foo foo" and "foo bar" remote suggestions should be included because
+ // they don't dupe anything
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foo",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ ],
+ });
+
+ // Add SERPs for "foo" and "food", and search for "foo". Expected results:
+ //
+ // * "foo" form history should be included even though it dupes the heuristic
+ // * "foobar" and "fooqux" form history should be included because they don't
+ // dupe anything
+ // * "foo" SERP depends on `showSearchSuggestionsFirst`, see below
+ // * "food" SERP should be include because it doesn't dupe anything
+ // * "foo" remote suggestion should not be included because it dupes the "foo"
+ // form history
+ // * "foo foo" and "foo bar" remote suggestions should be included because
+ // they don't dupe anything
+ let engine = await Services.search.getDefault();
+ let serpURLs = ["foo", "food"].map(
+ term => UrlbarUtils.getSearchQueryUrl(engine, term)[0]
+ );
+ await PlacesTestUtils.addVisits(serpURLs);
+
+ // With `showSearchSuggestionsFirst = false` so that general results appear
+ // before suggestions, the muxer visits the "foo" (and "food") SERPs before
+ // visiting the "foo" form history, and so it doesn't see that the "foo" SERP
+ // dupes the form history. The SERP is therefore included.
+ UrlbarPrefs.set("showSearchSuggestionsFirst", false);
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: `http://localhost:${port}/search?q=food`,
+ title: `test visit for http://localhost:${port}/search?q=food`,
+ }),
+ makeVisitResult(context, {
+ uri: `http://localhost:${port}/search?q=foo`,
+ title: `test visit for http://localhost:${port}/search?q=foo`,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foo",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ ],
+ });
+
+ // Now clear `showSearchSuggestionsFirst` so that suggestions appear before
+ // general results. Now the muxer will see that the "foo" SERP dupes the "foo"
+ // form history, so it will exclude it.
+ UrlbarPrefs.clear("showSearchSuggestionsFirst");
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foo",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ makeVisitResult(context, {
+ uri: `http://localhost:${port}/search?q=food`,
+ title: `test visit for http://localhost:${port}/search?q=food`,
+ }),
+ ],
+ });
+
+ await UrlbarTestUtils.formHistory.remove(formHistoryStrings);
+
+ await cleanUpSuggestions();
+ UrlbarPrefs.clear("experimental.hideHeuristic");
+});
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..1f70a421fa
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js
@@ -0,0 +1,364 @@
+/* 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 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;
+let port;
+
+add_task(async function setup() {
+ engine = await addTestSuggestionsEngine();
+ port = engine.getSubmission("").uri.port;
+
+ // 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.
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: DEFAULT_ENGINE_NAME,
+ search_url: "https://my.search.com/",
+ },
+ { setAsDefault: true }
+ );
+
+ // 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:${port}/search?q=`,
+ 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:${port}/search?q=`,
+ 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:${port}/search?q=`,
+ 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..da43463a69
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_suggestions_tail.js
@@ -0,0 +1,379 @@
+/* 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 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() {
+ 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,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF);
+ Services.prefs.clearUserPref(TAIL_SUGGESTIONS_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ });
+ Services.search.setDefault(engine, Ci.nsISearchService.CHANGE_REASON_UNKNOWN);
+ 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, Ci.nsISearchService.CHANGE_REASON_UNKNOWN);
+
+ const query = "hello world";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: query + " foo",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: query + " bar",
+ }),
+ ],
+ });
+
+ Services.search.setDefault(
+ tailEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ 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: TAIL_SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ suggestion: query + "oronto",
+ tail: "toronto",
+ }),
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_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: TAIL_SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_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: TAIL_SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_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",
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ // 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: TAIL_SUGGESTIONS_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: TAIL_SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ suggestion: tQuery + "oronto",
+ tail: "toronto",
+ }),
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_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: TAIL_SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ engineName: TAIL_SUGGESTIONS_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: TAIL_SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_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: TAIL_SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ Services.prefs.setBoolPref(TAIL_SUGGESTIONS_PREF, oldPrefValue);
+ await cleanUpSuggestions();
+});
diff --git a/browser/components/urlbar/tests/unit/test_special_search.js b/browser/components/urlbar/tests/unit/test_special_search.js
new file mode 100644
index 0000000000..863196909a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_special_search.js
@@ -0,0 +1,543 @@
+/* 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 bug 395161 that allows special searches that restrict results to
+ * history/bookmark/tagged items and title/url matches.
+ *
+ * Test 485122 by making sure results don't have tags when restricting result
+ * to just history either by default behavior or dynamic query restrict.
+ */
+
+testEngine_setup();
+
+function setSuggestPrefsToFalse() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+}
+
+const TRANSITION_TYPED = PlacesUtils.history.TRANSITION_TYPED;
+
+add_task(async function test_special_searches() {
+ let uri1 = Services.io.newURI("http://url/");
+ let uri2 = Services.io.newURI("http://url/2");
+ let uri3 = Services.io.newURI("http://foo.bar/");
+ let uri4 = Services.io.newURI("http://foo.bar/2");
+ let uri5 = Services.io.newURI("http://url/star");
+ let uri6 = Services.io.newURI("http://url/star/2");
+ let uri7 = Services.io.newURI("http://foo.bar/star");
+ let uri8 = Services.io.newURI("http://foo.bar/star/2");
+ let uri9 = Services.io.newURI("http://url/tag");
+ let uri10 = Services.io.newURI("http://url/tag/2");
+ let uri11 = Services.io.newURI("http://foo.bar/tag");
+ let uri12 = Services.io.newURI("http://foo.bar/tag/2");
+ await PlacesTestUtils.addVisits([
+ { uri: uri11, title: "title", transition: TRANSITION_TYPED },
+ { uri: uri6, title: "foo.bar" },
+ { uri: uri4, title: "foo.bar", transition: TRANSITION_TYPED },
+ { uri: uri3, title: "title" },
+ { uri: uri2, title: "foo.bar" },
+ { uri: uri1, title: "title", transition: TRANSITION_TYPED },
+ ]);
+
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri12,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri11,
+ title: "title",
+ tags: ["foo.bar"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri10,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri9,
+ title: "title",
+ tags: ["foo.bar"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({ uri: uri8, title: "foo.bar" });
+ await PlacesTestUtils.addBookmarkWithDetails({ uri: uri7, title: "title" });
+ await PlacesTestUtils.addBookmarkWithDetails({ uri: uri6, title: "foo.bar" });
+ await PlacesTestUtils.addBookmarkWithDetails({ uri: uri5, title: "title" });
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ // Order of frecency when not restricting, descending:
+ // uri11
+ // uri1
+ // uri4
+ // uri6
+ // uri5
+ // uri7
+ // uri8
+ // uri9
+ // uri10
+ // uri12
+ // uri2
+ // uri3
+
+ // Test restricting searches.
+
+ info("History restrict");
+ let context = createContext(UrlbarTokenizer.RESTRICT.HISTORY, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri11.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri1.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ ],
+ });
+
+ info("Star restrict");
+ context = createContext(UrlbarTokenizer.RESTRICT.BOOKMARK, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri11.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, { uri: uri5.spec, title: "title" }),
+ makeBookmarkResult(context, { uri: uri7.spec, title: "title" }),
+ makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, {
+ uri: uri9.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri10.spec,
+ title: "foo.bar",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri12.spec,
+ title: "foo.bar",
+ }),
+ ],
+ });
+
+ info("Tag restrict");
+ context = createContext(UrlbarTokenizer.RESTRICT.TAG, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri11.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri9.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri10.spec,
+ title: "foo.bar",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri12.spec,
+ title: "foo.bar",
+ }),
+ ],
+ });
+
+ info("Special as first word");
+ context = createContext(`${UrlbarTokenizer.RESTRICT.HISTORY} foo bar`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query: "foo bar",
+ alias: UrlbarTokenizer.RESTRICT.HISTORY,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri11.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ ],
+ });
+
+ info("Special as last word");
+ context = createContext(`foo bar ${UrlbarTokenizer.RESTRICT.HISTORY}`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri11.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ ],
+ });
+
+ // Test restricting and matching searches with a term.
+
+ info(`foo ${UrlbarTokenizer.RESTRICT.HISTORY} -> history`);
+ context = createContext(`foo ${UrlbarTokenizer.RESTRICT.HISTORY}`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri11.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ ],
+ });
+
+ info(`foo ${UrlbarTokenizer.RESTRICT.BOOKMARK} -> is star`);
+ context = createContext(`foo ${UrlbarTokenizer.RESTRICT.BOOKMARK}`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri11.spec,
+ title: "title",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, { uri: uri7.spec, title: "title" }),
+ makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, {
+ uri: uri9.spec,
+ title: "title",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri10.spec,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri12.spec,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ }),
+ ],
+ });
+
+ info(`foo ${UrlbarTokenizer.RESTRICT.TITLE} -> in title`);
+ context = createContext(`foo ${UrlbarTokenizer.RESTRICT.TITLE}`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri11.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri9.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri10.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri12.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }),
+ ],
+ });
+
+ info(`foo ${UrlbarTokenizer.RESTRICT.URL} -> in url`);
+ context = createContext(`foo ${UrlbarTokenizer.RESTRICT.URL}`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri11.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, { uri: uri7.spec, title: "title" }),
+ makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri12.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ ],
+ });
+
+ info(`foo ${UrlbarTokenizer.RESTRICT.TAG} -> is tag`);
+ context = createContext(`foo ${UrlbarTokenizer.RESTRICT.TAG}`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri11.spec,
+ title: "title",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri9.spec,
+ title: "title",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri10.spec,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri12.spec,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ }),
+ ],
+ });
+
+ // Test conflicting restrictions.
+
+ info(
+ `conflict ${UrlbarTokenizer.RESTRICT.TITLE} ${UrlbarTokenizer.RESTRICT.URL} -> url wins`
+ );
+ await PlacesTestUtils.addVisits([
+ {
+ uri: `http://conflict.com/${UrlbarTokenizer.RESTRICT.TITLE}`,
+ title: "test",
+ },
+ {
+ uri: "http://conflict.com/",
+ title: `test${UrlbarTokenizer.RESTRICT.TITLE}`,
+ },
+ ]);
+ context = createContext(
+ `conflict ${UrlbarTokenizer.RESTRICT.TITLE} ${UrlbarTokenizer.RESTRICT.URL}`,
+ { isPrivate: false }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: `http://conflict.com/${UrlbarTokenizer.RESTRICT.TITLE}`,
+ title: "test",
+ }),
+ ],
+ });
+
+ info(
+ `conflict ${UrlbarTokenizer.RESTRICT.HISTORY} ${UrlbarTokenizer.RESTRICT.BOOKMARK} -> bookmark wins`
+ );
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://bookmark.conflict.com/",
+ title: `conflict ${UrlbarTokenizer.RESTRICT.HISTORY}`,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ context = createContext(
+ `conflict ${UrlbarTokenizer.RESTRICT.HISTORY} ${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ { isPrivate: false }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://bookmark.conflict.com/",
+ title: `conflict ${UrlbarTokenizer.RESTRICT.HISTORY}`,
+ }),
+ ],
+ });
+
+ info(
+ `conflict ${UrlbarTokenizer.RESTRICT.BOOKMARK} ${UrlbarTokenizer.RESTRICT.TAG} -> tag wins`
+ );
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://tag.conflict.com/",
+ title: `conflict ${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ tags: ["one"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://nontag.conflict.com/",
+ title: `conflict ${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ context = createContext(
+ `conflict ${UrlbarTokenizer.RESTRICT.BOOKMARK} ${UrlbarTokenizer.RESTRICT.TAG}`,
+ { isPrivate: false }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag.conflict.com/",
+ title: `conflict ${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ }),
+ ],
+ });
+
+ // Disable autoFill for the next tests, see test_autoFill_default_behavior.js
+ // for specific tests.
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+
+ // Test default usage by setting certain browser.urlbar.suggest.* prefs
+ info("foo -> default history");
+ setSuggestPrefsToFalse();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri11.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ ],
+ });
+
+ info("foo -> default history, is star");
+ setSuggestPrefsToFalse();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ // The purpose of this test is to verify what is being sent by ProviderPlaces.
+ // It will send 10 results, but the heuristic result pushes the last result
+ // out of the panel. We set maxRichResults to a high value to test the full
+ // output of ProviderPlaces.
+ Services.prefs.setIntPref("browser.urlbar.maxRichResults", 20);
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri11.spec,
+ title: "title",
+ tags: ["foo.bar"],
+ }),
+ makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, { uri: uri7.spec, title: "title" }),
+ makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, {
+ uri: uri9.spec,
+ title: "title",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri10.spec,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri12.spec,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ }),
+ makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ ],
+ });
+ Services.prefs.clearUserPref("browser.urlbar.maxRichResults");
+
+ info("foo -> is star");
+ setSuggestPrefsToFalse();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri11.spec,
+ title: "title",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, { uri: uri7.spec, title: "title" }),
+ makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, {
+ uri: uri9.spec,
+ title: "title",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri10.spec,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri12.spec,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_suggestedIndex.js b/browser/components/urlbar/tests/unit/test_suggestedIndex.js
new file mode 100644
index 0000000000..f2f7fcc0d6
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_suggestedIndex.js
@@ -0,0 +1,562 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests results with suggestedIndex and resultSpan.
+
+"use strict";
+
+const MAX_RESULTS = 10;
+
+add_task(async function suggestedIndex() {
+ // Initialize maxRichResults for sanity.
+ UrlbarPrefs.set("maxRichResults", MAX_RESULTS);
+
+ let tests = [
+ // no result spans > 1
+ {
+ desc: "{ suggestedIndex: 0 }",
+ suggestedIndexes: [0],
+ expected: indexes([10, 1], [0, 9]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }",
+ suggestedIndexes: [1],
+ expected: indexes([0, 1], [10, 1], [1, 8]),
+ },
+ {
+ desc: "{ suggestedIndex: -1 }",
+ suggestedIndexes: [-1],
+ expected: indexes([0, 9], [10, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: -2 }",
+ suggestedIndexes: [-2],
+ expected: indexes([0, 8], [10, 1], [8, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -1 }",
+ suggestedIndexes: [0, -1],
+ expected: indexes([10, 1], [0, 8], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { suggestedIndex: -1 }",
+ suggestedIndexes: [1, -1],
+ expected: indexes([0, 1], [10, 1], [1, 7], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { suggestedIndex: -2 }",
+ suggestedIndexes: [1, -2],
+ expected: indexes([0, 1], [10, 1], [1, 6], [11, 1], [7, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, resultCount < max",
+ suggestedIndexes: [0],
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 5]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, resultCount < max",
+ suggestedIndexes: [1],
+ resultCount: 5,
+ expected: indexes([0, 1], [5, 1], [1, 4]),
+ },
+ {
+ desc: "{ suggestedIndex: -1 }, resultCount < max",
+ suggestedIndexes: [-1],
+ resultCount: 5,
+ expected: indexes([0, 5], [5, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: -2 }, resultCount < max",
+ suggestedIndexes: [-2],
+ resultCount: 5,
+ expected: indexes([0, 4], [5, 1], [4, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -1 }, resultCount < max",
+ suggestedIndexes: [0, -1],
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 5], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { suggestedIndex: -1 }, resultCount < max",
+ suggestedIndexes: [1, -1],
+ resultCount: 5,
+ expected: indexes([0, 1], [5, 1], [1, 4], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -2 }, resultCount < max",
+ suggestedIndexes: [0, -2],
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 4], [6, 1], [4, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { suggestedIndex: -2 }, resultCount < max",
+ suggestedIndexes: [1, -2],
+ resultCount: 5,
+ expected: indexes([0, 1], [5, 1], [1, 3], [6, 1], [4, 1]),
+ },
+
+ // one suggestedIndex with result span > 1
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }",
+ suggestedIndexes: [0],
+ spansByIndex: { 10: 2 },
+ expected: indexes([10, 1], [0, 8]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 3 }",
+ suggestedIndexes: [0],
+ spansByIndex: { 10: 3 },
+ expected: indexes([10, 1], [0, 7]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }",
+ suggestedIndexes: [1],
+ spansByIndex: { 10: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 7]),
+ },
+ {
+ desc: "suggestedIndex: 1, resultSpan:: 3 }",
+ suggestedIndexes: [1],
+ spansByIndex: { 10: 3 },
+ expected: indexes([0, 1], [10, 1], [1, 6]),
+ },
+ {
+ desc: "{ suggestedIndex: -1, resultSpan 2 }",
+ suggestedIndexes: [-1],
+ spansByIndex: { 10: 2 },
+ expected: indexes([0, 8], [10, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: -1, resultSpan: 3 }",
+ suggestedIndexes: [-1],
+ spansByIndex: { 10: 3 },
+ expected: indexes([0, 7], [10, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -1 }",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 10: 2 },
+ expected: indexes([10, 1], [0, 7], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 3 }, { suggestedIndex: -1 }",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 10: 3 },
+ expected: indexes([10, 1], [0, 6], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -1, resultSpan: 2 }",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 11: 2 },
+ expected: indexes([10, 1], [0, 7], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -1, resultSpan: 3 }",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 11: 3 },
+ expected: indexes([10, 1], [0, 6], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -1 }",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 10: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 6], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 3 }, { suggestedIndex: -1 }",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 10: 3 },
+ expected: indexes([0, 1], [10, 1], [1, 5], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { suggestedIndex: -1, resultSpan: 2 }",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 11: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 6], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { suggestedIndex: -1, resultSpan: 3 }",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 11: 3 },
+ expected: indexes([0, 1], [10, 1], [1, 5], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -2 }",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 10: 2 },
+ expected: indexes([10, 1], [0, 6], [11, 1], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 3 }, { suggestedIndex: -2 }",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 10: 3 },
+ expected: indexes([10, 1], [0, 5], [11, 1], [5, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -2 }",
+ suggestedIndexes: [1, -2],
+ spansByIndex: { 10: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 5], [11, 1], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 3 }, { suggestedIndex: -2 }",
+ suggestedIndexes: [1, -2],
+ spansByIndex: { 10: 3 },
+ expected: indexes([0, 1], [10, 1], [1, 4], [11, 1], [5, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -2, resultSpan: 2 }",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 11: 2 },
+ expected: indexes([10, 1], [0, 6], [11, 1], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -2, resultSpan: 3 }",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 11: 3 },
+ expected: indexes([10, 1], [0, 5], [11, 1], [5, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { suggestedIndex: -2, resultSpan: 2 }",
+ suggestedIndexes: [1, -2],
+ spansByIndex: { 11: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 5], [11, 1], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { suggestedIndex: -2, resultSpan: 3 }",
+ suggestedIndexes: [1, -2],
+ spansByIndex: { 11: 3 },
+ expected: indexes([0, 1], [10, 1], [1, 4], [11, 1], [5, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [0],
+ spansByIndex: { 5: 2 },
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 5]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [1],
+ spansByIndex: { 5: 2 },
+ resultCount: 5,
+ expected: indexes([0, 1], [5, 1], [1, 4]),
+ },
+ {
+ desc: "{ suggestedIndex: -1, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [-1],
+ spansByIndex: { 5: 2 },
+ resultCount: 5,
+ expected: indexes([0, 5], [5, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: -2, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [-2],
+ spansByIndex: { 5: 2 },
+ resultCount: 5,
+ expected: indexes([0, 4], [5, 1], [4, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -1 }, resultCount < max",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 5: 2 },
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 5], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -1 }, resultCount < max",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 5: 2 },
+ resultCount: 5,
+ expected: indexes([0, 1], [5, 1], [1, 4], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -2 }, resultCount < max",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 5: 2 },
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 4], [6, 1], [4, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -1, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 6: 2 },
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 5], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -2, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 6: 2 },
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 4], [6, 1], [4, 1]),
+ },
+
+ // two suggestedIndexes with result span > 1
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 2 }",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 10: 2, 11: 2 },
+ expected: indexes([10, 1], [0, 6], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 3 }, { suggestedIndex: -1, resultSpan: 2 }",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 10: 3, 11: 2 },
+ expected: indexes([10, 1], [0, 5], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 3 }",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 10: 2, 11: 3 },
+ expected: indexes([10, 1], [0, 5], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 2 }",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 10: 2, 11: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 5], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 3 }, { suggestedIndex: -1, resultSpan: 2 }",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 10: 3, 11: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 4], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 3 }",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 10: 2, 11: 3 },
+ expected: indexes([0, 1], [10, 1], [1, 4], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -2, resultSpan: 2 }",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 10: 2, 11: 2 },
+ expected: indexes([10, 1], [0, 5], [11, 1], [5, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 3 }, { suggestedIndex: -2, resultSpan: 2 }",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 10: 3, 11: 2 },
+ expected: indexes([10, 1], [0, 4], [11, 1], [4, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -2, resultSpan: 3 }",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 10: 2, 11: 3 },
+ expected: indexes([10, 1], [0, 4], [11, 1], [4, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -2, resultSpan: 2 }",
+ suggestedIndexes: [1, -2],
+ spansByIndex: { 10: 2, 11: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 4], [11, 1], [5, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 3 }, { suggestedIndex: -2, resultSpan: 2 }",
+ suggestedIndexes: [1, -2],
+ spansByIndex: { 10: 3, 11: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 3], [11, 1], [4, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -2, resultSpan: 3 }",
+ suggestedIndexes: [1, -2],
+ spansByIndex: { 10: 2, 11: 3 },
+ expected: indexes([0, 1], [10, 1], [1, 3], [11, 1], [4, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 5: 2, 6: 2 },
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 5], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 5: 2, 6: 2 },
+ resultCount: 5,
+ expected: indexes([0, 1], [5, 1], [1, 4], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -2, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 5: 2, 6: 2 },
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 4], [6, 1], [4, 1]),
+ },
+
+ // one suggestedIndex plus other result with resultSpan > 1
+ {
+ desc: "{ suggestedIndex: 0 }, { resultSpan: 2 } A",
+ suggestedIndexes: [0],
+ spansByIndex: { 0: 2 },
+ expected: indexes([10, 1], [0, 8]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { resultSpan: 2 } B",
+ suggestedIndexes: [0],
+ spansByIndex: { 8: 2 },
+ expected: indexes([10, 1], [0, 8]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { resultSpan: 2 } C",
+ suggestedIndexes: [0],
+ spansByIndex: { 9: 2 },
+ expected: indexes([10, 1], [0, 9]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { resultSpan: 2 } A",
+ suggestedIndexes: [1],
+ spansByIndex: { 0: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 7]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { resultSpan: 2 } B",
+ suggestedIndexes: [1],
+ spansByIndex: { 8: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 7]),
+ },
+ {
+ desc: "{ suggestedIndex: -1 }, { resultSpan: 2 }",
+ suggestedIndexes: [-1],
+ spansByIndex: { 0: 2 },
+ expected: indexes([0, 8], [10, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: -2 }, { resultSpan: 2 }",
+ suggestedIndexes: [-2],
+ spansByIndex: { 0: 2 },
+ expected: indexes([0, 7], [10, 1], [7, 1]),
+ },
+
+ // miscellaneous
+ {
+ desc: "no suggestedIndex, last result has resultSpan = 2",
+ suggestedIndexes: [],
+ spansByIndex: { 9: 2 },
+ expected: indexes([0, 9]),
+ },
+ {
+ desc: "{ suggestedIndex: -1 }, last result has resultSpan = 2",
+ suggestedIndexes: [-1],
+ spansByIndex: { 9: 2 },
+ expected: indexes([0, 9], [10, 1]),
+ },
+ {
+ desc: "no suggestedIndex, index 8 result has resultSpan = 2",
+ suggestedIndexes: [],
+ spansByIndex: { 8: 2 },
+ expected: indexes([0, 9]),
+ },
+ {
+ desc: "{ suggestedIndex: -1 }, index 8 result has resultSpan = 2",
+ suggestedIndexes: [-1],
+ spansByIndex: { 8: 2 },
+ expected: indexes([0, 8], [10, 1]),
+ },
+ ];
+
+ for (let test of tests) {
+ info("Running test: " + JSON.stringify(test));
+ await doSuggestedIndexTest(test);
+ }
+});
+
+/**
+ * Sets up a provider with some results with suggested indexes and result spans,
+ * performs a search, and then checks the results.
+ *
+ * @param {object} options
+ * Options for the test.
+ * @param {Array} options.suggestedIndexes
+ * For each of the indexes in this array, a new result with the given
+ * suggestedIndex will be returned by the provider.
+ * @param {Array} options.expected
+ * The indexes of the expected results within the array of results returned by
+ * the provider.
+ * @param {object} [options.spansByIndex]
+ * Maps indexes within the array of results returned by the provider to result
+ * spans to set on those results.
+ * @param {number} [options.resultCount]
+ * Aside from the results with suggested indexes, this is the number of
+ * results that the provider will return.
+ */
+async function doSuggestedIndexTest({
+ suggestedIndexes,
+ expected,
+ spansByIndex = {},
+ resultCount = MAX_RESULTS,
+}) {
+ // Make resultCount history results.
+ let results = [];
+ for (let i = 0; i < resultCount; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ {
+ url: "http://example.com/" + i,
+ }
+ )
+ );
+ }
+
+ // Make the suggested-index results.
+ for (let suggestedIndex of suggestedIndexes) {
+ results.push(
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ {
+ url: "http://example.com/si " + suggestedIndex,
+ }
+ ),
+ { suggestedIndex }
+ )
+ );
+ }
+
+ // Set resultSpan on each result as indicated by spansByIndex.
+ for (let [index, span] of Object.entries(spansByIndex)) {
+ results[index].resultSpan = span;
+ }
+
+ // Set up the provider, etc.
+ let provider = registerBasicTestProvider(results);
+ let context = createContext(undefined, { providers: [provider.name] });
+ let controller = UrlbarTestUtils.newMockController();
+
+ // Finally, search and check the results.
+ let expectedResults = expected.map(i => results[i]);
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, expectedResults);
+}
+
+/**
+ * Helper that generates an array of indexes. Pass in [index, length] tuples.
+ * Each tuple will produce the indexes starting from `index` to `index + length`
+ * (not including the index at `index + length`).
+ *
+ * Examples:
+ *
+ * indexes([0, 5]) => [0, 1, 2, 3, 4]
+ * indexes([0, 1], [4, 3], [8, 2]) => [0, 4, 5, 6, 8, 9]
+ *
+ * @param {Array} pairs
+ * [index, length] tuples as described above.
+ * @returns {Array}
+ * An array of indexes.
+ */
+function indexes(...pairs) {
+ return pairs.reduce((indexesArray, [start, len]) => {
+ for (let i = start; i < start + len; i++) {
+ indexesArray.push(i);
+ }
+ return indexesArray;
+ }, []);
+}
diff --git a/browser/components/urlbar/tests/unit/test_suggestedIndexRelativeToGroup.js b/browser/components/urlbar/tests/unit/test_suggestedIndexRelativeToGroup.js
new file mode 100644
index 0000000000..ede2f9cc66
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_suggestedIndexRelativeToGroup.js
@@ -0,0 +1,596 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests results with `suggestedIndex` and `isSuggestedIndexRelativeToGroup`.
+
+"use strict";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+const MAX_RESULTS = 10;
+const MAX_RICH_RESULTS_PREF = "browser.urlbar.maxRichResults";
+
+// Default result groups used in the tests below.
+const RESULT_GROUPS = {
+ children: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ ],
+};
+
+let sandbox;
+add_task(function setuo() {
+ sandbox = lazy.sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+});
+
+add_task(async function test() {
+ // Set a specific maxRichResults for sanity's sake.
+ Services.prefs.setIntPref(MAX_RICH_RESULTS_PREF, MAX_RESULTS);
+
+ // Create the default non-suggestedIndex results we'll use for tests that
+ // don't specify `otherResults`.
+ let basicResults = [
+ ...makeHistoryResults(),
+ ...makeFormHistoryResults(),
+ ...makeRemoteSuggestionResults(),
+ ];
+
+ // Test cases follow. Each object in `tests` has the following properties:
+ //
+ // * {string} desc
+ // * {object} suggestedIndexResults
+ // Describes the suggestedIndex results the test provider should return.
+ // Properties:
+ // * {number} suggestedIndex
+ // * {UrlbarUtils.RESULT_GROUP} group
+ // This will force the result to have the given group.
+ // * {array} expected
+ // Describes the expected results the muxer should return, i.e., the search
+ // results. Each object in the array describes a slice of expected results.
+ // The size of the slice is defined by the `count` property.
+ // * {UrlbarUtils.RESULT_GROUP} group
+ // The expected group of the results in the slice.
+ // * {number} count
+ // The number of results in the slice.
+ // * {number} [offset]
+ // Can be used to offset the starting index of the slice in the results.
+ // * {array} [otherResults]
+ // An array of results besides the group-relative suggestedIndex results
+ // that the provider should return. If not specified `basicResults` is used.
+ // * {array} [resultGroups]
+ // The result groups to use. If not specified `RESULT_GROUPS` is used.
+ let tests = [
+ {
+ desc: "First result in GENERAL",
+ suggestedIndexResults: [
+ {
+ suggestedIndex: 0,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ count: 4,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ suggestedIndex: 0,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ count: 2,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ // The muxer will remove the first 4 remote suggestions because they
+ // dupe the earlier form history.
+ offset: 4,
+ count: 3,
+ },
+ ],
+ },
+
+ {
+ desc: "Last result in GENERAL",
+ suggestedIndexResults: [
+ {
+ suggestedIndex: -1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ count: 4,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ count: 2,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ suggestedIndex: -1,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ // The muxer will remove the first 4 remote suggestions because they
+ // dupe the earlier form history.
+ offset: 4,
+ count: 3,
+ },
+ ],
+ },
+
+ {
+ desc: "First result in GENERAL_PARENT",
+ suggestedIndexResults: [
+ {
+ suggestedIndex: 0,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ suggestedIndex: 0,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ count: 3,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ count: 3,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ // The muxer will remove the first 3 remote suggestions because they
+ // dupe the earlier form history.
+ offset: 3,
+ count: 3,
+ },
+ ],
+ },
+
+ {
+ desc: "Last result in GENERAL_PARENT",
+ suggestedIndexResults: [
+ {
+ suggestedIndex: -1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ count: 3,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ count: 3,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ // The muxer will remove the first 3 remote suggestions because they
+ // dupe the earlier form history.
+ offset: 3,
+ count: 3,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ suggestedIndex: -1,
+ },
+ ],
+ },
+
+ {
+ desc: "First and last results in GENERAL",
+ suggestedIndexResults: [
+ {
+ suggestedIndex: 0,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ suggestedIndex: -1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ count: 4,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ suggestedIndex: 0,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ count: 1,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ suggestedIndex: -1,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ // The muxer will remove the first 4 remote suggestions because they
+ // dupe the earlier form history.
+ offset: 4,
+ count: 3,
+ },
+ ],
+ },
+
+ {
+ desc: "First and last results in GENERAL_PARENT",
+ suggestedIndexResults: [
+ {
+ suggestedIndex: 0,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ },
+ {
+ suggestedIndex: -1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ suggestedIndex: 0,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ count: 3,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ count: 3,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ // The muxer will remove the first 3 remote suggestions because they
+ // dupe the earlier form history.
+ offset: 3,
+ count: 2,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ suggestedIndex: -1,
+ },
+ ],
+ },
+
+ {
+ desc: "First result in GENERAL_PARENT, first result in GENERAL",
+ suggestedIndexResults: [
+ {
+ suggestedIndex: 0,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ },
+ {
+ suggestedIndex: 0,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ suggestedIndex: 0,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ count: 3,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ suggestedIndex: 0,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ count: 2,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ // The muxer will remove the first 3 remote suggestions because they
+ // dupe the earlier form history.
+ offset: 3,
+ count: 3,
+ },
+ ],
+ },
+
+ {
+ desc: "Results in sibling group, no other results in same group",
+ otherResults: makeFormHistoryResults(),
+ suggestedIndexResults: [
+ {
+ suggestedIndex: -1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ count: 9,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ suggestedIndex: -1,
+ },
+ ],
+ },
+
+ {
+ desc: "Results in sibling group, no other results in same group, has child group",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ ],
+ },
+ otherResults: makeRemoteSuggestionResults(),
+ suggestedIndexResults: [
+ {
+ suggestedIndex: -1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ count: 9,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ suggestedIndex: -1,
+ },
+ ],
+ },
+
+ {
+ desc: "Complex group nesting with global suggestedIndex with resultSpan",
+ resultGroups: {
+ children: [
+ {
+ maxResultCount: 1,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ ],
+ },
+ ],
+ },
+ otherResults: [
+ // heuristic
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "test",
+ suggestion: "foo",
+ lowerCaseSuggestion: "foo",
+ }
+ ),
+ {
+ heuristic: true,
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ }
+ ),
+ // global suggestedIndex with resultSpan = 2
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "test",
+ }
+ ),
+ {
+ suggestedIndex: 1,
+ resultSpan: 2,
+ }
+ ),
+ // remote suggestions
+ ...makeRemoteSuggestionResults(),
+ ],
+ suggestedIndexResults: [
+ {
+ suggestedIndex: -1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ count: 1,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.SUGGESTED_INDEX,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ count: 1,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ count: 6,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ suggestedIndex: -1,
+ },
+ ],
+ },
+ ];
+
+ let controller = UrlbarTestUtils.newMockController();
+
+ for (let {
+ desc,
+ suggestedIndexResults,
+ expected,
+ resultGroups,
+ otherResults,
+ } of tests) {
+ info(`Running test: ${desc}`);
+
+ setResultGroups(resultGroups || RESULT_GROUPS);
+
+ // Make the array of all results and do a search.
+ let results = (otherResults || basicResults).concat(
+ makeSuggestedIndexResults(suggestedIndexResults)
+ );
+ let provider = registerBasicTestProvider(results);
+ let context = createContext(undefined, { providers: [provider.name] });
+ await UrlbarProvidersManager.startQuery(context, controller);
+
+ // Make the list of expected results.
+ let expectedResults = [];
+ for (let { group, offset, count, suggestedIndex } of expected) {
+ // Find the index in `results` of the expected result.
+ let index = results.findIndex(
+ r =>
+ UrlbarUtils.getResultGroup(r) == group &&
+ r.suggestedIndex === suggestedIndex
+ );
+ Assert.notEqual(
+ index,
+ -1,
+ "Sanity check: Expected result is in `results`"
+ );
+ if (offset) {
+ index += offset;
+ }
+
+ // Extract the expected number of results from `results` and append them
+ // to the expected results array.
+ count = count || 1;
+ expectedResults.push(...results.slice(index, index + count));
+ }
+
+ Assert.deepEqual(context.results, expectedResults);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ }
+});
+
+function makeHistoryResults(count = MAX_RESULTS) {
+ let results = [];
+ for (let i = 0; i < count; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://example.com/" + i }
+ )
+ );
+ }
+ return results;
+}
+
+function makeRemoteSuggestionResults(count = MAX_RESULTS) {
+ let results = [];
+ for (let i = 0; i < count; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "test",
+ query: "test",
+ suggestion: "test " + i,
+ lowerCaseSuggestion: "test " + i,
+ }
+ )
+ );
+ }
+ return results;
+}
+
+function makeFormHistoryResults(count = MAX_RESULTS) {
+ let results = [];
+ for (let i = 0; i < count; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ {
+ engine: "test",
+ suggestion: "test " + i,
+ lowerCaseSuggestion: "test " + i,
+ }
+ )
+ );
+ }
+ return results;
+}
+
+function makeSuggestedIndexResults(objects) {
+ return objects.map(({ suggestedIndex, group }) =>
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ url: "http://example.com/si " + suggestedIndex,
+ }
+ ),
+ {
+ group,
+ suggestedIndex,
+ isSuggestedIndexRelativeToGroup: true,
+ }
+ )
+ );
+}
+
+function setResultGroups(resultGroups) {
+ sandbox.restore();
+ if (resultGroups) {
+ sandbox.stub(UrlbarPrefs, "resultGroups").get(() => resultGroups);
+ }
+}
diff --git a/browser/components/urlbar/tests/unit/test_tab_matches.js b/browser/components/urlbar/tests/unit/test_tab_matches.js
new file mode 100644
index 0000000000..f4061bf3db
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_tab_matches.js
@@ -0,0 +1,354 @@
+/* -*- 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/. */
+
+testEngine_setup();
+
+add_task(async function test_tab_matches() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ });
+
+ let uri1 = Services.io.newURI("http://abc.com/");
+ let uri2 = Services.io.newURI("http://xyz.net/");
+ let uri3 = Services.io.newURI("about:mozilla");
+ let uri4 = Services.io.newURI("data:text/html,test");
+ let uri5 = Services.io.newURI("http://foobar.org");
+ await PlacesTestUtils.addVisits([
+ {
+ uri: uri5,
+ title: "foobar.org - much better than ABC, definitely better than XYZ",
+ },
+ { uri: uri2, title: "xyz.net - we're better than ABC" },
+ { uri: uri1, title: "ABC rocks" },
+ ]);
+ await addOpenPages(uri1, 1);
+ // Pages that cannot be registered in history.
+ await addOpenPages(uri3, 1);
+ await addOpenPages(uri4, 1);
+
+ info("basic tab match");
+ let context = createContext("abc.com", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ }),
+ ],
+ });
+
+ info("three results, one tab match");
+ context = createContext("abc", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "xyz.net - we're better than ABC",
+ }),
+ makeVisitResult(context, {
+ uri: uri5.spec,
+ title: "foobar.org - much better than ABC, definitely better than XYZ",
+ }),
+ ],
+ });
+
+ info("three results, both normal results are tab matches");
+ await addOpenPages(uri2, 1);
+ context = createContext("abc", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://xyz.net/",
+ title: "xyz.net - we're better than ABC",
+ }),
+ makeVisitResult(context, {
+ uri: uri5.spec,
+ title: "foobar.org - much better than ABC, definitely better than XYZ",
+ }),
+ ],
+ });
+
+ info("a container tab is not visible in 'switch to tab'");
+ await addOpenPages(uri5, 1, /* userContextId: */ 3);
+ context = createContext("abc", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://xyz.net/",
+ title: "xyz.net - we're better than ABC",
+ }),
+ makeVisitResult(context, {
+ uri: uri5.spec,
+ title: "foobar.org - much better than ABC, definitely better than XYZ",
+ }),
+ ],
+ });
+
+ info(
+ "a container tab should not see 'switch to tab' for other container tabs"
+ );
+ context = createContext("abc", { isPrivate: false, userContextId: 3 });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "ABC rocks",
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "xyz.net - we're better than ABC",
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://foobar.org/",
+ title: "foobar.org - much better than ABC, definitely better than XYZ",
+ }),
+ ],
+ });
+
+ info("a different container tab should not see any 'switch to tab'");
+ context = createContext("abc", { isPrivate: false, userContextId: 2 });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri1.spec, title: "ABC rocks" }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "xyz.net - we're better than ABC",
+ }),
+ makeVisitResult(context, {
+ uri: uri5.spec,
+ title: "foobar.org - much better than ABC, definitely better than XYZ",
+ }),
+ ],
+ });
+
+ info(
+ "three results, both normal results are tab matches, one has multiple tabs"
+ );
+ await addOpenPages(uri2, 5);
+ context = createContext("abc", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://xyz.net/",
+ title: "xyz.net - we're better than ABC",
+ }),
+ makeVisitResult(context, {
+ uri: uri5.spec,
+ title: "foobar.org - much better than ABC, definitely better than XYZ",
+ }),
+ ],
+ });
+
+ info("three results, no tab matches");
+ await removeOpenPages(uri1, 1);
+ await removeOpenPages(uri2, 6);
+ context = createContext("abc", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "ABC rocks",
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "xyz.net - we're better than ABC",
+ }),
+ makeVisitResult(context, {
+ uri: uri5.spec,
+ title: "foobar.org - much better than ABC, definitely better than XYZ",
+ }),
+ ],
+ });
+
+ info("tab match search with restriction character");
+ await addOpenPages(uri1, 1);
+ context = createContext(UrlbarTokenizer.RESTRICT.OPENPAGE + " abc", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query: "abc",
+ alias: UrlbarTokenizer.RESTRICT.OPENPAGE,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ }),
+ ],
+ });
+
+ info("tab match with not-addable pages");
+ context = createContext("mozilla", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "about:mozilla",
+ title: "about:mozilla",
+ }),
+ ],
+ });
+
+ info("tab match with not-addable pages, no boundary search");
+ context = createContext("ut:mo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "about:mozilla",
+ title: "about:mozilla",
+ }),
+ ],
+ });
+
+ info("tab match with not-addable pages and restriction character");
+ context = createContext(UrlbarTokenizer.RESTRICT.OPENPAGE + " mozilla", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query: "mozilla",
+ alias: UrlbarTokenizer.RESTRICT.OPENPAGE,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "about:mozilla",
+ title: "about:mozilla",
+ }),
+ ],
+ });
+
+ info("tab match with not-addable pages and only restriction character");
+ context = createContext(UrlbarTokenizer.RESTRICT.OPENPAGE, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "data:text/html,test",
+ title: "data:text/html,test",
+ iconUri: UrlbarUtils.ICON.DEFAULT,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "about:mozilla",
+ title: "about:mozilla",
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ }),
+ ],
+ });
+
+ info("tab match should not return tags as part of the title");
+ // Bookmark one of the pages, and add tags to it, to check they don't appear
+ // in the title.
+ let bm = await PlacesUtils.bookmarks.insert({
+ url: uri1,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+ PlacesUtils.tagging.tagURI(uri1, ["test-tag"]);
+ context = createContext("abc.com", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ }),
+ ],
+ });
+ await PlacesUtils.bookmarks.remove(bm);
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_tags_caseInsensitivity.js b/browser/components/urlbar/tests/unit/test_tags_caseInsensitivity.js
new file mode 100644
index 0000000000..f7994326ee
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_tags_caseInsensitivity.js
@@ -0,0 +1,137 @@
+/* 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/. */
+
+testEngine_setup();
+
+/**
+ * Checks the results of a search for `searchTerm`.
+ *
+ * @param {Array} uris
+ * A 2-element array containing [{string} uri, {array} tags}], where `tags`
+ * is a comma-separated list of the tags expected to appear in the search.
+ * @param {string} searchTerm
+ * The term to search for
+ */
+async function ensure_tag_results(uris, searchTerm) {
+ print("Searching for '" + searchTerm + "'");
+ let context = createContext(searchTerm, { isPrivate: false });
+ let urlbarResults = [];
+ for (let [uri, tags] of uris) {
+ urlbarResults.push(
+ makeBookmarkResult(context, {
+ uri,
+ title: "A title",
+ tags,
+ })
+ );
+ }
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...urlbarResults,
+ ],
+ });
+}
+
+const uri1 = "http://site.tld/1";
+const uri2 = "http://site.tld/2";
+const uri3 = "http://site.tld/3";
+const uri4 = "http://site.tld/4";
+const uri5 = "http://site.tld/5";
+const uri6 = "http://site.tld/6";
+
+/**
+ * Properly tags a uri adding it to bookmarks.
+ *
+ * @param {string} url
+ * The URI to tag.
+ * @param {Array} tags
+ * The tags to add.
+ */
+async function tagURI(url, tags) {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url,
+ title: "A title",
+ });
+ PlacesUtils.tagging.tagURI(Services.io.newURI(url), tags);
+}
+
+/**
+ * Test bug #408221
+ */
+add_task(async function test_tags_search_case_insensitivity() {
+ // always search in history + bookmarks, no matter what the default is
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmarks", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmarks");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ await tagURI(uri6, ["muD"]);
+ await tagURI(uri6, ["baR"]);
+ await tagURI(uri5, ["mud"]);
+ await tagURI(uri5, ["bar"]);
+ await tagURI(uri4, ["MUD"]);
+ await tagURI(uri4, ["BAR"]);
+ await tagURI(uri3, ["foO"]);
+ await tagURI(uri2, ["FOO"]);
+ await tagURI(uri1, ["Foo"]);
+
+ await ensure_tag_results(
+ [
+ [uri1, ["Foo"]],
+ [uri2, ["Foo"]],
+ [uri3, ["Foo"]],
+ ],
+ "foo"
+ );
+ await ensure_tag_results(
+ [
+ [uri1, ["Foo"]],
+ [uri2, ["Foo"]],
+ [uri3, ["Foo"]],
+ ],
+ "Foo"
+ );
+ await ensure_tag_results(
+ [
+ [uri1, ["Foo"]],
+ [uri2, ["Foo"]],
+ [uri3, ["Foo"]],
+ ],
+ "foO"
+ );
+ await ensure_tag_results(
+ [
+ [uri4, ["BAR", "MUD"]],
+ [uri5, ["BAR", "MUD"]],
+ [uri6, ["BAR", "MUD"]],
+ ],
+ "bar mud"
+ );
+ await ensure_tag_results(
+ [
+ [uri4, ["BAR", "MUD"]],
+ [uri5, ["BAR", "MUD"]],
+ [uri6, ["BAR", "MUD"]],
+ ],
+ "BAR MUD"
+ );
+ await ensure_tag_results(
+ [
+ [uri4, ["BAR", "MUD"]],
+ [uri5, ["BAR", "MUD"]],
+ [uri6, ["BAR", "MUD"]],
+ ],
+ "Bar Mud"
+ );
+});
diff --git a/browser/components/urlbar/tests/unit/test_tags_extendedUnicode.js b/browser/components/urlbar/tests/unit/test_tags_extendedUnicode.js
new file mode 100644
index 0000000000..596b439be5
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_tags_extendedUnicode.js
@@ -0,0 +1,66 @@
+/* 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 autocomplete for non-English URLs that match the tag bug 416214. Also
+ * test bug 417441 by making sure escaped ascii characters like "+" remain
+ * escaped.
+ *
+ * - add a visit for a page with a non-English URL
+ * - add a tag for the page
+ * - search for the tag
+ * - test number of matches (should be exactly one)
+ * - make sure the url is decoded
+ */
+
+testEngine_setup();
+
+add_task(async function test_tag_match_url() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+ info(
+ "Make sure tag matches return the right url as well as '+' remain escaped"
+ );
+ let uri1 = Services.io.newURI("http://escaped/ユニコード");
+ let uri2 = Services.io.newURI("http://asciiescaped/blocking-firefox3%2B");
+ await PlacesTestUtils.addVisits([
+ { uri: uri1, title: "title" },
+ { uri: uri2, title: "title" },
+ ]);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri1,
+ title: "title",
+ tags: ["superTag"],
+ style: ["bookmark-tag"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri2,
+ title: "title",
+ tags: ["superTag"],
+ style: ["bookmark-tag"],
+ });
+ let context = createContext("superTag", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri2.spec,
+ title: "title",
+ tags: ["superTag"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri1.spec,
+ title: "title",
+ tags: ["superTag"],
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_tags_general.js b/browser/components/urlbar/tests/unit/test_tags_general.js
new file mode 100644
index 0000000000..c2c620c152
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_tags_general.js
@@ -0,0 +1,207 @@
+/* -*- 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/. */
+
+testEngine_setup();
+
+/**
+ * Checks the results of a search for `searchTerm`.
+ *
+ * @param {Array} uris
+ * A 2-element array containing [{string} uri, {array} tags}], where `tags`
+ * is a comma-separated list of the tags expected to appear in the search.
+ * @param {string} searchTerm
+ * The term to search for
+ */
+async function ensure_tag_results(uris, searchTerm) {
+ print("Searching for '" + searchTerm + "'");
+ let context = createContext(searchTerm, { isPrivate: false });
+ let urlbarResults = [];
+ for (let [uri, tags] of uris) {
+ urlbarResults.push(
+ makeBookmarkResult(context, {
+ uri,
+ title: "A title",
+ tags,
+ })
+ );
+ }
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...urlbarResults,
+ ],
+ });
+}
+
+var uri1 = "http://site.tld/1/aaa";
+var uri2 = "http://site.tld/2/bbb";
+var uri3 = "http://site.tld/3/aaa";
+var uri4 = "http://site.tld/4/bbb";
+var uri5 = "http://site.tld/5/aaa";
+var uri6 = "http://site.tld/6/bbb";
+
+var tests = [
+ () =>
+ ensure_tag_results(
+ [
+ [uri1, ["foo"]],
+ [uri4, ["foo bar"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "foo"
+ ),
+ () => ensure_tag_results([[uri1, ["foo"]]], "foo aaa"),
+ () =>
+ ensure_tag_results(
+ [
+ [uri4, ["foo bar"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "foo bbb"
+ ),
+ () =>
+ ensure_tag_results(
+ [
+ [uri2, ["bar"]],
+ [uri4, ["foo bar"]],
+ [uri5, ["bar cheese"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "bar"
+ ),
+ () => ensure_tag_results([[uri5, ["bar cheese"]]], "bar aaa"),
+ () =>
+ ensure_tag_results(
+ [
+ [uri2, ["bar"]],
+ [uri4, ["foo bar"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "bar bbb"
+ ),
+ () =>
+ ensure_tag_results(
+ [
+ [uri3, ["cheese"]],
+ [uri5, ["bar cheese"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "cheese"
+ ),
+ () =>
+ ensure_tag_results(
+ [
+ [uri3, ["cheese"]],
+ [uri5, ["bar cheese"]],
+ ],
+ "chees aaa"
+ ),
+ () => ensure_tag_results([[uri6, ["foo bar cheese"]]], "chees bbb"),
+ () =>
+ ensure_tag_results(
+ [
+ [uri4, ["foo bar"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "fo bar"
+ ),
+ () => ensure_tag_results([], "fo bar aaa"),
+ () =>
+ ensure_tag_results(
+ [
+ [uri4, ["foo bar"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "fo bar bbb"
+ ),
+ () =>
+ ensure_tag_results(
+ [
+ [uri4, ["foo bar"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "ba foo"
+ ),
+ () => ensure_tag_results([], "ba foo aaa"),
+ () =>
+ ensure_tag_results(
+ [
+ [uri4, ["foo bar"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "ba foo bbb"
+ ),
+ () =>
+ ensure_tag_results(
+ [
+ [uri5, ["bar cheese"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "ba chee"
+ ),
+ () => ensure_tag_results([[uri5, ["bar cheese"]]], "ba chee aaa"),
+ () => ensure_tag_results([[uri6, ["foo bar cheese"]]], "ba chee bbb"),
+ () =>
+ ensure_tag_results(
+ [
+ [uri5, ["bar cheese"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "cheese bar"
+ ),
+ () => ensure_tag_results([[uri5, ["bar cheese"]]], "cheese bar aaa"),
+ () => ensure_tag_results([[uri6, ["foo bar cheese"]]], "chees bar bbb"),
+ () => ensure_tag_results([[uri6, ["foo bar cheese"]]], "cheese bar foo"),
+ () => ensure_tag_results([], "foo bar cheese aaa"),
+ () => ensure_tag_results([[uri6, ["foo bar cheese"]]], "foo bar cheese bbb"),
+];
+
+/**
+ * Properly tags a uri adding it to bookmarks.
+ *
+ * @param {string} url
+ * The URI to tag.
+ * @param {Array} tags
+ * The tags to add.
+ */
+async function tagURI(url, tags) {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url,
+ title: "A title",
+ });
+ PlacesUtils.tagging.tagURI(Services.io.newURI(url), tags);
+}
+
+/**
+ * Test history autocomplete
+ */
+add_task(async function test_history_autocomplete_tags() {
+ // always search in history + bookmarks, no matter what the default is
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmarks", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmarks");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ await tagURI(uri6, ["foo bar cheese"]);
+ await tagURI(uri5, ["bar cheese"]);
+ await tagURI(uri4, ["foo bar"]);
+ await tagURI(uri3, ["cheese"]);
+ await tagURI(uri2, ["bar"]);
+ await tagURI(uri1, ["foo"]);
+
+ for (let tagTest of tests) {
+ await tagTest();
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_tags_matchBookmarkTitles.js b/browser/components/urlbar/tests/unit/test_tags_matchBookmarkTitles.js
new file mode 100644
index 0000000000..98d12ebe32
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_tags_matchBookmarkTitles.js
@@ -0,0 +1,42 @@
+/* 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 bug 416211 to make sure results that match the tag show the bookmark
+ * title instead of the page title.
+ */
+
+testEngine_setup();
+
+add_task(async function test_tag_match_has_bookmark_title() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ info("Make sure the tag match gives the bookmark title");
+ let uri = Services.io.newURI("http://theuri/");
+ await PlacesTestUtils.addVisits({ uri, title: "Page title" });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri,
+ title: "Bookmark title",
+ tags: ["superTag"],
+ });
+ let context = createContext("superTag", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri.spec,
+ title: "Bookmark title",
+ tags: ["superTag"],
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_tags_returnedInSearches.js b/browser/components/urlbar/tests/unit/test_tags_returnedInSearches.js
new file mode 100644
index 0000000000..d5f18278fd
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_tags_returnedInSearches.js
@@ -0,0 +1,125 @@
+/* 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 bug 418257 by making sure tags are returned with the title as part of
+ * the "comment" if there are tags even if we didn't match in the tags. They
+ * are separated from the title by a endash.
+ */
+
+testEngine_setup();
+
+add_task(async function test() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ });
+
+ let uri1 = Services.io.newURI("http://page1");
+ let uri2 = Services.io.newURI("http://page2");
+ let uri3 = Services.io.newURI("http://page3");
+ let uri4 = Services.io.newURI("http://page4");
+ await PlacesTestUtils.addVisits([
+ { uri: uri1, title: "tagged" },
+ { uri: uri2, title: "tagged" },
+ { uri: uri3, title: "tagged" },
+ { uri: uri4, title: "tagged" },
+ ]);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri1,
+ title: "tagged",
+ tags: ["tag1"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri2,
+ title: "tagged",
+ tags: ["tag1", "tag2"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri3,
+ title: "tagged",
+ tags: ["tag1", "tag3"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri4,
+ title: "tagged",
+ tags: ["tag1", "tag2", "tag3"],
+ });
+ info("Make sure tags come back in the title when matching tags");
+ let context = createContext("page1 tag", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri1.spec,
+ title: "tagged",
+ tags: ["tag1"],
+ }),
+ ],
+ });
+
+ info("Check tags in title for page2");
+ context = createContext("page2 tag", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri2.spec,
+ title: "tagged",
+ tags: ["tag1", "tag2"],
+ }),
+ ],
+ });
+
+ info("Tags do not appear when not matching the tag");
+ context = createContext("page3", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri3.spec,
+ title: "tagged",
+ tags: [],
+ }),
+ ],
+ });
+
+ info("Extra test just to make sure we match the title");
+ context = createContext("tag2", { isPrivate: true });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri4.spec,
+ title: "tagged",
+ tags: ["tag2"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri2.spec,
+ title: "tagged",
+ tags: ["tag2"],
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
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..835d1a5909
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_tokenizer.js
@@ -0,0 +1,449 @@
+/* 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..5f0b340b53
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_trimming.js
@@ -0,0 +1,171 @@
+/* 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/",
+ fallbackTitle: "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: "test visit for 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/",
+ fallbackTitle: "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: "test visit for 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/",
+ fallbackTitle: "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: "test visit for http://www.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.HISTORY,
+ uri: "https://www.mozilla.org/%E5%95%8A-test",
+ title: "test visit for https://www.mozilla.org/%E5%95%8A-test",
+ iconUri: "page-icon:https://www.mozilla.org/%E5%95%8A-test",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_unitConversion.js b/browser/components/urlbar/tests/unit/test_unitConversion.js
new file mode 100644
index 0000000000..fc0620e7aa
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_unitConversion.js
@@ -0,0 +1,504 @@
+/* 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";
+
+/**
+ * Unit test for unit conversion module.
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderUnitConversion:
+ "resource:///modules/UrlbarProviderUnitConversion.sys.mjs",
+});
+
+const TEST_DATA = [
+ {
+ category: "angle",
+ cases: [
+ { queryString: "1 d to d", expected: "1 deg" },
+ { queryString: "-1 d to d", expected: "-1 deg" },
+ { queryString: "1 d in d", expected: "1 deg" },
+ { queryString: "1 d = d", expected: "1 deg" },
+ { queryString: "1 D=D", expected: "1 deg" },
+ { queryString: "1 d to degree", expected: "1 deg" },
+ { queryString: "2 d to degree", expected: "2 deg" },
+ {
+ queryString: "1 d to radian",
+ expected: `${round(Math.PI / 180)} radian`,
+ },
+ {
+ queryString: "2 d to radian",
+ expected: `${round((Math.PI / 180) * 2)} radian`,
+ },
+ { queryString: "1 d to rad", expected: `${round(Math.PI / 180)} radian` },
+ { queryString: "1 d to r", expected: `${round(Math.PI / 180)} radian` },
+ { queryString: "1 d to gradian", expected: `${round(1 / 0.9)} gradian` },
+ { queryString: "1 d to g", expected: `${round(1 / 0.9)} gradian` },
+ { queryString: "1 d to minute", expected: "60 min" },
+ { queryString: "1 d to min", expected: "60 min" },
+ { queryString: "1 d to m", expected: "60 min" },
+ { queryString: "1 d to second", expected: "3,600 sec" },
+ { queryString: "1 d to sec", expected: "3,600 sec" },
+ { queryString: "1 d to s", expected: "3,600 sec" },
+ { queryString: "1 d to sign", expected: `${round(1 / 30)} sign` },
+ { queryString: "1 d to mil", expected: `${round(1 / 0.05625)} mil` },
+ {
+ queryString: "1 d to revolution",
+ expected: `${round(1 / 360)} revolution`,
+ },
+ { queryString: "1 d to circle", expected: `${round(1 / 360)} circle` },
+ { queryString: "1 d to turn", expected: `${round(1 / 360)} turn` },
+ { queryString: "1 d to quadrant", expected: `${round(1 / 90)} quadrant` },
+ {
+ queryString: "1 d to rightangle",
+ expected: `${round(1 / 90)} rightangle`,
+ },
+ { queryString: "1 d to sextant", expected: `${round(1 / 60)} sextant` },
+ { queryString: "1 degree to d", expected: "1 deg" },
+ { queryString: "1 radian to d", expected: `${round(180 / Math.PI)} deg` },
+ {
+ queryString: "1 r to g",
+ expected: `${round(180 / Math.PI / 0.9)} gradian`,
+ },
+ ],
+ },
+ {
+ category: "force",
+ cases: [
+ { queryString: "1 n to n", expected: "1 newton" },
+ { queryString: "-1 n to n", expected: "-1 newton" },
+ { queryString: "1 n in n", expected: "1 newton" },
+ { queryString: "1 n = n", expected: "1 newton" },
+ { queryString: "1 N=N", expected: "1 newton" },
+ { queryString: "1 n to newton", expected: "1 newton" },
+ { queryString: "1 n to kilonewton", expected: "0.001 kilonewton" },
+ { queryString: "1 n to kn", expected: "0.001 kilonewton" },
+ {
+ queryString: "1 n to gram-force",
+ expected: `${round(101.9716213)} gram-force`,
+ },
+ {
+ queryString: "1 n to gf",
+ expected: `${round(101.9716213)} gram-force`,
+ },
+ {
+ queryString: "1 n to kilogram-force",
+ expected: `${round(0.1019716213)} kilogram-force`,
+ },
+ {
+ queryString: "1 n to kgf",
+ expected: `${round(0.1019716213)} kilogram-force`,
+ },
+ {
+ queryString: "1 n to ton-force",
+ expected: `${round(0.0001019716213)} ton-force`,
+ },
+ {
+ queryString: "1 n to tf",
+ expected: `${round(0.0001019716213)} ton-force`,
+ },
+ {
+ queryString: "1 n to exanewton",
+ expected: `${round(1.0e-18)} exanewton`,
+ },
+ { queryString: "1 n to en", expected: `${round(1.0e-18)} exanewton` },
+ {
+ queryString: "1 n to petanewton",
+ expected: `${round(1.0e-15)} petanewton`,
+ },
+ { queryString: "1 n to PN", expected: `${round(1.0e-15)} petanewton` },
+ { queryString: "1 n to Pn", expected: `${round(1.0e-15)} petanewton` },
+ {
+ queryString: "1 n to teranewton",
+ expected: `${round(1.0e-12)} teranewton`,
+ },
+ { queryString: "1 n to tn", expected: `${round(1.0e-12)} teranewton` },
+ {
+ queryString: "1 n to giganewton",
+ expected: `${round(1.0e-9)} giganewton`,
+ },
+ { queryString: "1 n to gn", expected: `${round(1.0e-9)} giganewton` },
+ { queryString: "1 n to meganewton", expected: "0.000001 meganewton" },
+ { queryString: "1 n to MN", expected: "0.000001 meganewton" },
+ { queryString: "1 n to Mn", expected: "0.000001 meganewton" },
+ { queryString: "1 n to hectonewton", expected: "0.01 hectonewton" },
+ { queryString: "1 n to hn", expected: "0.01 hectonewton" },
+ { queryString: "1 n to dekanewton", expected: "0.1 dekanewton" },
+ { queryString: "1 n to dan", expected: "0.1 dekanewton" },
+ { queryString: "1 n to decinewton", expected: "10 decinewton" },
+ { queryString: "1 n to dn", expected: "10 decinewton" },
+ { queryString: "1 n to centinewton", expected: "100 centinewton" },
+ { queryString: "1 n to cn", expected: "100 centinewton" },
+ { queryString: "1 n to millinewton", expected: "1000 millinewton" },
+ { queryString: "1 n to mn", expected: "1000 millinewton" },
+ { queryString: "1 n to micronewton", expected: "1000000 micronewton" },
+ { queryString: "1 n to µn", expected: "1000000 micronewton" },
+ {
+ queryString: "1 n to nanonewton",
+ expected: "1000000000 nanonewton",
+ },
+ { queryString: "1 n to nn", expected: "1000000000 nanonewton" },
+ {
+ queryString: "1 n to piconewton",
+ expected: "1000000000000 piconewton",
+ },
+ { queryString: "1 n to pn", expected: "1000000000000 piconewton" },
+ {
+ queryString: "1 n to femtonewton",
+ expected: "1000000000000000 femtonewton",
+ },
+ { queryString: "1 n to fn", expected: "1000000000000000 femtonewton" },
+ {
+ queryString: "1 n to attonewton",
+ expected: "1000000000000000000 attonewton",
+ },
+ { queryString: "1 n to an", expected: "1000000000000000000 attonewton" },
+ { queryString: "1 n to dyne", expected: "100000 dyne" },
+ { queryString: "1 n to dyn", expected: "100000 dyne" },
+ { queryString: "1 n to joule/meter", expected: "1 joule/meter" },
+ { queryString: "1 n to j/m", expected: "1 joule/meter" },
+ {
+ queryString: "1 n to joule/centimeter",
+ expected: "100 joule/centimeter",
+ },
+ { queryString: "1 n to j/cm", expected: "100 joule/centimeter" },
+ {
+ queryString: "1 n to ton-force-short",
+ expected: `${round(0.0001124045)} ton-force-short`,
+ },
+ {
+ queryString: "1 n to short",
+ expected: `${round(0.0001124045)} ton-force-short`,
+ },
+ {
+ queryString: "1 n to ton-force-long",
+ expected: `${round(0.0001003611)} ton-force-long`,
+ },
+ {
+ queryString: "1 n to tonf",
+ expected: `${round(0.0001003611)} ton-force-long`,
+ },
+ {
+ queryString: "1 n to kip-force",
+ expected: `${round(0.0002248089)} kip-force`,
+ },
+ {
+ queryString: "1 n to kipf",
+ expected: `${round(0.0002248089)} kip-force`,
+ },
+ {
+ queryString: "1 n to pound-force",
+ expected: `${round(0.2248089431)} pound-force`,
+ },
+ {
+ queryString: "1 n to lbf",
+ expected: `${round(0.2248089431)} pound-force`,
+ },
+ {
+ queryString: "1 n to ounce-force",
+ expected: `${round(3.5969430896)} ounce-force`,
+ },
+ {
+ queryString: "1 n to ozf",
+ expected: `${round(3.5969430896)} ounce-force`,
+ },
+ {
+ queryString: "1 n to poundal",
+ expected: `${round(7.2330138512)} poundal`,
+ },
+ { queryString: "1 n to pdl", expected: `${round(7.2330138512)} poundal` },
+ { queryString: "1 n to pond", expected: `${round(101.9716213)} pond` },
+ { queryString: "1 n to p", expected: `${round(101.9716213)} pond` },
+ {
+ queryString: "1 n to kilopond",
+ expected: `${round(0.1019716213)} kilopond`,
+ },
+ { queryString: "1 n to kp", expected: `${round(0.1019716213)} kilopond` },
+ { queryString: "1 kilonewton to n", expected: "1000 newton" },
+ ],
+ },
+ {
+ category: "length",
+ cases: [
+ { queryString: "1 meter to meter", expected: "1 m" },
+ { queryString: "-1 meter to meter", expected: "-1 m" },
+ { queryString: "1 meter in meter", expected: "1 m" },
+ { queryString: "1 meter = meter", expected: "1 m" },
+ { queryString: "1 METER=METER", expected: "1 m" },
+ { queryString: "1 m to meter", expected: "1 m" },
+ { queryString: "1 m to nanometer", expected: "1000000000 nanometer" },
+ { queryString: "1 m to micrometer", expected: "1000000 micrometer" },
+ { queryString: "1 m to millimeter", expected: "1,000 mm" },
+ { queryString: "1 m to mm", expected: "1,000 mm" },
+ { queryString: "1 m to centimeter", expected: "100 cm" },
+ { queryString: "1 m to cm", expected: "100 cm" },
+ { queryString: "1 m to kilometer", expected: "0.001 km" },
+ { queryString: "1 m to km", expected: "0.001 km" },
+ { queryString: "1 m to mile", expected: `${round(0.0006213689)} mi` },
+ { queryString: "1 m to mi", expected: `${round(0.0006213689)} mi` },
+ { queryString: "1 m to yard", expected: `${round(1.0936132983)} yd` },
+ { queryString: "1 m to yd", expected: `${round(1.0936132983)} yd` },
+ { queryString: "1 m to foot", expected: `${round(3.280839895)} ft` },
+ { queryString: "1 m to ft", expected: `${round(3.280839895)} ft` },
+ { queryString: "1 m to inch", expected: `${round(39.37007874)} in` },
+ { queryString: "1 inch to m", expected: `${round(1 / 39.37007874)} m` },
+ ],
+ },
+ {
+ category: "mass",
+ cases: [
+ { queryString: "1 kg to kg", expected: "1 kg" },
+ { queryString: "-1 kg to kg", expected: "-1 kg" },
+ { queryString: "1 kg in kg", expected: "1 kg" },
+ { queryString: "1 kg = kg", expected: "1 kg" },
+ { queryString: "1 KG=KG", expected: "1 kg" },
+ { queryString: "1 kg to kilogram", expected: "1 kg" },
+ { queryString: "1 kg to gram", expected: "1,000 g" },
+ { queryString: "1 kg to g", expected: "1,000 g" },
+ { queryString: "1 kg to milligram", expected: "1000000 milligram" },
+ { queryString: "1 kg to mg", expected: "1000000 milligram" },
+ { queryString: "1 kg to ton", expected: "0.001 ton" },
+ { queryString: "1 kg to t", expected: "0.001 ton" },
+ {
+ queryString: "1 kg to longton",
+ expected: `${round(0.0009842073)} longton`,
+ },
+ {
+ queryString: "1 kg to l.t.",
+ expected: `${round(0.0009842073)} longton`,
+ },
+ {
+ queryString: "1 kg to l/t",
+ expected: `${round(0.0009842073)} longton`,
+ },
+ {
+ queryString: "1 kg to shortton",
+ expected: `${round(0.0011023122)} shortton`,
+ },
+ {
+ queryString: "1 kg to s.t.",
+ expected: `${round(0.0011023122)} shortton`,
+ },
+ {
+ queryString: "1 kg to s/t",
+ expected: `${round(0.0011023122)} shortton`,
+ },
+ {
+ queryString: "1 kg to pound",
+ expected: `${round(2.2046244202)} lb`,
+ },
+ { queryString: "1 kg to lbs", expected: `${round(2.2046244202)} lb` },
+ {
+ queryString: "1 kg to lb",
+ expected: `${round(2.2046244202)} lb`,
+ },
+ {
+ queryString: "1 kg to ounce",
+ expected: `${round(35.273990723)} oz`,
+ },
+ { queryString: "1 kg to oz", expected: `${round(35.273990723)} oz` },
+ { queryString: "1 kg to carat", expected: "5000 carat" },
+ { queryString: "1 kg to ffd", expected: "5000 ffd" },
+ { queryString: "1 ffd to kg", expected: `${round(1 / 5000)} kg` },
+ ],
+ },
+ {
+ category: "temperature",
+ cases: [
+ { queryString: "0 c to c", expected: "0°C" },
+ { queryString: "0 c in c", expected: "0°C" },
+ { queryString: "0 c = c", expected: "0°C" },
+ { queryString: "0 C=C", expected: "0°C" },
+ { queryString: "0 c to celsius", expected: "0°C" },
+ { queryString: "0 c to kelvin", expected: "273.15 kelvin" },
+ { queryString: "0 c to k", expected: "273.15 kelvin" },
+ { queryString: "10 c to k", expected: "283.15 kelvin" },
+ { queryString: "0 c to fahrenheit", expected: "32°F" },
+ { queryString: "0 c to f", expected: "32°F" },
+ { queryString: "10 c to f", expected: `${round(10 * 1.8 + 32)}°F` },
+ {
+ queryString: "10 f to kelvin",
+ expected: `${round((10 - 32) / 1.8 + 273.15)} kelvin`,
+ },
+ { queryString: "-10 c to f", expected: "14°F" },
+ ],
+ },
+ {
+ category: "timezone",
+ cases: [
+ { queryString: "0 utc to utc", expected: "00:00 UTC" },
+ { queryString: "0 utc in utc", expected: "00:00 UTC" },
+ { queryString: "0 utc = utc", expected: "00:00 UTC" },
+ { queryString: "0 UTC=UTC", expected: "00:00 UTC" },
+ { queryString: "11 pm utc to utc", expected: "11:00 PM UTC" },
+ { queryString: "11 am utc to utc", expected: "11:00 AM UTC" },
+ { queryString: "11:30 utc to utc", expected: "11:30 UTC" },
+ { queryString: "11:30 PM utc to utc", expected: "11:30 PM UTC" },
+ { queryString: "1 utc to idlw", expected: "13:00 IDLW" },
+ { queryString: "1 pm utc to idlw", expected: "1:00 AM IDLW" },
+ { queryString: "1 am utc to idlw", expected: "1:00 PM IDLW" },
+ { queryString: "1 utc to idlw", expected: "13:00 IDLW" },
+ { queryString: "1 PM utc to idlw", expected: "1:00 AM IDLW" },
+ { queryString: "0 utc to nt", expected: "13:00 NT" },
+ { queryString: "0 utc to hst", expected: "14:00 HST" },
+ { queryString: "0 utc to akst", expected: "15:00 AKST" },
+ { queryString: "0 utc to pst", expected: "16:00 PST" },
+ { queryString: "0 utc to akdt", expected: "16:00 AKDT" },
+ { queryString: "0 utc to mst", expected: "17:00 MST" },
+ { queryString: "0 utc to pdt", expected: "17:00 PDT" },
+ { queryString: "0 utc to cst", expected: "18:00 CST" },
+ { queryString: "0 utc to mdt", expected: "18:00 MDT" },
+ { queryString: "0 utc to est", expected: "19:00 EST" },
+ { queryString: "0 utc to cdt", expected: "19:00 CDT" },
+ { queryString: "0 utc to edt", expected: "20:00 EDT" },
+ { queryString: "0 utc to ast", expected: "20:00 AST" },
+ { queryString: "0 utc to guy", expected: "21:00 GUY" },
+ { queryString: "0 utc to adt", expected: "21:00 ADT" },
+ { queryString: "0 utc to at", expected: "22:00 AT" },
+ { queryString: "0 utc to gmt", expected: "00:00 GMT" },
+ { queryString: "0 utc to z", expected: "00:00 Z" },
+ { queryString: "0 utc to wet", expected: "00:00 WET" },
+ { queryString: "0 utc to west", expected: "01:00 WEST" },
+ { queryString: "0 utc to cet", expected: "01:00 CET" },
+ { queryString: "0 utc to bst", expected: "01:00 BST" },
+ { queryString: "0 utc to ist", expected: "01:00 IST" },
+ { queryString: "0 utc to cest", expected: "02:00 CEST" },
+ { queryString: "0 utc to eet", expected: "02:00 EET" },
+ { queryString: "0 utc to eest", expected: "03:00 EEST" },
+ { queryString: "0 utc to msk", expected: "03:00 MSK" },
+ { queryString: "0 utc to msd", expected: "04:00 MSD" },
+ { queryString: "0 utc to zp4", expected: "04:00 ZP4" },
+ { queryString: "0 utc to zp5", expected: "05:00 ZP5" },
+ { queryString: "0 utc to zp6", expected: "06:00 ZP6" },
+ { queryString: "0 utc to wast", expected: "07:00 WAST" },
+ { queryString: "0 utc to awst", expected: "08:00 AWST" },
+ { queryString: "0 utc to wst", expected: "08:00 WST" },
+ { queryString: "0 utc to jst", expected: "09:00 JST" },
+ { queryString: "0 utc to acst", expected: "09:30 ACST" },
+ { queryString: "0 utc to aest", expected: "10:00 AEST" },
+ { queryString: "0 utc to acdt", expected: "10:30 ACDT" },
+ { queryString: "0 utc to aedt", expected: "11:00 AEDT" },
+ { queryString: "0 utc to nzst", expected: "12:00 NZST" },
+ { queryString: "0 utc to idle", expected: "12:00 IDLE" },
+ { queryString: "0 utc to nzd", expected: "13:00 NZD" },
+ { queryString: "9:00 jst to utc", expected: "00:00 UTC" },
+ { queryString: "8:00 jst to utc", expected: "23:00 UTC" },
+ { queryString: "8:00 am jst to utc", expected: "11:00 PM UTC" },
+ { queryString: "9:00 jst to pdt", expected: "17:00 PDT" },
+ { queryString: "12 pm pst to cet", expected: "9:00 PM CET" },
+ { queryString: "12 am pst to cet", expected: "9:00 AM CET" },
+ { queryString: "12:30 pm pst to cet", expected: "9:30 PM CET" },
+ { queryString: "12:30 am pst to cet", expected: "9:30 AM CET" },
+ { queryString: "23 pm pst to cet", expected: "8:00 AM CET" },
+ { queryString: "23:30 pm pst to cet", expected: "8:30 AM CET" },
+ {
+ queryString: "10:00 JST to here",
+ timezone: "UTC",
+ expected: "01:00 UTC-000",
+ },
+ {
+ queryString: "1:00 to JST",
+ timezone: "UTC",
+ expected: "10:00 JST",
+ },
+ {
+ queryString: "1 am to JST",
+ timezone: "UTC",
+ expected: "10:00 AM JST",
+ },
+ {
+ queryString: "now to JST",
+ timezone: "UTC",
+ assertResult: output => {
+ const outputRegexResult = /([0-9]+):([0-9]+)/.exec(output);
+ const outputMinutes =
+ parseInt(outputRegexResult[1]) * 60 +
+ parseInt(outputRegexResult[2]);
+ const nowDate = new Date();
+ // Apply JST time difference.
+ nowDate.setHours(nowDate.getHours() + 9);
+ let nowMinutes = nowDate.getHours() * 60 + nowDate.getMinutes();
+ // When we cross the day between the unit converter calculation and the
+ // assertion here.
+ nowMinutes =
+ outputMinutes > nowMinutes ? nowMinutes + 1440 : nowMinutes;
+ Assert.lessOrEqual(nowMinutes - outputMinutes, 1);
+ },
+ },
+ {
+ queryString: "now to here",
+ timezone: "UTC",
+ assertResult: output => {
+ const outputRegexResult = /([0-9]+):([0-9]+)/.exec(output);
+ const outputMinutes =
+ parseInt(outputRegexResult[1]) * 60 +
+ parseInt(outputRegexResult[2]);
+ const nowDate = new Date();
+ let nowMinutes = nowDate.getHours() * 60 + nowDate.getMinutes();
+ nowMinutes =
+ outputMinutes > nowMinutes ? nowMinutes + 1440 : nowMinutes;
+ Assert.lessOrEqual(nowMinutes - outputMinutes, 1);
+ },
+ },
+ ],
+ },
+ {
+ category: "invalid",
+ cases: [
+ { queryString: "1 to cm" },
+ { queryString: "1cm to newton" },
+ { queryString: "1cm to foo" },
+ { queryString: "0:00:00 utc to jst" },
+ ],
+ },
+];
+
+add_task(function () {
+ // Enable unit conversion.
+ Services.prefs.setBoolPref("browser.urlbar.unitConversion.enabled", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.unitConversion.enabled");
+ });
+
+ for (const { category, cases } of TEST_DATA) {
+ for (const { queryString, timezone, expected, assertResult } of cases) {
+ info(`Test "${queryString}" in ${category}`);
+
+ let originalTimezone;
+ if (timezone) {
+ originalTimezone = Cu.getJSTestingFunctions().getTimeZone();
+ info(`Set timezone ${timezone}`);
+ Cu.getJSTestingFunctions().setTimeZone(timezone);
+ }
+
+ const context = createContext(queryString);
+ const isActive = UrlbarProviderUnitConversion.isActive(context);
+ Assert.equal(isActive, !!expected || !!assertResult);
+
+ if (isActive) {
+ UrlbarProviderUnitConversion.startQuery(context, (module, result) => {
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC);
+ Assert.equal(result.source, UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL);
+ Assert.equal(result.suggestedIndex, 1);
+ Assert.equal(result.payload.input, queryString);
+
+ if (expected) {
+ Assert.equal(result.payload.output, expected);
+ } else {
+ assertResult(result.payload.output);
+ }
+ });
+ }
+
+ if (originalTimezone) {
+ Cu.getJSTestingFunctions().setTimeZone(originalTimezone);
+ }
+ }
+ }
+});
+
+function round(number) {
+ return parseFloat(number.toPrecision(10));
+}
diff --git a/browser/components/urlbar/tests/unit/test_word_boundary_search.js b/browser/components/urlbar/tests/unit/test_word_boundary_search.js
new file mode 100644
index 0000000000..f015a29fb5
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_word_boundary_search.js
@@ -0,0 +1,403 @@
+/* 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 to make sure matches against the url, title, tags are first made on word
+ * boundaries, instead of in the middle of words, and later are extended to the
+ * whole words. For this test it is critical to check sorting of the matches.
+ *
+ * Make sure we don't try matching one after a CamelCase because the upper-case
+ * isn't really a word boundary. (bug 429498)
+ */
+
+testEngine_setup();
+
+var katakana = ["\u30a8", "\u30c9"]; // E, Do
+var ideograph = ["\u4efb", "\u5929", "\u5802"]; // Nin Ten Do
+
+add_task(async function test_escape() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false);
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+ });
+
+ await PlacesTestUtils.addVisits([
+ { uri: "http://matchme/", title: "title1" },
+ { uri: "http://dontmatchme/", title: "title1" },
+ { uri: "http://title/1", title: "matchme2" },
+ { uri: "http://title/2", title: "dontmatchme3" },
+ { uri: "http://tag/1", title: "title1" },
+ { uri: "http://tag/2", title: "title1" },
+ { uri: "http://crazytitle/", title: "!@#$%^&*()_+{}|:<>?word" },
+ { uri: "http://katakana/", title: katakana.join("") },
+ { uri: "http://ideograph/", title: ideograph.join("") },
+ { uri: "http://camel/pleaseMatchMe/", title: "title1" },
+ ]);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://tag/1",
+ title: "title1",
+ tags: ["matchme2"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://tag/2",
+ title: "title1",
+ tags: ["dontmatchme3"],
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ info("Match 'match' at the beginning or after / or on a CamelCase");
+ let context = createContext("match", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/1",
+ title: "title1",
+ tags: ["matchme2"],
+ }),
+ makeVisitResult(context, {
+ uri: "http://camel/pleaseMatchMe/",
+ title: "title1",
+ }),
+ makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }),
+ makeVisitResult(context, { uri: "http://matchme/", title: "title1" }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/2",
+ title: "title1",
+ tags: ["dontmatchme3"],
+ }),
+ makeVisitResult(context, {
+ uri: "http://title/2",
+ title: "dontmatchme3",
+ }),
+ makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }),
+ ],
+ });
+
+ info("Match 'dont' at the beginning or after /");
+ context = createContext("dont", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/2",
+ title: "title1",
+ tags: ["dontmatchme3"],
+ }),
+ makeVisitResult(context, {
+ uri: "http://title/2",
+ title: "dontmatchme3",
+ }),
+ makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }),
+ ],
+ });
+
+ info("Match 'match' at the beginning or after / or on a CamelCase");
+ context = createContext("2", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/2",
+ title: "title1",
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/1",
+ title: "title1",
+ tags: ["matchme2"],
+ }),
+ makeVisitResult(context, {
+ uri: "http://title/2",
+ title: "dontmatchme3",
+ }),
+ makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }),
+ ],
+ });
+
+ info("Match 't' at the beginning or after /");
+ context = createContext("t", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/2",
+ title: "title1",
+ tags: ["dontmatchme3"],
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/1",
+ title: "title1",
+ tags: ["matchme2"],
+ }),
+ makeVisitResult(context, {
+ uri: "http://camel/pleaseMatchMe/",
+ title: "title1",
+ }),
+ makeVisitResult(context, {
+ uri: "http://title/2",
+ title: "dontmatchme3",
+ }),
+ makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }),
+ makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }),
+ makeVisitResult(context, { uri: "http://matchme/", title: "title1" }),
+ makeVisitResult(context, {
+ uri: "http://katakana/",
+ title: katakana.join(""),
+ }),
+ makeVisitResult(context, {
+ uri: "http://crazytitle/",
+ title: "!@#$%^&*()_+{}|:<>?word",
+ }),
+ ],
+ });
+
+ info("Match 'word' after many consecutive word boundaries");
+ context = createContext("word", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://crazytitle/",
+ title: "!@#$%^&*()_+{}|:<>?word",
+ }),
+ ],
+ });
+
+ info("Match a word boundary '/' for everything");
+ context = createContext("/", { isPrivate: false });
+ // UNIX platforms can search for a file:// URL by typing a forward slash.
+ let heuristicSlashResult =
+ AppConstants.platform == "win"
+ ? makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ })
+ : makeVisitResult(context, {
+ uri: "file:///",
+ fallbackTitle: "file:///",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ });
+ await check_results({
+ context,
+ matches: [
+ heuristicSlashResult,
+ makeBookmarkResult(context, {
+ uri: "http://tag/2",
+ title: "title1",
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/1",
+ title: "title1",
+ }),
+ makeVisitResult(context, {
+ uri: "http://camel/pleaseMatchMe/",
+ title: "title1",
+ }),
+ makeVisitResult(context, {
+ uri: "http://ideograph/",
+ title: ideograph.join(""),
+ }),
+ makeVisitResult(context, {
+ uri: "http://katakana/",
+ title: katakana.join(""),
+ }),
+ makeVisitResult(context, {
+ uri: "http://crazytitle/",
+ title: "!@#$%^&*()_+{}|:<>?word",
+ }),
+ makeVisitResult(context, {
+ uri: "http://title/2",
+ title: "dontmatchme3",
+ }),
+ makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }),
+ makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }),
+ ],
+ });
+
+ info("Match word boundaries '()_' that are among word boundaries");
+ context = createContext("()_", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://crazytitle/",
+ title: "!@#$%^&*()_+{}|:<>?word",
+ }),
+ ],
+ });
+
+ info("Katakana characters form a string, so match the beginning");
+ context = createContext(katakana[0], { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://katakana/",
+ title: katakana.join(""),
+ }),
+ ],
+ });
+
+ /*
+ info("Middle of a katakana word shouldn't be matched");
+ await check_autocomplete({
+ search: katakana[1],
+ matches: [ ],
+ });
+*/
+
+ info("Ideographs are treated as words so 'nin' is one word");
+ context = createContext(ideograph[0], { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://ideograph/",
+ title: ideograph.join(""),
+ }),
+ ],
+ });
+
+ info("Ideographs are treated as words so 'ten' is another word");
+ context = createContext(ideograph[1], { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://ideograph/",
+ title: ideograph.join(""),
+ }),
+ ],
+ });
+
+ info("Ideographs are treated as words so 'do' is yet another word");
+ context = createContext(ideograph[2], { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://ideograph/",
+ title: ideograph.join(""),
+ }),
+ ],
+ });
+
+ info("Match in the middle. Should just be sorted by frecency.");
+ context = createContext("ch", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/2",
+ title: "title1",
+ tags: ["dontmatchme3"],
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/1",
+ title: "title1",
+ tags: ["matchme2"],
+ }),
+ makeVisitResult(context, {
+ uri: "http://camel/pleaseMatchMe/",
+ title: "title1",
+ }),
+ makeVisitResult(context, {
+ uri: "http://title/2",
+ title: "dontmatchme3",
+ }),
+ makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }),
+ makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }),
+ makeVisitResult(context, { uri: "http://matchme/", title: "title1" }),
+ ],
+ });
+
+ // Also this test should just be sorted by frecency.
+ info(
+ "Don't match one character after a camel-case word boundary (bug 429498). Should just be sorted by frecency."
+ );
+ context = createContext("atch", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/2",
+ title: "title1",
+ tags: ["dontmatchme3"],
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/1",
+ title: "title1",
+ tags: ["matchme2"],
+ }),
+ makeVisitResult(context, {
+ uri: "http://camel/pleaseMatchMe/",
+ title: "title1",
+ }),
+ makeVisitResult(context, {
+ uri: "http://title/2",
+ title: "dontmatchme3",
+ }),
+ makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }),
+ makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }),
+ makeVisitResult(context, { uri: "http://matchme/", title: "title1" }),
+ ],
+ });
+
+ 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..18a608e5f3
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/xpcshell.ini
@@ -0,0 +1,99 @@
+[DEFAULT]
+skip-if = toolkit == 'android' # bug 1730213
+head = head.js
+firefox-appdir = browser
+support-files =
+ data/engine.xml
+
+[test_000_frecency.js]
+[test_about_urls.js]
+[test_autofill_adaptiveHistory.js]
+[test_autofill_bookmarked.js]
+[test_autofill_do_not_trim.js]
+[test_autofill_functional.js]
+[test_autofill_origins.js]
+[test_autofill_origins_alt_frecency.js]
+prefs = places.frecency.origins.alternative.featureGate=true
+[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_calculator.js]
+[test_casing.js]
+[test_dedupe_prefix.js]
+[test_dedupe_switchTab.js]
+[test_download_embed_bookmarks.js]
+[test_empty_search.js]
+[test_encoded_urls.js]
+[test_escaping_badEscapedURI.js]
+[test_escaping_escapeSelf.js]
+[test_exposure.js]
+[test_frecency.js]
+[test_frecency_alternative_nimbus.js]
+[test_heuristic_cancel.js]
+[test_hideSponsoredHistory.js]
+[test_keywords.js]
+skip-if = os == 'linux' # bug 1474616
+[test_l10nCache.js]
+[test_local_suggest_prefs.js]
+[test_match_javascript.js]
+[test_multi_word_search.js]
+[test_muxer.js]
+[test_protocol_ignore.js]
+[test_protocol_swap.js]
+[test_providerAliasEngines.js]
+[test_providerHeuristicFallback.js]
+[test_providerHistoryUrlHeuristic.js]
+[test_providerKeywords.js]
+[test_providerOmnibox.js]
+[test_providerOpenTabs.js]
+skip-if =
+ os == "mac" && debug # Bug 1781972
+ os == "win" && debug # Bug 1781972
+[test_providerPlaces.js]
+[test_providerPlaces_duplicate_entries.js]
+[test_providerPlaces_nonEnglish.js]
+[test_providerPreloaded.js]
+[test_providersManager.js]
+[test_providersManager_filtering.js]
+[test_providersManager_maxResults.js]
+[test_providerTabToSearch.js]
+[test_providerTabToSearch_partialHost.js]
+[test_query_url.js]
+[test_queryScorer.js]
+[test_quickactions.js]
+[test_remote_tabs.js]
+skip-if = !sync
+[test_resultGroups.js]
+[test_search_engine_host.js]
+[test_search_engine_restyle.js]
+[test_search_suggestions.js]
+[test_search_suggestions_aliases.js]
+[test_search_suggestions_tail.js]
+[test_special_search.js]
+[test_suggestedIndex.js]
+[test_suggestedIndexRelativeToGroup.js]
+[test_tab_matches.js]
+[test_tags_caseInsensitivity.js]
+[test_tags_extendedUnicode.js]
+[test_tags_general.js]
+[test_tags_matchBookmarkTitles.js]
+[test_tags_returnedInSearches.js]
+[test_tokenizer.js]
+[test_trimming.js]
+[test_unitConversion.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.js]
+[test_UrlbarUtils_addToUrlbarHistory.js]
+[test_UrlbarUtils_getShortcutOrURIAndPostData.js]
+[test_UrlbarUtils_getTokenMatches.js]
+[test_UrlbarUtils_unEscapeURIForUI.js]
+[test_word_boundary_search.js]