Bug 798249 - track when we're 'inside' a loadURI call when dealing with Stop() calls resulting from the same, r?mconley draft
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Tue, 16 Feb 2016 14:08:14 +0000
changeset 336972 a4136a9a97a6ffaddc4e442b4c6f24c79b31f3f3
parent 336504 6f9ea75e0270ae093bf285729612dd9d960f1aa9
child 515541 89291eb98f8374b040d6d8fc7c9233723532b892
push id12228
push usergijskruitbosch@gmail.com
push dateFri, 04 Mar 2016 16:28:41 +0000
reviewersmconley
bugs798249
milestone47.0a1
Bug 798249 - track when we're 'inside' a loadURI call when dealing with Stop() calls resulting from the same, r?mconley MozReview-Commit-ID: 48xQn03HtjZ
browser/base/content/browser.js
browser/base/content/tabbrowser.xml
browser/base/content/test/general/browser_urlbarStop.js
browser/base/content/test/general/head.js
browser/base/content/test/urlbar/browser.ini
browser/base/content/test/urlbar/browser_urlbar_stop_pending.js
browser/base/content/test/urlbar/slow-page.sjs
toolkit/content/browser-child.js
toolkit/modules/RemoteWebProgress.jsm
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -825,20 +825,26 @@ function _loadURIWithFlags(browser, uri,
                         Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT);
   let charset = params.charset;
   let postData = params.postData;
 
   if (!(flags & browser.webNavigation.LOAD_FLAGS_FROM_EXTERNAL)) {
     browser.userTypedClear++;
   }
 
+  let wasRemote = browser.isRemoteBrowser;
+
   let process = browser.isRemoteBrowser ? Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT
                                         : Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
   let mustChangeProcess = gMultiProcessBrowser &&
                           !E10SUtils.canLoadURIInProcess(uri, process);
+  if ((!wasRemote && !mustChangeProcess) ||
+      (wasRemote && mustChangeProcess)) {
+    browser.inLoadURI = true;
+  }
   try {
     if (!mustChangeProcess) {
       browser.webNavigation.loadURIWithOptions(uri, flags,
                                                referrer, referrerPolicy,
                                                postData, null, null);
     } else {
       if (postData) {
         postData = NetUtil.readInputStreamToString(postData, postData.available());
@@ -862,16 +868,20 @@ function _loadURIWithFlags(browser, uri,
       Cu.reportError(e);
       gBrowser.updateBrowserRemotenessByURL(browser, uri);
       browser.webNavigation.loadURIWithOptions(uri, flags, referrer, referrerPolicy,
                                                postData, null, null);
     } else {
       throw e;
     }
   } finally {
+    if ((!wasRemote && !mustChangeProcess) ||
+        (wasRemote && mustChangeProcess)) {
+      browser.inLoadURI = false;
+    }
     if (browser.userTypedClear) {
       browser.userTypedClear--;
     }
   }
 }
 
 // Starts a new load in the browser first switching the browser to the correct
 // process
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -679,18 +679,20 @@
                   if (!Components.isSuccessCode(aStatus) &&
                       !isTabEmpty(this.mTab)) {
                     // Restore the current document's location in case the
                     // request was stopped (possibly from a content script)
                     // before the location changed.
 
                     this.mBrowser.userTypedValue = null;
 
-                    if (this.mTab.selected && gURLBar)
+                    let inLoadURI = this.mBrowser.inLoadURI;
+                    if (this.mTab.selected && gURLBar && !inLoadURI) {
                       URLBarSetURI();
+                    }
                   } else {
                     // The document is done loading, we no longer want the
                     // value cleared.
 
                     if (this.mBrowser.userTypedClear > 1)
                       this.mBrowser.userTypedClear -= 2;
                     else if (this.mBrowser.userTypedClear > 0)
                       this.mBrowser.userTypedClear--;
@@ -1315,17 +1317,17 @@
         A newly created tab has position Infinity in the cache.
         If a tab is closed, it has no effect on the position of other
         tabs in the cache since we assume that closing a tab doesn't
         cause us to load in any other tabs.
 
         We ignore the effect of dragging tabs between windows.
       -->
       <method name="_recordTabAccess">
-	<parameter name="aTab"/>
+        <parameter name="aTab"/>
         <body><![CDATA[
           if (!Services.telemetry.canRecordExtended) {
             return;
           }
 
           let tabs = Array.from(this.visibleTabs);
 
           let pos = aTab.cachePosition;
--- a/browser/base/content/test/general/browser_urlbarStop.js
+++ b/browser/base/content/test/general/browser_urlbarStop.js
@@ -20,12 +20,12 @@ add_task(function* () {
   gBrowser.removeCurrentTab();
 });
 
 function typeAndSubmitAndStop(url) {
   gBrowser.userTypedValue = url;
   URLBarSetURI();
   is(gURLBar.textValue, gURLBar.trimValue(url), "location bar reflects loading page");
 
-  let promise = waitForDocLoadAndStopIt(url);
+  let promise = waitForDocLoadAndStopIt(url, gBrowser.selectedBrowser, false);
   gURLBar.handleCommand();
   return promise;
 }
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -376,40 +376,55 @@ function promiseHistoryClearedState(aURI
 
 /**
  * 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.
+ * @param aStopFromProgressListener
+ *        Whether to cancel the load directly from the progress listener. Defaults to true.
+ *        If you're using this method to avoid hitting the network, you want the default (true).
+ *        However, the browser UI will behave differently for loads stopped directly from
+ *        the progress listener (effectively in the middle of a call to loadURI) and so there
+ *        are cases where you may want to avoid stopping the load directly from within the
+ *        progress listener callback.
  * @return promise
  */
-function waitForDocLoadAndStopIt(aExpectedURL, aBrowser=gBrowser.selectedBrowser) {
-  function content_script() {
+function waitForDocLoadAndStopIt(aExpectedURL, aBrowser=gBrowser.selectedBrowser, aStopFromProgressListener=true) {
+  function content_script(aStopFromProgressListener) {
     let { interfaces: Ci, utils: Cu } = Components;
     Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
     let wp = docShell.QueryInterface(Ci.nsIWebProgress);
 
+    function stopContent(now, uri) {
+      if (now) {
+        /* Hammer time. */
+        content.stop();
+
+        /* Let the parent know we're done. */
+        sendAsyncMessage("Test:WaitForDocLoadAndStopIt", { uri });
+      } else {
+        setTimeout(stopContent.bind(null, true, uri), 0);
+      }
+    }
+
     let progressListener = {
       onStateChange: function (webProgress, req, flags, status) {
         dump("waitForDocLoadAndStopIt: onStateChange " + flags.toString(16) + ": " + req.name + "\n");
 
         if (webProgress.isTopLevel &&
             flags & Ci.nsIWebProgressListener.STATE_START) {
           wp.removeProgressListener(progressListener);
 
           let chan = req.QueryInterface(Ci.nsIChannel);
           dump(`waitForDocLoadAndStopIt: Document start: ${chan.URI.spec}\n`);
 
-          /* Hammer time. */
-          content.stop();
-
-          /* Let the parent know we're done. */
-          sendAsyncMessage("Test:WaitForDocLoadAndStopIt", { uri: chan.originalURI.spec });
+          stopContent(aStopFromProgressListener, chan.originalURI.spec);
         }
       },
       QueryInterface: XPCOMUtils.generateQI(["nsISupportsWeakReference"])
     };
     wp.addProgressListener(progressListener, wp.NOTIFY_STATE_WINDOW);
 
     /**
      * As |this| is undefined and we can't extend |docShell|, adding an unload
@@ -426,17 +441,17 @@ function waitForDocLoadAndStopIt(aExpect
   return new Promise((resolve, reject) => {
     function complete({ data }) {
       is(data.uri, aExpectedURL, "waitForDocLoadAndStopIt: The expected URL was loaded");
       mm.removeMessageListener("Test:WaitForDocLoadAndStopIt", complete);
       resolve();
     }
 
     let mm = aBrowser.messageManager;
-    mm.loadFrameScript("data:,(" + content_script.toString() + ")();", true);
+    mm.loadFrameScript("data:,(" + content_script.toString() + ")(" + aStopFromProgressListener + ");", true);
     mm.addMessageListener("Test:WaitForDocLoadAndStopIt", complete);
     info("waitForDocLoadAndStopIt: Waiting for URL: " + aExpectedURL);
   });
 }
 
 /**
  * Waits for the next load to complete in any browser or the given browser.
  * If a <tabbrowser> is given it waits for a load in any of its browsers.
--- a/browser/base/content/test/urlbar/browser.ini
+++ b/browser/base/content/test/urlbar/browser.ini
@@ -1,4 +1,7 @@
 [browser_moz_action_link.js]
 [browser_urlbar_blanking.js]
 support-files =
   file_blank_but_not_blank.html
+[browser_urlbar_stop_pending.js]
+support-files =
+  slow-page.sjs
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbar_stop_pending.js
@@ -0,0 +1,42 @@
+"use strict";
+
+const SLOW_PAGE = "http://www.example.com/browser/browser/base/content/test/urlbar/slow-page.sjs";
+const SLOW_PAGE2 = "http://mochi.test:8888/browser/browser/base/content/test/urlbar/slow-page.sjs?faster";
+
+/**
+ * Check that if we:
+ * 1) have a loaded page
+ * 2) load a separate URL
+ * 3) before the URL for step 2 has finished loading, load a third URL
+ * we don't revert to the URL from (1).
+ */
+add_task(function*() {
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home", true);
+
+  let expectedURLBarChange = SLOW_PAGE;
+  let sawChange = false;
+  let handler = e => {
+    sawChange = true;
+    is(gURLBar.value, expectedURLBarChange, "Should not change URL bar value!");
+  };
+
+  let obs = new MutationObserver(handler);
+
+  obs.observe(gURLBar, {attributes: true});
+  gURLBar.value = SLOW_PAGE;
+  gURLBar.handleCommand();
+
+  // If this ever starts going intermittent, we've broken this.
+  yield new Promise(resolve => setTimeout(resolve, 200));
+  expectedURLBarChange = SLOW_PAGE2;
+  let pageLoadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  gURLBar.value = expectedURLBarChange;
+  gURLBar.handleCommand();
+  is(gURLBar.value, expectedURLBarChange, "Should not have changed URL bar value synchronously.");
+  yield pageLoadPromise;
+  ok(sawChange, "The URL bar change handler should have been called by the time the page was loaded");
+  obs.disconnect();
+  obs = null;
+  yield BrowserTestUtils.removeTab(tab);
+});
+
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/slow-page.sjs
@@ -0,0 +1,22 @@
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+let timer;
+
+const DELAY_MS = 5000;
+function handleRequest(request, response) {
+  if (request.queryString.endsWith("faster")) {
+    response.setHeader("Content-Type", "text/html", false);
+    response.write("<body>Not so slow!</body>");
+    return;
+  }
+  response.processAsync();
+  timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  timer.init(() => {
+    response.setHeader("Content-Type", "text/html", false);
+    response.write("<body>This was the slow load. You should never see this.</body>");
+    response.finish();
+  }, DELAY_MS, Ci.nsITimer.TYPE_ONE_SHOT);
+}
--- a/toolkit/content/browser-child.js
+++ b/toolkit/content/browser-child.js
@@ -127,16 +127,17 @@ var WebProgressListener = {
     // will want to know some details, so we'll update it with
     // the documentURI.
     if (aWebProgress && aWebProgress.isTopLevel) {
       json.documentURI = content.document.documentURIObject.spec;
       json.charset = content.document.characterSet;
       json.mayEnableCharacterEncodingMenu = docShell.mayEnableCharacterEncodingMenu;
     }
 
+    json.inLoadURI = WebNavigation.inLoadURI;
     this._send("Content:StateChange", json, objects);
   },
 
   onProgressChange: function onProgressChange(aWebProgress, aRequest, aCurSelf, aMaxSelf, aCurTotal, aMaxTotal) {
     let json = this._setupJSON(aWebProgress, aRequest);
     let objects = this._setupObjects(aWebProgress, aRequest);
 
     json.curSelf = aCurSelf;
@@ -203,16 +204,20 @@ var WebProgressListener = {
 
     this._send("Content:SecurityChange", json, objects);
   },
 
   onRefreshAttempted: function onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) {
     return true;
   },
 
+  sendLoadCallResult(aResult) {
+    this._send("Content:LoadURIResult", aResult);
+  },
+
   QueryInterface: function QueryInterface(aIID) {
     if (aIID.equals(Ci.nsIWebProgressListener) ||
         aIID.equals(Ci.nsIWebProgressListener2) ||
         aIID.equals(Ci.nsISupportsWeakReference) ||
         aIID.equals(Ci.nsISupports)) {
         return this;
     }
 
@@ -234,16 +239,22 @@ var WebNavigation =  {
     addMessageListener("WebNavigation:Reload", this);
     addMessageListener("WebNavigation:Stop", this);
   },
 
   get webNavigation() {
     return docShell.QueryInterface(Ci.nsIWebNavigation);
   },
 
+  _inLoadURI: false,
+
+  get inLoadURI() {
+    return this._inLoadURI;
+  },
+
   receiveMessage: function(message) {
     switch (message.name) {
       case "WebNavigation:GoBack":
         this.goBack();
         break;
       case "WebNavigation:GoForward":
         this.goForward();
         break;
@@ -295,18 +306,34 @@ var WebNavigation =  {
     if (referrer)
       referrer = Services.io.newURI(referrer, null, null);
     if (postData)
       postData = makeInputStream(postData);
     if (headers)
       headers = makeInputStream(headers);
     if (baseURI)
       baseURI = Services.io.newURI(baseURI, null, null);
-    this.webNavigation.loadURIWithOptions(uri, flags, referrer, referrerPolicy,
-                                          postData, headers, baseURI);
+    this._inLoadURI = true;
+    let exception = null;
+    try {
+      this.webNavigation.loadURIWithOptions(uri, flags, referrer, referrerPolicy,
+                                            postData, headers, baseURI);
+    } catch (ex) {
+      exception = ex;
+    } finally {
+      this._inLoadURI = false;
+      let loadCallResult = {
+        success: !exception,
+        uri,
+        flags,
+        exception,
+        baseURI
+      };
+      WebProgressListener.sendLoadCallResult(loadCallResult);
+    }
   },
 
   reload: function(flags) {
     this.webNavigation.reload(flags);
   },
 
   stop: function(flags) {
     this.webNavigation.stop(flags);
--- a/toolkit/modules/RemoteWebProgress.jsm
+++ b/toolkit/modules/RemoteWebProgress.jsm
@@ -112,25 +112,27 @@ RemoteWebProgressManager.argumentsForAdd
 RemoteWebProgressManager.prototype = {
   swapBrowser: function(aBrowser) {
     if (this._messageManager) {
       this._messageManager.removeMessageListener("Content:StateChange", this);
       this._messageManager.removeMessageListener("Content:LocationChange", this);
       this._messageManager.removeMessageListener("Content:SecurityChange", this);
       this._messageManager.removeMessageListener("Content:StatusChange", this);
       this._messageManager.removeMessageListener("Content:ProgressChange", this);
+      this._messageManager.removeMessageListener("Content:LoadURIResult", this);
     }
 
     this._browser = aBrowser;
     this._messageManager = aBrowser.messageManager;
     this._messageManager.addMessageListener("Content:StateChange", this);
     this._messageManager.addMessageListener("Content:LocationChange", this);
     this._messageManager.addMessageListener("Content:SecurityChange", this);
     this._messageManager.addMessageListener("Content:StatusChange", this);
     this._messageManager.addMessageListener("Content:ProgressChange", this);
+    this._messageManager.addMessageListener("Content:LoadURIResult", this);
   },
 
   get topLevelWebProgress() {
     return this._topLevelWebProgress;
   },
 
   addProgressListener: function (aListener) {
     let listener = aListener.QueryInterface(Ci.nsIWebProgressListener);
@@ -206,16 +208,17 @@ RemoteWebProgressManager.prototype = {
       request = new RemoteWebProgressRequest(json.requestURI,
                                              json.originalRequestURI,
                                              objects.request);
     }
 
     if (isTopLevel) {
       this._browser._contentWindow = objects.contentWindow;
       this._browser._documentContentType = json.documentContentType;
+      this._browser.inLoadURI = json.inLoadURI;
       if (json.charset) {
         this._browser._characterSet = json.charset;
         this._browser._mayEnableCharacterEncodingMenu = json.mayEnableCharacterEncodingMenu;
       }
     }
 
     switch (aMessage.name) {
     case "Content:StateChange":
@@ -263,11 +266,14 @@ RemoteWebProgressManager.prototype = {
 
     case "Content:StatusChange":
       this._callProgressListeners("onStatusChange", webProgress, request, json.status, json.message);
       break;
 
     case "Content:ProgressChange":
       this._callProgressListeners("onProgressChange", webProgress, request, json.curSelf, json.maxSelf, json.curTotal, json.maxTotal);
       break;
+    case "Content:LoadURIResult":
+      this._browser.inLoadURI = false;
+      break;
     }
   },
 };