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 499350 e7c817964e523db7d5326b2da88f4601dc99f2b9
parent 499349 e7f9ba7e8ac2707c9ad1caf569ad134635caec1e
child 499351 dbab4e3611fb95778c944fc66ab9edca42c88133
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdao
bugs1498181
milestone64.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 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;