Bug 1498181 - Initial methods for handling result selection in the new address bar architecture. r=dao
authorMark Banner <standard8@mozilla.com>
Fri, 12 Oct 2018 16:13:42 +0000
changeset 489345 e7c817964e523db7d5326b2da88f4601dc99f2b9
parent 489344 e7f9ba7e8ac2707c9ad1caf569ad134635caec1e
child 489346 dbab4e3611fb95778c944fc66ab9edca42c88133
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersdao
bugs1498181
milestone64.0a1
Bug 1498181 - Initial methods for handling result selection in the new address bar architecture. r=dao The intent here is that the input/view deal with the handling of clicks/pressing enter, and the controller only deals with opening the required item. Differential Revision: https://phabricator.services.mozilla.com/D8382
browser/components/urlbar/UrlbarController.jsm
browser/components/urlbar/UrlbarInput.jsm
browser/components/urlbar/UrlbarPrefs.jsm
browser/components/urlbar/UrlbarView.jsm
browser/components/urlbar/tests/browser/browser.ini
browser/components/urlbar/tests/browser/browser_UrlbarController_resultOpening.js
browser/components/urlbar/tests/browser/browser_UrlbarInput_unit.js
browser/components/urlbar/tests/browser/head.js
browser/components/urlbar/tests/unit/test_UrlbarController_integration.js
browser/components/urlbar/tests/unit/test_UrlbarController_unit.js
browser/components/urlbar/tests/unit/test_providersManager.js
--- a/browser/components/urlbar/UrlbarController.jsm
+++ b/browser/components/urlbar/UrlbarController.jsm
@@ -1,18 +1,24 @@
 /* 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";
 
 var EXPORTED_SYMBOLS = ["QueryContext", "UrlbarController"];
 
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-ChromeUtils.import("resource:///modules/UrlbarProvidersManager.jsm");
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  // BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.jsm",
+  Services: "resource://gre/modules/Services.jsm",
+  UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+  UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+  UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
 
 /**
  * QueryContext defines a user's autocomplete input from within the Address Bar.
  * It supplements it with details of how the search results should be obtained
  * and what they consist of.
  */
 class QueryContext {
   /**
@@ -73,24 +79,31 @@ class QueryContext {
  * - onQueryResults(queryContext)
  * - onQueryCancelled(queryContext)
  */
 class UrlbarController {
   /**
    * Initialises the class. The manager may be overridden here, this is for
    * test purposes.
    *
-   * @param {object} [options]
+   * @param {object} options
    *   The initial options for UrlbarController.
+   * @param {object} options.window
+   *   The window this controller is operating within.
    * @param {object} [options.manager]
    *   Optional fake providers manager to override the built-in providers manager.
    *   Intended for use in unit tests only.
    */
   constructor(options = {}) {
+    if (!options.window) {
+      throw new Error("Missing options: window");
+    }
+
     this.manager = options.manager || UrlbarProvidersManager;
+    this.window = options.window;
 
     this._listeners = new Set();
   }
 
   /**
    * Takes a query context and starts the query based on the user input.
    *
    * @param {QueryContext} queryContext The query details.
@@ -120,16 +133,114 @@ class UrlbarController {
    *
    * @param {QueryContext} queryContext The query details.
    */
   receiveResults(queryContext) {
     this._notify("onQueryResults", queryContext);
   }
 
   /**
+   * Handles the case where a url or other text has been entered into the
+   * urlbar. This will either load the URL, or some text that could be a keyword
+   * or a simple value to load via the default search engine.
+   *
+   * @param {Event} event The event that triggered this.
+   * @param {string} text The text that was entered into the urlbar.
+   * @param {string} [openWhere] Where we expect the result to be opened.
+   * @param {object} [openParams]
+   *   The parameters related to how and where the result will be opened.
+   *   For possible properties @see {_loadURL}
+   */
+  handleEnteredText(event, text, openWhere, openParams = {}) {
+    let browser = this.window.gBrowser.selectedBrowser;
+    let where = openWhere || this._whereToOpen(event);
+
+    openParams.postData = null;
+    openParams.allowInheritPrincipal = false;
+
+    // TODO: Work out how we get the user selection behavior, probably via passing
+    // it in, since we don't have the old autocomplete controller to work with.
+    // BrowserUsageTelemetry.recordUrlbarSelectedResultMethod(
+    //   event, this.userSelectionBehavior);
+
+    text = text.trim();
+
+    try {
+      new URL(text);
+    } catch (ex) {
+      // TODO: Figure out why we need lastLocationChange here.
+      // TODO: Possibly move getShortcutOrURIAndPostData into a utility function
+      // in a jsm (there's nothing window specific about it).
+      // let lastLocationChange = browser.lastLocationChange;
+      // getShortcutOrURIAndPostData(text).then(data => {
+      //   if (where != "current" ||
+      //       browser.lastLocationChange == lastLocationChange) {
+      //     params.postData = data.postData;
+      //     params.allowInheritPrincipal = data.mayInheritPrincipal;
+      //     this._loadURL(data.url, browser, where,
+      //                   openUILinkParams);
+      //   }
+      // });
+      return;
+    }
+
+    this._loadURL(text, browser, where, openParams);
+  }
+
+  /**
+   * Opens a specific result that has been selected.
+   *
+   * @param {Event} event The event that triggered this.
+   * @param {UrlbarMatch} result The result that was selected.
+   * @param {string} [openWhere] Where we expect the result to be opened.
+   * @param {object} [openParams]
+   *   The parameters related to how and where the result will be opened.
+   *   For possible properties @see {_loadURL}
+   */
+  resultSelected(event, result, openWhere, openParams = {}) {
+    // TODO: Work out how we get the user selection behavior, probably via passing
+    // it in, since we don't have the old autocomplete controller to work with.
+    // BrowserUsageTelemetry.recordUrlbarSelectedResultMethod(
+    //   event, this.userSelectionBehavior);
+
+    let where = openWhere || this._whereToOpen(event);
+    openParams.postData = null;
+    openParams.allowInheritPrincipal = false;
+    let browser = this.window.gBrowser.selectedBrowser;
+    let url = result.url;
+
+    switch (result.type) {
+      case UrlbarUtils.MATCH_TYPE.TAB_SWITCH: {
+        // TODO: Implement handleRevert or equivalent on the input.
+        // this.input.handleRevert();
+        let prevTab = this.window.gBrowser.selectedTab;
+        let loadOpts = {
+          adoptIntoActiveWindow: UrlbarPrefs.get("switchTabs.adoptIntoActiveWindow"),
+        };
+
+        if (this.window.switchToTabHavingURI(url, false, loadOpts) &&
+            this.window.isTabEmpty(prevTab)) {
+          this.window.gBrowser.removeTab(prevTab);
+        }
+        return;
+
+        // TODO: How to handle meta chars?
+        // Once we get here, we got a TAB_SWITCH match but the user
+        // bypassed it by pressing shift/meta/ctrl. Those modifiers
+        // might otherwise affect where we open - we always want to
+        // open in the current tab.
+        // where = "current";
+
+      }
+    }
+
+    this._loadURL(url, browser, where, openParams);
+  }
+
+  /**
    * Adds a listener for query actions and results.
    *
    * @param {object} listener The listener to add.
    * @throws {TypeError} Throws if the listener is not an object.
    */
   addQueryListener(listener) {
     if (!listener || typeof listener != "object") {
       throw new TypeError("Expected listener to be an object");
@@ -164,9 +275,118 @@ class UrlbarController {
     for (let listener of this._listeners) {
       try {
         listener[name](...params);
       } catch (ex) {
         Cu.reportError(ex);
       }
     }
   }
+
+  /**
+   * Loads the url in the appropriate place.
+   *
+   * @param {string} url
+   *   The URL to open.
+   * @param {object} browser
+   *   The browser to open it in.
+   * @param {string} openUILinkWhere
+   *   Where we expect the result to be opened.
+   * @param {object} params
+   *   The parameters related to how and where the result will be opened.
+   *   Further supported paramters are listed in utilityOverlay.js#openUILinkIn.
+   * @param {object} params.triggeringPrincipal
+   *   The principal that the action was triggered from.
+   * @param {nsIInputStream} [params.postData]
+   *   The POST data associated with a search submission.
+   * @param {boolean} [params.allowInheritPrincipal]
+   *   If the principal may be inherited
+   */
+  _loadURL(url, browser, openUILinkWhere, params) {
+    // TODO: These should probably be set by the input field.
+    // this.value = url;
+    // browser.userTypedValue = url;
+    if (this.window.gInitialPages.includes(url)) {
+      browser.initialPageLoadedFromURLBar = url;
+    }
+    try {
+      // TODO: Move function to PlacesUIUtils.
+      this.window.addToUrlbarHistory(url);
+    } catch (ex) {
+      // Things may go wrong when adding url to session history,
+      // but don't let that interfere with the loading of the url.
+      Cu.reportError(ex);
+    }
+
+    params.allowThirdPartyFixup = true;
+
+    if (openUILinkWhere == "current") {
+      params.targetBrowser = browser;
+      params.indicateErrorPageLoad = true;
+      params.allowPinnedTabHostChange = true;
+      params.allowPopups = url.startsWith("javascript:");
+    } else {
+      params.initiatingDoc = this.window.document;
+    }
+
+    // Focus the content area before triggering loads, since if the load
+    // occurs in a new tab, we want focus to be restored to the content
+    // area when the current tab is re-selected.
+    browser.focus();
+
+    if (openUILinkWhere != "current") {
+      // TODO: Implement handleRevert or equivalent on the input.
+      // this.input.handleRevert();
+    }
+
+    try {
+      this.window.openTrustedLinkIn(url, openUILinkWhere, params);
+    } catch (ex) {
+      // This load can throw an exception in certain cases, which means
+      // we'll want to replace the URL with the loaded URL:
+      if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
+        // TODO: Implement handleRevert or equivalent on the input.
+        // this.input.handleRevert();
+      }
+    }
+
+    // TODO This should probably be handed via input.
+    // Ensure the start of the URL is visible for usability reasons.
+    // this.selectionStart = this.selectionEnd = 0;
+  }
+
+  /**
+   * Determines where a URL/page should be opened.
+   *
+   * @param {Event} event the event triggering the opening.
+   * @returns {"current" | "tabshifted" | "tab" | "save" | "window"}
+   */
+  _whereToOpen(event) {
+    let isMouseEvent = event instanceof this.window.MouseEvent;
+    let reuseEmpty = !isMouseEvent;
+    let where = undefined;
+    if (!isMouseEvent && event && event.altKey) {
+      // We support using 'alt' to open in a tab, because ctrl/shift
+      // might be used for canonizing URLs:
+      where = event.shiftKey ? "tabshifted" : "tab";
+    } else if (!isMouseEvent && this._ctrlCanonizesURLs && event && event.ctrlKey) {
+      // If we're allowing canonization, and this is a key event with ctrl
+      // pressed, open in current tab to allow ctrl-enter to canonize URL.
+      where = "current";
+    } else {
+      where = this.window.whereToOpenLink(event, false, false);
+    }
+    if (this.openInTab) {
+      if (where == "current") {
+        where = "tab";
+      } else if (where == "tab") {
+        where = "current";
+      }
+      reuseEmpty = true;
+    }
+    if (where == "tab" &&
+        reuseEmpty &&
+        this.window.isTabEmpty(this.window.gBrowser.selectedTab)) {
+      where = "current";
+    }
+    return where;
+  }
 }
--- a/browser/components/urlbar/UrlbarInput.jsm
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -39,17 +39,19 @@ class UrlbarInput {
    *   Intended for use in unit tests only.
    */
   constructor(options = {}) {
     this.textbox = options.textbox;
     this.textbox.clickSelectsAll = UrlbarPrefs.get("clickSelectsAll");
 
     this.panel = options.panel;
     this.window = this.textbox.ownerGlobal;
-    this.controller = options.controller || new UrlbarController();
+    this.controller = options.controller || new UrlbarController({
+      window: this.window,
+    });
     this.view = new UrlbarView(this);
     this.valueIsTyped = false;
     this.userInitiatedFocus = false;
     this.isPrivate = PrivateBrowsingUtils.isWindowPrivate(this.window);
 
     // Forward textbox methods and properties.
     const METHODS = ["addEventListener", "removeEventListener",
       "setAttribute", "hasAttribute", "removeAttribute", "getAttribute",
@@ -93,16 +95,17 @@ class UrlbarInput {
     this.inputField.addEventListener("blur", this);
     this.inputField.addEventListener("focus", this);
     this.inputField.addEventListener("mousedown", this);
     this.inputField.addEventListener("mouseover", this);
     this.inputField.addEventListener("overflow", this);
     this.inputField.addEventListener("underflow", this);
     this.inputField.addEventListener("scrollend", this);
     this.inputField.addEventListener("select", this);
+    this.inputField.addEventListener("keyup", this);
 
     this.inputField.controllers.insertControllerAt(0, new CopyCutController(this));
   }
 
   /**
    * Shortens the given value, usually by removing http:// and trailing slashes,
    * such that calling nsIURIFixup::createFixupURI with the result will produce
    * the same URI.
@@ -151,29 +154,89 @@ class UrlbarInput {
     try {
       return Services.uriFixup.createExposableURI(uri);
     } catch (ex) {}
 
     return uri;
   }
 
   /**
-   * Passes DOM events for the textbox to the _on<event type> methods.
+   * Passes DOM events for the textbox to the _on_<event type> methods.
    * @param {Event} event
    *   DOM event from the <textbox>.
    */
   handleEvent(event) {
     let methodName = "_on_" + event.type;
     if (methodName in this) {
       this[methodName](event);
     } else {
       throw "Unrecognized urlbar event: " + event.type;
     }
   }
 
+  /**
+   * Handles an event which would cause a url or text to be opened.
+   * XXX the name is currently handleCommand which is compatible with
+   * urlbarBindings. However, it is no longer called automatically by autocomplete,
+   * See _on_keyup.
+   *
+   * @param {Event} event The event triggering the open.
+   * @param {string} [openWhere] Where we expect the result to be opened.
+   * @param {object} [openParams]
+   *   The parameters related to where the result will be opened.
+   * @param {object} [triggeringPrincipal]
+   *   The principal that the action was triggered from.
+   */
+  handleCommand(event, openWhere, openParams, triggeringPrincipal) {
+    let isMouseEvent = event instanceof this.window.MouseEvent;
+    if (isMouseEvent && event.button == 2) {
+      // Do nothing for right clicks.
+      return;
+    }
+
+    // TODO: Hook up one-off button handling.
+    // Determine whether to use the selected one-off search button.  In
+    // one-off search buttons parlance, "selected" means that the button
+    // has been navigated to via the keyboard.  So we want to use it if
+    // the triggering event is not a mouse click -- i.e., it's a Return
+    // key -- or if the one-off was mouse-clicked.
+    // let selectedOneOff = this.popup.oneOffSearchButtons.selectedButton;
+    // if (selectedOneOff &&
+    //     isMouseEvent &&
+    //     event.originalTarget != selectedOneOff) {
+    //   selectedOneOff = null;
+    // }
+    //
+    // // Do the command of the selected one-off if it's not an engine.
+    // if (selectedOneOff && !selectedOneOff.engine) {
+    //   selectedOneOff.doCommand();
+    //   return;
+    // }
+
+    let url = this.value;
+    if (!url) {
+      return;
+    }
+
+    this.controller.handleEnteredText(event, url);
+
+    this.view.close();
+  }
+
+  /**
+   * Called by the view when a result is selected.
+   *
+   * @param {Event} event The event that selected the result.
+   * @param {UrlbarMatch} result The result that was selected.
+   */
+  resultSelected(event, result) {
+    // TODO: Set the input value to the target url.
+    this.controller.resultSelected(event, result);
+  }
+
   // Getters and Setters below.
 
   get focused() {
     return this.textbox.getAttribute("focused") == "true";
   }
 
   get value() {
     return this.inputField.value;
@@ -410,16 +473,25 @@ class UrlbarInput {
 
   _on_scrollend(event) {
     this._updateTextOverflow();
   }
 
   _on_TabSelect(event) {
     this.controller.tabContextChanged();
   }
+
+  _on_keyup(event) {
+    // TODO: We may have an autoFill entry, so we should use that instead.
+    // TODO: We should have an input bufferrer so that we can use search results
+    // if appropriate.
+    if (event.key == "Enter") {
+      this.handleCommand(event);
+    }
+  }
 }
 
 /**
  * Handles copy and cut commands for the urlbar.
  */
 class CopyCutController {
   /**
    * @param {UrlbarInput} urlbar
--- a/browser/components/urlbar/UrlbarPrefs.jsm
+++ b/browser/components/urlbar/UrlbarPrefs.jsm
@@ -103,16 +103,20 @@ const PREF_URLBAR_DEFAULTS = new Map([
   ["suggest.history.onlyTyped", false],
 
   // Results will include switch-to-tab results when this is true.
   ["suggest.openpage", true],
 
   // Results will include search suggestions when this is true.
   ["suggest.searches", false],
 
+  // When using switch to tabs, if set to true this will move the tab into the
+  // active window.
+  ["switchTabs.adoptIntoActiveWindow", false],
+
   // Remove redundant portions from URLs.
   ["trimURLs", true],
 
   // Results will include a built-in set of popular domains when this is true.
   ["usepreloadedtopurls.enabled", true],
 
   // After this many days from the profile creation date, the built-in set of
   // popular domains will no longer be included in the results.
--- a/browser/components/urlbar/UrlbarView.jsm
+++ b/browser/components/urlbar/UrlbarView.jsm
@@ -70,46 +70,51 @@ class UrlbarView {
 
     this._rows.firstElementChild.toggleAttribute("selected", true);
   }
 
   /**
    * Closes the autocomplete results popup.
    */
   close() {
+    this.panel.hidePopup();
   }
 
   // UrlbarController listener methods.
   onQueryStarted(queryContext) {
     this._rows.textContent = "";
   }
 
   onQueryResults(queryContext) {
     // XXX For now, clear the results for each set received. We should really
     // be updating the existing list.
     this._rows.textContent = "";
-    for (let result of queryContext.results) {
-      this._addRow(result);
+    this._queryContext = queryContext;
+    for (let resultIndex in queryContext.results) {
+      this._addRow(resultIndex);
     }
     this.open();
   }
 
   // Private methods below.
 
   _getBoundsWithoutFlushing(element) {
     return this.window.windowUtils.getBoundsWithoutFlushing(element);
   }
 
   _createElement(name) {
     return this.document.createElementNS("http://www.w3.org/1999/xhtml", name);
   }
 
-  _addRow(result) {
+  _addRow(resultIndex) {
+    let result = this._queryContext.results[resultIndex];
     let item = this._createElement("div");
     item.className = "urlbarView-row";
+    item.addEventListener("click", this);
+    item.setAttribute("resultIndex", resultIndex);
     if (result.type == UrlbarUtils.MATCH_TYPE.TAB_SWITCH) {
       item.setAttribute("action", "switch-to-tab");
     }
 
     let content = this._createElement("span");
     content.className = "urlbarView-row-inner";
     item.appendChild(content);
 
@@ -134,9 +139,36 @@ class UrlbarView {
     } else {
       secondary.classList.add("urlbarView-url");
       secondary.textContent = result.url;
     }
     content.appendChild(secondary);
 
     this._rows.appendChild(item);
   }
+
+  /**
+   * Passes DOM events for the view to the _on_<event type> methods.
+   * @param {Event} event
+   *   DOM event from the <view>.
+   */
+  handleEvent(event) {
+    let methodName = "_on_" + event.type;
+    if (methodName in this) {
+      this[methodName](event);
+    } else {
+      throw "Unrecognized urlbar event: " + event.type;
+    }
+  }
+
+  _on_click(event) {
+    let row = event.target;
+    while (!row.classList.contains("urlbarView-row")) {
+      row = row.parentNode;
+    }
+    let resultIndex = row.getAttribute("resultIndex");
+    let result = this._queryContext.results[resultIndex];
+    if (result) {
+      this.urlbar.resultSelected(event, result);
+    }
+    this.close();
+  }
 }
--- a/browser/components/urlbar/tests/browser/browser.ini
+++ b/browser/components/urlbar/tests/browser/browser.ini
@@ -1,8 +1,11 @@
 # 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/.
 
 [DEFAULT]
+support-files =
+  head.js
 
+[browser_UrlbarController_resultOpening.js]
 [browser_UrlbarInput_unit.js]
 support-files = empty.xul
new file mode 100644
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarController_resultOpening.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests unit test the result/url loading functionality of UrlbarController.
+ */
+
+"use strict";
+
+let controller;
+
+add_task(async function setup() {
+  sandbox = sinon.sandbox.create();
+
+  controller = new UrlbarController({
+    window,
+  });
+
+  registerCleanupFunction(async () => {
+    sandbox.restore();
+  });
+});
+
+add_task(function test_handleEnteredText_url() {
+  sandbox.stub(window, "openTrustedLinkIn");
+
+  const event = new KeyboardEvent("keyup", {key: "Enter"});
+  // Additional spaces in the url to check that we trim correctly.
+  controller.handleEnteredText(event, " https://example.com ");
+
+  Assert.ok(window.openTrustedLinkIn.calledOnce,
+    "Should have triggered opening the url.");
+
+  let args = window.openTrustedLinkIn.args[0];
+
+  Assert.equal(args[0], "https://example.com",
+    "Should have triggered opening with the correct url");
+  Assert.equal(args[1], "current",
+    "Should be opening in the current browser");
+  Assert.deepEqual(args[2], {
+    allowInheritPrincipal: false,
+    allowPinnedTabHostChange: true,
+    allowPopups: false,
+    allowThirdPartyFixup: true,
+    indicateErrorPageLoad: true,
+    postData: null,
+    targetBrowser: gBrowser.selectedBrowser,
+  }, "Should have the correct additional parameters for opening");
+
+  sandbox.restore();
+});
+
+add_task(function test_resultSelected_switchtab() {
+  sandbox.stub(window, "switchToTabHavingURI").returns(true);
+  sandbox.stub(window, "isTabEmpty").returns(false);
+  sandbox.stub(window.gBrowser, "removeTab");
+
+  const event = new MouseEvent("click", {button: 0});
+  const url = "https://example.com/1";
+  const result = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH, {url});
+
+  controller.resultSelected(event, result);
+
+  Assert.ok(window.switchToTabHavingURI.calledOnce,
+    "Should have triggered switching to the tab");
+
+  let args = window.switchToTabHavingURI.args[0];
+
+  Assert.equal(args[0], url, "Should have passed the expected url");
+  Assert.ok(!args[1], "Should not attempt to open a new tab");
+  Assert.deepEqual(args[2], {
+    adoptIntoActiveWindow: UrlbarPrefs.get("switchTabs.adoptIntoActiveWindow"),
+  }, "Should have the correct additional parameters for opening");
+
+  sandbox.restore();
+});
--- a/browser/components/urlbar/tests/browser/browser_UrlbarInput_unit.js
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_unit.js
@@ -4,30 +4,20 @@
 /**
  * These tests unit test the functionality of UrlbarController by stubbing out the
  * model and providing stubs to be called.
  */
 
 "use strict";
 
 let fakeController;
-let sandbox;
 let generalListener;
 let input;
 let inputOptions;
 
-ChromeUtils.import("resource:///modules/UrlbarController.jsm", this);
-
-/* global sinon */
-Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js");
-
-registerCleanupFunction(function() {
-  delete window.sinon;
-});
-
 /**
  * Asserts that the query context has the expected values.
  *
  * @param {QueryContext} context
  * @param {object} expectedValues The expected values for the QueryContext.
  */
 function assertContextMatches(context, expectedValues) {
   Assert.ok(context instanceof QueryContext,
@@ -64,17 +54,19 @@ function checkStartQueryCall(stub, expec
     Assert.deepEqual(queryContext[name],
      value, `Should have the correct value for queryContext.${name}`);
   }
 }
 
 add_task(async function setup() {
   sandbox = sinon.sandbox.create();
 
-  fakeController = new UrlbarController();
+  fakeController = new UrlbarController({
+    window,
+  });
 
   sandbox.stub(fakeController, "startQuery");
   sandbox.stub(PrivateBrowsingUtils, "isWindowPrivate").returns(false);
 
   // Open a new window, so we don't affect other tests by adding extra
   // UrbarInput wrappers around the urlbar.
   let gTestRoot = getRootDirectory(gTestPath);
 
new file mode 100644
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/head.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests unit test the result/url loading functionality of UrlbarController.
+ */
+
+"use strict";
+
+let sandbox;
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Services: "resource://gre/modules/Services.jsm",
+  QueryContext: "resource:///modules/UrlbarController.jsm",
+  UrlbarController: "resource:///modules/UrlbarController.jsm",
+  UrlbarMatch: "resource:///modules/UrlbarMatch.jsm",
+  UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+/* global sinon */
+Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js");
+
+registerCleanupFunction(function() {
+  delete window.sinon;
+});
--- a/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js
@@ -25,17 +25,19 @@ function assertContextMatches(context, e
 
   for (let [key, value] of Object.entries(expectedValues)) {
     Assert.equal(context[key], value,
       `Should have the expected value for ${key} in the QueryContext`);
   }
 }
 
 add_task(async function setup() {
-  controller = new UrlbarController();
+  controller = new UrlbarController({
+    window: {},
+  });
 });
 
 add_task(async function test_basic_search() {
   const context = createContext(TEST_URL);
 
   registerBasicTestProvider([match]);
 
   let startedPromise = promiseControllerNotification(controller, "onQueryStarted");
--- a/browser/components/urlbar/tests/unit/test_UrlbarController_unit.js
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_unit.js
@@ -41,16 +41,17 @@ add_task(function setup() {
   generalListener = {
     onQueryStarted: sandbox.stub(),
     onQueryResults: sandbox.stub(),
     onQueryCancelled: sandbox.stub(),
   };
 
   controller = new UrlbarController({
     manager: fPM,
+    window: {},
   });
   controller.addQueryListener(generalListener);
 });
 
 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");
--- a/browser/components/urlbar/tests/unit/test_providersManager.js
+++ b/browser/components/urlbar/tests/unit/test_providersManager.js
@@ -3,17 +3,19 @@
 
 "use strict";
 
 add_task(async function test_providers() {
   let match = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH, { url: "http://mozilla.org/foo/" });
   registerBasicTestProvider([match]);
 
   let context = createContext();
-  let controller = new UrlbarController();
+  let controller = new UrlbarController({
+    window: {},
+  });
   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;