Bug 1096488 - Detect and handle switching from remote to non-remote pages and back in marionette.;r=automatedtester
authorChris Manchester <cmanchester@mozilla.com>
Tue, 10 Mar 2015 16:19:40 -0700
changeset 232884 bca6e36655a49a08db87df6f67f210ef8baab3da
parent 232883 d6956145538bcbcb4a7ab643d1494d06ecd955c1
child 232885 69c682891670e5f6dee1ced41ad68455c2841b94
push id56671
push usercmanchester@mozilla.com
push dateTue, 10 Mar 2015 23:19:46 +0000
treeherdermozilla-inbound@bca6e36655a4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersautomatedtester
bugs1096488
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1096488 - Detect and handle switching from remote to non-remote pages and back in marionette.;r=automatedtester
testing/marionette/marionette-frame-manager.js
testing/marionette/marionette-listener.js
testing/marionette/marionette-server.js
--- a/testing/marionette/marionette-frame-manager.js
+++ b/testing/marionette/marionette-frame-manager.js
@@ -203,16 +203,17 @@ FrameManager.prototype = {
     messageManager.addWeakMessageListener("Marionette:runEmulatorShell", this.server);
     messageManager.addWeakMessageListener("Marionette:shareData", this.server);
     messageManager.addWeakMessageListener("Marionette:switchToModalOrigin", this.server);
     messageManager.addWeakMessageListener("Marionette:switchToFrame", this.server);
     messageManager.addWeakMessageListener("Marionette:switchedToFrame", this.server);
     messageManager.addWeakMessageListener("Marionette:addCookie", this.server);
     messageManager.addWeakMessageListener("Marionette:getVisibleCookies", this.server);
     messageManager.addWeakMessageListener("Marionette:deleteCookie", this.server);
+    messageManager.addWeakMessageListener("Marionette:listenersAttached", this.server);
     messageManager.addWeakMessageListener("MarionetteFrame:handleModal", this);
     messageManager.addWeakMessageListener("MarionetteFrame:getCurrentFrameId", this);
     messageManager.addWeakMessageListener("MarionetteFrame:getInterruptedState", this);
   },
 
   /**
    * Removes listeners for messages from content frame scripts.
    * We do not remove the "MarionetteFrame:getInterruptedState" or the
@@ -234,13 +235,13 @@ FrameManager.prototype = {
     messageManager.removeWeakMessageListener("Marionette:register", this.server);
     messageManager.removeWeakMessageListener("Marionette:runEmulatorCmd", this.server);
     messageManager.removeWeakMessageListener("Marionette:runEmulatorShell", this.server);
     messageManager.removeWeakMessageListener("Marionette:switchToFrame", this.server);
     messageManager.removeWeakMessageListener("Marionette:switchedToFrame", this.server);
     messageManager.removeWeakMessageListener("Marionette:addCookie", this.server);
     messageManager.removeWeakMessageListener("Marionette:getVisibleCookies", this.server);
     messageManager.removeWeakMessageListener("Marionette:deleteCookie", this.server);
+    messageManager.removeWeakMessageListener("Marionette:listenersAttached", this.server);
     messageManager.removeWeakMessageListener("MarionetteFrame:handleModal", this);
     messageManager.removeWeakMessageListener("MarionetteFrame:getCurrentFrameId", this);
   },
-
 };
--- a/testing/marionette/marionette-listener.js
+++ b/testing/marionette/marionette-listener.js
@@ -58,16 +58,19 @@ let asyncTestTimeoutId;
 let inactivityTimeoutId = null;
 let heartbeatCallback = function () {}; // Called by the simpletest methods.
 
 let originalOnError;
 //timer for doc changes
 let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 //timer for readystate
 let readyStateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+// timer for navigation commands.
+let navTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+let onDOMContentLoaded;
 // Send move events about this often
 let EVENT_INTERVAL = 30; // milliseconds
 // For assigning unique ids to all touches
 let nextTouchId = 1000;
 //Keep track of active Touches
 let touchIds = {};
 // last touch for each fingerId
 let multiLast = {};
@@ -92,30 +95,34 @@ let modalHandler = function() {
 };
 
 /**
  * Called when listener is first started up.
  * The listener sends its unique window ID and its current URI to the actor.
  * If the actor returns an ID, we start the listeners. Otherwise, nothing happens.
  */
 function registerSelf() {
-  let msg = {value: winUtil.outerWindowID, href: content.location.href};
+  let msg = {value: winUtil.outerWindowID}
   // register will have the ID and a boolean describing if this is the main process or not
   let register = sendSyncMessage("Marionette:register", msg);
 
   if (register[0]) {
-    listenerId = register[0][0].id;
-    if (typeof listenerId != "undefined") {
+    let {id, remotenessChange} = register[0][0];
+    listenerId = id;
+    if (typeof id != "undefined") {
       // check if we're the main process
       if (register[0][1] == true) {
         addMessageListener("MarionetteMainListener:emitTouchEvent", emitTouchEventForIFrame);
       }
       importedScripts = FileUtils.getDir('TmpD', [], false);
       importedScripts.append('marionetteContentScripts');
       startListeners();
+      if (remotenessChange) {
+        sendAsyncMessage("Marionette:listenersAttached", {listenerId: id});
+      }
     }
   }
 }
 
 function emitTouchEventForIFrame(message) {
   message = message.json;
   let identifier = nextTouchId;
 
@@ -165,16 +172,18 @@ function startListeners() {
   addMessageListenerId("Marionette:newSession", newSession);
   addMessageListenerId("Marionette:executeScript", executeScript);
   addMessageListenerId("Marionette:executeAsyncScript", executeAsyncScript);
   addMessageListenerId("Marionette:executeJSScript", executeJSScript);
   addMessageListenerId("Marionette:singleTap", singleTap);
   addMessageListenerId("Marionette:actionChain", actionChain);
   addMessageListenerId("Marionette:multiAction", multiAction);
   addMessageListenerId("Marionette:get", get);
+  addMessageListenerId("Marionette:pollForReadyState", pollForReadyState);
+  addMessageListenerId("Marionette:cancelRequest", cancelRequest);
   addMessageListenerId("Marionette:getCurrentUrl", getCurrentUrl);
   addMessageListenerId("Marionette:getTitle", getTitle);
   addMessageListenerId("Marionette:getPageSource", getPageSource);
   addMessageListenerId("Marionette:goBack", goBack);
   addMessageListenerId("Marionette:goForward", goForward);
   addMessageListenerId("Marionette:refresh", refresh);
   addMessageListenerId("Marionette:findElementContent", findElementContent);
   addMessageListenerId("Marionette:findElementsContent", findElementsContent);
@@ -268,16 +277,18 @@ function deleteSession(msg) {
   removeMessageListenerId("Marionette:newSession", newSession);
   removeMessageListenerId("Marionette:executeScript", executeScript);
   removeMessageListenerId("Marionette:executeAsyncScript", executeAsyncScript);
   removeMessageListenerId("Marionette:executeJSScript", executeJSScript);
   removeMessageListenerId("Marionette:singleTap", singleTap);
   removeMessageListenerId("Marionette:actionChain", actionChain);
   removeMessageListenerId("Marionette:multiAction", multiAction);
   removeMessageListenerId("Marionette:get", get);
+  removeMessageListenerId("Marionette:pollForReadyState", pollForReadyState);
+  removeMessageListenerId("Marionette:cancelRequest", cancelRequest);
   removeMessageListenerId("Marionette:getTitle", getTitle);
   removeMessageListenerId("Marionette:getPageSource", getPageSource);
   removeMessageListenerId("Marionette:getCurrentUrl", getCurrentUrl);
   removeMessageListenerId("Marionette:goBack", goBack);
   removeMessageListenerId("Marionette:goForward", goForward);
   removeMessageListenerId("Marionette:refresh", refresh);
   removeMessageListenerId("Marionette:findElementContent", findElementContent);
   removeMessageListenerId("Marionette:findElementsContent", findElementsContent);
@@ -1399,84 +1410,119 @@ function multiAction(msg) {
     let pendingTouches = [];
     setDispatch(concurrentEvent, pendingTouches, command_id);
   }
   catch (e) {
     sendError(e.message, e.code, e.stack, msg.json.command_id);
   }
 }
 
+/*
+ * This implements the latter part of a get request (for the case we need to resume one
+ * when a remoteness update happens in the middle of a navigate request). This is most of
+ * of the work of a navigate request, but doesn't assume DOMContentLoaded is yet to fire.
+ */
+function pollForReadyState(msg, start, callback) {
+  let {pageTimeout, url, command_id} = msg.json;
+  start = start ? start : new Date().getTime();
+
+  if (!callback) {
+    callback = () => {};
+  }
+
+  let end = null;
+  function checkLoad() {
+    navTimer.cancel();
+    end = new Date().getTime();
+    let aboutErrorRegex = /about:.+(error)\?/;
+    let elapse = end - start;
+    if (pageTimeout == null || elapse <= pageTimeout) {
+      if (curFrame.document.readyState == "complete") {
+        callback();
+        sendOk(command_id);
+      } else if (curFrame.document.readyState == "interactive" &&
+                 aboutErrorRegex.exec(curFrame.document.baseURI) &&
+                 !curFrame.document.baseURI.startsWith(url)) {
+        // We have reached an error url without requesting it.
+        callback();
+        sendError("Error loading page", 13, null, command_id);
+      } else if (curFrame.document.readyState == "interactive" &&
+                 curFrame.document.baseURI.startsWith("about:")) {
+        callback();
+        sendOk(command_id);
+      } else {
+        navTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
+      }
+    }
+    else {
+      callback();
+      sendError("Error loading page, timed out (checkLoad)", 21, null,
+                command_id);
+    }
+  }
+  checkLoad();
+}
+
 /**
  * Navigate to the given URL.  The operation will be performed on the
  * current browser context, and handles the case where we navigate
  * within an iframe.  All other navigation is handled by the server
  * (in chrome space).
  */
 function get(msg) {
-  let command_id = msg.json.command_id;
+  let start = new Date().getTime();
 
-  let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-  let start = new Date().getTime();
-  let end = null;
-  function checkLoad() {
-    checkTimer.cancel();
-    end = new Date().getTime();
-    let aboutErrorRegex = /about:.+(error)\?/;
-    let elapse = end - start;
-    if (msg.json.pageTimeout == null || elapse <= msg.json.pageTimeout) {
-      if (curFrame.document.readyState == "complete") {
-        removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
-        sendOk(command_id);
-      } else if (curFrame.document.readyState == "interactive" &&
-                 aboutErrorRegex.exec(curFrame.document.baseURI) &&
-                 !curFrame.document.baseURI.startsWith(msg.json.url)) {
-        // We have reached an error url without requesting it.
-        removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
-        sendError("Error loading page", 13, null, command_id);
-      } else if (curFrame.document.readyState == "interactive" &&
-                 curFrame.document.baseURI.startsWith("about:")) {
-        removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
-        sendOk(command_id);
-      } else {
-        checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
-      }
-    }
-    else {
-      removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
-      sendError("Error loading page, timed out (checkLoad)", 21, null,
-                command_id);
-    }
-  }
   // Prevent DOMContentLoaded events from frames from invoking this
   // code, unless the event is coming from the frame associated with
   // the current window (i.e. someone has used switch_to_frame).
-  let onDOMContentLoaded = function onDOMContentLoaded(event) {
+  onDOMContentLoaded = function onDOMContentLoaded(event) {
     if (!event.originalTarget.defaultView.frameElement ||
         event.originalTarget.defaultView.frameElement == curFrame.frameElement) {
-      checkLoad();
+      pollForReadyState(msg, start, () => {
+        removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
+        onDOMContentLoaded = null;
+      });
     }
   };
 
   function timerFunc() {
     removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
     sendError("Error loading page, timed out (onDOMContentLoaded)", 21,
-              null, command_id);
+              null, msg.json.command_id);
   }
   if (msg.json.pageTimeout != null) {
-    checkTimer.initWithCallback(timerFunc, msg.json.pageTimeout, Ci.nsITimer.TYPE_ONE_SHOT);
+    navTimer.initWithCallback(timerFunc, msg.json.pageTimeout, Ci.nsITimer.TYPE_ONE_SHOT);
   }
   addEventListener("DOMContentLoaded", onDOMContentLoaded, false);
   curFrame.location = msg.json.url;
 }
 
+ /**
+ * Cancel the polling and remove the event listener associated with a current
+ * navigation request in case we're interupted by an onbeforeunload handler
+ * and navigation doesn't complete.
+ */
+function cancelRequest() {
+  navTimer.cancel();
+  if (onDOMContentLoaded) {
+    removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
+  }
+}
+
 /**
  * Get URL of the top level browsing context.
  */
 function getCurrentUrl(msg) {
-  sendResponse({value: curFrame.location.href}, msg.json.command_id);
+  let url;
+  if (msg.json.isB2G) {
+    url = curFrame.location.href;
+  } else {
+    url = content.location.href;
+  }
+  sendResponse({value: url}, msg.json.command_id);
 }
 
 /**
  * Get the current Title of the window
  */
 function getTitle(msg) {
   sendResponse({value: curFrame.top.document.title}, msg.json.command_id);
 }
--- a/testing/marionette/marionette-server.js
+++ b/testing/marionette/marionette-server.js
@@ -283,18 +283,20 @@ MarionetteServerConnection.prototype = {
               break;
           }
           let code = error.hasOwnProperty('code') ? e.code : 500;
           this.sendError(error.toString(), code, error.stack, commandId);
         }
       }
     }
     else {
-      this.messageManager.broadcastAsyncMessage(
-        "Marionette:" + name + this.curBrowser.curFrameId, values);
+      this.curBrowser.executeWhenReady(() => {
+        this.messageManager.broadcastAsyncMessage(
+          "Marionette:" + name + this.curBrowser.curFrameId, values);
+      });
     }
     return success;
   },
 
   logRequest: function MDA_logRequest(type, data) {
     logger.debug("Got request: " + type + ", data: " + JSON.stringify(data) + ", id: " + this.command_id);
   },
 
@@ -324,16 +326,21 @@ MarionetteServerConnection.prototype = {
         logger.warn("ignoring duplicate response for command_id " + command_id);
         return;
       }
       else if (this.command_id != command_id) {
         logger.warn("ignoring out-of-sync response");
         return;
       }
     }
+
+    if (this.curBrowser !== null) {
+      this.curBrowser.pendingCommands = [];
+    }
+
     this.conn.send(msg);
     if (command_id != -1) {
       // Don't unset this.command_id if this message is to process an
       // emulator callback, since another response for this command_id is
       // expected, after the containing call to execute_async_script finishes.
       this.command_id = null;
     }
   },
@@ -1273,17 +1280,27 @@ MarionetteServerConnection.prototype = {
    * authentication is required, the page load is assumed to be
    * complete.  This does not include FORM-based authentication.
    *
    * @param object aRequest where <code>url</code> property holds the
    *        URL to navigate to
    */
   get: function MDA_get(aRequest) {
     let command_id = this.command_id = this.getCommandId();
+
     if (this.context != "chrome") {
+      // If a remoteness update interrupts our page load, this will never return
+      // We need to re-issue this request to correctly poll for readyState and
+      // send errors.
+      this.curBrowser.pendingCommands.push(() => {
+        aRequest.parameters.command_id = command_id;
+        this.messageManager.broadcastAsyncMessage(
+          "Marionette:pollForReadyState" + this.curBrowser.curFrameId,
+          aRequest.parameters);
+      });
       aRequest.command_id = command_id;
       aRequest.parameters.pageTimeout = this.pageTimeout;
       this.sendAsync("get", aRequest.parameters, command_id);
       return;
     }
 
     // At least on desktop, navigating in chrome scope does not
     // correspond to something a user can do, and leaves marionette
@@ -1332,25 +1349,17 @@ MarionetteServerConnection.prototype = {
    */
   getCurrentUrl: function MDA_getCurrentUrl() {
     let isB2G = appName == "B2G";
     this.command_id = this.getCommandId();
     if (this.context === "chrome") {
       this.sendResponse(this.getCurrentWindow().location.href, this.command_id);
     }
     else {
-      if (isB2G) {
-        this.sendAsync("getCurrentUrl", {}, this.command_id);
-      }
-      else {
-        this.sendResponse(this.curBrowser
-                              .tab
-                              .linkedBrowser
-                              .contentWindowAsCPOW.location.href, this.command_id);
-      }
+      this.sendAsync("getCurrentUrl", {isB2G: isB2G}, this.command_id);
     }
   },
 
   /**
    * Gets the current title of the window
    */
   getTitle: function MDA_getTitle() {
     this.command_id = this.getCommandId();
@@ -1435,16 +1444,23 @@ MarionetteServerConnection.prototype = {
       if (this.curBrowser == this.browsers[i]) {
         this.sendResponse(i, this.command_id);
         return;
       }
     }
   },
 
   /**
+   * Forces an update for the given browser's id.
+   */
+  updateIdForBrowser: function (browser, newId) {
+    this._browserIds.set(browser.permanentKey, newId);
+  },
+
+  /**
    * Retrieves a listener id for the given xul browser element. In case
    * the browser is not known, an attempt is made to retrieve the id from
    * a CPOW, and null is returned if this fails.
    */
   getIdForBrowser: function (browser) {
     if (browser === null) {
       return null;
     }
@@ -1596,18 +1612,17 @@ MarionetteServerConnection.prototype = {
         if (this.browsers[outerId] === undefined) {
           //enable Marionette in that browser window
           this.startBrowser(win, false);
         } else {
           utils.window = win;
           this.curBrowser = this.browsers[outerId];
           if (contentWindowId) {
             // The updated id corresponds to switching to a new tab.
-            this.curBrowser.curFrameId = contentWindowId;
-            win.gBrowser.selectTabAtIndex(ind);
+            this.curBrowser.switchToTab(ind);
           }
           this.sendOk(command_id);
         }
         return true;
       }
       return false;
     }
 
@@ -3067,16 +3082,17 @@ MarionetteServerConnection.prototype = {
       return;
     }
 
     if (topic == "common-dialog-loaded") {
       this._dialogWindowRef = Cu.getWeakReference(subject);
     }
 
     if (this.command_id) {
+      this.sendAsync("cancelRequest", {});
       // This is a shortcut to get the client to accept our response whether
       // the expected key is 'ok' (in case a click or similar got us here)
       // or 'value' (in case an execute script or similar got us here).
       this.sendToClient({from:this.actorID, ok: true, value: null}, this.command_id);
     }
   },
 
   /**
@@ -3209,18 +3225,19 @@ MarionetteServerConnection.prototype = {
         } catch (ex) {
           // browserType remains undefined.
         }
         let reg = {};
         // this will be sent to tell the content process if it is the main content
         let mainContent = (this.curBrowser.mainContentId == null);
         if (!browserType || browserType != "content") {
           //curBrowser holds all the registered frames in knownFrames
-          let listenerId = this.generateFrameId(message.json.value);
-          reg.id = this.curBrowser.register(listenerId);
+          let uid = this.generateFrameId(message.json.value);
+          reg.id = uid;
+          reg.remotenessChange = this.curBrowser.register(uid, message.target);
         }
         // set to true if we updated mainContentId
         mainContent = ((mainContent == true) && (this.curBrowser.mainContentId != null));
         if (mainContent) {
           this.mainContentFrameId = this.curBrowser.curFrameId;
         }
         this.curBrowser.elementManager.seenItems[reg.id] = Cu.getWeakReference(listenerWindow);
         if (nullPrevious && (this.curBrowser.curFrameId != null)) {
@@ -3248,16 +3265,29 @@ MarionetteServerConnection.prototype = {
         }
         return [reg, mainContent];
       case "Marionette:emitTouchEvent":
         let globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"]
                              .getService(Ci.nsIMessageBroadcaster);
         globalMessageManager.broadcastAsyncMessage(
           "MarionetteMainListener:emitTouchEvent", message.json);
         return;
+      case "Marionette:listenersAttached":
+        if (message.json.listenerId === this.curBrowser.curFrameId) {
+          // If remoteness gets updated we need to call newSession. In the case
+          // of desktop this just sets up a small amount of state that doesn't
+          // change over the course of a session.
+          let newSessionValues = {
+            B2G: (appName == "B2G"),
+            raisesAccessibilityExceptions: this.sessionCapabilities.raisesAccessibilityExceptions
+          };
+          this.sendAsync("newSession", newSessionValues);
+          this.curBrowser.flushPendingCommands();
+        }
+        return;
     }
   }
 };
 
 MarionetteServerConnection.prototype.requestTypes = {
   "getMarionetteID": MarionetteServerConnection.prototype.getMarionetteID,
   "sayHello": MarionetteServerConnection.prototype.sayHello,
   "newSession": MarionetteServerConnection.prototype.newSession,
@@ -3365,25 +3395,59 @@ function BrowserObj(win, server) {
   this.curFrameId = null;
   this.startPage = "about:blank";
   this.mainContentId = null; // used in B2G to identify the homescreen content page
   this.newSession = true; //used to set curFrameId upon new session
   this.elementManager = new ElementManager([NAME, LINK_TEXT, PARTIAL_LINK_TEXT]);
   this.setBrowser(win);
   this.frameManager = new FrameManager(server); //We should have one FM per BO so that we can handle modals in each Browser
 
+  // A reference to the tab corresponding to the current window handle, if any.
+  this.tab = null;
+  this.pendingCommands = [];
+
   //register all message listeners
   this.frameManager.addMessageManagerListeners(server.messageManager);
   this.getIdForBrowser = server.getIdForBrowser.bind(server);
+  this.updateIdForBrowser = server.updateIdForBrowser.bind(server);
+  this._curFrameId = null;
+  this._browserWasRemote = null;
+  this._hasRemotenessChange = false;
 }
 
 BrowserObj.prototype = {
-  get tab () {
-    // A reference to the currently selected tab, if any
-    return this.browser ? this.browser.selectedTab : null;
+
+  /**
+   * This function intercepts commands interacting with content and queues
+   * or executes them as needed.
+   *
+   * No commands interacting with content are safe to process until
+   * the new listener script is loaded and registers itself.
+   * This occurs when a command whose effect is asynchronous (such
+   * as goBack) results in a remoteness change and new commands
+   * are subsequently posted to the server.
+   */
+  executeWhenReady: function (callback) {
+    if (this.hasRemotenessChange()) {
+      this.pendingCommands.push(callback);
+    } else {
+      callback();
+    }
+  },
+
+  /**
+   * Re-sets this BrowserObject's current tab and updates remoteness tracking.
+   */
+  switchToTab: function (ind) {
+    if (this.browser) {
+      this.browser.selectTabAtIndex(ind);
+      this.tab = this.browser.selectedTab;
+    }
+    this._browserWasRemote = this.browser.getBrowserForTab(this.tab).isRemoteBrowser;
+    this._hasRemotenessChange = false;
   },
 
   /**
    * Retrieves the current tabmodal ui object. According to the browser associated
    * with the currently selected tab.
    */
   getTabModalUI: function MDA__getTabModaUI () {
     let browserForTab = this.browser.getBrowserForTab(this.tab);
@@ -3413,25 +3477,41 @@ BrowserObj.prototype = {
           appName = "B2G";
         }
         break;
       case "Fennec":
         this.browser = win.BrowserApp;
         break;
     }
   },
+
+  // The current frame id is managed per browser element on desktop in case
+  // the id needs to be refreshed. The currently selected window is identified
+  // within BrowserObject by a tab.
+  get curFrameId () {
+    if (appName != "Firefox") {
+      return this._curFrameId;
+    }
+    if (this.tab) {
+      let browser = this.browser.getBrowserForTab(this.tab);
+      return this.getIdForBrowser(browser);
+    }
+    return null;
+  },
+
+  set curFrameId (id) {
+    if (appName != "Firefox") {
+      this._curFrameId = id;
+    }
+  },
+
   /**
    * Called when we start a session with this browser.
    */
   startSession: function BO_startSession(newSession, win, callback) {
-    if (appName == "Firefox" &&
-        win.gMultiProcessBrowser &&
-        !win.gBrowser.selectedBrowser.isRemoteBrowser) {
-      win.XULBrowserWindow.forceInitialBrowserRemote();
-    }
     callback(win, newSession);
   },
 
   /**
    * Closes current tab
    */
   closeTab: function BO_closeTab() {
     if (this.browser &&
@@ -3453,38 +3533,79 @@ BrowserObj.prototype = {
 
   /**
    * Registers a new frame, and sets its current frame id to this frame
    * if it is not already assigned, and if a) we already have a session
    * or b) we're starting a new session and it is the right start frame.
    *
    * @param string uid
    *        frame uid for use by marionette
+   * @param the XUL <browser> that was the target of the originating message.
    */
-  register: function BO_register(uid) {
-    if (this.curFrameId == null) {
-      let currWinId = null;
+  register: function BO_register(uid, target) {
+    let remotenessChange = this.hasRemotenessChange();
+    if (this.curFrameId === null || remotenessChange) {
       if (this.browser) {
         // If we're setting up a new session on Firefox, we only process the
-        // registration for this frame if it belongs to the tab we've just
-        // created.
+        // registration for this frame if it belongs to the current tab.
+        if (!this.tab) {
+          this.switchToTab(this.browser.selectedIndex);
+        }
+
         let browser = this.browser.getBrowserForTab(this.tab);
-        currWinId = this.getIdForBrowser(browser);
-      }
-      if ((!this.newSession) ||
-          (this.newSession &&
-            ((appName != "Firefox") ||
-             uid === currWinId))) {
-        this.curFrameId = uid;
+        if (target == browser) {
+          this.updateIdForBrowser(browser, uid);
+          this.mainContentId = uid;
+        }
+      } else {
+        this._curFrameId = uid;
         this.mainContentId = uid;
       }
     }
+
     this.knownFrames.push(uid); //used to delete sessions
-    return uid;
+    return remotenessChange;
   },
+
+  /**
+   * When navigating between pages results in changing a browser's process, we
+   * need to take measures not to lose contact with a listener script. This
+   * function does the necessary bookkeeping.
+   */
+  hasRemotenessChange: function () {
+    // None of these checks are relevant on b2g or if we don't have a tab yet,
+    // and may not apply on Fennec.
+    if (appName != "Firefox" || this.tab === null) {
+      return false;
+    }
+    if (this._hasRemotenessChange) {
+      return true;
+    }
+    let currentIsRemote = this.browser.getBrowserForTab(this.tab).isRemoteBrowser;
+    this._hasRemotenessChange = this._browserWasRemote !== currentIsRemote;
+    this._browserWasRemote = currentIsRemote;
+    return this._hasRemotenessChange;
+  },
+
+  /**
+   * Flushes any pending commands queued when a remoteness change is being
+   * processed and mark this remotenessUpdate as complete.
+   */
+  flushPendingCommands: function () {
+    if (!this._hasRemotenessChange) {
+      return;
+    }
+
+    this._hasRemotenessChange = false;
+    this.pendingCommands.forEach((callback) => {
+      callback();
+    });
+    this.pendingCommands = [];
+  }
+
 }
 
 /**
  * Marionette server -- this class holds a reference to a socket and creates
  * MarionetteServerConnection objects as needed.
  */
 this.MarionetteServer = function MarionetteServer(port, forceLocal) {
   let flags = Ci.nsIServerSocket.KeepWhenOffline;