Bug 1270067, bug 1267289, bug 1241085, bug 1272317 - fix various url-bar and sessionstore related issues, especially in e10s, r=mconley r=mikedeboer, a=sylvestre
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Mon, 16 May 2016 22:36:35 +0100
changeset 333153 c560b2cb8b972cb607d9be8b164e4f4f30e14d1b
parent 333152 c74c00d5cd6fb5b82239ecf337067dbfd103e2d2
child 333154 de472f31c8a3f1711bdf65beecc8dfa983950b64
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley, mikedeboer, sylvestre
bugs1270067, 1267289, 1241085, 1272317, 522545
milestone48.0a2
Bug 1270067, bug 1267289, bug 1241085, bug 1272317 - fix various url-bar and sessionstore related issues, especially in e10s, r=mconley r=mikedeboer, a=sylvestre Uplift fold of: Bug 1270067 - fix browser_bug522545.js to actually test what it intends to test, r=mconley Bug 1267289 - add more URL bar tests and fix issue with error pages, r=mikedeboer,mconley Bug 1241085 - Private tab should not clear location bar when it finishes loading if I already typed something (especially noticeable in e10s) Bug 1272317 - fix URL bar state when switching to a non-remote browser, r=mconley MozReview-Commit-ID: 4dmgz6iHfdK
browser/base/content/browser.js
browser/base/content/tabbrowser.xml
browser/base/content/test/urlbar/browser.ini
browser/base/content/test/urlbar/browser_urlbarHashChangeProxyState.js
browser/base/content/test/urlbar/browser_urlbarKeepStateAcrossTabSwitches.js
browser/base/content/test/urlbar/browser_urlbarPrivateBrowsingWindowChange.js
browser/base/content/test/urlbar/browser_urlbarUpdateForDomainCompletion.js
browser/components/sessionstore/SessionStore.jsm
browser/components/sessionstore/TabState.jsm
browser/components/sessionstore/content/content-sessionStore.js
browser/components/sessionstore/test/browser.ini
browser/components/sessionstore/test/browser_522545.js
browser/components/sessionstore/test/browser_newtab_userTypedValue.js
browser/components/sessionstore/test/browser_parentProcessRestoreHash.js
testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
toolkit/content/browser-child.js
toolkit/content/widgets/browser.xml
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -811,20 +811,16 @@ function _loadURIWithFlags(browser, uri,
   }
   let flags = params.flags || 0;
   let referrer = params.referrerURI;
   let referrerPolicy = ('referrerPolicy' in params ? params.referrerPolicy :
                         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)) {
@@ -862,19 +858,16 @@ function _loadURIWithFlags(browser, uri,
     } 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
 function LoadInOtherProcess(browser, loadOptions, historyIndex = -1) {
   let tab = gBrowser.getTabForBrowser(browser);
   SessionStore.navigateAndRestore(tab, loadOptions, historyIndex);
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -606,16 +606,22 @@
             onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) {
               if (!aRequest)
                 return;
 
               var oldBlank = this.mBlank;
 
               const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
               const nsIChannel = Components.interfaces.nsIChannel;
+              let location, originalLocation;
+              try {
+                aRequest.QueryInterface(nsIChannel)
+                location = aRequest.URI;
+                originalLocation = aRequest.originalURI;
+              } catch (ex) {}
 
               if (aStateFlags & nsIWebProgressListener.STATE_START) {
                 this.mRequestCount++;
               }
               else if (aStateFlags & nsIWebProgressListener.STATE_STOP) {
                 const NS_ERROR_UNKNOWN_HOST = 2152398878;
                 if (--this.mRequestCount > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) {
                   // to prevent bug 235825: wait for the request handled
@@ -624,27 +630,31 @@
                 }
                 // since we (try to) only handle STATE_STOP of the last request,
                 // the count of open requests should now be 0
                 this.mRequestCount = 0;
               }
 
               if (aStateFlags & nsIWebProgressListener.STATE_START &&
                   aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
-                // It's okay to clear what the user typed when we start
-                // loading a document. If the user types, this counter gets
-                // set to zero, if the document load ends without an
-                // onLocationChange, this counter gets decremented
-                // (so we keep it while switching tabs after failed loads)
-                // We need to add 2 because loadURIWithFlags may have
-                // cancelled a pending load which would have cleared
-                // its anchor scroll detection temporary increment.
                 if (aWebProgress.isTopLevel) {
-                  this.mBrowser.userTypedClear += 2;
-
+                  // Need to use originalLocation rather than location because things
+                  // like about:home and about:privatebrowsing arrive with nsIRequest
+                  // pointing to their resolved jar: or file: URIs.
+                  if (!(originalLocation && gInitialPages.includes(originalLocation.spec) &&
+                        originalLocation != "about:blank" &&
+                        this.mBrowser.currentURI && this.mBrowser.currentURI.spec == "about:blank")) {
+                    // This will trigger clearing the location bar. Don't do it if
+                    // we loaded off a blank browser and this is an initial page load
+                    // (e.g. about:privatebrowsing, about:newtab, etc.) so we avoid
+                    // clearing the location bar in case the user is typing in it.
+                    // loading about:blank shouldn't trigger this, either, because its
+                    // loads are "special".
+                    this.mBrowser.urlbarChangeTracker.startedLoad();
+                  }
                   // If the browser is loading it must not be crashed anymore
                   this.mTab.removeAttribute("crashed");
                 }
 
                 if (this._shouldShowProgress(aRequest)) {
                   if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) {
                     this.mTab.setAttribute("busy", "true");
 
@@ -664,47 +674,39 @@
                   this.mTab.removeAttribute("busy");
                   this.mTabBrowser._tabAttrModified(this.mTab, ["busy"]);
                   if (!this.mTab.selected)
                     this.mTab.setAttribute("unread", "true");
                 }
                 this.mTab.removeAttribute("progress");
 
                 if (aWebProgress.isTopLevel) {
-                  if (!Components.isSuccessCode(aStatus) &&
-                      !isTabEmpty(this.mTab)) {
+                  let isSuccessful = Components.isSuccessCode(aStatus);
+                  if (!isSuccessful && !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;
 
                     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--;
+                  } else if (isSuccessful) {
+                    this.mBrowser.urlbarChangeTracker.finishedLoad();
                   }
 
                   if (!this.mBrowser.mIconURL)
                     this.mTabBrowser.useDefaultIcon(this.mTab);
                 }
 
                 if (this.mBlank)
                   this.mBlank = false;
 
-                var location = aRequest.QueryInterface(nsIChannel).URI;
-
                 // For keyword URIs clear the user typed value since they will be changed into real URIs
                 if (location.scheme == "keyword")
                   this.mBrowser.userTypedValue = null;
 
                 if (this.mTab.label == this.mTabBrowser.mStringBundle.getString("tabs.connecting"))
                   this.mTabBrowser.setTabTitle(this.mTab);
 
                 if (this.mTab.selected)
@@ -739,28 +741,27 @@
                                         aFlags) {
               // OnLocationChange is called for both the top-level content
               // and the subframes.
               let topLevel = aWebProgress.isTopLevel;
 
               if (topLevel) {
                 let isSameDocument =
                   !!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
-                // If userTypedClear > 0, the document loaded correctly and we should be
-                // clearing the user typed value. We also need to clear the typed value
+                // We need to clear the typed value
                 // if the document failed to load, to make sure the urlbar reflects the
                 // failed URI (particularly for SSL errors). However, don't clear the value
                 // if the error page's URI is about:blank, because that causes complete
                 // loss of urlbar contents for invalid URI errors (see bug 867957).
                 // Another reason to clear the userTypedValue is if this was an anchor
                 // navigation initiated by the user.
-                if (this.mBrowser.userTypedClear > 0 ||
+                if (this.mBrowser.didStartLoadSinceLastUserTyping() ||
                     ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) &&
                      aLocation.spec != "about:blank") ||
-                     (isSameDocument && this.mBrowser.inLoadURI)) {
+                    (isSameDocument && this.mBrowser.inLoadURI)) {
                   this.mBrowser.userTypedValue = null;
                 }
 
                 // If the browser was playing audio, we should remove the playing state.
                 if (this.mTab.hasAttribute("soundplaying") && !isSameDocument) {
                   this.mTab.removeAttribute("soundplaying");
                   this.mTabBrowser._tabAttrModified(this.mTab, ["soundplaying"]);
                 }
@@ -1564,28 +1565,36 @@
             let filter = this._tabFilters.get(tab);
             let listener = this._tabListeners.get(tab);
             aBrowser.webProgress.removeProgressListener(filter);
             filter.removeProgressListener(listener);
 
             // We'll be creating a new listener, so destroy the old one.
             listener.destroy();
 
+            let oldUserTypedValue = aBrowser.userTypedValue;
+            let hadStartedLoad = aBrowser.didStartLoadSinceLastUserTyping();
+
             // Make sure the browser is destroyed so it unregisters from observer notifications
             aBrowser.destroy();
 
             // Make sure to restore the original droppedLinkHandler.
             let droppedLinkHandler = aBrowser.droppedLinkHandler;
 
             // Change the "remote" attribute.
             let parent = aBrowser.parentNode;
             parent.removeChild(aBrowser);
             aBrowser.setAttribute("remote", aShouldBeRemote ? "true" : "false");
             parent.appendChild(aBrowser);
 
+            aBrowser.userTypedValue = oldUserTypedValue;
+            if (hadStartedLoad) {
+              aBrowser.urlbarChangeTracker.startedLoad();
+            }
+
             aBrowser.droppedLinkHandler = droppedLinkHandler;
 
             // Switching a browser's remoteness will create a new frameLoader.
             // As frameLoaders start out with an active docShell we have to
             // deactivate it if this is not the selected tab's browser or the
             // browser window is minimized.
             aBrowser.docShellIsActive = (aBrowser == this.selectedBrowser &&
                                          window.windowState != window.STATE_MINIMIZED);
@@ -4160,20 +4169,16 @@
                   offset *= -1;
                 this.tabContainer.advanceSelectedTab(offset, true);
                 aEvent.preventDefault();
             }
           }
         ]]></body>
       </method>
 
-      <property name="userTypedClear"
-                onget="return this.mCurrentBrowser.userTypedClear;"
-                onset="return this.mCurrentBrowser.userTypedClear = val;"/>
-
       <property name="userTypedValue"
                 onget="return this.mCurrentBrowser.userTypedValue;"
                 onset="return this.mCurrentBrowser.userTypedValue = val;"/>
 
       <method name="createTooltip">
         <parameter name="event"/>
         <body><![CDATA[
           event.stopPropagation();
--- a/browser/base/content/test/urlbar/browser.ini
+++ b/browser/base/content/test/urlbar/browser.ini
@@ -46,32 +46,36 @@ skip-if = os == 'linux' # Bug 1104755
 [browser_urlbarCopying.js]
 support-files =
   authenticate.sjs
 [browser_urlbarDecode.js]
 [browser_urlbarDelete.js]
 [browser_urlbarEnter.js]
 [browser_urlbarEnterAfterMouseOver.js]
 skip-if = os == "linux" # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
+[browser_urlbarHashChangeProxyState.js]
+[browser_urlbarKeepStateAcrossTabSwitches.js]
+[browser_urlbarPrivateBrowsingWindowChange.js]
 [browser_urlbarRevert.js]
 [browser_urlbarSearchSingleWordNotification.js]
 [browser_urlbarSearchSuggestions.js]
 support-files =
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
 [browser_urlbarSearchSuggestionsNotification.js]
 support-files =
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
 [browser_urlbarSearchTelemetry.js]
 support-files =
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
 [browser_urlbarStop.js]
 [browser_urlbarTrimURLs.js]
+[browser_urlbarUpdateForDomainCompletion.js]
 [browser_urlbar_autoFill_backspaced.js]
 [browser_urlbar_blanking.js]
 support-files =
   file_blank_but_not_blank.html
 [browser_urlbar_locationchange_urlbar_edit_dos.js]
 support-files =
   file_urlbar_edit_dos.html
 [browser_urlbar_searchsettings.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarHashChangeProxyState.js
@@ -0,0 +1,111 @@
+"use strict";
+
+/**
+ * Check that navigating through both the URL bar and using in-page hash- or ref-
+ * based links and back or forward navigation updates the URL bar and identity block correctly.
+ */
+add_task(function* () {
+  let baseURL = "https://example.org/browser/browser/base/content/test/urlbar/dummy_page.html";
+  let url = baseURL + "#foo";
+  yield BrowserTestUtils.withNewTab({ gBrowser, url }, function(browser) {
+    let identityBox = document.getElementById("identity-box");
+    let expectedURL = url;
+
+    let verifyURLBarState = testType => {
+      is(gURLBar.textValue, expectedURL, "URL bar visible value should be correct " + testType);
+      is(gURLBar.value, expectedURL, "URL bar value should be correct " + testType);
+      ok(identityBox.classList.contains("verifiedDomain"), "Identity box should know we're doing SSL " + testType);
+      is(gURLBar.getAttribute("pageproxystate"), "valid", "URL bar is in valid page proxy state");
+    };
+
+    verifyURLBarState("at the beginning");
+
+    let locationChangePromise;
+    let resolveLocationChangePromise;
+    let expectURL = url => {
+      expectedURL = url;
+      locationChangePromise = new Promise(r => resolveLocationChangePromise = r);
+    };
+    let wpl = {
+      onLocationChange(wpl, request, location, flags) {
+        is(location.spec, expectedURL, "Got the expected URL");
+        resolveLocationChangePromise();
+      },
+    };
+    gBrowser.addProgressListener(wpl);
+
+    expectURL(baseURL + "#foo");
+    gURLBar.select();
+    EventUtils.sendKey("return");
+
+    yield locationChangePromise;
+    verifyURLBarState("after hitting enter on the same URL a second time");
+
+    expectURL(baseURL + "#bar");
+    gURLBar.value = expectedURL;
+    gURLBar.select();
+    EventUtils.sendKey("return");
+
+    yield locationChangePromise;
+    verifyURLBarState("after a URL bar hash navigation");
+
+    expectURL(baseURL + "#foo");
+    yield ContentTask.spawn(browser, null, function() {
+      let a = content.document.createElement("a");
+      a.href = "#foo";
+      a.textContent = "Foo Link";
+      content.document.body.appendChild(a);
+      a.click();
+    });
+
+    yield locationChangePromise;
+    verifyURLBarState("after a page link hash navigation");
+
+    expectURL(baseURL + "#bar");
+    gBrowser.goBack();
+
+    yield locationChangePromise;
+    verifyURLBarState("after going back");
+
+    expectURL(baseURL + "#foo");
+    gBrowser.goForward();
+
+    yield locationChangePromise;
+    verifyURLBarState("after going forward");
+
+    expectURL(baseURL + "#foo");
+    gURLBar.select();
+    EventUtils.sendKey("return");
+
+    yield locationChangePromise;
+    verifyURLBarState("after hitting enter on the same URL");
+
+    gBrowser.removeProgressListener(wpl);
+  });
+});
+
+/**
+ * Check that initial secure loads that swap remoteness
+ * get the correct page icon when finished.
+ */
+add_task(function* () {
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:newtab", false);
+  // NB: CPOW usage because new tab pages can be preloaded, in which case no
+  // load events fire.
+  yield BrowserTestUtils.waitForCondition(() => !tab.linkedBrowser.contentDocument.hidden)
+  let url = "https://example.org/browser/browser/base/content/test/urlbar/dummy_page.html#foo";
+  gURLBar.value = url;
+  gURLBar.select();
+  EventUtils.sendKey("return");
+  yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+  is(gURLBar.textValue, url, "URL bar visible value should be correct when the page loads from about:newtab");
+  is(gURLBar.value, url, "URL bar value should be correct when the page loads from about:newtab");
+  let identityBox = document.getElementById("identity-box");
+  ok(identityBox.classList.contains("verifiedDomain"),
+     "Identity box should know we're doing SSL when the page loads from about:newtab");
+  is(gURLBar.getAttribute("pageproxystate"), "valid",
+     "URL bar is in valid page proxy state when SSL page with hash loads from about:newtab");
+  yield BrowserTestUtils.removeTab(tab);
+});
+
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarKeepStateAcrossTabSwitches.js
@@ -0,0 +1,49 @@
+"use strict";
+
+/**
+ * Verify user typed text remains in the URL bar when tab switching, even when
+ * loads fail.
+ */
+add_task(function* () {
+  let input = "i-definitely-dont-exist.example.com";
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:newtab", false);
+  // NB: CPOW usage because new tab pages can be preloaded, in which case no
+  // load events fire.
+  yield BrowserTestUtils.waitForCondition(() => !tab.linkedBrowser.contentDocument.hidden)
+  let errorPageLoaded = BrowserTestUtils.waitForErrorPage(tab.linkedBrowser);
+  gURLBar.value = input;
+  gURLBar.select();
+  EventUtils.sendKey("return");
+  yield errorPageLoaded;
+  is(gURLBar.textValue, input, "Text is still in URL bar");
+  yield BrowserTestUtils.switchTab(gBrowser, tab.previousSibling);
+  yield BrowserTestUtils.switchTab(gBrowser, tab);
+  is(gURLBar.textValue, input, "Text is still in URL bar after tab switch");
+  yield BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Invalid URIs fail differently (that is, immediately, in the loadURI call)
+ * if keyword searches are turned off. Test that this works, too.
+ */
+add_task(function* () {
+  let input = "To be or not to be-that is the question";
+  yield new Promise(resolve => SpecialPowers.pushPrefEnv({set: [["keyword.enabled", false]]}, resolve));
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:newtab", false);
+  // NB: CPOW usage because new tab pages can be preloaded, in which case no
+  // load events fire.
+  yield BrowserTestUtils.waitForCondition(() => !tab.linkedBrowser.contentDocument.hidden)
+  let errorPageLoaded = BrowserTestUtils.waitForErrorPage(tab.linkedBrowser);
+  gURLBar.value = input;
+  gURLBar.select();
+  EventUtils.sendKey("return");
+  yield errorPageLoaded;
+  is(gURLBar.textValue, input, "Text is still in URL bar");
+  is(tab.linkedBrowser.userTypedValue, input, "Text still stored on browser");
+  yield BrowserTestUtils.switchTab(gBrowser, tab.previousSibling);
+  yield BrowserTestUtils.switchTab(gBrowser, tab);
+  is(gURLBar.textValue, input, "Text is still in URL bar after tab switch");
+  is(tab.linkedBrowser.userTypedValue, input, "Text still stored on browser");
+  yield BrowserTestUtils.removeTab(tab);
+});
+
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarPrivateBrowsingWindowChange.js
@@ -0,0 +1,43 @@
+"use strict";
+
+/**
+ * Test that when opening a private browsing window and typing in it before about:privatebrowsing
+ * loads, we don't clear the URL bar.
+ */
+add_task(function*() {
+  let urlbarTestValue = "Mary had a little lamb";
+  let win = OpenBrowserWindow({private: true});
+  let delayedStartupFinished = TestUtils.topicObserved("browser-delayed-startup-finished",
+                                                       subject => subject == win);
+  yield BrowserTestUtils.waitForEvent(win, "load");
+  let urlbar = win.document.getElementById("urlbar");
+  urlbar.value = urlbarTestValue;
+  // Need this so the autocomplete controller attaches:
+  let focusEv = new FocusEvent("focus", {});
+  urlbar.dispatchEvent(focusEv);
+  // And so we know input happened:
+  let inputEv = new InputEvent("input", {data: "", view: win, bubbles: true});
+  urlbar.onInput(inputEv);
+  // Check it worked:
+  is(urlbar.value, urlbarTestValue, "URL bar value should be there");
+  is(win.gBrowser.selectedBrowser.userTypedValue, urlbarTestValue, "browser object should know the url bar value");
+
+  let continueTest;
+  let continuePromise = new Promise(resolve => continueTest = resolve);
+  let wpl = {
+    onLocationChange(aWebProgress, aRequest, aLocation) {
+      if (aLocation && aLocation.spec == "about:privatebrowsing") {
+        continueTest();
+      }
+    },
+  };
+  win.gBrowser.addProgressListener(wpl);
+
+  yield continuePromise;
+  is(urlbar.value, urlbarTestValue,
+     "URL bar value should be the same once about:privatebrowsing has loaded");
+  is(win.gBrowser.selectedBrowser.userTypedValue, urlbarTestValue,
+     "browser object should still know url bar value once about:privatebrowsing has loaded");
+  win.gBrowser.removeProgressListener(wpl);
+  yield BrowserTestUtils.closeWindow(win);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarUpdateForDomainCompletion.js
@@ -0,0 +1,17 @@
+"use strict";
+
+/**
+ * Disable keyword.enabled (so no keyword search), and check that when you type in
+ * "example" and hit enter, the browser loads and the URL bar is updated accordingly.
+ */
+add_task(function* () {
+  yield new Promise(resolve => SpecialPowers.pushPrefEnv({set: [["keyword.enabled", false]]}, resolve));
+  yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" }, function* (browser) {
+    gURLBar.value = "example";
+    gURLBar.select();
+    let loadPromise = BrowserTestUtils.browserLoaded(browser, false, url => url == "http://www.example.com/");
+    EventUtils.sendKey("return");
+    yield loadPromise;
+    is(gURLBar.textValue, "www.example.com");
+  });
+});
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -770,20 +770,28 @@ var SessionStoreInternal = {
           }
         }
         break;
       case "SessionStore:restoreHistoryComplete":
         // Notify the tabbrowser that the tab chrome has been restored.
         let tabData = TabState.collect(tab);
 
         // wall-paper fix for bug 439675: make sure that the URL to be loaded
-        // is always visible in the address bar
+        // is always visible in the address bar if no other value is present
         let activePageData = tabData.entries[tabData.index - 1] || null;
         let uri = activePageData ? activePageData.url || null : null;
-        browser.userTypedValue = uri;
+        // NB: we won't set initial URIs (about:home, about:newtab, etc.) here
+        // because their load will not normally trigger a location bar clearing
+        // when they finish loading (to avoid race conditions where we then
+        // clear user input instead), so we shouldn't set them here either.
+        // They also don't fall under the issues in bug 439675 where user input
+        // needs to be preserved if the load doesn't succeed.
+        if (!browser.userTypedValue && uri && !win.gInitialPages.includes(uri)) {
+          browser.userTypedValue = uri;
+        }
 
         // If the page has a title, set it.
         if (activePageData) {
           if (activePageData.title) {
             tab.label = activePageData.title;
             tab.crop = "end";
           } else if (activePageData.url != "about:blank") {
             tab.label = activePageData.url;
@@ -807,23 +815,23 @@ var SessionStoreInternal = {
         event.initEvent("SSTabRestoring", true, false);
         tab.dispatchEvent(event);
         break;
       case "SessionStore:restoreTabContentStarted":
         if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
           // If a load not initiated by sessionstore was started in a
           // previously pending tab. Mark the tab as no longer pending.
           this.markTabAsRestoring(tab);
-        } else {
+        } else if (!data.isRemotenessUpdate) {
           // If the user was typing into the URL bar when we crashed, but hadn't hit
           // enter yet, then we just need to write that value to the URL bar without
-          // loading anything. This must happen after the load, since it will clear
+          // loading anything. This must happen after the load, as the load will clear
           // userTypedValue.
           let tabData = TabState.collect(tab);
-          if (tabData.userTypedValue && !tabData.userTypedClear) {
+          if (tabData.userTypedValue && !tabData.userTypedClear && !browser.userTypedValue) {
             browser.userTypedValue = tabData.userTypedValue;
             win.URLBarSetURI();
           }
 
           // Remove state we don't need any longer.
           TabStateCache.update(browser, {
             userTypedValue: null, userTypedClear: null
           });
@@ -2522,17 +2530,16 @@ var SessionStoreInternal = {
 
       let tabState = TabState.clone(tab);
       let options = {restoreImmediately: true};
 
       if (historyIndex >= 0) {
         tabState.index = historyIndex + 1;
         tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
       } else {
-        tabState.userTypedValue = null;
         options.loadArguments = recentLoadArguments;
       }
 
       // Need to reset restoring tabs.
       if (tab.linkedBrowser.__SS_restoreState) {
         this._resetLocalTabRestoringState(tab);
       }
 
--- a/browser/components/sessionstore/TabState.jsm
+++ b/browser/components/sessionstore/TabState.jsm
@@ -124,22 +124,27 @@ var TabStateInternal = {
 
     // Store the tab icon.
     if (!("image" in tabData)) {
       let tabbrowser = tab.ownerDocument.defaultView.gBrowser;
       tabData.image = tabbrowser.getIcon(tab);
     }
 
     // If there is a userTypedValue set, then either the user has typed something
-    // in the URL bar, or a new tab was opened with a URI to load. userTypedClear
-    // is used to indicate whether the tab was in some sort of loading state with
-    // userTypedValue.
+    // in the URL bar, or a new tab was opened with a URI to load.
+    // If so, we also track whether we were still in the process of loading something.
     if (!("userTypedValue" in tabData) && browser.userTypedValue) {
       tabData.userTypedValue = browser.userTypedValue;
-      tabData.userTypedClear = browser.userTypedClear;
+      // We always used to keep track of the loading state as an integer, where
+      // '0' indicated the user had typed since the last load (or no load was
+      // ongoing), and any positive value indicated we had started a load since
+      // the last time the user typed in the URL bar. Mimic this to keep the
+      // session store representation in sync, even though we now represent this
+      // more explicitly:
+      tabData.userTypedClear = browser.didStartLoadSinceLastUserTyping() ? 1 : 0;
     }
 
     return tabData;
   },
 
   /**
    * Copy data for the given |browser| from the cache to |tabData|.
    *
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -175,17 +175,17 @@ var MessageListener = {
 
     // We need to pass the value of didStartLoad back to SessionStore.jsm.
     let didStartLoad = gContentRestore.restoreTabContent(loadArguments, isRemotenessUpdate, () => {
       // Tell SessionStore.jsm that it may want to restore some more tabs,
       // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time.
       sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch, isRemotenessUpdate});
     });
 
-    sendAsyncMessage("SessionStore:restoreTabContentStarted", {epoch});
+    sendAsyncMessage("SessionStore:restoreTabContentStarted", {epoch, isRemotenessUpdate});
 
     if (!didStartLoad) {
       // Pretend that the load succeeded so that event handlers fire correctly.
       sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch, isRemotenessUpdate});
     }
   },
 
   flush({id}) {
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -216,10 +216,13 @@ skip-if = true
 skip-if = os == "mac"
 
 [browser_911547.js]
 [browser_send_async_message_oom.js]
 [browser_multiple_navigateAndRestore.js]
 run-if = e10s
 [browser_async_window_flushing.js]
 [browser_forget_async_closings.js]
+[browser_newtab_userTypedValue.js]
+[browser_parentProcessRestoreHash.js]
+run-if = e10s
 [browser_sessionStoreContainer.js]
 [browser_1234021.js]
--- a/browser/components/sessionstore/test/browser_522545.js
+++ b/browser/components/sessionstore/test/browser_522545.js
@@ -23,18 +23,18 @@ function test() {
     };
 
     waitForBrowserState(state, function() {
       let browser = gBrowser.selectedBrowser;
       is(browser.currentURI.spec, "about:blank",
          "No history entries still sets currentURI to about:blank");
       is(browser.userTypedValue, "example.com",
          "userTypedValue was correctly restored");
-      is(browser.userTypedClear, 0,
-         "userTypeClear restored as expected");
+      ok(!browser.didStartLoadSinceLastUserTyping(),
+         "We still know that no load is ongoing");
       is(gURLBar.value, "example.com",
          "Address bar's value correctly restored");
       // Change tabs to make sure address bar value gets updated
       gBrowser.selectedTab = gBrowser.tabContainer.getItemAtIndex(0);
       is(gURLBar.value, "about:mozilla",
          "Address bar's value correctly updated");
       runNextTest();
     });
@@ -55,18 +55,18 @@ function test() {
     };
 
     waitForBrowserState(state, function() {
       let browser = gBrowser.getBrowserAtIndex(1);
       is(browser.currentURI.spec, "about:blank",
          "No history entries still sets currentURI to about:blank");
       is(browser.userTypedValue, "example.org",
          "userTypedValue was correctly restored");
-      is(browser.userTypedClear, 0,
-         "userTypeClear restored as expected");
+      ok(!browser.didStartLoadSinceLastUserTyping(),
+         "We still know that no load is ongoing");
       is(gURLBar.value, "about:mozilla",
          "Address bar's value correctly restored");
       // Change tabs to make sure address bar value gets updated
       gBrowser.selectedTab = gBrowser.tabContainer.getItemAtIndex(1);
       is(gURLBar.value, "example.org",
          "Address bar's value correctly updated");
       runNextTest();
     });
@@ -88,18 +88,18 @@ function test() {
     };
 
     waitForBrowserState(state, function() {
       let browser = gBrowser.selectedBrowser;
       is(browser.currentURI.spec, "about:config",
          "browser.currentURI set to current entry in SH");
       is(browser.userTypedValue, "example.com",
          "userTypedValue was correctly restored");
-      is(browser.userTypedClear, 0,
-         "userTypeClear restored as expected");
+      ok(!browser.didStartLoadSinceLastUserTyping(),
+         "We still know that no load is ongoing");
       is(gURLBar.value, "example.com",
          "Address bar's value correctly restored to userTypedValue");
       runNextTest();
     });
   }
 
   // This tests the following use case:
   // User is in a tab with session history, presses back at some point, then
@@ -117,18 +117,18 @@ function test() {
     };
 
     waitForBrowserState(state, function() {
       let browser = gBrowser.selectedBrowser;
       is(browser.currentURI.spec, "about:mozilla",
          "browser.currentURI set to current entry in SH");
       is(browser.userTypedValue, "example.org",
          "userTypedValue was correctly restored");
-      is(browser.userTypedClear, 0,
-         "userTypeClear restored as expected");
+      ok(!browser.didStartLoadSinceLastUserTyping(),
+         "We still know that no load is ongoing");
       is(gURLBar.value, "example.org",
          "Address bar's value correctly restored to userTypedValue");
       runNextTest();
     });
   }
 
   // This test simulates lots of tabs opening at once and then quitting/crashing.
   function test_getBrowserState_lotsOfTabsOpening() {
@@ -154,26 +154,23 @@ function test() {
     function firstLocationChange() {
       let state = JSON.parse(ss.getBrowserState());
       let hasUTV = state.windows[0].tabs.some(function(aTab) {
         return aTab.userTypedValue && aTab.userTypedClear && !aTab.entries.length;
       });
 
       ok(hasUTV, "At least one tab has a userTypedValue with userTypedClear with no loaded URL");
 
-      BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(firstLoad);
+      BrowserTestUtils.waitForMessage(gBrowser.selectedBrowser.messageManager, "SessionStore:update").then(firstLoad);
     }
 
     function firstLoad() {
-      let state = JSON.parse(ss.getBrowserState());
-      let hasSH = state.windows[0].tabs.some(function(aTab) {
-        return !("userTypedValue" in aTab) && aTab.entries[0].url;
-      });
-
-      ok(hasSH, "At least one tab has its entry in SH");
+      let state = JSON.parse(ss.getTabState(gBrowser.selectedTab));
+      let hasSH = !("userTypedValue" in state) && state.entries[0].url;
+      ok(hasSH, "The selected tab has its entry in SH");
 
       runNextTest();
     }
 
     gBrowser.loadTabs(uris);
   }
 
   // This simulates setting a userTypedValue and ensures that just typing in the
@@ -184,28 +181,29 @@ function test() {
         tabs: [{ entries: [] }]
       }]
     };
 
     waitForBrowserState(state, function() {
       let browser = gBrowser.selectedBrowser;
       // Make sure this tab isn't loading and state is clear before we test.
       is(browser.userTypedValue, null, "userTypedValue is empty to start");
-      is(browser.userTypedClear, 0, "userTypedClear is 0 to start");
+      ok(!browser.didStartLoadSinceLastUserTyping(),
+         "Initially, no load should be ongoing");
 
       let inputText = "example.org";
       gURLBar.focus();
       gURLBar.value = inputText.slice(0, -1);
       EventUtils.synthesizeKey(inputText.slice(-1) , {});
 
       executeSoon(function () {
         is(browser.userTypedValue, "example.org",
            "userTypedValue was set when changing URLBar value");
-        is(browser.userTypedClear, 0,
-           "userTypedClear was not changed when changing URLBar value");
+        ok(!browser.didStartLoadSinceLastUserTyping(),
+           "No load started since changing URLBar value");
 
         // Now make sure ss gets these values too
         let newState = JSON.parse(ss.getBrowserState());
         is(newState.windows[0].tabs[0].userTypedValue, "example.org",
            "sessionstore got correct userTypedValue");
         is(newState.windows[0].tabs[0].userTypedClear, 0,
            "sessionstore got correct userTypedClear");
         runNextTest();
@@ -226,18 +224,18 @@ function test() {
     };
 
     waitForBrowserState(state, function() {
       let browser = gBrowser.selectedBrowser;
       is(browser.currentURI.spec, "http://example.com/",
          "userTypedClear=2 caused userTypedValue to be loaded");
       is(browser.userTypedValue, null,
          "userTypedValue was null after loading a URI");
-      is(browser.userTypedClear, 0,
-         "userTypeClear reset to 0");
+      ok(!browser.didStartLoadSinceLastUserTyping(),
+         "We should have reset the load state when the tab loaded");
       is(gURLBar.textValue, gURLBar.trimValue("http://example.com/"),
          "Address bar's value set after loading URI");
       runNextTest();
     });
   }
 
 
   let tests = [test_newTabFocused, test_newTabNotFocused,
@@ -247,17 +245,25 @@ function test() {
   let originalState = JSON.parse(ss.getBrowserState());
   let state = {
     windows: [{
       tabs: [{ entries: [{ url: "about:blank" }] }]
     }]
   };
   function runNextTest() {
     if (tests.length) {
-      waitForBrowserState(state, tests.shift());
+      waitForBrowserState(state, function() {
+        gBrowser.selectedBrowser.userTypedValue = null;
+        URLBarSetURI();
+        (tests.shift())();
+      });
     } else {
-      waitForBrowserState(originalState, finish);
+      waitForBrowserState(originalState, function() {
+        gBrowser.selectedBrowser.userTypedValue = null;
+        URLBarSetURI();
+        finish();
+      });
     }
   }
 
   // Run the tests!
   runNextTest();
 }
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_newtab_userTypedValue.js
@@ -0,0 +1,72 @@
+"use strict";
+
+requestLongerTimeout(2);
+
+/**
+ * Test that when restoring an 'initial page' with session restore, it
+ * produces an empty URL bar, rather than leaving its URL explicitly
+ * there as a 'user typed value'.
+ */
+add_task(function* () {
+  let win = yield BrowserTestUtils.openNewBrowserWindow();
+  yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:logo");
+  let tabOpenedAndSwitchedTo = BrowserTestUtils.switchTab(win.gBrowser, () => {});
+
+  // This opens about:newtab:
+  win.BrowserOpenTab();
+  let tab = yield tabOpenedAndSwitchedTo;
+  is(win.gURLBar.value, "", "URL bar should be empty");
+  is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null");
+  let state = JSON.parse(SessionStore.getTabState(tab));
+  ok(!state.userTypedValue, "userTypedValue should be undefined on the tab's state");
+  tab = null;
+
+  yield BrowserTestUtils.closeWindow(win);
+
+  ok(SessionStore.getClosedWindowCount(), "Should have a closed window");
+
+  win = SessionStore.undoCloseWindow(0);
+  yield TestUtils.topicObserved("sessionstore-single-window-restored",
+                                subject => subject == win);
+  // Don't wait for load here because it's about:newtab and we may have swapped in
+  // a preloaded browser.
+  yield TabStateFlusher.flush(win.gBrowser.selectedBrowser);
+
+  is(win.gURLBar.value, "", "URL bar should be empty");
+  tab = win.gBrowser.selectedTab;
+  is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null");
+  state = JSON.parse(SessionStore.getTabState(tab));
+  ok(!state.userTypedValue, "userTypedValue should be undefined on the tab's state");
+
+  yield BrowserTestUtils.removeTab(tab);
+
+  for (let url of gInitialPages) {
+    if (url == BROWSER_NEW_TAB_URL) {
+      continue; // We tested about:newtab using BrowserOpenTab() above.
+    }
+    info("Testing " + url + " - " + new Date());
+    yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
+    yield BrowserTestUtils.closeWindow(win);
+
+    ok(SessionStore.getClosedWindowCount(), "Should have a closed window");
+
+    win = SessionStore.undoCloseWindow(0);
+    yield TestUtils.topicObserved("sessionstore-single-window-restored",
+                                  subject => subject == win);
+    yield BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+    yield TabStateFlusher.flush(win.gBrowser.selectedBrowser);
+
+    is(win.gURLBar.value, "", "URL bar should be empty");
+    tab = win.gBrowser.selectedTab;
+    is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null");
+    state = JSON.parse(SessionStore.getTabState(tab));
+    ok(!state.userTypedValue, "userTypedValue should be undefined on the tab's state");
+
+    info("Removing tab - " + new Date());
+    yield BrowserTestUtils.removeTab(tab);
+    info("Finished removing tab - " + new Date());
+  }
+  info("Removing window - " + new Date());
+  yield BrowserTestUtils.closeWindow(win);
+  info("Finished removing window - " + new Date());
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_parentProcessRestoreHash.js
@@ -0,0 +1,95 @@
+"use strict";
+
+const SELFCHROMEURL =
+  "chrome://mochitests/content/browser/browser/" +
+  "components/sessionstore/test/browser_parentProcessRestoreHash.js";
+
+const Cm = Components.manager;
+
+const TESTCLASSID = "78742c04-3630-448c-9be3-6c5070f062de";
+
+const TESTURL = "about:testpageforsessionrestore#foo";
+
+
+let TestAboutPage = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
+  getURIFlags: function(aURI) {
+    // No CAN_ or MUST_LOAD_IN_CHILD means this loads in the parent:
+    return Ci.nsIAboutModule.ALLOW_SCRIPT |
+           Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT |
+           Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT;
+  },
+
+  newChannel: function(aURI, aLoadInfo) {
+    // about: page inception!
+    let newURI = Services.io.newURI(SELFCHROMEURL, null, null);
+    let channel = Services.io.newChannelFromURIWithLoadInfo(newURI,
+                                                            aLoadInfo);
+    channel.originalURI = aURI;
+    return channel;
+  },
+
+  createInstance: function(outer, iid) {
+    if (outer != null) {
+      throw Cr.NS_ERROR_NO_AGGREGATION;
+    }
+    return this.QueryInterface(iid);
+  },
+
+  register: function() {
+    Cm.QueryInterface(Ci.nsIComponentRegistrar).registerFactory(
+      Components.ID(TESTCLASSID), "Only here for a test",
+      "@mozilla.org/network/protocol/about;1?what=testpageforsessionrestore", this);
+  },
+
+  unregister: function() {
+    Cm.QueryInterface(Ci.nsIComponentRegistrar).unregisterFactory(
+      Components.ID(TESTCLASSID), this);
+  }
+};
+
+
+/**
+ * Test that switching from a remote to a parent process browser
+ * correctly clears the userTypedValue
+ */
+add_task(function* () {
+  TestAboutPage.register();
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/", true, true);
+  ok(tab.linkedBrowser.isRemoteBrowser, "Browser should be remote");
+
+  let resolveLocationChangePromise;
+  let locationChangePromise = new Promise(r => resolveLocationChangePromise = r);
+  let wpl = {
+    onStateChange(wpl, request, state, status) {
+      let location = request.QueryInterface(Ci.nsIChannel).originalURI;
+      // Ignore about:blank loads.
+      let docStop = Ci.nsIWebProgressListener.STATE_STOP |
+                    Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+      if (location.spec == "about:blank" || (state & docStop == docStop)) {
+        return;
+      }
+      is(location.spec, TESTURL, "Got the expected URL");
+      resolveLocationChangePromise();
+    },
+  };
+  gBrowser.addProgressListener(wpl);
+
+  gURLBar.value = TESTURL;
+  gURLBar.select();
+  EventUtils.sendKey("return");
+
+  yield locationChangePromise;
+
+  ok(!tab.linkedBrowser.isRemoteBrowser, "Browser should no longer be remote");
+
+  is(gURLBar.textValue, TESTURL, "URL bar visible value should be correct.");
+  is(gURLBar.value, TESTURL, "URL bar value should be correct.");
+  is(gURLBar.getAttribute("pageproxystate"), "valid", "URL bar is in valid page proxy state");
+
+  ok(!tab.linkedBrowser.userTypedValue, "No userTypedValue should be on the browser.");
+
+  yield BrowserTestUtils.removeTab(tab);
+  gBrowser.removeProgressListener(wpl);
+  TestAboutPage.unregister();
+});
--- a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
+++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
@@ -523,51 +523,89 @@ this.BrowserTestUtils = {
    *        Name of the event to listen to.
    * @param {bool} capture [optional]
    *        Whether to use a capturing listener.
    * @param {function} checkFn [optional]
    *        Called with the Event object as argument, should return true if the
    *        event is the expected one, or false if it should be ignored and
    *        listening should continue. If not specified, the first event with
    *        the specified name resolves the returned promise.
+   * @param {bool} wantsUntrusted [optional]
+   *        Whether to accept untrusted events
    *
    * @note Because this function is intended for testing, any error in checkFn
    *       will cause the returned promise to be rejected instead of waiting for
    *       the next event, since this is probably a bug in the test.
    *
    * @returns {Promise}
    */
-  waitForContentEvent(browser, eventName, capture, checkFn) {
-    let parameters = { eventName,
-                       capture,
-                       checkFnSource: checkFn ? checkFn.toSource() : null };
+  waitForContentEvent(browser, eventName, capture = false, checkFn, wantsUntrusted = false) {
+    let parameters = {
+      eventName,
+      capture,
+      checkFnSource: checkFn ? checkFn.toSource() : null,
+      wantsUntrusted,
+    };
     return ContentTask.spawn(browser, parameters,
-        function({ eventName, capture, checkFnSource }) {
+        function({ eventName, capture, checkFnSource, wantsUntrusted }) {
           let checkFn;
           if (checkFnSource) {
             checkFn = eval(`(() => (${checkFnSource}))()`);
           }
           return new Promise((resolve, reject) => {
             addEventListener(eventName, function listener(event) {
               let completion = resolve;
               try {
                 if (checkFn && !checkFn(event)) {
                   return;
                 }
               } catch (e) {
                 completion = () => reject(e);
               }
               removeEventListener(eventName, listener, capture);
               completion();
-            }, capture);
+            }, capture, wantsUntrusted);
           });
         });
   },
 
   /**
+   * Like browserLoaded, but waits for an error page to appear.
+   * This explicitly deals with cases where the browser is not currently remote and a
+   * remoteness switch will occur before the error page is loaded, which is tricky
+   * because error pages don't fire 'regular' load events that we can rely on.
+   *
+   * @param {xul:browser} browser
+   *        A xul:browser.
+   *
+   * @return {Promise}
+   * @resolves When an error page has been loaded in the browser.
+   */
+  waitForErrorPage(browser) {
+    let waitForLoad = () =>
+      this.waitForContentEvent(browser, "AboutNetErrorLoad", false, null, true);
+
+    let win = browser.ownerDocument.defaultView;
+    let tab = win.gBrowser.getTabForBrowser(browser);
+    if (!tab || browser.isRemoteBrowser || !win.gMultiProcessBrowser) {
+      return waitForLoad();
+    }
+
+    // We're going to switch remoteness when loading an error page. We need to be
+    // quite careful in order to make sure we're adding the listener in time to
+    // get this event:
+    return new Promise((resolve, reject) => {
+      tab.addEventListener("TabRemotenessChange", function onTRC() {
+        tab.removeEventListener("TabRemotenessChange", onTRC);
+        waitForLoad().then(resolve, reject);
+      });
+    });
+  },
+
+  /**
    *  Versions of EventUtils.jsm synthesizeMouse functions that synthesize a
    *  mouse event in a child process and return promises that resolve when the
    *  event has fired and completed. Instead of a window, a browser is required
    *  to be passed to this function.
    *
    * @param target
    *        One of the following:
    *        - a selector string that identifies the element to target. The syntax is as
--- a/toolkit/content/browser-child.js
+++ b/toolkit/content/browser-child.js
@@ -272,29 +272,40 @@ var WebNavigation =  {
         this.reload(message.data.flags);
         break;
       case "WebNavigation:Stop":
         this.stop(message.data.flags);
         break;
     }
   },
 
+  _wrapURIChangeCall(fn) {
+    this._inLoadURI = true;
+    try {
+      fn();
+    } finally {
+      this._inLoadURI = false;
+      WebProgressListener.sendLoadCallResult();
+    }
+  },
+
   goBack: function() {
     if (this.webNavigation.canGoBack) {
-      this.webNavigation.goBack();
+      this._wrapURIChangeCall(() => this.webNavigation.goBack());
     }
   },
 
   goForward: function() {
-    if (this.webNavigation.canGoForward)
-      this.webNavigation.goForward();
+    if (this.webNavigation.canGoForward) {
+      this._wrapURIChangeCall(() => this.webNavigation.goForward());
+    }
   },
 
   gotoIndex: function(index) {
-    this.webNavigation.gotoIndex(index);
+    this._wrapURIChangeCall(() => this.webNavigation.gotoIndex(index));
   },
 
   loadURI: function(uri, flags, referrer, referrerPolicy, postData, headers, baseURI) {
     if (AppConstants.MOZ_CRASHREPORTER && CrashReporter.enabled) {
       let annotation = uri;
       try {
         let url = Services.io.newURI(uri, null, null);
         // If the current URI contains a username/password, remove it.
@@ -307,24 +318,20 @@ 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._inLoadURI = true;
-    try {
-      this.webNavigation.loadURIWithOptions(uri, flags, referrer, referrerPolicy,
-                                            postData, headers, baseURI);
-    } finally {
-      this._inLoadURI = false;
-      WebProgressListener.sendLoadCallResult();
-    }
+    this._wrapURIChangeCall(() => {
+      return this.webNavigation.loadURIWithOptions(uri, flags, referrer, referrerPolicy,
+                                                   postData, headers, baseURI);
+    });
   },
 
   reload: function(flags) {
     this.webNavigation.reload(flags);
   },
 
   stop: function(flags) {
     this.webNavigation.stop(flags);
--- a/toolkit/content/widgets/browser.xml
+++ b/toolkit/content/widgets/browser.xml
@@ -34,46 +34,51 @@
       <property name="canGoBack"
                 onget="return this.webNavigation.canGoBack;"
                 readonly="true"/>
 
       <property name="canGoForward"
                 onget="return this.webNavigation.canGoForward;"
                 readonly="true"/>
 
+      <method name="_wrapURIChangeCall">
+        <parameter name="fn"/>
+        <body>
+          <![CDATA[
+            if (!this.isRemoteBrowser) {
+              this.inLoadURI = true;
+              try {
+                fn();
+              } finally {
+                this.inLoadURI = false;
+              }
+            } else {
+              fn();
+            }
+          ]]>
+        </body>
+      </method>
+
+
       <method name="goBack">
         <body>
           <![CDATA[
             var webNavigation = this.webNavigation;
-            if (webNavigation.canGoBack) {
-              try {
-                this.userTypedClear++;
-                webNavigation.goBack();
-              } finally {
-                if (this.userTypedClear)
-                  this.userTypedClear--;
-              }
-            }
+            if (webNavigation.canGoBack)
+              this._wrapURIChangeCall(() => webNavigation.goBack());
           ]]>
         </body>
       </method>
 
       <method name="goForward">
         <body>
           <![CDATA[
             var webNavigation = this.webNavigation;
-            if (webNavigation.canGoForward) {
-              try {
-                this.userTypedClear++;
-                webNavigation.goForward();
-              } finally {
-                if (this.userTypedClear)
-                  this.userTypedClear--;
-              }
-            }
+            if (webNavigation.canGoForward)
+              this._wrapURIChangeCall(() => webNavigation.goForward());
           ]]>
         </body>
       </method>
 
       <method name="reload">
         <body>
           <![CDATA[
             const nsIWebNavigation = Components.interfaces.nsIWebNavigation;
@@ -106,17 +111,18 @@
       <method name="loadURI">
         <parameter name="aURI"/>
         <parameter name="aReferrerURI"/>
         <parameter name="aCharset"/>
         <body>
           <![CDATA[
             const nsIWebNavigation = Components.interfaces.nsIWebNavigation;
             const flags = nsIWebNavigation.LOAD_FLAGS_NONE;
-            this.loadURIWithFlags(aURI, flags, aReferrerURI, aCharset);
+            this._wrapURIChangeCall(() =>
+              this.loadURIWithFlags(aURI, flags, aReferrerURI, aCharset));
           ]]>
         </body>
       </method>
 
       <!-- throws exception for unknown schemes -->
       <method name="loadURIWithFlags">
         <parameter name="aURI"/>
         <parameter name="aFlags"/>
@@ -137,27 +143,20 @@
               aReferrerURI = params.referrerURI;
               if ('referrerPolicy' in params) {
                 aReferrerPolicy = params.referrerPolicy;
               }
               aCharset = params.charset;
               aPostData = params.postData;
             }
 
-            if (!(aFlags & this.webNavigation.LOAD_FLAGS_FROM_EXTERNAL))
-              this.userTypedClear++;
-
-            try {
+            this._wrapURIChangeCall(() =>
               this.webNavigation.loadURIWithOptions(
                   aURI, aFlags, aReferrerURI, aReferrerPolicy,
-                  aPostData, null, null);
-            } finally {
-              if (this.userTypedClear)
-                this.userTypedClear--;
-            }
+                  aPostData, null, null));
           ]]>
         </body>
       </method>
 
       <method name="goHome">
         <body>
           <![CDATA[
             try {
@@ -189,23 +188,17 @@
           ]]>
         </setter>
       </property>
 
       <method name="gotoIndex">
         <parameter name="aIndex"/>
         <body>
           <![CDATA[
-            try {
-              this.userTypedClear++;
-              this.webNavigation.gotoIndex(aIndex);
-            } finally {
-              if (this.userTypedClear)
-                this.userTypedClear--;
-            }
+            this._wrapURIChangeCall(() => this.webNavigation.gotoIndex(aIndex));
           ]]>
         </body>
       </method>
 
       <property name="currentURI" readonly="true">
        <getter><![CDATA[
           if (this.webNavigation) {
             return this.webNavigation.currentURI;
@@ -768,67 +761,47 @@
         <parameter name="priority"/>
         <body><![CDATA[
           let loadGroup = this.webNavigation.QueryInterface(Components.interfaces.nsIDocumentLoader)
                               .loadGroup.QueryInterface(Components.interfaces.nsISupportsPriority);
           loadGroup.priority = priority;
         ]]></body>
       </method>
 
-      <!--
-        This field tracks the location bar state. The value that the user typed
-        in to the location bar may not be changed while this field is zero.
-        However invoking a load will temporarily increase this field to allow
-        the location bar to be updated to the new URL.
-
-        Case 1: Anchor scroll
-          The user appends the anchor to the URL. This sets the location bar
-          into typed state, and disables changes to the location bar. The user
-          then requests the scroll. loadURIWithFlags temporarily increases the
-          flag by 1 so that the anchor scroll's location change resets the
-          location bar state.
-
-        Case 2: Interrupted load
-          The user types in and submits the URL. This triggers an asynchronous
-          network load which increases the flag by 2. (The temporary increase
-          from loadURIWithFlags is not noticeable in this case.) When the load
-          is interrupted the flag returns to zero, and the location bar stays
-          in typed state.
+      <field name="urlbarChangeTracker">
+        ({
+          _startedLoadSinceLastUserTyping: false,
 
-        Case 3: New load
-          This works like case 2, but as the load is not interrupted the
-          location changes while the flag is still 2 thus resetting the
-          location bar state.
+          startedLoad() {
+            this._startedLoadSinceLastUserTyping = true;
+          },
+          finishedLoad() {
+            this._startedLoadSinceLastUserTyping = false;
+          },
+          userTyped() {
+            this._startedLoadSinceLastUserTyping = false;
+          },
+        })
+      </field>
 
-        Case 4: Corrected load
-          This is a combination of case 2 and case 3, except that the original
-          load is interrupted by the new load. Normally cancelling and starting
-          a new load would reset the flag to 0 and then increase it to 2 again.
-          However both actions occur as a consequence of the loadURIWithFlags
-          invocation, which adds its temporary increase in to the mix. Since
-          the new URL would have been typed in the flag would have been reset
-          before loadURIWithFlags incremented it. The interruption resets the
-          flag to 0 and increases it to 2. Although loadURIWithFlags will
-          decrement the flag it remains at 1 thus allowing the location bar
-          state to be reset when the new load changes the location.
-          This case also applies when loading into a new browser, as this
-          interrupts the default load of about:blank.
-      -->
-      <field name="userTypedClear">
-        1
-      </field>
+      <method name="didStartLoadSinceLastUserTyping">
+        <body><![CDATA[
+          return !this.inLoadURI &&
+                 this.urlbarChangeTracker._startedLoadSinceLastUserTyping;
+        ]]></body>
+      </method>
 
       <field name="_userTypedValue">
         null
       </field>
 
       <property name="userTypedValue"
                 onget="return this._userTypedValue;">
         <setter><![CDATA[
-          this.userTypedClear = 0;
+          this.urlbarChangeTracker.userTyped();
           this._userTypedValue = val;
           return val;
         ]]></setter>
       </property>
 
       <field name="mFormFillAttached">
         false
       </field>