Bug 977043 - Use same ProgressListener event across actors. r=bgrins,past
authorAlexandre Poirot <poirot.alex@gmail.com>
Tue, 22 Apr 2014 20:17:30 +0200
changeset 198193 2957a9bc0e4850b6417f43782dea75e1f84b0892
parent 198192 6ec2eefe9692d8f7828e76c27fc3644f3a5fd23c
child 198194 6e826c32b6f26ecdbeba399cd8b7f71519fe98e7
push id3624
push userasasaki@mozilla.com
push dateMon, 09 Jun 2014 21:49:01 +0000
treeherdermozilla-beta@b1a5da15899a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins, past
bugs977043
milestone31.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 - Use same ProgressListener event across actors. r=bgrins,past
toolkit/devtools/server/actors/childtab.js
toolkit/devtools/server/actors/inspector.js
toolkit/devtools/server/actors/webbrowser.js
toolkit/devtools/server/actors/webconsole.js
toolkit/devtools/server/tests/browser/browser.ini
toolkit/devtools/server/tests/browser/browser_navigateEvents.js
toolkit/devtools/server/tests/browser/navigate-first.html
toolkit/devtools/server/tests/browser/navigate-second.html
toolkit/devtools/server/tests/mochitest/inspector-helpers.js
--- a/toolkit/devtools/server/actors/childtab.js
+++ b/toolkit/devtools/server/actors/childtab.js
@@ -17,27 +17,28 @@
  *
  * @param connection DebuggerServerConnection
  *        The conection to the client.
  * @param chromeGlobal
  *        The content script global holding |content| and |docShell| properties for a tab.
  */
 function ContentActor(connection, chromeGlobal)
 {
+  this._chromeGlobal = chromeGlobal;
   TabActor.call(this, connection, chromeGlobal);
   this.traits.reconfigure = false;
 }
 
 ContentActor.prototype = Object.create(TabActor.prototype);
 
 ContentActor.prototype.constructor = ContentActor;
 
 Object.defineProperty(ContentActor.prototype, "docShell", {
   get: function() {
-    return this.chromeEventHandler.docShell;
+    return this._chromeGlobal.docShell;
   },
   enumerable: true,
   configurable: false
 });
 
 ContentActor.prototype.exit = function() {
   TabActor.prototype.exit.call(this);
 };
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -771,74 +771,16 @@ let traversalMethod = {
     whatToShow: Option(1)
   },
   response: {
     node: RetVal("nullable:domnode")
   }
 }
 
 /**
- * We need to know when a document is navigating away so that we can kill
- * the nodes underneath it.  We also need to know when a document is
- * navigated to so that we can send a mutation event for the iframe node.
- *
- * The nsIWebProgressListener is the easiest/best way to watch these
- * loads that works correctly with the bfcache.
- *
- * See nsIWebProgressListener for details
- * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIWebProgressListener
- */
-var ProgressListener = Class({
-  extends: Unknown,
-  interfaces: ["nsIWebProgressListener", "nsISupportsWeakReference"],
-
-  initialize: function(tabActor) {
-    Unknown.prototype.initialize.call(this);
-    this.webProgress = tabActor.webProgress;
-    this.webProgress.addProgressListener(
-      this, Ci.nsIWebProgress.NOTIFY_ALL
-    );
-  },
-
-  destroy: function() {
-    try {
-      this.webProgress.removeProgressListener(this);
-    } catch(ex) {
-      // This can throw during browser shutdown.
-    }
-    this.webProgress = null;
-  },
-
-  onStateChange: makeInfallible(function stateChange(progress, request, flags, status) {
-    if (!this.webProgress) {
-      console.warn("got an onStateChange after destruction");
-      return;
-    }
-
-    let isWindow = flags & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
-    let isDocument = flags & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
-    if (!(isWindow || isDocument)) {
-      return;
-    }
-
-    if (isDocument && (flags & Ci.nsIWebProgressListener.STATE_START)) {
-      events.emit(this, "windowchange-start", progress.DOMWindow);
-    }
-    if (isWindow && (flags & Ci.nsIWebProgressListener.STATE_STOP)) {
-      events.emit(this, "windowchange-stop", progress.DOMWindow);
-    }
-  }),
-
-  onProgressChange: function() {},
-  onSecurityChange: function() {},
-  onStatusChange: function() {},
-  onLocationChange: function() {},
-});
-
-/**
  * Server side of the DOM walker.
  */
 var WalkerActor = protocol.ActorClass({
   typeName: "domwalker",
 
   events: {
     "new-mutations" : {
       type: "newMutations"
@@ -884,20 +826,18 @@ var WalkerActor = protocol.ActorClass({
     // even when it is orphaned with the `retainNode` method.  This
     // list contains orphaned nodes that were so retained.
     this._retainedOrphans = new Set();
 
     this.onMutations = this.onMutations.bind(this);
     this.onFrameLoad = this.onFrameLoad.bind(this);
     this.onFrameUnload = this.onFrameUnload.bind(this);
 
-    this.progressListener = ProgressListener(tabActor);
-
-    events.on(this.progressListener, "windowchange-start", this.onFrameUnload);
-    events.on(this.progressListener, "windowchange-stop", this.onFrameLoad);
+    events.on(tabActor, "will-navigate", this.onFrameUnload);
+    events.on(tabActor, "navigate", this.onFrameLoad);
 
     // Ensure that the root document node actor is ready and
     // managed.
     this.rootNode = this.document();
   },
 
   // Returns the JSON representation of this object over the wire.
   form: function() {
@@ -910,17 +850,16 @@ var WalkerActor = protocol.ActorClass({
   toString: function() {
     return "[WalkerActor " + this.actorID + "]";
   },
 
   destroy: function() {
     this._hoveredNode = null;
     this.clearPseudoClassLocks();
     this._activePseudoClassLocks = null;
-    this.progressListener.destroy();
     this.rootDoc = null;
     events.emit(this, "destroyed");
     protocol.Actor.prototype.destroy.call(this);
   },
 
   release: method(function() {}, { release: true }),
 
   unmanage: function(actor) {
@@ -1961,27 +1900,26 @@ var WalkerActor = protocol.ActorClass({
         mutation.numChildren = change.target.childNodes.length;
         mutation.removed = removedActors;
         mutation.added = addedActors;
       }
       this.queueMutation(mutation);
     }
   },
 
-  onFrameLoad: function(window) {
-    let frame = this.layoutHelpers.getFrameElement(window);
-    let isTopLevel = this.layoutHelpers.isTopLevelWindow(window);
-    if (!frame && !this.rootDoc && isTopLevel) {
+  onFrameLoad: function({ window, isTopLevel }) {
+    if (!this.rootDoc && isTopLevel) {
       this.rootDoc = window.document;
       this.rootNode = this.document();
       this.queueMutation({
         type: "newRoot",
         target: this.rootNode.form()
       });
     }
+    let frame = this.layoutHelpers.getFrameElement(window);
     let frameActor = this._refMap.get(frame);
     if (!frameActor) {
       return;
     }
 
     this.queueMutation({
       type: "frameLoad",
       target: frameActor.actorID,
@@ -2003,17 +1941,17 @@ var WalkerActor = protocol.ActorClass({
       if (win === window) {
         return true;
       }
       win = this.layoutHelpers.getFrameElement(win);
     }
     return false;
   },
 
-  onFrameUnload: function(window) {
+  onFrameUnload: function({ window }) {
     // Any retained orphans that belong to this document
     // or its children need to be released, and a mutation sent
     // to notify of that.
     let releasedOrphans = [];
 
     for (let retained of this._retainedOrphans) {
       if (Cu.isDeadWrapper(retained.rawNode) ||
           this._childOfWindow(window, retained.rawNode)) {
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -5,16 +5,23 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 let {Cu} = require("chrome");
 let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
 
+// Assumptions on events module:
+// events needs to be dispatched synchronously,
+// by calling the listeners in the order or registration.
+XPCOMUtils.defineLazyGetter(this, "events", () => {
+  return devtools.require("sdk/event/core");
+});
+
 /**
  * Browser-specific actors.
  */
 
 /**
  * Yield all windows of type |aWindowType|, from the oldest window to the
  * youngest, using nsIWindowMediator::getEnumerator. We're usually
  * interested in "navigator:browser" windows.
@@ -480,36 +487,34 @@ BrowserTabList.prototype.onCloseWindow =
  * ContentActor. Subclasses are expected to implement a getter
  * the docShell properties.
  *
  * @param aConnection DebuggerServerConnection
  *        The conection to the client.
  * @param aChromeEventHandler
  *        An object on which listen for DOMWindowCreated and pageshow events.
  */
-function TabActor(aConnection, aChromeEventHandler)
+function TabActor(aConnection)
 {
   this.conn = aConnection;
-  this._chromeEventHandler = aChromeEventHandler;
   this._tabActorPool = null;
   // A map of actor names to actor instances provided by extensions.
   this._extraActors = {};
-
-  this._onWindowCreated = this.onWindowCreated.bind(this);
+  this._exited = false;
 
   this.traits = { reconfigure: true };
 }
 
 // XXX (bug 710213): TabActor attach/detach/exit/disconnect is a
 // *complete* mess, needs to be rethought asap.
 
 TabActor.prototype = {
   traits: null,
 
-  get exited() { return !this._chromeEventHandler; },
+  get exited() { return this._exited; },
   get attached() { return !!this._attached; },
 
   _tabPool: null,
   get tabActorPool() { return this._tabPool; },
 
   _contextPool: null,
   get contextActorPool() { return this._contextPool; },
 
@@ -517,17 +522,20 @@ TabActor.prototype = {
 
   // A constant prefix that will be used to form the actor ID by the server.
   actorPrefix: "tab",
 
   /**
    * An object on which listen for DOMWindowCreated and pageshow events.
    */
   get chromeEventHandler() {
-    return this._chromeEventHandler;
+    // TODO: bug 992778, fix docShell.chromeEventHandler in child processes
+    return this.docShell.chromeEventHandler ||
+           this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                        .getInterface(Ci.nsIContentFrameMessageManager);
   },
 
   /**
    * Getter for the nsIMessageManager associated to the tab.
    */
   get messageManager() {
     return this._chromeEventHandler;
   },
@@ -626,33 +634,33 @@ TabActor.prototype = {
   },
 
   /**
    * Called when the actor is removed from the connection.
    */
   disconnect: function BTA_disconnect() {
     this._detach();
     this._extraActors = null;
-    this._chromeEventHandler = null;
+    this._exited = true;
   },
 
   /**
    * Called by the root actor when the underlying tab is closed.
    */
   exit: function BTA_exit() {
     if (this.exited) {
       return;
     }
 
     if (this._detach()) {
       this.conn.send({ from: this.actorID,
                        type: "tabDetached" });
     }
 
-    this._chromeEventHandler = null;
+    this._exited = true;
   },
 
   /* Support for DebuggerServer.addTabActor. */
   _createExtraActors: CommonCreateExtraActors,
   _appendExtraActors: CommonAppendExtraActors,
 
   /**
    * Does the actual work of attching to a tab.
@@ -665,20 +673,18 @@ TabActor.prototype = {
     // Create a pool for tab-lifetime actors.
     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();
 
-    // Watch for globals being created in this tab.
-    this.chromeEventHandler.addEventListener("DOMWindowCreated", this._onWindowCreated, true);
-    this.chromeEventHandler.addEventListener("pageshow", this._onWindowCreated, true);
     this._progressListener = new DebuggerProgressListener(this);
+    this._progressListener.watch(this.docShell);
 
     this._attached = true;
   },
 
   /**
    * Creates a thread actor and a pool for context-lifetime actors. It then sets
    * up the content window for debugging.
    */
@@ -710,20 +716,22 @@ TabActor.prototype = {
    *
    * @returns false if the tab wasn't attached or true of detahing succeeds.
    */
   _detach: function BTA_detach() {
     if (!this.attached) {
       return false;
     }
 
-    this._progressListener.destroy();
-
-    this.chromeEventHandler.removeEventListener("DOMWindowCreated", this._onWindowCreated, true);
-    this.chromeEventHandler.removeEventListener("pageshow", this._onWindowCreated, true);
+    // Check for docShell availability, as it can be already gone
+    // during Firefox shutdown.
+    if (this.docShell) {
+      this._progressListener.unwatch(this.docShell);
+    }
+    this._progressListener = null;
 
     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);
@@ -904,38 +912,126 @@ TabActor.prototype = {
     }
   },
 
   /**
    * Handle location changes, by clearing the previous debuggees and enabling
    * debugging, which may have been disabled temporarily by the
    * DebuggerProgressListener.
    */
-  onWindowCreated:
-  DevToolsUtils.makeInfallible(function BTA_onWindowCreated(evt) {
-    // pageshow events for non-persisted pages have already been handled by a
-    // prior DOMWindowCreated event.
-    if (!this._attached || (evt.type == "pageshow" && !evt.persisted)) {
-      return;
-    }
-    if (evt.target === this.contentDocument) {
-      this.threadActor.clearDebuggees();
-      if (this.threadActor.dbg) {
-        this.threadActor.dbg.enabled = true;
-        this.threadActor.global = evt.target.defaultView;
-        this.threadActor.maybePauseOnExceptions();
+  _windowReady: function (window) {
+    let isTopLevel = window == this.window;
+    dumpn("window-ready: " + window.location + " isTopLevel:" + isTopLevel);
+
+    events.emit(this, "window-ready", {
+      window: window,
+      isTopLevel: isTopLevel
+    });
+
+    // 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();
       }
     }
 
     // Refresh the debuggee list when a new window object appears (top window or
     // iframe).
-    if (this.threadActor.attached) {
-      this.threadActor.findGlobals();
+    if (threadActor.attached) {
+      threadActor.findGlobals();
+    }
+  },
+
+  /**
+   * Start notifying server codebase and client about a new document
+   * being loaded in the currently targeted context.
+   */
+  _willNavigate: function (window, newURI, request) {
+    let isTopLevel = window == this.window;
+
+    // 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,
+      isTopLevel: isTopLevel,
+      newURI: newURI,
+      request: request
+    });
+
+
+    // We don't do anything for inner frames in TabActor.
+    // (we will only update thread actor on window-ready)
+    if (!isTopLevel) {
+      return;
+    }
+
+    // Proceed normally only if the debuggee is not paused.
+    // TODO bug 997119: move that code to ThreadActor by listening to will-navigate
+    let threadActor = this.threadActor;
+    if (request && threadActor.state == "paused") {
+      request.suspend();
+      threadActor.onResume();
+      threadActor.dbg.enabled = false;
+      this._pendingNavigation = request;
     }
-  }, "TabActor.prototype.onWindowCreated"),
+    threadActor.disableAllBreakpoints();
+
+    this.conn.send({
+      from: this.actorID,
+      type: "tabNavigated",
+      url: newURI,
+      nativeConsoleAPI: true,
+      state: "start"
+    });
+  },
+
+  /**
+   * Notify server and client about a new document done loading in the current
+   * targeted context.
+   */
+  _navigate: function (window) {
+    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,
+      isTopLevel: isTopLevel
+    });
+
+    // We don't do anything for inner frames in TabActor.
+    // (we will only update thread actor on window-ready)
+    if (!isTopLevel) {
+      return;
+    }
+
+    // TODO bug 997119: move that code to ThreadActor by listening to navigate
+    let threadActor = this.threadActor;
+    if (threadActor.state == "running") {
+      threadActor.dbg.enabled = true;
+    }
+
+    this.conn.send({
+      from: this.actorID,
+      type: "tabNavigated",
+      url: this.url,
+      title: this.title,
+      nativeConsoleAPI: this.hasNativeConsoleAPI(this.window),
+      state: "stop"
+    });
+  },
 
   /**
    * Tells if the window.console object is native or overwritten by script in
    * the page.
    *
    * @param nsIDOMWindow aWindow
    *        The window object you want to check.
    * @return boolean
@@ -994,22 +1090,25 @@ Object.defineProperty(BrowserTabActor.pr
     return this._browser.messageManager;
   },
   enumerable: true,
   configurable: false
 });
 
 Object.defineProperty(BrowserTabActor.prototype, "title", {
   get: function() {
-    let title = this.contentDocument.contentTitle;
+    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.
     if (!title && this._tabbrowser) {
-      title = this._tabbrowser._getTabForContentWindow(this.window).label;
+      let tab = this._tabbrowser._getTabForContentWindow(this.window);
+      if (tab) {
+        title = tab.label;
+      }
     }
     return title;
   },
   enumerable: true,
   configurable: false
 });
 
 Object.defineProperty(BrowserTabActor.prototype, "browser", {
@@ -1249,87 +1348,91 @@ BrowserAddonActor.prototype.requestTypes
  * navigate away from a paused page, the listener makes sure that the debuggee
  * is resumed before the navigation begins.
  *
  * @param TabActor aTabActor
  *        The tab actor associated with this listener.
  */
 function DebuggerProgressListener(aTabActor) {
   this._tabActor = aTabActor;
-  this._tabActor.webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_ALL);
-  let EventEmitter = devtools.require("devtools/toolkit/event-emitter");
-  EventEmitter.decorate(this);
+  this._onWindowCreated = this.onWindowCreated.bind(this);
 }
 
 DebuggerProgressListener.prototype = {
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsIWebProgressListener,
     Ci.nsISupportsWeakReference,
     Ci.nsISupports,
   ]),
 
-  onStateChange:
-  DevToolsUtils.makeInfallible(function DPL_onStateChange(aProgress, aRequest, aFlag, aStatus) {
-    let isStart = aFlag & Ci.nsIWebProgressListener.STATE_START;
-    let isStop = aFlag & Ci.nsIWebProgressListener.STATE_STOP;
-    let isDocument = aFlag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
-    let isNetwork = aFlag & Ci.nsIWebProgressListener.STATE_IS_NETWORK;
-    let isRequest = aFlag & Ci.nsIWebProgressListener.STATE_IS_REQUEST;
-    let isWindow = aFlag & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
+  watch: function DPL_watch(docShell) {
+    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIWebProgress);
+    webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATUS |
+                                          Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
+                                          Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+
+    // TODO: fix docShell.chromeEventHandler in child processes!
+    let chromeEventHandler = docShell.chromeEventHandler ||
+                             docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                                     .getInterface(Ci.nsIContentFrameMessageManager);
+
+    // Watch for globals being created in this docshell tree.
+    chromeEventHandler.addEventListener("DOMWindowCreated",
+                                        this._onWindowCreated, true);
+    chromeEventHandler.addEventListener("pageshow",
+                                        this._onWindowCreated, true);
+  },
 
-    // Skip non-interesting states.
-    if (!isWindow || !isNetwork ||
-        aProgress.DOMWindow != this._tabActor.window) {
+  unwatch: function DPL_unwatch(docShell) {
+    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIWebProgress);
+    webProgress.removeProgressListener(this);
+
+    // TODO: fix docShell.chromeEventHandler in child processes!
+    let chromeEventHandler = docShell.chromeEventHandler ||
+                             docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                                     .getInterface(Ci.nsIContentFrameMessageManager);
+    chromeEventHandler.removeEventListener("DOMWindowCreated",
+                                           this._onWindowCreated, true);
+    chromeEventHandler.removeEventListener("pageshow",
+                                           this._onWindowCreated, true);
+  },
+
+  onWindowCreated:
+  DevToolsUtils.makeInfallible(function DPL_onWindowCreated(evt) {
+    // Ignore any event if the tab actor isn't attached.
+    if (!this._tabActor.attached) {
       return;
     }
 
-    if (isStart && aRequest instanceof Ci.nsIChannel) {
-      // Proceed normally only if the debuggee is not paused.
-      if (this._tabActor.threadActor.state == "paused") {
-        aRequest.suspend();
-        this._tabActor.threadActor.onResume();
-        this._tabActor.threadActor.dbg.enabled = false;
-        this._tabActor._pendingNavigation = aRequest;
-      }
-
-      let packet = {
-        from: this._tabActor.actorID,
-        type: "tabNavigated",
-        url: aRequest.URI.spec,
-        nativeConsoleAPI: true,
-        state: "start"
-      };
-      this._tabActor.threadActor.disableAllBreakpoints();
-      this._tabActor.conn.send(packet);
-      this.emit("will-navigate", packet);
-    } else if (isStop) {
-      if (this._tabActor.threadActor.state == "running") {
-        this._tabActor.threadActor.dbg.enabled = true;
-      }
-
-      let window = this._tabActor.window;
-      let packet = {
-        from: this._tabActor.actorID,
-        type: "tabNavigated",
-        url: this._tabActor.url,
-        title: this._tabActor.title,
-        nativeConsoleAPI: this._tabActor.hasNativeConsoleAPI(window),
-        state: "stop"
-      };
-      this._tabActor.conn.send(packet);
-      this.emit("navigate", packet);
-    }
-  }, "DebuggerProgressListener.prototype.onStateChange"),
-
-  /**
-   * Destroy the progress listener instance.
-   */
-  destroy: function DPL_destroy() {
-    try {
-      this._tabActor.webProgress.removeProgressListener(this);
-    } catch (ex) {
-      // This can throw during browser shutdown.
+    // pageshow events for non-persisted pages have already been handled by a
+    // prior DOMWindowCreated event.
+    if (evt.type == "pageshow" && !evt.persisted) {
+      return;
     }
 
-    this._tabActor._progressListener = null;
-    this._tabActor = null;
-  }
+    let window = evt.target.defaultView;
+    this._tabActor._windowReady(window);
+  }, "DebuggerProgressListener.prototype.onWindowCreated"),
+
+  onStateChange:
+  DevToolsUtils.makeInfallible(function DPL_onStateChange(aProgress, aRequest, aFlag, aStatus) {
+    // Ignore any event if the tab actor isn't attached.
+    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;
+
+    let window = aProgress.DOMWindow;
+    if (isDocument && isStart) {
+      let newURI = aRequest instanceof Ci.nsIChannel ? aRequest.URI.spec : null;
+      this._tabActor._willNavigate(window, newURI, aRequest);
+    }
+    if (isWindow && isStop) {
+      this._tabActor._navigate(window);
+    }
+  }, "DebuggerProgressListener.prototype.onStateChange")
 };
--- a/toolkit/devtools/server/actors/webconsole.js
+++ b/toolkit/devtools/server/actors/webconsole.js
@@ -232,18 +232,18 @@ WebConsoleActor.prototype =
   _evalWindow: null,
   get evalWindow() {
     return this._evalWindow || this.window;
   },
 
   set evalWindow(aWindow) {
     this._evalWindow = aWindow;
 
-    if (!this._progressListenerActive && this.parentActor._progressListener) {
-      this.parentActor._progressListener.once("will-navigate", this._onWillNavigate);
+    if (!this._progressListenerActive) {
+      events.on(this.parentActor, "will-navigate", this._onWillNavigate);
       this._progressListenerActive = true;
     }
   },
 
   /**
    * Flag used to track if we are listening for events from the progress
    * listener of the tab actor. We use the progress listener to clear
    * this.evalWindow on page navigation.
@@ -1369,20 +1369,23 @@ WebConsoleActor.prototype =
         break;
     }
   },
 
   /**
    * The "will-navigate" progress listener. This is used to clear the current
    * eval scope.
    */
-  _onWillNavigate: function WCA__onWillNavigate()
+  _onWillNavigate: function WCA__onWillNavigate({ window, isTopLevel })
   {
-    this._evalWindow = null;
-    this._progressListenerActive = false;
+    if (isTopLevel) {
+      this._evalWindow = null;
+      events.off(this.parentActor, "will-navigate", this._onWillNavigate);
+      this._progressListenerActive = false;
+    }
   },
 };
 
 WebConsoleActor.prototype.requestTypes =
 {
   startListeners: WebConsoleActor.prototype.onStartListeners,
   stopListeners: WebConsoleActor.prototype.onStopListeners,
   getCachedMessages: WebConsoleActor.prototype.onGetCachedMessages,
--- a/toolkit/devtools/server/tests/browser/browser.ini
+++ b/toolkit/devtools/server/tests/browser/browser.ini
@@ -3,12 +3,15 @@ skip-if = e10s # Bug ?????? - devtools t
 subsuite = devtools
 support-files =
   head.js
   storage-dynamic-windows.html
   storage-listings.html
   storage-unsecured-iframe.html
   storage-updates.html
   storage-secured-iframe.html
+  navigate-first.html
+  navigate-second.html
 
 [browser_storage_dynamic_windows.js]
 [browser_storage_listings.js]
 [browser_storage_updates.js]
+[browser_navigateEvents.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_navigateEvents.js
@@ -0,0 +1,169 @@
+
+let Cu = Components.utils;
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+
+const URL1 = MAIN_DOMAIN + "navigate-first.html";
+const URL2 = MAIN_DOMAIN + "navigate-second.html";
+
+let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
+let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
+
+let devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
+let events = devtools.require("sdk/event/core");
+
+let client;
+
+// State machine to check events order
+let i = 0;
+function assertEvent(event, data) {
+  let x = 0;
+  switch(i++) {
+    case x++:
+      is(event, "request", "Get first page load");
+      is(data, URL1);
+      break;
+    case x++:
+      is(event, "load-new-document", "Ask to load the second page");
+      break;
+    case x++:
+      is(event, "unload-dialog", "We get the dialog on first page unload");
+      break;
+    case x++:
+      is(event, "will-navigate", "The very first event is will-navigate on server side");
+      is(data.newURI, URL2, "newURI property is correct");
+      break;
+    case x++:
+      is(event, "tabNavigated", "Right after will-navigate, the client receive tabNavigated");
+      is(data.state, "start", "state is start");
+      is(data.url, URL2, "url property is correct");
+      break;
+    case x++:
+      is(event, "request", "Given that locally, the Debugger protocol is sync, the request happens after tabNavigated");
+      is(data, URL2);
+      break;
+    case x++:
+      is(event, "DOMContentLoaded");
+      is(content.document.readyState, "interactive");
+      break;
+    case x++:
+      is(event, "load");
+      is(content.document.readyState, "complete");
+      break;
+    case x++:
+      is(event, "navigate", "Then once the second doc is loaded, we get the navigate event");
+      is(content.document.readyState, "complete", "navigate is emitted only once the document is fully loaded");
+      break;
+    case x++:
+      is(event, "tabNavigated", "Finally, the receive the client event");
+      is(data.state, "stop", "state is stop");
+      is(data.url, URL2, "url property is correct");
+
+      // End of test!
+      cleanup();
+      break;
+  }
+}
+
+function waitForOnBeforeUnloadDialog(browser, callback) {
+  browser.addEventListener("DOMWillOpenModalDialog", function onModalDialog() {
+    browser.removeEventListener("DOMWillOpenModalDialog", onModalDialog, true);
+
+    executeSoon(() => {
+      let stack = browser.parentNode;
+      let dialogs = stack.getElementsByTagName("tabmodalprompt");
+      let {button0, button1} = dialogs[0].ui;
+      callback(button0, button1);
+    });
+  }, true);
+}
+
+let httpObserver = function (subject, topic, state) {
+  let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+  let url = channel.URI.spec;
+  // Only listen for our document request, as many other requests can happen
+  if (url == URL1 || url == URL2) {
+    assertEvent("request", url);
+  }
+};
+Services.obs.addObserver(httpObserver, "http-on-modify-request", false);
+
+function onDOMContentLoaded() {
+  assertEvent("DOMContentLoaded");
+}
+function onLoad() {
+  assertEvent("load");
+}
+
+function getServerTabActor(callback) {
+  // Ensure having a minimal server
+  if (!DebuggerServer.initialized) {
+    DebuggerServer.init(function () { return true; });
+    DebuggerServer.addBrowserActors();
+  }
+
+  // Connect to this tab
+  let transport = DebuggerServer.connectPipe();
+  client = new DebuggerClient(transport);
+  client.connect(function onConnect() {
+    client.listTabs(function onListTabs(aResponse) {
+      // Fetch the BrowserTabActor for this tab
+      let actorID = aResponse.tabs[aResponse.selected].actor;
+      client.attachTab(actorID, function(aResponse, aTabClient) {
+        // !Hack! Retrieve a server side object, the BrowserTabActor instance
+        let conn = transport._serverConnection;
+        let tabActor = conn.getActor(actorID);
+        callback(tabActor);
+      });
+    });
+  });
+
+  client.addListener("tabNavigated", function (aEvent, aPacket) {
+    assertEvent("tabNavigated", aPacket);
+  });
+}
+
+function test() {
+  waitForExplicitFinish();
+
+  // Open a test tab
+  addTab(URL1, function(doc) {
+    getServerTabActor(function (tabActor) {
+      // In order to listen to internal will-navigate/navigate events
+      events.on(tabActor, "will-navigate", function (data) {
+        assertEvent("will-navigate", data);
+      });
+      events.on(tabActor, "navigate", function (data) {
+        assertEvent("navigate", data);
+      });
+
+      // Start listening for page load events
+      let browser = gBrowser.selectedTab.linkedBrowser;
+      browser.addEventListener("DOMContentLoaded", onDOMContentLoaded, true);
+      browser.addEventListener("load", onLoad, true);
+
+      // Listen for alert() call being made in navigate-first during unload
+      waitForOnBeforeUnloadDialog(browser, function (btnLeave, btnStay) {
+        assertEvent("unload-dialog");
+        // accept to quit this page to another
+        btnLeave.click();
+      });
+
+      // Load another document in this doc to dispatch these events
+      assertEvent("load-new-document");
+      content.location = URL2;
+    });
+
+  });
+}
+
+function cleanup() {
+  let browser = gBrowser.selectedTab.linkedBrowser;
+  browser.removeEventListener("DOMContentLoaded", onDOMContentLoaded);
+  browser.removeEventListener("load", onLoad);
+  client.close(function () {
+    Services.obs.addObserver(httpObserver, "http-on-modify-request", false);
+    DebuggerServer.destroy();
+    finish();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/navigate-first.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+</head>
+<body>
+First
+<script>
+
+window.onbeforeunload=function(e){
+  e.returnValue="?";
+};
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/navigate-second.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+</head>
+<body>
+Second
+</body>
+</html>
--- a/toolkit/devtools/server/tests/mochitest/inspector-helpers.js
+++ b/toolkit/devtools/server/tests/mochitest/inspector-helpers.js
@@ -56,22 +56,24 @@ function attachURL(url, callback) {
   window.addEventListener("message", function loadListener(event) {
     if (event.data === "ready") {
       client = new DebuggerClient(DebuggerServer.connectPipe());
       client.connect((applicationType, traits) => {
         client.listTabs(response => {
           for (let tab of response.tabs) {
             if (tab.url === url) {
               window.removeEventListener("message", loadListener, false);
-              try {
-                callback(null, client, tab, win.document);
-              } catch(ex) {
-                Cu.reportError(ex);
-                dump(ex);
-              }
+              client.attachTab(tab.actor, function(aResponse, aTabClient) {
+                try {
+                  callback(null, client, tab, win.document);
+                } catch(ex) {
+                  Cu.reportError(ex);
+                  dump(ex);
+                }
+              });
               break;
             }
           }
         });
       });
     }
   }, false);