Bug 977043 - Tweak TabActor to support changing its targeted context to an iframe. r=bgrins,past
authorAlexandre Poirot <poirot.alex@gmail.com>
Wed, 27 Aug 2014 12:19:30 +0200
changeset 223412 ceb3967c0d39e27d1590e5f51f1f0cd90626b28f
parent 223411 a23b7b404770cbfa0ef81ea7b82f49f175e717c9
child 223413 f74dd51cbfb9b60ed30e6cd568e30835ef9c4592
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins, past
bugs977043
milestone34.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 977043 - Tweak TabActor to support changing its targeted context to an iframe. r=bgrins,past
toolkit/devtools/client/dbg-client.jsm
toolkit/devtools/server/actors/childtab.js
toolkit/devtools/server/actors/webbrowser.js
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -220,16 +220,17 @@ const UnsolicitedNotifications = {
   "newGlobal": "newGlobal",
   "newScript": "newScript",
   "newSource": "newSource",
   "tabDetached": "tabDetached",
   "tabListChanged": "tabListChanged",
   "reflowActivity": "reflowActivity",
   "addonListChanged": "addonListChanged",
   "tabNavigated": "tabNavigated",
+  "frameUpdate": "frameUpdate",
   "pageError": "pageError",
   "documentLoad": "documentLoad",
   "enteredFrame": "enteredFrame",
   "exitedFrame": "exitedFrame",
   "appOpen": "appOpen",
   "appClose": "appClose",
   "appInstall": "appInstall",
   "appUninstall": "appUninstall"
--- a/toolkit/devtools/server/actors/childtab.js
+++ b/toolkit/devtools/server/actors/childtab.js
@@ -33,17 +33,17 @@ ContentActor.prototype = Object.create(T
 
 ContentActor.prototype.constructor = ContentActor;
 
 Object.defineProperty(ContentActor.prototype, "docShell", {
   get: function() {
     return this._chromeGlobal.docShell;
   },
   enumerable: true,
-  configurable: false
+  configurable: true
 });
 
 ContentActor.prototype.exit = function() {
   TabActor.prototype.exit.call(this);
 };
 
 // Override grip just to rename this._tabActorPool to this._tabActorPool2
 // in order to prevent it to be cleaned on detach.
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -28,19 +28,21 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyGetter(this, "events", () => {
   return require("sdk/event/core");
 });
 
 XPCOMUtils.defineLazyGetter(this, "StyleSheetActor", () => {
   return require("devtools/server/actors/stylesheets").StyleSheetActor;
 });
 
-// Also depends on following symbols, shared by common scope with main.js:
-// DebuggerServer, CommonCreateExtraActors, CommonAppendExtraActors, ActorPool,
-// ThreadActor
+function getWindowID(window) {
+  return window.QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIDOMWindowUtils)
+               .currentInnerWindowID;
+}
 
 /**
  * Browser-specific actors.
  */
 
 /**
  * Yield all windows of type |aWindowType|, from the oldest window to the
  * youngest, using nsIWindowMediator::getEnumerator. We're usually
@@ -554,17 +556,17 @@ function TabActor(aConnection)
 
   this._shouldAddNewGlobalAsDebuggee = this._shouldAddNewGlobalAsDebuggee.bind(this);
 
   this.makeDebugger = makeDebugger.bind(null, {
     findDebuggees: () => this.windows,
     shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee
   });
 
-  this.traits = { reconfigure: true };
+  this.traits = { reconfigure: true, frames: true };
 }
 
 // XXX (bug 710213): TabActor attach/detach/exit/disconnect is a
 // *complete* mess, needs to be rethought asap.
 
 TabActor.prototype = {
   traits: null,
 
@@ -615,17 +617,20 @@ TabActor.prototype = {
   get docShells() {
     let docShellsEnum = this.docShell.getDocShellEnumerator(
       Ci.nsIDocShellTreeItem.typeAll,
       Ci.nsIDocShell.ENUMERATE_FORWARDS
     );
 
     let docShells = [];
     while (docShellsEnum.hasMoreElements()) {
-      docShells.push(docShellsEnum.getNext());
+      let docShell = docShellsEnum.getNext();
+      docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+              .getInterface(Ci.nsIWebProgress);
+      docShells.push(docShell);
     }
 
     return docShells;
   },
 
   /**
    * Getter for the tab content's DOM window.
    */
@@ -802,19 +807,184 @@ TabActor.prototype = {
     dbg_assert(!this._tabPool, "Shouldn't have a tab pool if we weren't attached.");
     this._tabPool = new ActorPool(this.conn);
     this.conn.addActorPool(this._tabPool);
 
     // ... and a pool for context-lifetime actors.
     this._pushContext();
 
     this._progressListener = new DebuggerProgressListener(this);
+
+    // Save references to the original document we attached to
+    this._originalWindow = this.window;
+
+    // Ensure replying to attach() request first
+    // before notifying about new docshells.
+    DevToolsUtils.executeSoon(() => this._watchDocshells());
+
+    this._attached = true;
+  },
+
+  _watchDocshells: function BTA_watchDocshells() {
+    // In child processes, we watch all docshells living in the process.
+    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+      Services.obs.addObserver(this, "webnavigation-create", false);
+    }
+    Services.obs.addObserver(this, "webnavigation-destroy", false);
+
+    // We watch for all child docshells under the current document,
     this._progressListener.watch(this.docShell);
 
-    this._attached = true;
+    // And list all already existing ones.
+    this._updateChildDocShells();
+  },
+
+  onSwitchToFrame: function BTA_onSwitchToFrame(aRequest) {
+    let windowId = aRequest.windowId;
+    let win;
+    try {
+      win = Services.wm.getOuterWindowWithId(windowId);
+    } catch(e) {}
+    if (!win) {
+      return { error: "noWindow",
+               message: "The related docshell is destroyed or not found" };
+    } else if (win == this.window) {
+      return {};
+    }
+
+    // Reply first before changing the document
+    DevToolsUtils.executeSoon(() => this._changeTopLevelDocument(win));
+
+    return {};
+  },
+
+  onListFrames: function BTA_onListFrames(aRequest) {
+    let windows = this._docShellsToWindows(this.docShells);
+    return { frames: windows };
+  },
+
+  observe: function (aSubject, aTopic, aData) {
+    // Ignore any event that comes before/after the tab actor is attached
+    // That typically happens during firefox shutdown.
+    if (!this.attached) {
+      return;
+    }
+    if (aTopic == "webnavigation-create") {
+      aSubject.QueryInterface(Ci.nsIDocShell);
+      // webnavigation-create is fired very early during docshell construction.
+      // In new root docshells within child processes, involving TabChild,
+      // this event is from within this call:
+      //   http://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l912
+      // whereas the chromeEventHandler (and most likely other stuff) is set later:
+      //   http://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l944
+      // So wait a tick before watching it:
+      DevToolsUtils.executeSoon(() => {
+        // In child processes, we have new root docshells,
+        // let's watch them and all their child docshells.
+        if (this._isRootDocShell(aSubject)) {
+          this._progressListener.watch(aSubject);
+        }
+        this._notifyDocShellsUpdate([aSubject]);
+      });
+    } else if (aTopic == "webnavigation-destroy") {
+      let webProgress = aSubject.QueryInterface(Ci.nsIInterfaceRequestor)
+                                .getInterface(Ci.nsIWebProgress);
+      this._notifyDocShellDestroy(webProgress);
+    }
+  },
+
+  _isRootDocShell: function (docShell) {
+    // Root docshells like top level xul windows don't have chromeEventHandler.
+    // Root docshells in child processes have one, it is TabChildGlobal,
+    // which isn't a DOM Element.
+    // Non-root docshell have a chromeEventHandler that is either
+    // xul:iframe, xul:browser or html:iframe.
+    return !docShell.chromeEventHandler ||
+           !(docShell.chromeEventHandler instanceof Ci.nsIDOMElement);
+  },
+
+  // Convert docShell list to windows objects list being sent to the client
+  _docShellsToWindows: function (docshells) {
+    return docshells.map(docShell => {
+      let window = docShell.DOMWindow;
+      let id = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                     .getInterface(Ci.nsIDOMWindowUtils)
+                     .outerWindowID;
+      let parentID = undefined;
+      // Ignore the parent of the original document on non-e10s firefox,
+      // as we get the xul window as parent and don't care about it.
+      if (window.parent && window != this._originalWindow) {
+        parentID = window.parent
+                         .QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIDOMWindowUtils)
+                         .outerWindowID;
+      }
+      return {
+        id: id,
+        url: window.location.href,
+        title: window.title,
+        parentID: parentID
+      };
+    });
+  },
+
+  _notifyDocShellsUpdate: function (docshells) {
+    let windows = this._docShellsToWindows(docshells);
+    this.conn.send({ from: this.actorID,
+                     type: "frameUpdate",
+                     frames: windows
+                   });
+  },
+
+  _updateChildDocShells: function () {
+    this._notifyDocShellsUpdate(this.docShells);
+  },
+
+  _notifyDocShellDestroy: function (webProgress) {
+    let id = webProgress.DOMWindow
+                        .QueryInterface(Ci.nsIInterfaceRequestor)
+                        .getInterface(Ci.nsIDOMWindowUtils)
+                        .outerWindowID;
+    this.conn.send({ from: this.actorID,
+                     type: "frameUpdate",
+                     frames: [{
+                       id: id,
+                       destroy: true
+                     }]
+                   });
+
+    // Stop watching this docshell if it's a root one.
+    // (child processes spawn new root docshells)
+    webProgress.QueryInterface(Ci.nsIDocShell);
+    if (this._isRootDocShell(webProgress)) {
+      this._progressListener.unwatch(webProgress);
+    }
+
+    if (webProgress.DOMWindow == this._originalWindow) {
+      // If for some reason (typically during Firefox shutdown), the original
+      // document is destroyed, we detach the tab actor to unregister all listeners
+      // and prevent any exception.
+      this.exit();
+      return;
+    }
+
+    // If the currently targeted context is destroyed,
+    // and we aren't on the top-level document,
+    // we have to switch to the top-level one.
+    if (webProgress.DOMWindow == this.window &&
+        this.window != this._originalWindow) {
+      this._changeTopLevelDocument(this._originalWindow);
+    }
+  },
+
+  _notifyDocShellDestroyAll: function () {
+    this.conn.send({ from: this.actorID,
+                     type: "frameUpdate",
+                     destroyAll: true
+                   });
   },
 
   /**
    * Creates a thread actor and a pool for context-lifetime actors. It then sets
    * up the content window for debugging.
    */
   _pushContext: function BTA_pushContext() {
     dbg_assert(!this._contextPool, "Can't push multiple contexts");
@@ -851,16 +1021,23 @@ TabActor.prototype = {
 
     // Check for docShell availability, as it can be already gone
     // during Firefox shutdown.
     if (this.docShell) {
       this._progressListener.unwatch(this.docShell);
     }
     this._progressListener.destroy();
     this._progressListener = null;
+    this._originalWindow = null;
+
+    // Removes the observers being set in _watchDocShells
+    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+      Services.obs.removeObserver(this, "webnavigation-create", false);
+    }
+    Services.obs.removeObserver(this, "webnavigation-destroy", false);
 
     this._popContext();
 
     // Shut down actors that belong to this tab's pool.
     this.conn.removeActorPool(this._tabPool);
     this._tabPool = null;
     if (this._tabActorPool) {
       this.conn.removeActorPool(this._tabActorPool);
@@ -1038,66 +1215,133 @@ TabActor.prototype = {
     windowUtils.resumeTimeouts();
     windowUtils.suppressEventHandling(false);
     if (this._pendingNavigation) {
       this._pendingNavigation.resume();
       this._pendingNavigation = null;
     }
   },
 
+  _changeTopLevelDocument: function (window) {
+    // Fake a will-navigate on the previous document
+    // to let a chance to unregister it
+    this._willNavigate(this.window, window.location.href, null, true);
+
+    this._windowDestroyed(this.window);
+
+    DevToolsUtils.executeSoon(() => {
+      this._setWindow(window);
+
+      // Then fake window-ready and navigate on the given document
+      this._windowReady(window, true);
+      DevToolsUtils.executeSoon(() => {
+        this._navigate(window, true);
+      });
+    });
+  },
+
+  _setWindow: function (window) {
+    let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIWebNavigation)
+                         .QueryInterface(Ci.nsIDocShell);
+    // Here is the very important call where we switch the currently
+    // targeted context (it will indirectly update this.window and
+    // many other attributes defined from docShell).
+    Object.defineProperty(this, "docShell", {
+      value: docShell,
+      enumerable: true,
+      configurable: true
+    });
+    events.emit(this, "changed-toplevel-document");
+    let id = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsIDOMWindowUtils)
+                   .outerWindowID;
+    this.conn.send({ from: this.actorID,
+                     type: "frameUpdate",
+                     selected: id
+                   });
+  },
+
   /**
    * Handle location changes, by clearing the previous debuggees and enabling
    * debugging, which may have been disabled temporarily by the
    * DebuggerProgressListener.
    */
-  _windowReady: function (window) {
+  _windowReady: function (window, isFrameSwitching = false) {
     let isTopLevel = window == this.window;
 
+    // We just reset iframe list on WillNavigate, so we now list all existing
+    // frames when we load a new document in the original window
+    if (window == this._originalWindow && !isFrameSwitching) {
+      this._updateChildDocShells();
+    }
+
     events.emit(this, "window-ready", {
       window: window,
-      isTopLevel: isTopLevel
+      isTopLevel: isTopLevel,
+      id: getWindowID(window)
     });
 
     // TODO bug 997119: move that code to ThreadActor by listening to window-ready
     let threadActor = this.threadActor;
     if (isTopLevel) {
       threadActor.clearDebuggees();
       if (threadActor.dbg) {
         threadActor.dbg.enabled = true;
-        threadActor.global = window;
         threadActor.maybePauseOnExceptions();
       }
+      // Update the global no matter if the debugger is on or off,
+      // otherwise the global will be wrong when enabled later.
+      threadActor.global = window;
     }
 
     for (let sheetActor of this._styleSheetActors.values()) {
       this._tabPool.removeActor(sheetActor);
     }
     this._styleSheetActors.clear();
 
 
     // Refresh the debuggee list when a new window object appears (top window or
     // iframe).
     if (threadActor.attached) {
       threadActor.dbg.addDebuggees();
     }
   },
 
-  _windowDestroyed: function (window) {
+  _windowDestroyed: function (window, id = null) {
     events.emit(this, "window-destroyed", {
       window: window,
-      isTopLevel: window == this.window
+      isTopLevel: window == this.window,
+      id: id || getWindowID(window)
     });
   },
 
   /**
-   * Start notifying server codebase and client about a new document
+   * Start notifying server and client about a new document
    * being loaded in the currently targeted context.
    */
-  _willNavigate: function (window, newURI, request) {
+  _willNavigate: function (window, newURI, request, isFrameSwitching = false) {
     let isTopLevel = window == this.window;
+    let reset = false;
+
+    if (window == this._originalWindow && !isFrameSwitching) {
+      // Clear the iframe list if the original top-level document changes.
+      this._notifyDocShellDestroyAll();
+
+      // If the top level document changes and we are targeting
+      // an iframe, we need to reset to the upcoming new top level document.
+      // But for this will-navigate event, we will dispatch on the old window.
+      // (The inspector codebase expect to receive will-navigate for the currently
+      // displayed document in order to cleanup the markup view)
+      if (this.window != this._originalWindow) {
+        reset=true;
+        window = this.window;
+        isTopLevel = true;
+      }
+    }
 
     // will-navigate event needs to be dispatched synchronously,
     // by calling the listeners in the order or registration.
     // This event fires once navigation starts,
     // (all pending user prompts are dealt with),
     // but before the first request starts.
     events.emit(this, "will-navigate", {
       window: window,
@@ -1123,25 +1367,30 @@ TabActor.prototype = {
     }
     threadActor.disableAllBreakpoints();
 
     this.conn.send({
       from: this.actorID,
       type: "tabNavigated",
       url: newURI,
       nativeConsoleAPI: true,
-      state: "start"
+      state: "start",
+      isFrameSwitching: isFrameSwitching
     });
+
+    if (reset) {
+      this._setWindow(this._originalWindow);
+    }
   },
 
   /**
    * Notify server and client about a new document done loading in the current
    * targeted context.
    */
-  _navigate: function (window) {
+  _navigate: function (window, isFrameSwitching = false) {
     let isTopLevel = window == this.window;
 
     // navigate event needs to be dispatched synchronously,
     // by calling the listeners in the order or registration.
     // This event is fired once the document is loaded,
     // after the load event, it's document ready-state is 'complete'.
     events.emit(this, "navigate", {
       window: window,
@@ -1161,17 +1410,18 @@ TabActor.prototype = {
     }
 
     this.conn.send({
       from: this.actorID,
       type: "tabNavigated",
       url: this.url,
       title: this.title,
       nativeConsoleAPI: this.hasNativeConsoleAPI(this.window),
-      state: "stop"
+      state: "stop",
+      isFrameSwitching: isFrameSwitching
     });
   },
 
   /**
    * Tells if the window.console object is native or overwritten by script in
    * the page.
    *
    * @param nsIDOMWindow aWindow
@@ -1217,17 +1467,19 @@ TabActor.prototype = {
 /**
  * The request types this actor can handle.
  */
 TabActor.prototype.requestTypes = {
   "attach": TabActor.prototype.onAttach,
   "detach": TabActor.prototype.onDetach,
   "reload": TabActor.prototype.onReload,
   "navigateTo": TabActor.prototype.onNavigateTo,
-  "reconfigure": TabActor.prototype.onReconfigure
+  "reconfigure": TabActor.prototype.onReconfigure,
+  "switchToFrame": TabActor.prototype.onSwitchToFrame,
+  "listFrames": TabActor.prototype.onListFrames
 };
 
 exports.TabActor = TabActor;
 
 /**
  * Creates a tab actor for handling requests to a single in-process
  * <browser> tab. Most of the implementation comes from TabActor.
  *
@@ -1253,17 +1505,17 @@ Object.defineProperty(BrowserTabActor.pr
   get: function() {
     if (this._browser) {
       return this._browser.docShell;
     }
     // The tab is closed.
     return null;
   },
   enumerable: true,
-  configurable: false
+  configurable: true
 });
 
 Object.defineProperty(BrowserTabActor.prototype, "title", {
   get: function() {
     let title = this.contentDocument.title || this._browser.contentTitle;
     // If contentTitle is empty (e.g. on a not-yet-restored tab), but there is a
     // tabbrowser (i.e. desktop Firefox, but not Fennec), we can use the label
     // as the title.
@@ -1632,36 +1884,39 @@ DebuggerProgressListener.prototype = {
 
     handler.addEventListener("DOMWindowCreated", this._onWindowCreated, true);
     handler.addEventListener("pageshow", this._onWindowCreated, true);
     handler.addEventListener("pagehide", this._onWindowHidden, true);
 
     // Dispatch the _windowReady event on the tabActor for pre-existing windows
     for (let win of this._getWindowsInDocShell(docShell)) {
       this._tabActor._windowReady(win);
-      this._knownWindowIDs.set(this._getWindowID(win), win);
+      this._knownWindowIDs.set(getWindowID(win), win);
     }
   },
 
   unwatch: function(docShell) {
     let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                               .getInterface(Ci.nsIWebProgress);
-    webProgress.removeProgressListener(this);
+    // During process shutdown, the docshell may already be cleaned up and throw
+    try {
+      webProgress.removeProgressListener(this);
+    } catch(e) {}
 
     // TODO: fix docShell.chromeEventHandler in child processes!
     let handler = docShell.chromeEventHandler ||
                   docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                     .getInterface(Ci.nsIContentFrameMessageManager);
 
     handler.removeEventListener("DOMWindowCreated", this._onWindowCreated, true);
     handler.removeEventListener("pageshow", this._onWindowCreated, true);
     handler.removeEventListener("pagehide", this._onWindowHidden, true);
 
     for (let win of this._getWindowsInDocShell(docShell)) {
-      this._knownWindowIDs.delete(this._getWindowID(win));
+      this._knownWindowIDs.delete(getWindowID(win));
     }
   },
 
   _getWindowsInDocShell: function(docShell) {
     let docShellsEnum = docShell.getDocShellEnumerator(
       Ci.nsIDocShellTreeItem.typeAll,
       Ci.nsIDocShell.ENUMERATE_FORWARDS
     );
@@ -1670,39 +1925,33 @@ DebuggerProgressListener.prototype = {
     while (docShellsEnum.hasMoreElements()) {
       let w = docShellsEnum.getNext().QueryInterface(Ci.nsIInterfaceRequestor)
                                      .getInterface(Ci.nsIDOMWindow);
       windows.push(w);
     }
     return windows;
   },
 
-  _getWindowID: function(window) {
-    return window.QueryInterface(Ci.nsIInterfaceRequestor)
-                 .getInterface(Ci.nsIDOMWindowUtils)
-                 .currentInnerWindowID;
-  },
-
   onWindowCreated: DevToolsUtils.makeInfallible(function(evt) {
     if (!this._tabActor.attached) {
       return;
     }
 
     // pageshow events for non-persisted pages have already been handled by a
     // prior DOMWindowCreated event. For persisted pages, act as if the window
     // had just been created since it's been unfrozen from bfcache.
     if (evt.type == "pageshow" && !evt.persisted) {
       return;
     }
 
     let window = evt.target.defaultView;
     this._tabActor._windowReady(window);
 
     if (evt.type !== "pageshow") {
-      this._knownWindowIDs.set(this._getWindowID(window), window);
+      this._knownWindowIDs.set(getWindowID(window), window);
     }
   }, "DebuggerProgressListener.prototype.onWindowCreated"),
 
   onWindowHidden: DevToolsUtils.makeInfallible(function(evt) {
     if (!this._tabActor.attached) {
       return;
     }
 
@@ -1725,37 +1974,48 @@ DebuggerProgressListener.prototype = {
 
     // Because this observer will be called for all inner-window-destroyed in
     // the application, we need to filter out events for windows we are not
     // watching
     let innerID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
     let window = this._knownWindowIDs.get(innerID);
     if (window) {
       this._knownWindowIDs.delete(innerID);
-      this._tabActor._windowDestroyed(window);
+      this._tabActor._windowDestroyed(window, innerID);
     }
   }, "DebuggerProgressListener.prototype.observe"),
 
   onStateChange:
   DevToolsUtils.makeInfallible(function(aProgress, aRequest, aFlag, aStatus) {
     if (!this._tabActor.attached) {
       return;
     }
 
     let isStart = aFlag & Ci.nsIWebProgressListener.STATE_START;
     let isStop = aFlag & Ci.nsIWebProgressListener.STATE_STOP;
     let isDocument = aFlag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
     let isWindow = aFlag & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
 
+    // Catch any iframe location change
+    if (isDocument && isStop) {
+      // Watch document stop to ensure having the new iframe url.
+      aProgress.QueryInterface(Ci.nsIDocShell);
+      this._tabActor._notifyDocShellsUpdate([aProgress]);
+    }
+
     let window = aProgress.DOMWindow;
     if (isDocument && isStart) {
+      // One of the earliest events that tells us a new URI
+      // is being loaded in this window.
       let newURI = aRequest instanceof Ci.nsIChannel ? aRequest.URI.spec : null;
       this._tabActor._willNavigate(window, newURI, aRequest);
     }
     if (isWindow && isStop) {
+      // Somewhat equivalent of load event.
+      // (window.document.readyState == complete)
       this._tabActor._navigate(window);
     }
   }, "DebuggerProgressListener.prototype.onStateChange")
 };
 
 exports.register = function(handle) {
   handle.setRootActor(createRootActor);
 };