Bug 1723158 - Allow heuristic result to be experimentally hidden or not present. r=mak
authorDrew Willcoxon <adw@mozilla.com>
Mon, 09 Aug 2021 17:07:40 +0000
changeset 588247 904db8e18e53be836d984c7c99ff6559f323b044
parent 588246 9bc32566e1c4dcec6b84b7d584942ee501f8435e
child 588248 824f41098467eef473ccf0edac2a05f853e3c394
push id38689
push userimoraru@mozilla.com
push dateMon, 09 Aug 2021 21:33:53 +0000
treeherdermozilla-central@0f323b67aa6b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs1723158
milestone93.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1723158 - Allow heuristic result to be experimentally hidden or not present. r=mak This adds `browser.urlbar.experimental.hideHeuristic`. When true, the heuristic is hidden in the view except for the search tip heuristic. This is implemented as part of the larger prototype described in the JIRA ticket (see the bug for a link) and some Slack conversation. There isn't much of a spec in that ticket, and I think that's OK because we'd like to iterate on a prototype and we're not sure yet how exactly the UX should work. For example, should the heuristic always be hidden or only in certain cases? This revision always hides it (except search tips), but it's easy to imagine we'll want to introduce some more sophisticated logic. Or more simply we may want to always show specific types of heuristics, like omnibox, as this revision does for search tips. The implementation works by excluding the heuristic in the view. Each heuristic provider still creates their heuristics. When the view receives the heuristic, instead of adding and selecting it, it calls `input.setResultForCurrentValue()` so that the heuristic is set as the current result. When the user presses enter, the input checks `experimental.hideHeuristic` and whether the current result is a heuristic. Differential Revision: https://phabricator.services.mozilla.com/D121785
browser/components/urlbar/UrlbarInput.jsm
browser/components/urlbar/UrlbarMuxerUnifiedComplete.jsm
browser/components/urlbar/UrlbarPrefs.jsm
browser/components/urlbar/UrlbarView.jsm
browser/components/urlbar/tests/browser/browser.ini
browser/components/urlbar/tests/browser/browser_hideHeuristic.js
--- a/browser/components/urlbar/UrlbarInput.jsm
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -503,16 +503,28 @@ class UrlbarInput {
       !isComposing &&
       element &&
       (!oneOffParams?.engine || selectedPrivateEngineResult)
     ) {
       this.pickElement(element, event);
       return;
     }
 
+    // Use the hidden heuristic if it exists and there's no selection.
+    if (
+      UrlbarPrefs.get("experimental.hideHeuristic") &&
+      !element &&
+      !isComposing &&
+      !oneOffParams?.engine &&
+      this._resultForCurrentValue?.heuristic
+    ) {
+      this.pickResult(this._resultForCurrentValue, event);
+      return;
+    }
+
     // We don't select a heuristic result when we're autofilling a token alias,
     // but we want pressing Enter to behave like the first result was selected.
     if (!result && this.value.startsWith("@")) {
       let tokenAliasResult = this.view.getResultAtIndex(0);
       if (tokenAliasResult?.autofill && tokenAliasResult?.payload.keyword) {
         this.pickResult(tokenAliasResult, event);
         return;
       }
--- a/browser/components/urlbar/UrlbarMuxerUnifiedComplete.jsm
+++ b/browser/components/urlbar/UrlbarMuxerUnifiedComplete.jsm
@@ -73,16 +73,21 @@ class MuxerUnifiedComplete extends Urlba
       // suggestions.  Also includes the heuristic query string if the heuristic
       // is a search result.  All strings in the set are lowercased.
       suggestions: new Set(),
       canAddTabToSearch: true,
       hasUnitConversionResult: false,
       // When you add state, update _copyState() as necessary.
     };
 
+    // If the heuristic is hidden, increment the available span.
+    if (UrlbarPrefs.get("experimental.hideHeuristic")) {
+      state.availableResultSpan++;
+    }
+
     // Do the first pass over all results to build some state.
     for (let result of context.results) {
       if (result.providerName == "UrlbarProviderQuickSuggest") {
         // Quick suggest results are handled specially and are inserted at a
         // Nimbus-configurable position within the general bucket.
         // TODO (Bug 1710518): Come up with a more general solution.
         state.quickSuggestResult = result;
         this._updateStatePreAdd(result, state);
--- a/browser/components/urlbar/UrlbarPrefs.jsm
+++ b/browser/components/urlbar/UrlbarPrefs.jsm
@@ -83,16 +83,19 @@ const PREF_URLBAR_DEFAULTS = new Map([
 
   // Whether telemetry events should be recorded.
   ["eventTelemetry.enabled", false],
 
   // Whether we expand the font size when when the urlbar is
   // focused.
   ["experimental.expandTextOnFocus", false],
 
+  // Whether the heuristic result is hidden.
+  ["experimental.hideHeuristic", false],
+
   // Whether the urlbar displays a permanent search button.
   ["experimental.searchButton", false],
 
   // When we send events to extensions, we wait this amount of time in
   // milliseconds for them to respond before timing out.
   ["extension.timeout", 400],
 
   // When true, `javascript:` URLs are not included in search results.
--- a/browser/components/urlbar/UrlbarView.jsm
+++ b/browser/components/urlbar/UrlbarView.jsm
@@ -103,21 +103,18 @@ class UrlbarView {
    * Whether the panel is open.
    * @returns {boolean}
    */
   get isOpen() {
     return this.input.hasAttribute("open");
   }
 
   get allowEmptySelection() {
-    return !(
-      this._queryContext &&
-      this._queryContext.results[0] &&
-      this._queryContext.results[0].heuristic
-    );
+    let { heuristicResult } = this._queryContext;
+    return !heuristicResult || !this._shouldShowHeuristic(heuristicResult);
   }
 
   get selectedRowIndex() {
     if (!this.isOpen) {
       return -1;
     }
 
     let selectedRow = this._getSelectedRow();
@@ -646,20 +643,25 @@ class UrlbarView {
       );
     }
 
     if (!this.selectedElement && !this.oneOffSearchButtons.selectedButton) {
       if (firstResult.heuristic) {
         // Select the heuristic result.  The heuristic may not be the first
         // result added, which is why we do this check here when each result is
         // added and not above.
-        this._selectElement(this._getFirstSelectableElement(), {
-          updateInput: false,
-          setAccessibleFocus: this.controller._userSelectionBehavior == "arrow",
-        });
+        if (this._shouldShowHeuristic(firstResult)) {
+          this._selectElement(this._getFirstSelectableElement(), {
+            updateInput: false,
+            setAccessibleFocus:
+              this.controller._userSelectionBehavior == "arrow",
+          });
+        } else {
+          this.input.setResultForCurrentValue(firstResult);
+        }
       } else if (
         firstResult.payload.providesSearchMode &&
         queryContext.trimmedSearchString != "@"
       ) {
         // Filtered keyword offer results can be in the first position but not
         // be heuristic results. We do this so the user can press Tab to select
         // them, resembling tab-to-search. In that case, the input value is
         // still associated with the first result.
@@ -877,16 +879,26 @@ class UrlbarView {
     this.input.startLayoutExtend();
 
     this.window.addEventListener("resize", this);
     this.window.addEventListener("blur", this);
 
     this.controller.notify(this.controller.NOTIFICATIONS.VIEW_OPEN);
   }
 
+  _shouldShowHeuristic(result) {
+    if (!result?.heuristic) {
+      throw new Error("A heuristic result must be given");
+    }
+    return (
+      !UrlbarPrefs.get("experimental.hideHeuristic") ||
+      result.type == UrlbarUtils.RESULT_TYPE.TIP
+    );
+  }
+
   /**
    * Whether a result is a search suggestion.
    * @param {UrlbarResult} result The result to examine.
    * @returns {boolean} Whether the result is a search suggestion.
    */
   _resultIsSearchSuggestion(result) {
     return Boolean(
       result &&
@@ -951,16 +963,20 @@ class UrlbarView {
     // TODO: For now this just compares search suggestions to the rest, in the
     // future we should make it support any type of result. Or, even better,
     // results should be grouped, thus we can directly update groups.
 
     // Walk rows and find an insertion index for results. To avoid flicker, we
     // skip rows until we find one compatible with the result we want to apply.
     // If we couldn't find a compatible range, we'll just update.
     let results = queryContext.results;
+    if (results[0]?.heuristic && !this._shouldShowHeuristic(results[0])) {
+      // Exclude the heuristic.
+      results = results.slice(1);
+    }
     let rowIndex = 0;
     let resultIndex = 0;
     let visibleSpanCount = 0;
     let seenMisplacedResult = false;
     let seenSearchSuggestion = false;
 
     // We can have more rows than the visible ones.
     for (
--- a/browser/components/urlbar/tests/browser/browser.ini
+++ b/browser/components/urlbar/tests/browser/browser.ini
@@ -99,16 +99,17 @@ support-files =
 [browser_groupLabels.js]
 support-files =
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
 [browser_handleCommand_fallback.js]
 [browser_hashChangeProxyState.js]
 [browser_helpUrl.js]
 [browser_heuristicNotAddedFirst.js]
+[browser_hideHeuristic.js]
 [browser_ime_composition.js]
 [browser_inputHistory.js]
 https_first_disabled = true
 support-files =
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
 [browser_inputHistory_emptystring.js]
 [browser_keepStateAcrossTabSwitches.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_hideHeuristic.js
@@ -0,0 +1,487 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Basic smoke tests for the `browser.urlbar.experimental.hideHeuristic` pref,
+// which hides the heuristic result. Each task performs a search that triggers a
+// specific heuristic, verifies that it's hidden or shown as appropriate, and
+// verifies that it's picked when enter is pressed.
+//
+// If/when it becomes the default, we should update existing tests as necessary
+// and remove this one.
+
+"use strict";
+
+add_task(async function init() {
+  await SpecialPowers.pushPrefEnv({
+    set: [["browser.urlbar.experimental.hideHeuristic", true]],
+  });
+  await PlacesUtils.bookmarks.eraseEverything();
+  await PlacesUtils.history.clear();
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION should be hidden.
+add_task(async function extension() {
+  await BrowserTestUtils.withNewTab("about:blank", async () => {
+    await withVisits(async visitURLs => {
+      // Add an extension provider that returns a heuristic.
+      let url = "http://example.com/extension-test";
+      let provider = new UrlbarTestUtils.TestProvider({
+        name: "ExtensionTest",
+        type: UrlbarUtils.PROVIDER_TYPE.EXTENSION,
+        results: [
+          Object.assign(
+            new UrlbarResult(
+              UrlbarUtils.RESULT_TYPE.URL,
+              UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+              {
+                url,
+                title: "Test",
+              }
+            ),
+            { heuristic: true }
+          ),
+        ],
+      });
+      UrlbarProvidersManager.registerProvider(provider);
+
+      // Do a search that fetches the provider's result and check it.
+      let heuristic = await search({
+        value: "test",
+        expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION,
+      });
+      Assert.equal(heuristic.payload.url, url, "Heuristic URL is correct");
+
+      // Check the other visit results.
+      await checkVisitResults(visitURLs);
+
+      // Press enter to verify the heuristic result is loaded.
+      await synthesizeEnterAndAwaitLoad(url);
+
+      UrlbarProvidersManager.unregisterProvider(provider);
+    });
+  });
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX should be hidden.
+add_task(async function omnibox() {
+  await BrowserTestUtils.withNewTab("about:blank", async () => {
+    // Load an extension.
+    let extension = ExtensionTestUtils.loadExtension({
+      manifest: {
+        omnibox: {
+          keyword: "omniboxtest",
+        },
+      },
+      background() {
+        /* global browser */
+        browser.omnibox.onInputEntered.addListener(() => {
+          browser.test.sendMessage("onInputEntered");
+        });
+      },
+    });
+    await extension.startup();
+
+    // Do a search using the omnibox keyword and check the hidden heuristic
+    // result.
+    let heuristic = await search({
+      value: "omniboxtest foo",
+      expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX,
+    });
+    Assert.equal(
+      heuristic.payload.keyword,
+      "omniboxtest",
+      "Heuristic keyword is correct"
+    );
+
+    // Press enter to verify the heuristic result is picked.
+    let messagePromise = extension.awaitMessage("onInputEntered");
+    EventUtils.synthesizeKey("KEY_Enter");
+    await messagePromise;
+
+    await extension.unload();
+  });
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP should be shown.
+add_task(async function searchTip() {
+  await SpecialPowers.pushPrefEnv({
+    set: [["browser.urlbar.searchTips.test.ignoreShowLimits", true]],
+  });
+  await BrowserTestUtils.withNewTab(
+    {
+      gBrowser: window.gBrowser,
+      url: "about:newtab",
+      // `withNewTab` hangs waiting for about:newtab to load without this.
+      waitForLoad: false,
+    },
+    async () => {
+      await UrlbarTestUtils.promisePopupOpen(window, () => {});
+      Assert.ok(true, "View opened");
+      Assert.equal(UrlbarTestUtils.getResultCount(window), 1);
+      let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+      Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TIP);
+      Assert.ok(result.heuristic);
+      Assert.ok(UrlbarTestUtils.getSelectedElement(window), "Selection exists");
+    }
+  );
+  await SpecialPowers.popPrefEnv();
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_ENGINE_ALIAS should be hidden.
+add_task(async function engineAlias() {
+  await BrowserTestUtils.withNewTab("about:blank", async () => {
+    await withVisits(async visitURLs => {
+      // Add an engine with an alias.
+      await withEngine({ keyword: "test" }, async () => {
+        // Do a search using the alias and check the hidden heuristic result.
+        // The heuristic will be HEURISTIC_FALLBACK, not HEURISTIC_ENGINE_ALIAS,
+        // because two searches are performed and
+        // `UrlbarTestUtils.promiseAutocompleteResultPopup` waits for both. The
+        // first returns a HEURISTIC_ENGINE_ALIAS that triggers search mode and
+        // then an immediate second search, which returns HEURISTIC_FALLBACK.
+        let heuristic = await search({
+          value: "test foo",
+          expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK,
+        });
+        Assert.equal(
+          heuristic.payload.engine,
+          "Example",
+          "Heuristic engine is correct"
+        );
+        Assert.equal(
+          heuristic.payload.query,
+          "foo",
+          "Heuristic query is correct"
+        );
+        await UrlbarTestUtils.assertSearchMode(window, {
+          engineName: "Example",
+          entry: "typed",
+        });
+
+        // Check the other visit results.
+        await checkVisitResults(visitURLs);
+
+        // Press enter to verify the heuristic result is loaded.
+        await synthesizeEnterAndAwaitLoad("https://example.com/?q=foo");
+      });
+    });
+  });
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD should be hidden.
+add_task(async function bookmarkKeyword() {
+  await BrowserTestUtils.withNewTab("about:blank", async () => {
+    await withVisits(async visitURLs => {
+      // Add a bookmark with a keyword.
+      let keyword = "bm";
+      let bm = await PlacesUtils.bookmarks.insert({
+        parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+        url: "http://example.com/?q=%s",
+        title: "test",
+      });
+      await PlacesUtils.keywords.insert({ keyword, url: bm.url });
+
+      // Do a search using the keyword and check the hidden heuristic result.
+      let heuristic = await search({
+        value: "bm foo",
+        expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD,
+      });
+      Assert.equal(
+        heuristic.payload.keyword,
+        keyword,
+        "Heuristic keyword is correct"
+      );
+      let heuristicURL = "http://example.com/?q=foo";
+      Assert.equal(
+        heuristic.payload.url,
+        heuristicURL,
+        "Heuristic URL is correct"
+      );
+
+      // Check the other visit results.
+      await checkVisitResults(visitURLs);
+
+      // Press enter to verify the heuristic result is loaded.
+      await synthesizeEnterAndAwaitLoad(heuristicURL);
+
+      await PlacesUtils.bookmarks.eraseEverything();
+    });
+  });
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL should be hidden.
+add_task(async function autofill() {
+  await BrowserTestUtils.withNewTab("about:blank", async () => {
+    await withVisits(async visitURLs => {
+      // Do a search that triggers autofill and check the hidden heuristic
+      // result.
+      let heuristic = await search({
+        value: "ex",
+        expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL,
+      });
+      Assert.ok(heuristic.autofill, "Heuristic is autofill");
+      let heuristicURL = "http://example.com/";
+      Assert.equal(
+        heuristic.payload.url,
+        heuristicURL,
+        "Heuristic URL is correct"
+      );
+      Assert.equal(gURLBar.value, "example.com/", "Input has been autofilled");
+
+      // Check the other visit results.
+      await checkVisitResults(visitURLs);
+
+      // Press enter to verify the heuristic result is loaded.
+      await synthesizeEnterAndAwaitLoad(heuristicURL);
+    });
+  });
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK with an unknown URL should be
+// hidden.
+add_task(async function fallback_unknownURL() {
+  await BrowserTestUtils.withNewTab("about:blank", async () => {
+    // Do a search for an unknown URL and check the hidden heuristic result.
+    let url = "http://example.com/unknown-url";
+    let heuristic = await search({
+      value: url,
+      expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK,
+    });
+    Assert.equal(heuristic.payload.url, url, "Heuristic URL is correct");
+
+    // Press enter to verify the heuristic result is loaded.
+    await synthesizeEnterAndAwaitLoad(url);
+  });
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK with the search restriction token
+// should be hidden.
+add_task(async function fallback_searchRestrictionToken() {
+  await BrowserTestUtils.withNewTab("about:blank", async () => {
+    await withVisits(async visitURLs => {
+      // Add a mock default engine so we don't hit the network.
+      await withEngine({ makeDefault: true }, async () => {
+        // Do a search with `?` and check the hidden heuristic result.
+        let heuristic = await search({
+          value: UrlbarTokenizer.RESTRICT.SEARCH + " foo",
+          expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK,
+        });
+        Assert.equal(
+          heuristic.payload.engine,
+          "Example",
+          "Heuristic engine is correct"
+        );
+        Assert.equal(
+          heuristic.payload.query,
+          "foo",
+          "Heuristic query is correct"
+        );
+        await UrlbarTestUtils.assertSearchMode(window, {
+          engineName: "Example",
+          entry: "typed",
+        });
+
+        // Check the other visit results.
+        await checkVisitResults(visitURLs);
+
+        // Press enter to verify the heuristic result is loaded.
+        await synthesizeEnterAndAwaitLoad("https://example.com/?q=foo");
+      });
+    });
+  });
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK with a search string that falls
+// back to a search result should be hidden.
+add_task(async function fallback_search() {
+  await BrowserTestUtils.withNewTab("about:blank", async () => {
+    await withVisits(async visitURLs => {
+      // Add a mock default engine so we don't hit the network.
+      await withEngine({ makeDefault: true }, async () => {
+        // Do a search and check the hidden heuristic result.
+        let heuristic = await search({
+          value: "foo",
+          expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK,
+        });
+        Assert.equal(
+          heuristic.payload.engine,
+          "Example",
+          "Heuristic engine is correct"
+        );
+        Assert.equal(
+          heuristic.payload.query,
+          "foo",
+          "Heuristic query is correct"
+        );
+
+        // Check the other visit results.
+        await checkVisitResults(visitURLs);
+
+        // Press enter to verify the heuristic result is loaded.
+        await synthesizeEnterAndAwaitLoad("https://example.com/?q=foo");
+      });
+    });
+  });
+});
+
+// Picking a non-heuristic result should work correctly (and not pick the
+// heuristic).
+add_task(async function pickNonHeuristic() {
+  await BrowserTestUtils.withNewTab("about:blank", async () => {
+    await withVisits(async visitURLs => {
+      // Do a search that triggers autofill and check the hidden heuristic
+      // result.
+      let heuristic = await search({
+        value: "ex",
+        expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL,
+      });
+      Assert.ok(heuristic.autofill, "Heuristic is autofill");
+      Assert.equal(
+        heuristic.payload.url,
+        "http://example.com/",
+        "Heuristic URL is correct"
+      );
+
+      // Pick the first visit result.
+      Assert.notEqual(
+        heuristic.payload.url,
+        visitURLs[0],
+        "Sanity check: Heuristic and first results have different URLs"
+      );
+      EventUtils.synthesizeKey("KEY_ArrowDown");
+      await synthesizeEnterAndAwaitLoad(visitURLs[0]);
+    });
+  });
+});
+
+/**
+ * Adds `maxRichResults` visits, calls your callback, and clears history. We add
+ * `maxRichResults` visits to verify that the view correctly contains the
+ * maximum number of results when the heuristic is hidden.
+ *
+ * @param {function} callback
+ */
+async function withVisits(callback) {
+  let urls = [];
+  for (let i = 0; i < UrlbarPrefs.get("maxRichResults"); i++) {
+    urls.push("http://example.com/foo/" + i);
+  }
+  await PlacesTestUtils.addVisits(urls);
+
+  // The URLs will appear in the view in reverse order so that newer visits are
+  // first. Reverse the array now so callers to `checkVisitResults` or
+  // `checkVisitResults` itself doesn't need to do it.
+  urls.reverse();
+
+  await callback(urls);
+  await PlacesUtils.history.clear();
+}
+
+/**
+ * Adds a search engine, calls your callback, and removes the engine.
+ *
+ * @param {string} options.keyword
+ *   The keyword/alias for the engine.
+ * @param {boolean} options.makeDefault
+ *   Whether to make the engine default.
+ * @param {function} callback
+ */
+async function withEngine(
+  { keyword = undefined, makeDefault = false },
+  callback
+) {
+  await SearchTestUtils.installSearchExtension({ keyword });
+  let engine = Services.search.getEngineByName("Example");
+  let originalEngine;
+  if (makeDefault) {
+    originalEngine = await Services.search.getDefault();
+    await Services.search.setDefault(engine);
+  }
+  await callback();
+  if (originalEngine) {
+    await Services.search.setDefault(originalEngine);
+  }
+  await Services.search.removeEngine(engine);
+}
+
+/**
+ * Asserts the view contains visit results with the given URLs.
+ *
+ * @param {array} expectedURLs
+ */
+async function checkVisitResults(expectedURLs) {
+  Assert.equal(
+    UrlbarTestUtils.getResultCount(window),
+    expectedURLs.length,
+    "The view has other results"
+  );
+  for (let i = 0; i < expectedURLs.length; i++) {
+    let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+    Assert.equal(
+      result.type,
+      UrlbarUtils.RESULT_TYPE.URL,
+      "Other result type is correct at index " + i
+    );
+    Assert.equal(
+      result.source,
+      UrlbarUtils.RESULT_SOURCE.HISTORY,
+      "Other result source is correct at index " + i
+    );
+    Assert.equal(
+      result.url,
+      expectedURLs[i],
+      "Other result URL is correct at index " + i
+    );
+  }
+}
+
+/**
+ * Performs a search and makes some basic assertions under the assumption that
+ * the heuristic should be hidden.
+ *
+ * @param {string} value
+ *   The search string.
+ * @param {UrlbarUtils.RESULT_GROUP} expectedGroup
+ *   The expected result group of the hidden heuristic.
+ * @returns {UrlbarResult}
+ *   The hidden heuristic result.
+ */
+async function search({ value, expectedGroup }) {
+  await UrlbarTestUtils.promiseAutocompleteResultPopup({
+    window,
+    waitForFocus,
+    value,
+    fireInputEvent: true,
+  });
+
+  // _resultForCurrentValue should be the hidden heuristic result.
+  let { _resultForCurrentValue: result } = gURLBar;
+  Assert.ok(result, "_resultForCurrentValue is defined");
+  Assert.ok(result.heuristic, "_resultForCurrentValue.heuristic is true");
+  Assert.equal(
+    UrlbarUtils.getResultGroup(result),
+    expectedGroup,
+    "_resultForCurrentValue has expected group"
+  );
+
+  Assert.ok(!UrlbarTestUtils.getSelectedElement(window), "No selection exists");
+
+  return result;
+}
+
+/**
+ * Synthesizes the enter key and waits for a load in the current tab.
+ *
+ * @param {string} expectedURL
+ *   The URL that should load.
+ */
+async function synthesizeEnterAndAwaitLoad(expectedURL) {
+  let loadPromise = BrowserTestUtils.browserLoaded(
+    gBrowser.selectedBrowser,
+    false,
+    expectedURL
+  );
+  EventUtils.synthesizeKey("KEY_Enter");
+  await loadPromise;
+  await PlacesUtils.history.clear();
+}