Bug 1530675 - Enable WebNavigation transistion notifications for WebExtensions with QuantumBar. r=mak,rpl
authorMark Banner <standard8@mozilla.com>
Wed, 20 Mar 2019 20:22:35 +0000
changeset 465383 8aef30b8a2be10a22438549491fd05ee64b03fcf
parent 465382 d517ff77de9ef47f5a987e2821bed56a2f2c538a
child 465384 747a5da93708d6ad12832272d794b21d825a4bb9
push id35738
push userccoroiu@mozilla.com
push dateThu, 21 Mar 2019 21:59:09 +0000
treeherdermozilla-central@7eb8e627961c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak, rpl
bugs1530675
milestone68.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 1530675 - Enable WebNavigation transistion notifications for WebExtensions with QuantumBar. r=mak,rpl Differential Revision: https://phabricator.services.mozilla.com/D21218
browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js
browser/components/urlbar/UrlbarInput.jsm
browser/components/urlbar/UrlbarUtils.jsm
toolkit/modules/addons/WebNavigation.jsm
--- a/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js
@@ -1,27 +1,22 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 ChromeUtils.defineModuleGetter(this, "PlacesUtils",
                                "resource://gre/modules/PlacesUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "UrlbarTestUtils",
+                               "resource://testing-common/UrlbarTestUtils.jsm");
 
 const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
 const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
 
-async function promiseAutocompleteResultPopup(inputText) {
-  gURLBar.focus();
-  gURLBar.value = inputText;
-  gURLBar.controller.startSearch(inputText);
-  await promisePopupShown(gURLBar.popup);
-  await BrowserTestUtils.waitForCondition(() => {
-    return gURLBar.controller.searchStatus >=
-      Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH;
-  });
+function promiseAutocompleteResultPopup(inputText) {
+  return UrlbarTestUtils.promiseAutocompleteResultPopup(window, inputText, waitForFocus);
 }
 
 async function addBookmark(bookmark) {
   if (bookmark.keyword) {
     await PlacesUtils.keywords.insert({
       keyword: bookmark.keyword,
       url: bookmark.url,
     });
@@ -55,20 +50,17 @@ async function prepareSearchEngine() {
   let engine = await addSearchEngine(TEST_ENGINE_BASENAME);
   await Services.search.setDefault(engine);
 
   registerCleanupFunction(async function() {
     Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
     await Services.search.setDefault(oldDefaultEngine);
 
     // Make sure the popup is closed for the next test.
-    gURLBar.blur();
-    gURLBar.popup.selectedIndex = -1;
-    gURLBar.popup.hidePopup();
-    ok(!gURLBar.popup.popupOpen, "popup should be closed");
+    await UrlbarTestUtils.promisePopupClose(window);
 
     // Clicking suggestions causes visits to search results pages, so clear that
     // history now.
     await PlacesUtils.history.clear();
   });
 }
 
 add_task(async function test_webnavigation_urlbar_typed_transitions() {
@@ -104,17 +96,56 @@ add_task(async function test_webnavigati
   const inputValue = "http://example.com/?q=typed";
   gURLBar.inputField.value = inputValue.slice(0, -1);
   EventUtils.sendString(inputValue.slice(-1));
   EventUtils.synthesizeKey("VK_RETURN", {altKey: true});
 
   await extension.awaitFinish("webNavigation.from_address_bar.typed");
 
   await extension.unload();
-  info("extension unloaded");
+});
+
+add_task(async function test_webnavigation_urlbar_typed_closed_popup_transitions() {
+  function backgroundScript() {
+    browser.webNavigation.onCommitted.addListener((msg) => {
+      browser.test.assertEq("http://example.com/?q=typedClosed", msg.url,
+                            "Got the expected url");
+      // assert from_address_bar transition qualifier
+      browser.test.assertTrue(msg.transitionQualifiers &&
+                              msg.transitionQualifiers.includes("from_address_bar"),
+                              "Got the expected from_address_bar transitionQualifier");
+      browser.test.assertEq("typed", msg.transitionType,
+                            "Got the expected transitionType");
+      browser.test.notifyPass("webNavigation.from_address_bar.typed");
+    });
+
+    browser.test.sendMessage("ready");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: backgroundScript,
+    manifest: {
+      permissions: ["webNavigation"],
+    },
+  });
+
+  await extension.startup();
+  await SimpleTest.promiseFocus(window);
+
+  await extension.awaitMessage("ready");
+  await UrlbarTestUtils.promiseAutocompleteResultPopup(window, "http://example.com/?q=typedClosed", waitForFocus);
+  await UrlbarTestUtils.promiseSearchComplete(window);
+  // Closing the popup forces a different code route that handles no results
+  // being displayed.
+  await UrlbarTestUtils.promisePopupClose(window);
+  EventUtils.synthesizeKey("VK_RETURN", {});
+
+  await extension.awaitFinish("webNavigation.from_address_bar.typed");
+
+  await extension.unload();
 });
 
 add_task(async function test_webnavigation_urlbar_bookmark_transitions() {
   function backgroundScript() {
     browser.webNavigation.onCommitted.addListener((msg) => {
       browser.test.assertEq("http://example.com/?q=bookmark", msg.url,
                             "Got the expected url");
 
@@ -144,22 +175,21 @@ add_task(async function test_webnavigati
 
   await extension.startup();
   await SimpleTest.promiseFocus(window);
 
   await extension.awaitMessage("ready");
 
   await promiseAutocompleteResultPopup("Bookmark To Click");
 
-  let item = gURLBar.popup.richlistbox.getItemAtIndex(1);
-  item.click();
+  let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+  EventUtils.synthesizeMouseAtCenter(result.element.row, {});
   await extension.awaitFinish("webNavigation.from_address_bar.auto_bookmark");
 
   await extension.unload();
-  info("extension unloaded");
 });
 
 add_task(async function test_webnavigation_urlbar_keyword_transition() {
   function backgroundScript() {
     browser.webNavigation.onCommitted.addListener((msg) => {
       browser.test.assertEq(`http://example.com/?q=search`, msg.url,
                             "Got the expected url");
 
@@ -190,23 +220,22 @@ add_task(async function test_webnavigati
 
   await extension.startup();
   await SimpleTest.promiseFocus(window);
 
   await extension.awaitMessage("ready");
 
   await promiseAutocompleteResultPopup("testkw search");
 
-  let item = gURLBar.popup.richlistbox.getItemAtIndex(0);
-  item.click();
+  let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+  EventUtils.synthesizeMouseAtCenter(result.element.row, {});
 
   await extension.awaitFinish("webNavigation.from_address_bar.keyword");
 
   await extension.unload();
-  info("extension unloaded");
 });
 
 add_task(async function test_webnavigation_urlbar_search_transitions() {
   function backgroundScript() {
     browser.webNavigation.onCommitted.addListener((msg) => {
       browser.test.assertEq("http://mochi.test:8888/", msg.url,
                             "Got the expected url");
 
@@ -232,16 +261,15 @@ add_task(async function test_webnavigati
   await extension.startup();
   await SimpleTest.promiseFocus(window);
 
   await extension.awaitMessage("ready");
 
   await prepareSearchEngine();
   await promiseAutocompleteResultPopup("foo");
 
-  let item = gURLBar.popup.richlistbox.getItemAtIndex(0);
-  item.click();
+  let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+  EventUtils.synthesizeMouseAtCenter(result.element.row, {});
 
   await extension.awaitFinish("webNavigation.from_address_bar.generated");
 
   await extension.unload();
-  info("extension unloaded");
 });
--- a/browser/components/urlbar/UrlbarInput.jsm
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -340,17 +340,17 @@ class UrlbarInput {
       let browser = this.window.gBrowser.selectedBrowser;
       let lastLocationChange = browser.lastLocationChange;
 
       UrlbarUtils.getShortcutOrURIAndPostData(url).then(data => {
         if (where != "current" ||
             browser.lastLocationChange == lastLocationChange) {
           openParams.postData = data.postData;
           openParams.allowInheritPrincipal = data.mayInheritPrincipal;
-          this._loadURL(data.url, where, openParams, browser);
+          this._loadURL(data.url, where, openParams, null, browser);
         }
       });
       return;
     }
 
     this._loadURL(url, where, openParams);
   }
 
@@ -440,17 +440,20 @@ class UrlbarInput {
                                                   where);
         return;
       }
     }
 
     if (!url) {
       throw new Error(`Invalid url for result ${JSON.stringify(result)}`);
     }
-    this._loadURL(url, where, openParams);
+    this._loadURL(url, where, openParams, {
+      source: result.source,
+      type: result.type,
+    });
   }
 
   /**
    * Called by the view when moving through results with the keyboard, and when
    * picking a result.
    *
    * @param {UrlbarResult} [result]
    *   The result that was selected or picked, null if no result was selected.
@@ -981,19 +984,25 @@ class UrlbarInput {
    *   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
+   * @param {object} [result]
+   *   Details of the selected result, if any
+   * @param {UrlbarUtils.RESULT_TYPE} [result.type]
+   *   Details of the result type, if any.
+   * @param {UrlbarUtils.RESULT_SOURCE} [result.source]
+   *   Details of the result source, if any.
    * @param {object} browser [optional] the browser to use for the load.
    */
-  _loadURL(url, openUILinkWhere, params,
+  _loadURL(url, openUILinkWhere, params, result = {},
            browser = this.window.gBrowser.selectedBrowser) {
     this.value = url;
     browser.userTypedValue = url;
 
     if (this.window.gInitialPages.includes(url)) {
       browser.initialPageLoadedFromUserAction = url;
     }
     try {
@@ -1027,16 +1036,19 @@ class UrlbarInput {
     // 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") {
       this.handleRevert();
     }
 
+    // Notify about the start of navigation.
+    this._notifyStartNavigation(result);
+
     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) {
         this.handleRevert();
       }
@@ -1126,16 +1138,30 @@ class UrlbarInput {
       } else {
         pasteAndGo.setAttribute("disabled", "true");
       }
     });
 
     insertLocation.insertAdjacentElement("afterend", pasteAndGo);
   }
 
+  /**
+   * This notifies observers that the user has entered or selected something in
+   * the URL bar which will cause navigation.
+   *
+   * We use the observer service, so that we don't need to load extra facilities
+   * if they aren't being used, e.g. WebNavigation.
+   *
+   * @param {UrlbarResult} [result]
+   *   The result that was selected, if any.
+   */
+  _notifyStartNavigation(result) {
+    Services.obs.notifyObservers({result}, "urlbar-user-start-navigation");
+  }
+
   // Event handlers below.
 
   _on_blur(event) {
     this.formatValue();
     // Respect the autohide preference for easier inspecting/debugging via
     // the browser toolbox.
     if (!UrlbarPrefs.get("ui.popup.disable_autohide")) {
       this.view.close(UrlbarUtils.CANCEL_REASON.BLUR);
--- a/browser/components/urlbar/UrlbarUtils.jsm
+++ b/browser/components/urlbar/UrlbarUtils.jsm
@@ -61,16 +61,18 @@ var UrlbarUtils = {
     PROFILE: 2,
     // Can be delayed, contains results coming from the network.
     NETWORK: 3,
     // Can be delayed, contains results coming from unknown sources.
     EXTENSION: 4,
   },
 
   // Defines UrlbarResult types.
+  // If you add new result types, consider checking if consumers of
+  // "urlbar-user-start-navigation" need update as well.
   RESULT_TYPE: {
     // An open tab.
     // Payload: { icon, url, userContextId }
     TAB_SWITCH: 1,
     // A search suggestion or engine.
     // Payload: { icon, suggestion, keyword, query }
     SEARCH: 2,
     // A common url/title tuple, may be a bookmark with tags.
@@ -86,16 +88,18 @@ var UrlbarUtils = {
     // Payload: { url, icon, device, title }
     REMOTE_TAB: 6,
   },
 
   // This defines the source of results returned by a provider. Each provider
   // can return results from more than one source. This is used by the
   // ProvidersManager to decide which providers must be queried and which
   // results can be returned.
+  // If you add new source types, consider checking if consumers of
+  // "urlbar-user-start-navigation" need update as well.
   RESULT_SOURCE: {
     BOOKMARKS: 1,
     HISTORY: 2,
     SEARCH: 3,
     TABS: 4,
     OTHER_LOCAL: 5,
     OTHER_NETWORK: 6,
   },
--- a/toolkit/modules/addons/WebNavigation.jsm
+++ b/toolkit/modules/addons/WebNavigation.jsm
@@ -7,16 +7,18 @@
 const EXPORTED_SYMBOLS = ["WebNavigation"];
 
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 ChromeUtils.defineModuleGetter(this, "BrowserWindowTracker",
                                "resource:///modules/BrowserWindowTracker.jsm");
 ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
                                "resource://gre/modules/PrivateBrowsingUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "UrlbarUtils",
+                               "resource:///modules/UrlbarUtils.jsm");
 
 // Maximum amount of time that can be passed and still consider
 // the data recent (similar to how is done in nsNavHistory,
 // e.g. nsNavHistory::CheckIsRecentEvent, but with a lower threshold value).
 const RECENT_DATA_THRESHOLD = 5 * 1000000;
 
 var Manager = {
   // Map[string -> Map[listener -> URLFilter]]
@@ -28,32 +30,34 @@ var Manager = {
     this.recentTabTransitionData = new WeakMap();
 
     // Collect the pending created navigation target events that still have to
     // pair the message received from the source tab to the one received from
     // the new tab.
     this.createdNavigationTargetByOuterWindowId = new Map();
 
     Services.obs.addObserver(this, "autocomplete-did-enter-text", true);
+    Services.obs.addObserver(this, "urlbar-user-start-navigation", true);
 
     Services.obs.addObserver(this, "webNavigation-createdNavigationTarget");
 
     Services.mm.addMessageListener("Content:Click", this);
     Services.mm.addMessageListener("Extension:DOMContentLoaded", this);
     Services.mm.addMessageListener("Extension:StateChange", this);
     Services.mm.addMessageListener("Extension:DocumentChange", this);
     Services.mm.addMessageListener("Extension:HistoryChange", this);
     Services.mm.addMessageListener("Extension:CreatedNavigationTarget", this);
 
     Services.mm.loadFrameScript("resource://gre/modules/WebNavigationContent.js", true);
   },
 
   uninit() {
     // Stop collecting recent tab transition data and reset the WeakMap.
     Services.obs.removeObserver(this, "autocomplete-did-enter-text");
+    Services.obs.removeObserver(this, "urlbar-user-start-navigation");
     Services.obs.removeObserver(this, "webNavigation-createdNavigationTarget");
 
     Services.mm.removeMessageListener("Content:Click", this);
     Services.mm.removeMessageListener("Extension:StateChange", this);
     Services.mm.removeMessageListener("Extension:DocumentChange", this);
     Services.mm.removeMessageListener("Extension:HistoryChange", this);
     Services.mm.removeMessageListener("Extension:DOMContentLoaded", this);
     Services.mm.removeMessageListener("Extension:CreatedNavigationTarget", this);
@@ -103,17 +107,21 @@ var Manager = {
    * and webNavigation-createdNavigationTarget (to fire the onCreatedNavigationTarget
    * related to windows or tabs opened from the main process) topics.
    *
    * @param {nsIAutoCompleteInput|Object} subject
    * @param {string} topic
    * @param {string|undefined} data
    */
   observe: function(subject, topic, data) {
-    if (topic == "autocomplete-did-enter-text") {
+    if (topic == "urlbar-user-start-navigation") {
+      this.onURLBarUserStartNavigation(subject.wrappedJSObject);
+    } else if (topic == "autocomplete-did-enter-text") {
+      // autocomplete-did-enter-text supports the legacy urlbar. Bug 1535379 will
+      // clean this up.
       this.onURLBarAutoCompletion(subject);
     } else if (topic == "webNavigation-createdNavigationTarget") {
       // The observed notification is coming from privileged JavaScript components running
       // in the main process (e.g. when a new tab or window is opened using the context menu
       // or Ctrl/Shift + click on a link).
       const {
         createdTabBrowser,
         url,
@@ -129,83 +137,142 @@ var Manager = {
     }
   },
 
   /**
    * Recognize the type of urlbar user interaction (e.g. typing a new url,
    * clicking on an url generated from a searchengine or a keyword, or a
    * bookmark found by the urlbar autocompletion).
    *
+   * @param {object} acData
+   *   The data for the autocompleted item.
+   * @param {object} [acData.result]
+   *   The result information associated with the navigation action.
+   * @param {UrlbarUtils.RESULT_TYPE} [acData.result.type]
+   *   The result type associated with the navigation action.
+   * @param {UrlbarUtils.RESULT_SOURCE} [acData.result.source]
+   *   The result source associated with the navigation action.
+   */
+  onURLBarUserStartNavigation(acData) {
+    let tabTransitionData = {
+      from_address_bar: true,
+    };
+
+    if (!acData.result) {
+      tabTransitionData.typed = true;
+    } else {
+      switch (acData.result.type) {
+        case UrlbarUtils.RESULT_TYPE.KEYWORD:
+          tabTransitionData.keyword = true;
+          break;
+        case UrlbarUtils.RESULT_TYPE.SEARCH:
+          tabTransitionData.generated = true;
+          break;
+        case UrlbarUtils.RESULT_TYPE.URL:
+          if (acData.result.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS) {
+            tabTransitionData.auto_bookmark = true;
+          } else {
+            tabTransitionData.typed = true;
+          }
+          break;
+        case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
+          // Remote tab are autocomplete results related to
+          // tab urls from a remote synchronized Firefox.
+          tabTransitionData.typed = true;
+          break;
+        case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
+          // This "switchtab" autocompletion should be ignored, because
+          // it is not related to a navigation.
+          // Fall through.
+        case UrlbarUtils.RESULT_TYPE.OMNIBOX:
+          // "Omnibox" should be ignored as the add-on may or may not initiate
+          // a navigation on the item being selected.
+          throw new Error(`Unexpectedly received notification for ${acData.result.type}`);
+        default:
+          Cu.reportError(`Received unexpected result type ${acData.result.type}, falling back to typed transition.`);
+          // Fallback on "typed" if the type is unknown.
+          tabTransitionData.typed = true;
+      }
+    }
+
+    this.setRecentTabTransitionData(tabTransitionData);
+  },
+
+  /**
+   * Recognize the type of urlbar user interaction (e.g. typing a new url,
+   * clicking on an url generated from a searchengine or a keyword, or a
+   * bookmark found by the urlbar autocompletion).
+   *
    * @param {nsIAutoCompleteInput} input
    */
   onURLBarAutoCompletion(input) {
     if (input && input instanceof Ci.nsIAutoCompleteInput) {
       // We are only interested in urlbar autocompletion events
       if (input.id !== "urlbar") {
         return;
       }
 
       let controller = input.popup.view.QueryInterface(Ci.nsIAutoCompleteController);
       let idx = input.popup.selectedIndex;
 
-      let tabTransistionData = {
+      let tabTransitionData = {
         from_address_bar: true,
       };
 
       if (idx < 0 || idx >= controller.matchCount) {
         // Recognize when no valid autocomplete results has been selected.
-        tabTransistionData.typed = true;
+        tabTransitionData.typed = true;
       } else {
         let value = controller.getValueAt(idx);
         let action = input._parseActionUrl(value);
 
         if (action) {
           // Detect keyword and generated and more typed scenarios.
           switch (action.type) {
             case "keyword":
-              tabTransistionData.keyword = true;
+              tabTransitionData.keyword = true;
               break;
             case "searchengine":
             case "searchsuggestion":
-              tabTransistionData.generated = true;
+              tabTransitionData.generated = true;
               break;
             case "visiturl":
               // Visiturl are autocompletion results related to
               // history suggestions.
-              tabTransistionData.typed = true;
+              tabTransitionData.typed = true;
               break;
             case "remotetab":
               // Remote tab are autocomplete results related to
               // tab urls from a remote synchronized Firefox.
-              tabTransistionData.typed = true;
+              tabTransitionData.typed = true;
               break;
             case "switchtab":
               // This "switchtab" autocompletion should be ignored, because
               // it is not related to a navigation.
               return;
             default:
               // Fallback on "typed" if unable to detect a known moz-action type.
-              tabTransistionData.typed = true;
+              tabTransitionData.typed = true;
           }
         } else {
           // Special handling for bookmark urlbar autocompletion
           // (which happens when we got a null action and a valid selectedIndex)
           let styles = new Set(controller.getStyleAt(idx).split(/\s+/));
 
           if (styles.has("bookmark")) {
-            tabTransistionData.auto_bookmark = true;
+            tabTransitionData.auto_bookmark = true;
           } else {
             // Fallback on "typed" if unable to detect a specific actionType
             // (and when in the styles there are "autofill" or "history").
-            tabTransistionData.typed = true;
+            tabTransitionData.typed = true;
           }
         }
       }
 
-      this.setRecentTabTransitionData(tabTransistionData);
+      this.setRecentTabTransitionData(tabTransitionData);
     }
   },
 
   /**
    * Keep track of a recent user interaction and cache it in a
    * map associated to the current selected tab.
    *
    * @param {object} tabTransitionData