Bug 1355451 - Speculative connect to websites on mousedown on awesomebar item. r=mak draft
authorEvelyn Hung <jj.evelyn@gmail.com>
Tue, 15 Aug 2017 15:02:42 +0800
changeset 651949 11f87d5c22f554966b6908b973e428a261f94251
parent 650228 128a79130ecd6f277190d031a623f991c73c5272
child 727935 440386f044da1a52d09bdfd9b4aba010ad32d15b
push id75890
push userbmo:ehung@mozilla.com
push dateThu, 24 Aug 2017 08:27:29 +0000
reviewersmak
bugs1355451
milestone57.0a1
Bug 1355451 - Speculative connect to websites on mousedown on awesomebar item. r=mak We preconnect to an explicit URL or the url in moz-action:remotetab on the popup. MozReview-Commit-ID: 3KqlO2Bio3V
browser/base/content/test/urlbar/browser.ini
browser/base/content/test/urlbar/browser_urlbar_search_speculative_connect.js
browser/base/content/test/urlbar/browser_urlbar_search_speculative_connect_engine.js
browser/base/content/test/urlbar/browser_urlbar_search_speculative_connect_mousedown.js
browser/base/content/test/urlbar/head.js
browser/base/content/urlbarBindings.xml
--- a/browser/base/content/test/urlbar/browser.ini
+++ b/browser/base/content/test/urlbar/browser.ini
@@ -110,16 +110,17 @@ support-files =
 support-files =
   file_urlbar_edit_dos.html
 [browser_urlbar_searchsettings.js]
 [browser_urlbar_search_speculative_connect.js]
 [browser_urlbar_search_speculative_connect_engine.js]
 support-files =
   searchSuggestionEngine2.xml
   searchSuggestionEngine.sjs
+[browser_urlbar_search_speculative_connect_mousedown.js]
 [browser_urlbar_stop_pending.js]
 support-files =
   slow-page.sjs
 [browser_urlbar_remoteness_switch.js]
 run-if = e10s
 [browser_urlHighlight.js]
 [browser_wyciwyg_urlbarCopying.js]
 subsuite = clipboard
--- a/browser/base/content/test/urlbar/browser_urlbar_search_speculative_connect.js
+++ b/browser/base/content/test/urlbar/browser_urlbar_search_speculative_connect.js
@@ -2,40 +2,31 @@
  * 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";
 
 // This test ensures that we setup a speculative network
 // connection for autoFilled values.
 
-let {HttpServer} = Cu.import("resource://testing-common/httpd.js", {});
 let gHttpServer = null;
 let gScheme = "http";
 let gHost = "localhost"; // 'localhost' by default.
 let gPort = -1;
 let gPrivateWin = null;
 let gIsSpeculativeConnected = false;
 
 add_task(async function setup() {
-  if (!gHttpServer) {
-    gHttpServer = new HttpServer();
-    try {
-      gHttpServer.start(gPort);
-      gPort = gHttpServer.identity.primaryPort;
-      gHttpServer.identity.setPrimary(gScheme, gHost, gPort);
-    } catch (ex) {
-      info("We can't launch our http server successfully.")
-    }
-  }
-  is(gHttpServer.identity.has(gScheme, gHost, gPort), true, "make sure we have this domain listed");
+  gHttpServer = runHttpServer(gScheme, gHost);
+  // The server will be run on a random port if the port number wasn't given.
+  gPort = gHttpServer.identity.primaryPort;
 
   await SpecialPowers.pushPrefEnv({
     set: [["browser.urlbar.autoFill", true],
-          // Turn off speculative connect to the search engine.
+          // Turn off search suggestion so we won't speculative connect to the search engine.
           ["browser.search.suggest.enabled", false],
           ["browser.urlbar.speculativeConnect.enabled", true],
           // In mochitest this number is 0 by default but we have to turn it on.
           ["network.http.speculative-parallel-limit", 6],
           // The http server is using IPv4, so it's better to disable IPv6 to avoid weird
           // networking problem.
           ["network.dns.disableIPv6", true]],
   });
@@ -45,17 +36,17 @@ add_task(async function setup() {
     title: "test visit for speculative connection",
     transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
   }]);
 
   gPrivateWin = await BrowserTestUtils.openNewBrowserWindow({private: true});
   is(PrivateBrowsingUtils.isWindowPrivate(gPrivateWin), true, "A private window created.");
 
   // Bug 764062 - we can't get port number from autocomplete result, so we have to mock
-  // this function to add it manually.
+  // this function and add it manually.
   let oldSpeculativeConnect = gURLBar.popup.maybeSetupSpeculativeConnect.bind(gURLBar.popup);
   let newSpeculativeConnect = (uriString) => {
     gIsSpeculativeConnected = true;
     info(`Original uri is ${uriString}`);
     let newUriString = uriString.substr(0, uriString.length - 1) +
                        ":" + gPort + "/";
     info(`New uri is ${newUriString}`);
     oldSpeculativeConnect(newUriString);
@@ -83,16 +74,17 @@ const test = {
 add_task(async function autofill_tests() {
   gIsSpeculativeConnected = false;
   info(`Searching for '${test.search}'`);
   await promiseAutocompleteResultPopup(test.search, window, true);
   is(gURLBar.inputField.value, test.autofilledValue,
      `Autofilled value is as expected for search '${test.search}'`);
   is(gIsSpeculativeConnected, true, "Speculative connection should be called");
   await promiseSpeculativeConnection(gHttpServer);
+  is(gHttpServer.connectionNumber, 1, `${gHttpServer.connectionNumber} speculative connection has been setup.`);
 });
 
 add_task(async function privateContext_test() {
   info("In private context.");
   gIsSpeculativeConnected = false;
   info(`Searching for '${test.search}'`);
   await promiseAutocompleteResultPopup(test.search, gPrivateWin, true);
   is(gPrivateWin.gURLBar.inputField.value, test.autofilledValue,
--- a/browser/base/content/test/urlbar/browser_urlbar_search_speculative_connect_engine.js
+++ b/browser/base/content/test/urlbar/browser_urlbar_search_speculative_connect_engine.js
@@ -2,39 +2,28 @@
  * 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";
 
 // This test ensures that we setup a speculative network connection to
 // current search engine if the first result is 'searchengine'.
 
-let {HttpServer} = Cu.import("resource://testing-common/httpd.js", {});
 let gHttpServer = null;
 let gScheme = "http";
 let gHost = "localhost"; // 'localhost' by default.
 let gPort = 20709; // the port number must be identical to what we said in searchSuggestionEngine2.xml
 const TEST_ENGINE_BASENAME = "searchSuggestionEngine2.xml";
 
 add_task(async function setup() {
-  if (!gHttpServer) {
-    gHttpServer = new HttpServer();
-    try {
-      gHttpServer.start(gPort);
-      gPort = gHttpServer.identity.primaryPort;
-      gHttpServer.identity.setPrimary(gScheme, gHost, gPort);
-    } catch (ex) {
-      info("We can't launch our http server successfully.")
-    }
-  }
-  is(gHttpServer.identity.has(gScheme, gHost, gPort), true, "make sure we have this domain listed");
+  gHttpServer = runHttpServer(gScheme, gHost, gPort);
 
   await SpecialPowers.pushPrefEnv({
     set: [["browser.urlbar.autoFill", true],
-          // Turn off speculative connect to the search engine.
+          // Make sure search suggestion for location bar is enabled
           ["browser.search.suggest.enabled", true],
           ["browser.urlbar.suggest.searches", true],
           ["browser.urlbar.speculativeConnect.enabled", true],
           // In mochitest this number is 0 by default but we have to turn it on.
           ["network.http.speculative-parallel-limit", 6],
           // The http server is using IPv4, so it's better to disable IPv6 to avoid weird
           // networking problem.
           ["network.dns.disableIPv6", true]],
@@ -49,18 +38,19 @@ add_task(async function setup() {
     Services.search.currentEngine = oldCurrentEngine;
     gHttpServer.identity.remove(gScheme, gHost, gPort);
     gHttpServer.stop(() => {
       gHttpServer = null;
     });
   });
 });
 
-add_task(async function autofill_tests() {
+add_task(async function connect_search_engine_tests() {
   info("Searching for 'foo'");
   await promiseAutocompleteResultPopup("foo", window, true);
   // Check if the first result is with type "searchengine"
   let controller = gURLBar.popup.input.controller;
   let style = controller.getStyleAt(0);
   is(style.includes("searchengine"), true, "The first result type is searchengine");
   await promiseSpeculativeConnection(gHttpServer);
+  is(gHttpServer.connectionNumber, 1, `${gHttpServer.connectionNumber} speculative connection has been setup.`);
 });
 
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbar_search_speculative_connect_mousedown.js
@@ -0,0 +1,73 @@
+/* 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";
+
+// This test ensures that we setup a speculative network connection to
+// the site in mousedown event before the http request happens(in mouseup).
+
+let gHttpServer = null;
+let gScheme = "http";
+let gHost = "localhost"; // 'localhost' by default.
+let gPort = -1;
+let gIsSpeculativeConnected = false;
+
+add_task(async function setup() {
+  gHttpServer = runHttpServer(gScheme, gHost, gPort);
+  // The server will be run on a random port if the port number wasn't given.
+  gPort = gHttpServer.identity.primaryPort;
+
+  await PlacesTestUtils.addVisits([{
+    uri: `${gScheme}://${gHost}:${gPort}`,
+    title: "test visit for speculative connection",
+    transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+  }]);
+
+  await SpecialPowers.pushPrefEnv({
+    set: [["browser.urlbar.autoFill", true],
+          // Turn off search suggestion so we won't speculative connect to the search engine.
+          ["browser.search.suggest.enabled", false],
+          ["browser.urlbar.speculativeConnect.enabled", true],
+          // In mochitest this number is 0 by default but we have to turn it on.
+          ["network.http.speculative-parallel-limit", 6],
+          // The http server is using IPv4, so it's better to disable IPv6 to avoid weird
+          // networking problem.
+          ["network.dns.disableIPv6", true]],
+  });
+
+  registerCleanupFunction(async function() {
+    await PlacesUtils.history.clear();
+    gHttpServer.identity.remove(gScheme, gHost, gPort);
+    gHttpServer.stop(() => {
+      gHttpServer = null;
+    });
+  });
+});
+
+add_task(async function popup_mousedown_tests() {
+  const test = {
+    // To not trigger autofill, search keyword starts from the second character.
+    search: gHost.substr(1, 4),
+    completeValue: `${gScheme}://${gHost}:${gPort}/`
+  };
+  info(`Searching for '${test.search}'`);
+  await promiseAutocompleteResultPopup(test.search, window, true);
+  // Check if the first result is with type "searchengine"
+  let controller = gURLBar.popup.input.controller;
+  // The first item should be 'Search with ...' thus we wan the second.
+  let value = controller.getFinalCompleteValueAt(1);
+  info(`The value of the second item is ${value}`);
+  is(value, test.completeValue, "The second item has the url we visited.");
+
+  await BrowserTestUtils.waitForCondition(() => {
+    return !!gURLBar.popup.richlistbox.childNodes[1] &&
+           is_visible(gURLBar.popup.richlistbox.childNodes[1]);
+  }, "the node is there.");
+
+  let listitem = gURLBar.popup.richlistbox.childNodes[1];
+  EventUtils.synthesizeMouse(listitem, 10, 10, {type: "mousedown"}, window);
+  is(gURLBar.popup.richlistbox.selectedIndex, 1, "The second item is selected");
+  await promiseSpeculativeConnection(gHttpServer);
+  is(gHttpServer.connectionNumber, 1, `${gHttpServer.connectionNumber} speculative connection has been setup.`);
+});
--- a/browser/base/content/test/urlbar/head.js
+++ b/browser/base/content/test/urlbar/head.js
@@ -3,16 +3,18 @@
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
   "resource://testing-common/PlacesTestUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
   "resource://gre/modules/Preferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
+  "resource://testing-common/httpd.js");
 
 /**
  * Waits for the next top-level document load in the current browser.  The URI
  * of the document is compared against aExpectedURL.  The load is then stopped
  * before it actually starts.
  *
  * @param aExpectedURL
  *        The URL of the document that is expected to load.
@@ -124,16 +126,29 @@ function is_element_visible(element, msg
   ok(is_visible(element), msg || "Element should be visible");
 }
 
 function is_element_hidden(element, msg) {
   isnot(element, null, "Element should not be null, when checking visibility");
   ok(is_hidden(element), msg || "Element should be hidden");
 }
 
+function runHttpServer(scheme, host, port = -1) {
+  let httpserver = new HttpServer();
+  try {
+    httpserver.start(port);
+    port = httpserver.identity.primaryPort;
+    httpserver.identity.setPrimary(scheme, host, port);
+  } catch (ex) {
+    info("We can't launch our http server successfully.")
+  }
+  is(httpserver.identity.has(scheme, host, port), true, `${scheme}://${host}:${port} is listening.`);
+  return httpserver;
+}
+
 function promisePopupEvent(popup, eventSuffix) {
   let endState = {shown: "open", hidden: "closed"}[eventSuffix];
 
   if (popup.state == endState)
     return Promise.resolve();
 
   let eventType = "popup" + eventSuffix;
   return new Promise(resolve => {
@@ -147,44 +162,52 @@ function promisePopupEvent(popup, eventS
 function promisePopupShown(popup) {
   return promisePopupEvent(popup, "shown");
 }
 
 function promisePopupHidden(popup) {
   return promisePopupEvent(popup, "hidden");
 }
 
-function promiseSearchComplete(win = window) {
+function promiseSearchComplete(win = window, dontAnimate = false) {
   return promisePopupShown(win.gURLBar.popup).then(() => {
     function searchIsComplete() {
-      return win.gURLBar.controller.searchStatus >=
-        Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH;
+      let isComplete = win.gURLBar.controller.searchStatus >=
+                       Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH;
+      if (isComplete) {
+        info(`Restore popup dontAnimate value to ${dontAnimate}`);
+        win.gURLBar.popup.setAttribute("dontanimate", dontAnimate);
+      }
+      return isComplete;
     }
 
     // Wait until there are at least two matches.
     return BrowserTestUtils.waitForCondition(searchIsComplete, "waiting urlbar search to complete");
   });
 }
 
 function promiseAutocompleteResultPopup(inputText,
                                         win = window,
                                         fireInputEvent = false) {
+  let dontAnimate = !!win.gURLBar.popup.getAttribute("dontanimate");
   waitForFocus(() => {
+    info(`Disable popup animation. Change dontAnimate value from ${dontAnimate} to true.`);
+    win.gURLBar.popup.setAttribute("dontanimate", "true");
     win.gURLBar.focus();
     win.gURLBar.value = inputText;
     if (fireInputEvent) {
       // This is necessary to get the urlbar to set gBrowser.userTypedValue.
       let event = document.createEvent("Events");
       event.initEvent("input", true, true);
       win.gURLBar.dispatchEvent(event);
     }
     win.gURLBar.controller.startSearch(inputText);
   }, win);
 
-  return promiseSearchComplete(win);
+  return promiseSearchComplete(win, dontAnimate);
 }
 
 function promiseNewSearchEngine(basename) {
   return new Promise((resolve, reject) => {
     info("Waiting for engine to be added: " + basename);
     let url = getRootDirectory(gTestPath) + basename;
     Services.search.addEngine(url, null, "", false, {
       onSuccess(engine) {
@@ -250,15 +273,13 @@ function promisePageActionViewShown() {
       }, 5000);
     }, { once: true });
   });
 }
 
 function promiseSpeculativeConnection(httpserver) {
   return BrowserTestUtils.waitForCondition(() => {
     if (httpserver) {
-      is(httpserver.connectionNumber, 1,
-         `${httpserver.connectionNumber} speculative connection has been setup.`)
       return httpserver.connectionNumber == 1;
     }
     return false;
   }, "Waiting for connection setup");
 }
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -2310,16 +2310,62 @@ file, You can obtain one at http://mozil
 
       <handler event="mousedown"><![CDATA[
         // Required to make the xul:label.text-link elements in the search
         // suggestions notification work correctly when clicked on Linux.
         // This is copied from the mousedown handler in
         // browser-search-autocomplete-result-popup, which apparently had a
         // similar problem.
         event.preventDefault();
+
+        if (!this.input.speculativeConnectEnabled) {
+          return;
+        }
+        if (event.button == 2) {
+          // Ignore right-clicks.
+          return;
+        }
+        // Ensure the user is clicking on an url instead of other buttons
+        // on the popup.
+        let elt = event.originalTarget;
+        while (elt && elt.localName != "richlistitem" && elt != this) {
+          elt = elt.parentNode;
+        }
+        if (!elt || elt.localName != "richlistitem") {
+          return;
+        }
+        // The user might click on a ghost entry which was removed because of
+        // the coming new results.
+        if (this.input.controller.matchCount <= this.selectedIndex) {
+          return;
+        }
+
+        let url = this.input.controller.getFinalCompleteValueAt(this.selectedIndex);
+
+        // Whitelist the cases that we want to speculative connect, and ignore
+        // other moz-action uris or fancy protocols.
+        // Note that it's likely we've speculatively connected to the first
+        // url because it is a heuristic "autofill" result (see bug 1348275).
+        // "moz-action:searchengine" is also the same case. (see bug 1355443)
+        // So we won't duplicate the effort here.
+        if (url.startsWith("http") && this.selectedIndex > 0) {
+          this.maybeSetupSpeculativeConnect(url);
+        } else if (url.startsWith("moz-action:remotetab")) {
+          // URL is in the format moz-action:ACTION,PARAMS
+          // Where PARAMS is a JSON encoded object.
+          const MOZ_ACTION_REGEX = /^moz-action:([^,]+),(.*)$/;
+          if (!MOZ_ACTION_REGEX.test(url))
+            return;
+
+          let params = JSON.parse(url.match(MOZ_ACTION_REGEX)[2]);
+          if (params.url) {
+            this.maybeSetupSpeculativeConnect(decodeURIComponent(params.url));
+          }
+        }
+
       ]]></handler>
 
     </handlers>
   </binding>
 
   <binding id="addon-progress-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
     <implementation>
       <constructor><![CDATA[