Bug 1659204 - Don't show a heuristic in local search modes except for autofill. r=mak,harry
authorDrew Willcoxon <adw@mozilla.com>
Sat, 19 Sep 2020 00:34:26 +0000
changeset 549372 763a0359244d88b37c9e3ccab354e3849563003b
parent 549371 bf270919afa6400aa7a784ae81f3763137dce887
child 549373 56ef52f179cbc136b9bb134b638b23f192bbe01e
push id126730
push userdwillcoxon@mozilla.com
push dateSat, 19 Sep 2020 00:36:57 +0000
treeherderautoland@763a0359244d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak, harry
bugs1659204
milestone82.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 1659204 - Don't show a heuristic in local search modes except for autofill. r=mak,harry Differential Revision: https://phabricator.services.mozilla.com/D90671
browser/components/urlbar/UrlbarInput.jsm
browser/components/urlbar/UrlbarProvidersManager.jsm
browser/components/urlbar/tests/browser/browser.ini
browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js
browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js
browser/components/urlbar/tests/browser/browser_oneOffs.js
browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js
browser/components/urlbar/tests/browser/browser_searchMode_no_results.js
browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js
browser/components/urlbar/tests/browser/browser_waitForLoadOrTimeout.js
browser/components/urlbar/tests/browser/head.js
--- a/browser/components/urlbar/UrlbarInput.jsm
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -439,17 +439,20 @@ class UrlbarInput {
   }
 
   /**
    * Handles an event which would cause a URL or text to be opened.
    *
    * @param {Event} [event]
    *   The event triggering the open.
    * @param {object} [oneOffParams]
-   *   Optional. Pass if this navigation was triggered by a one-off.
+   *   Optional. Pass if this navigation was triggered by a one-off. Practically
+   *   speaking, UrlbarSearchOneOffs passes this when the user holds certain key
+   *   modifiers while picking a one-off. In those cases, we do an immediate
+   *   search using the one-off's engine instead of entering search mode.
    * @param {string} oneOffParams.openWhere
    *   Where we expect the result to be opened.
    * @param {object} oneOffParams.openParams
    *   The parameters related to where the result will be opened.
    * @param {Node} oneOffParams.engine
    *   The selected one-off's engine.
    * @param {object} [triggeringPrincipal]
    *   The principal that the action was triggered from.
@@ -501,16 +504,27 @@ class UrlbarInput {
       url = this.untrimmedValue;
       openParams.postData = null;
     }
 
     if (!url) {
       return;
     }
 
+    // When the user hits enter in a local search mode and there's no selected
+    // result or one-off, don't do anything.
+    if (
+      this.searchMode &&
+      !this.searchMode.engineName &&
+      !result &&
+      !oneOffParams
+    ) {
+      return;
+    }
+
     this.controller.recordSelectedResult(
       event,
       result || this.view.selectedResult
     );
 
     let where = oneOffParams?.openWhere || this._whereToOpen(event);
     if (selectedPrivateResult) {
       where = "window";
--- a/browser/components/urlbar/UrlbarProvidersManager.jsm
+++ b/browser/components/urlbar/UrlbarProvidersManager.jsm
@@ -481,22 +481,25 @@ class Query {
     // result(s) first.
     this.context.pendingHeuristicProviders.delete(provider.name);
 
     // Stop returning results as soon as we've been canceled.
     if (this.canceled) {
       return;
     }
 
-    // When in search mode and the search string is empty, don't allow heuristic
-    // results since they don't make sense.
+    // In search mode, don't allow heuristic results in the following cases
+    // since they don't make sense:
+    //   * When the search string is empty, or
+    //   * In local search mode, except for autofill results
     if (
       result.heuristic &&
-      !this.context.trimmedSearchString &&
-      this.context.searchMode
+      this.context.searchMode &&
+      (!this.context.trimmedSearchString ||
+        (!this.context.searchMode.engineName && !result.autofill))
     ) {
       return;
     }
 
     // Check if the result source should be filtered out. Pay attention to the
     // heuristic result though, that is supposed to be added regardless.
     if (
       !this.acceptableSources.includes(result.source) &&
--- a/browser/components/urlbar/tests/browser/browser.ini
+++ b/browser/components/urlbar/tests/browser/browser.ini
@@ -149,16 +149,17 @@ run-if = e10s
 support-files =
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
 [browser_searchMode_autofill.js]
 [browser_searchMode_clickLink.js]
 support-files =
   dummy_page.html
 [browser_searchMode_engineRemoval.js]
+[browser_searchMode_heuristic.js]
 [browser_searchMode_no_results.js]
 [browser_searchMode_pickResult.js]
 [browser_searchMode_preview.js]
 [browser_searchMode_setURI.js]
 [browser_searchMode_suggestions.js]
 support-files =
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
@@ -248,9 +249,10 @@ support-files = file_userTypedValue.html
 [browser_valueOnTabSwitch.js]
 [browser_view_emptyResultSet.js]
 [browser_view_resultDisplay.js]
 [browser_view_resultTypes_display.js]
 support-files =
   print_postdata.sjs
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
+[browser_waitForLoadOrTimeout.js]
 [browser_whereToOpen.js]
--- a/browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js
@@ -52,24 +52,29 @@ async function initAccessibilityService(
 add_task(async function switchToTab() {
   let tab = BrowserTestUtils.addTab(gBrowser, "about:robots");
   await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
 
   await UrlbarTestUtils.promiseAutocompleteResultPopup({
     window,
     value: "% robots",
   });
-  let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+
+  let index = UrlbarPrefs.get("update2") ? 0 : 1;
+  let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
   Assert.equal(
     result.type,
     UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
     "Should have a switch tab result"
   );
 
-  let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+  let element = await UrlbarTestUtils.waitForAutocompleteResultAt(
+    window,
+    index
+  );
   is(
     await getResultText(element),
     "about: robots— Switch to Tab",
     "Result a11y label should be: <title>— Switch to Tab"
   );
 
   await UrlbarTestUtils.promisePopupClose(window);
   gURLBar.handleRevert();
--- a/browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js
@@ -106,28 +106,39 @@ add_task(async function() {
     await addTagItem(testcase.tagName);
     for (let prefName of Object.keys(testcase.prefs)) {
       Services.prefs.setBoolPref(
         `browser.urlbar.${prefName}`,
         testcase.prefs[prefName]
       );
     }
 
-    await UrlbarTestUtils.promiseAutocompleteResultPopup({
+    let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
       window,
       value: testcase.input,
     });
 
+    // If testcase.input triggers local search mode, there won't be a heuristic.
+    let resultIndex =
+      UrlbarPrefs.get("update2") &&
+      context.searchMode &&
+      !context.searchMode.engineName
+        ? 0
+        : 1;
+
     Assert.greaterOrEqual(
       UrlbarTestUtils.getResultCount(window),
-      2,
-      "Should be at least two results"
+      resultIndex + 1,
+      `Should be at least ${resultIndex + 1} results`
     );
 
-    let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+    let result = await UrlbarTestUtils.getDetailsOfResultAt(
+      window,
+      resultIndex
+    );
 
     Assert.equal(
       result.type,
       UrlbarUtils.RESULT_TYPE.URL,
       "Should have a URL result type"
     );
     // The Quantum Bar differs from the legacy urlbar in the fact that, if
     // bookmarks are filtered out, it won't show tags for history results.
--- a/browser/components/urlbar/tests/browser/browser_oneOffs.js
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs.js
@@ -68,17 +68,19 @@ add_task(async function init() {
     0,
     { type: "mousemove" },
     window
   );
 });
 
 // Opens the view without showing the one-offs.  They should be hidden and arrow
 // key selection should work properly.
-add_task(async function noOneOffs() {
+//
+// This task can be removed when update2 is enabled by default.
+add_task(async function noOneOffs_legacy() {
   // Do a search for "@" since we hide the one-offs in that case.
   let value = "@";
   await UrlbarTestUtils.promiseAutocompleteResultPopup({
     window,
     value,
     fireInputEvent: true,
   });
   await TestUtils.waitForCondition(
@@ -123,20 +125,19 @@ add_task(async function noOneOffs() {
 
   // Key up again.  Nothing should be selected.
   EventUtils.synthesizeKey("KEY_ArrowUp");
   assertState(-1, -1, value);
 
   await hidePopup();
 });
 
-// The same as the previous task but with update 2 enabled.  Opens the view
-// without showing the one-offs.  Makes sure they're hidden and that arrow key
-// selection works properly.
-add_task(async function noOneOffsUpdate2() {
+// Opens the view without showing the one-offs.  They should be hidden and arrow
+// key selection should work properly.
+add_task(async function noOneOffs() {
   // Set the update2 prefs.
   await SpecialPowers.pushPrefEnv({
     set: [
       ["browser.urlbar.update2", true],
       ["browser.urlbar.update2.localOneOffs", true],
       ["browser.urlbar.update2.oneOffsRefresh", true],
     ],
   });
@@ -191,18 +192,18 @@ add_task(async function noOneOffsUpdate2
   // Key up again.  Nothing should be selected.
   EventUtils.synthesizeKey("KEY_ArrowUp");
   assertState(-1, -1, value);
 
   await hidePopup();
   await SpecialPowers.popPrefEnv();
 });
 
-// Opens the top-sites view with update2 enabled.  The one-offs should be shown.
-add_task(async function topSitesUpdate2() {
+// Opens the top-sites view.  The one-offs should be shown.
+add_task(async function topSites() {
   // Set the update2 prefs.
   await SpecialPowers.pushPrefEnv({
     set: [
       ["browser.urlbar.update2", true],
       ["browser.urlbar.update2.localOneOffs", true],
       ["browser.urlbar.update2.oneOffsRefresh", true],
     ],
   });
@@ -617,17 +618,19 @@ add_task(async function hiddenWhenUsingS
     UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
     true,
     "Should be showing the one-off buttons"
   );
   await hidePopup();
 });
 
 // Makes sure local search mode one-offs don't exist without update2.
-add_task(async function localOneOffsWithoutUpdate2() {
+//
+// This task can be removed when update2 is enabled by default.
+add_task(async function localOneOffs_legacy() {
   await SpecialPowers.pushPrefEnv({
     set: [["browser.urlbar.update2", false]],
   });
 
   // Null out _engines so that the one-offs rebuild themselves when the view
   // opens.
   oneOffSearchButtons._engines = null;
   let rebuildPromise = BrowserTestUtils.waitForEvent(
@@ -656,18 +659,18 @@ add_task(async function localOneOffsWith
     null,
     "History one-off should not exist"
   );
 
   await hidePopup();
   await SpecialPowers.popPrefEnv();
 });
 
-// Makes sure local search mode one-offs exist with update2.
-add_task(async function localOneOffsWithUpdate2() {
+// Makes sure local search mode one-offs exist.
+add_task(async function localOneOffs() {
   // Null out _engines so that the one-offs rebuild themselves when the view
   // opens.
   oneOffSearchButtons._engines = null;
   await doLocalOneOffsShownTest();
 });
 
 // Clicks a local search mode one-off.
 add_task(async function localOneOffClick() {
@@ -760,17 +763,26 @@ add_task(async function localOneOffRetur
     EventUtils.synthesizeKey("KEY_ArrowDown", {
       altKey: true,
       repeat: index + 1,
     });
     await TestUtils.waitForCondition(
       () => oneOffSearchButtons.selectedButtonIndex == index,
       "Waiting for local one-off to become selected"
     );
-    assertState(0, index, typedValue);
+
+    let expectedSelectedResultIndex = -1;
+    let count = UrlbarTestUtils.getResultCount(window);
+    if (count > 0) {
+      let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+      if (result.heuristic) {
+        expectedSelectedResultIndex = 0;
+      }
+    }
+    assertState(expectedSelectedResultIndex, index, typedValue);
 
     Assert.ok(button.source, "Sanity check: Button has a source");
     let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
     EventUtils.synthesizeKey("KEY_Enter");
     await searchPromise;
     Assert.ok(
       UrlbarTestUtils.isPopupOpen(window),
       "Urlbar view is still open."
new file mode 100644
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js
@@ -0,0 +1,234 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests heuristic results in search mode.
+ */
+
+"use strict";
+
+add_task(async function setup() {
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      ["browser.urlbar.update2", true],
+      ["browser.urlbar.update2.localOneOffs", true],
+      ["browser.urlbar.update2.oneOffsRefresh", true],
+    ],
+  });
+
+  await PlacesUtils.history.clear();
+  await PlacesUtils.bookmarks.eraseEverything();
+
+  // Add a new mock default engine so we don't hit the network.
+  let oldDefaultEngine = await Services.search.getDefault();
+  let engine = await Services.search.addEngineWithDetails("Test", {
+    template: "http://example.com/?search={searchTerms}",
+  });
+  await Services.search.setDefault(engine);
+  registerCleanupFunction(async () => {
+    await Services.search.setDefault(oldDefaultEngine);
+    await Services.search.removeEngine(engine);
+  });
+
+  // Add one bookmark we'll use below.
+  await PlacesUtils.bookmarks.insert({
+    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+    url: "http://example.com/bookmark",
+  });
+  registerCleanupFunction(async () => {
+    await PlacesUtils.bookmarks.eraseEverything();
+  });
+});
+
+// Enters search mode with no results.
+add_task(async function noResults() {
+  // Do a search that doesn't match our bookmark and enter bookmark search mode.
+  await UrlbarTestUtils.promiseAutocompleteResultPopup({
+    window,
+    value: "doesn't match anything",
+  });
+  await UrlbarTestUtils.enterSearchMode(window, {
+    source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+  });
+
+  Assert.equal(
+    UrlbarTestUtils.getResultCount(window),
+    0,
+    "Zero results since no bookmark matches"
+  );
+
+  // Press enter.  Nothing should happen.
+  let loadPromise = waitForLoadOrTimeout();
+  EventUtils.synthesizeKey("KEY_Enter");
+  let loadEvent = await loadPromise;
+  Assert.ok(!loadEvent, "Nothing should have loaded");
+
+  await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Enters a local search mode (bookmarks) with a matching result.  No heuristic
+// should be present.
+add_task(async function localNoHeuristic() {
+  // Do a search that matches our bookmark and enter bookmarks search mode.
+  await UrlbarTestUtils.promiseAutocompleteResultPopup({
+    window,
+    value: "bookmark",
+  });
+  await UrlbarTestUtils.enterSearchMode(window, {
+    source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+  });
+
+  Assert.equal(
+    UrlbarTestUtils.getResultCount(window),
+    1,
+    "There should be one result"
+  );
+
+  let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+  Assert.equal(
+    result.source,
+    UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+    "Result source should be BOOKMARKS"
+  );
+  Assert.equal(
+    result.type,
+    UrlbarUtils.RESULT_TYPE.URL,
+    "Result type should be URL"
+  );
+  Assert.equal(
+    result.url,
+    "http://example.com/bookmark",
+    "Result URL is our bookmark URL"
+  );
+  Assert.ok(!result.heuristic, "Result should not be heuristic");
+
+  // Press enter.  Nothing should happen.
+  let loadPromise = waitForLoadOrTimeout();
+  EventUtils.synthesizeKey("KEY_Enter");
+  let loadEvent = await loadPromise;
+  Assert.ok(!loadEvent, "Nothing should have loaded");
+
+  await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Enters a local search mode (bookmarks) with a matching autofill result.  The
+// result should be the heuristic.
+add_task(async function localAutofill() {
+  await BrowserTestUtils.withNewTab("about:blank", async () => {
+    // Do a search that autofills our bookmark's origin and enter bookmarks
+    // search mode.
+    await UrlbarTestUtils.promiseAutocompleteResultPopup({
+      window,
+      value: "example",
+    });
+    await UrlbarTestUtils.enterSearchMode(window, {
+      source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+    });
+
+    Assert.equal(
+      UrlbarTestUtils.getResultCount(window),
+      2,
+      "There should be two results"
+    );
+
+    let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+    Assert.equal(
+      result.source,
+      UrlbarUtils.RESULT_SOURCE.HISTORY,
+      "Result source should be HISTORY"
+    );
+    Assert.equal(
+      result.type,
+      UrlbarUtils.RESULT_TYPE.URL,
+      "Result type should be URL"
+    );
+    Assert.equal(
+      result.url,
+      "http://example.com/",
+      "Result URL is our bookmark's origin"
+    );
+    Assert.ok(result.heuristic, "Result should be heuristic");
+    Assert.ok(result.autofill, "Result should be autofill");
+
+    result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+    Assert.equal(
+      result.source,
+      UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+      "Result source should be BOOKMARKS"
+    );
+    Assert.equal(
+      result.type,
+      UrlbarUtils.RESULT_TYPE.URL,
+      "Result type should be URL"
+    );
+    Assert.equal(
+      result.url,
+      "http://example.com/bookmark",
+      "Result URL is our bookmark URL"
+    );
+
+    // Press enter.  Our bookmark's origin should be loaded.
+    let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+    EventUtils.synthesizeKey("KEY_Enter");
+    await loadPromise;
+    Assert.equal(
+      gBrowser.currentURI.spec,
+      "http://example.com/",
+      "Bookmark's origin should have loaded"
+    );
+  });
+});
+
+// Enters a remote engine search mode.  There should be a heuristic.
+add_task(async function remote() {
+  await BrowserTestUtils.withNewTab("about:blank", async () => {
+    // Do a search and enter search mode with our test engine.
+    await UrlbarTestUtils.promiseAutocompleteResultPopup({
+      window,
+      value: "remote",
+    });
+    await UrlbarTestUtils.enterSearchMode(window, {
+      engineName: "Test",
+    });
+
+    Assert.equal(
+      UrlbarTestUtils.getResultCount(window),
+      1,
+      "There should be one result"
+    );
+
+    let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+    Assert.equal(
+      result.source,
+      UrlbarUtils.RESULT_SOURCE.SEARCH,
+      "Result source should be SEARCH"
+    );
+    Assert.equal(
+      result.type,
+      UrlbarUtils.RESULT_TYPE.SEARCH,
+      "Result type should be SEARCH"
+    );
+    Assert.ok(result.searchParams, "searchParams should be present");
+    Assert.equal(
+      result.searchParams.engine,
+      "Test",
+      "searchParams.engine should be our test engine"
+    );
+    Assert.equal(
+      result.searchParams.query,
+      "remote",
+      "searchParams.query should be our query"
+    );
+    Assert.ok(result.heuristic, "Result should be heuristic");
+
+    // Press enter.  The engine's SERP should load.
+    let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+    EventUtils.synthesizeKey("KEY_Enter");
+    await loadPromise;
+    Assert.equal(
+      gBrowser.currentURI.spec,
+      "http://example.com/?search=remote",
+      "Engine's SERP should have loaded"
+    );
+  });
+});
--- a/browser/components/urlbar/tests/browser/browser_searchMode_no_results.js
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_no_results.js
@@ -188,27 +188,26 @@ add_task(async function backspaceRemainO
       "Panel has results, therefore should not have noresults attribute"
     );
 
     // Enter search mode by clicking the bookmarks one-off.
     await UrlbarTestUtils.enterSearchMode(win, {
       source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
     });
 
-    // The heursitic should now be shown since we always show it in search mode
-    // when the search string is *not* empty, even when there are no other
-    // results.
-    Assert.greater(
+    // The heursitic should not be shown since we don't show it in local search
+    // modes.
+    Assert.equal(
       UrlbarTestUtils.getResultCount(win),
       0,
-      "At least the heuristic result should be shown"
+      "No results should be present"
     );
     Assert.ok(
-      !win.gURLBar.panel.hasAttribute("noresults"),
-      "Panel has results, therefore should not have noresults attribute"
+      win.gURLBar.panel.hasAttribute("noresults"),
+      "Panel has no results, therefore should have noresults attribute"
     );
 
     // Backspace.  The search string will now be empty.
     let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
     EventUtils.synthesizeKey("KEY_Backspace", {}, win);
     await searchPromise;
     Assert.ok(UrlbarTestUtils.isPopupOpen(win), "View remains open");
     await UrlbarTestUtils.assertSearchMode(win, {
--- a/browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js
@@ -62,16 +62,20 @@ async function doPickResultTest(initialU
       fireInputEvent: true,
     });
 
     await UrlbarTestUtils.enterSearchMode(window, {
       source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
     });
 
     // Arrow down to the bookmark result.
+    let firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+    if (!firstResult.heuristic) {
+      EventUtils.synthesizeKey("KEY_ArrowDown");
+    }
     let foundResult = false;
     for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
       let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
       if (result.url == BOOKMARK_URL) {
         foundResult = true;
         break;
       }
       EventUtils.synthesizeKey("KEY_ArrowDown");
new file mode 100644
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_waitForLoadOrTimeout.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the waitForLoadOrTimeout test helper function in head.js.
+ */
+
+"use strict";
+
+add_task(async function load() {
+  await BrowserTestUtils.withNewTab("about:blank", async () => {
+    let url = "http://example.com/";
+    await UrlbarTestUtils.promiseAutocompleteResultPopup({
+      window,
+      value: url,
+    });
+
+    let loadPromise = waitForLoadOrTimeout();
+    EventUtils.synthesizeKey("KEY_Enter");
+    let loadEvent = await loadPromise;
+
+    Assert.ok(loadEvent, "Page should have loaded before timeout");
+    Assert.equal(
+      loadEvent.target.currentURI.spec,
+      url,
+      "example.com should have loaded"
+    );
+  });
+});
+
+add_task(async function timeout() {
+  let loadEvent = await waitForLoadOrTimeout();
+  Assert.ok(
+    !loadEvent,
+    "No page should have loaded, and timeout should have fired"
+  );
+});
--- a/browser/components/urlbar/tests/browser/head.js
+++ b/browser/components/urlbar/tests/browser/head.js
@@ -48,16 +48,17 @@ async function selectAndPaste(str, win =
   win.gURLBar.select();
   win.document.commandDispatcher
     .getControllerForCommand("cmd_paste")
     .doCommand("cmd_paste");
 }
 
 /**
  * Updates the Top Sites feed.
+ *
  * @param {function} condition
  *   A callback that returns true after Top Sites are successfully updated.
  * @param {boolean} searchShortcuts
  *   True if Top Sites search shortcuts should be enabled.
  */
 async function updateTopSites(condition, searchShortcuts = false) {
   // Toggle the pref to clear the feed cache and force an update.
   await SpecialPowers.pushPrefEnv({
@@ -90,8 +91,42 @@ async function updateTopSites(condition,
  *   `val` with a space appended if it's a token alias, or just `val` otherwise.
  */
 function getAutofillSearchString(val) {
   if (!val.startsWith("@")) {
     return val;
   }
   return val + " ";
 }
+
+/**
+ * Waits for a load in any browser or a timeout, whichever comes first.
+ *
+ * @param {window} win
+ *   The top-level browser window to listen in.
+ * @param {number} timeoutMs
+ *   The timeout in ms.
+ * @returns {event|null}
+ *   If a load event was detected before the timeout fired, then the event is
+ *   returned.  event.target will be the browser in which the load occurred.  If
+ *   the timeout fired before a load was detected, null is returned.
+ */
+async function waitForLoadOrTimeout(win = window, timeoutMs = 1000) {
+  let event;
+  let listener;
+  let timeout;
+  let eventName = "BrowserTestUtils:ContentEvent:load";
+  try {
+    event = await Promise.race([
+      new Promise(resolve => {
+        listener = resolve;
+        win.addEventListener(eventName, listener, true);
+      }),
+      new Promise(resolve => {
+        timeout = win.setTimeout(resolve, timeoutMs);
+      }),
+    ]);
+  } finally {
+    win.removeEventListener(eventName, listener, true);
+    win.clearTimeout(timeout);
+  }
+  return event || null;
+}