Bug 1147826 - Remove deprecated TabActor reference in runAt:server GCLI commands. r=jwalker, r=miker, a=sledru
authorPatrick Brosset <pbrosset@mozilla.com>
Mon, 01 Jun 2015 14:41:12 +0200
changeset 262565 9f264bf59621bddb2c0195c2e1c68fc73250130b
parent 262564 5332c7f30351a5d3bad3ee510a9f4397803f6a67
child 262566 a5e4918145cfe33b85d69488f18fc62a5ac6818a
push id8103
push userryanvm@gmail.com
push dateThu, 11 Jun 2015 16:02:50 +0000
treeherdermozilla-aurora@ec4e2ed27521 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker, miker, sledru
bugs1147826
milestone40.0a2
Bug 1147826 - Remove deprecated TabActor reference in runAt:server GCLI commands. r=jwalker, r=miker, a=sledru We used to rely on the TabActor a lot in /toolkit/devtools/server/actors/highlighter. This object was being passed around to various highlighter classes, helper classes and functions. It was a useful way to get access to the window the highlighters are shown, listen to DOM events in that window, and also listen to tabActor's will-navigate events. Using this object isn't the best idea because it assumes the debugger server is started and attached to a tab, which makes re-using highlighters outside of this context impossible (or harder). Plus we wanted to get rid of the tabActor reference in gcli command contexts. This change introduces a HighlighterEnvironment that fulfills the requirements above without relying on a tabActor. It needs to be initialized either with a tabActor, when we have one (when using the highlighter from the inspector panel), or with a window (which is what gcli commands will use). This change also fixes the highlight command and the rulers command (which didn't work well with e10s either).
toolkit/devtools/gcli/commands/highlight.js
toolkit/devtools/gcli/commands/rulers.js
toolkit/devtools/server/actors/gcli.js
toolkit/devtools/server/actors/highlighter.js
toolkit/devtools/server/tests/browser/browser_canvasframe_helper_01.js
toolkit/devtools/server/tests/browser/browser_canvasframe_helper_02.js
toolkit/devtools/server/tests/browser/browser_canvasframe_helper_03.js
toolkit/devtools/server/tests/browser/browser_canvasframe_helper_04.js
toolkit/devtools/server/tests/browser/browser_canvasframe_helper_05.js
toolkit/devtools/server/tests/browser/browser_canvasframe_helper_06.js
--- a/toolkit/devtools/gcli/commands/highlight.js
+++ b/toolkit/devtools/gcli/commands/highlight.js
@@ -1,41 +1,52 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* globals nodesSelected, PluralForm */
 
 "use strict";
 
-const {Cc, Ci, Cu} = require("chrome");
 const l10n = require("gcli/l10n");
 require("devtools/server/actors/inspector");
-const {BoxModelHighlighter} = require("devtools/server/actors/highlighter");
+const {
+  BoxModelHighlighter,
+  HighlighterEnvironment
+} = require("devtools/server/actors/highlighter");
 
 XPCOMUtils.defineLazyGetter(this, "nodesSelected", function() {
   return Services.strings.createBundle("chrome://global/locale/devtools/gclicommands.properties");
 });
-XPCOMUtils.defineLazyModuleGetter(this, "PluralForm","resource://gre/modules/PluralForm.jsm");
-const events = require("sdk/event/core");
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
 
 // How many maximum nodes can be highlighted in parallel
 const MAX_HIGHLIGHTED_ELEMENTS = 100;
 
+// Store the environment object used to create highlighters so it can be
+// destroyed later.
+let highlighterEnv;
+
 // Stores the highlighters instances so they can be destroyed later.
 // also export them so tests can access those and assert they got created
 // correctly.
 exports.highlighters = [];
 
 /**
  * Destroy all existing highlighters
  */
 function unhighlightAll() {
   for (let highlighter of exports.highlighters) {
     highlighter.destroy();
   }
   exports.highlighters.length = 0;
+
+  if (highlighterEnv) {
+    highlighterEnv.destroy();
+    highlighterEnv = null;
+  }
 }
 
 exports.items = [
   {
     item: "command",
     runAt: "server",
     name: "highlight",
     description: l10n.lookup("highlightDesc"),
@@ -84,50 +95,52 @@ exports.items = [
             description: l10n.lookup("highlightFillDesc"),
             manual: l10n.lookup("highlightFillManual"),
             defaultValue: null
           },
           {
             name: "keep",
             type: "boolean",
             description: l10n.lookup("highlightKeepDesc"),
-            manual: l10n.lookup("highlightKeepManual"),
+            manual: l10n.lookup("highlightKeepManual")
           }
         ]
       }
     ],
     exec: function(args, context) {
       // Remove all existing highlighters unless told otherwise
       if (!args.keep) {
         unhighlightAll();
       }
 
       let env = context.environment;
+      highlighterEnv = new HighlighterEnvironment();
+      highlighterEnv.initFromWindow(env.window);
 
       // Unhighlight on navigate
-      events.on(env.__deprecatedTabActor, "will-navigate", unhighlightAll);
+      highlighterEnv.once("will-navigate", unhighlightAll);
 
       let i = 0;
       for (let node of args.selector) {
         if (!args.showall && i >= MAX_HIGHLIGHTED_ELEMENTS) {
           break;
         }
 
-        let highlighter = new BoxModelHighlighter(env.__deprecatedTabActor);
+        let highlighter = new BoxModelHighlighter(highlighterEnv);
         if (args.fill) {
           highlighter.regionFill[args.region] = args.fill;
         }
         highlighter.show(node, {
           region: args.region,
           hideInfoBar: !args.showinfobar,
           hideGuides: args.hideguides,
           showOnly: args.region
         });
         exports.highlighters.push(highlighter);
-        i ++;
+        i++;
       }
 
       let highlightText = nodesSelected.GetStringFromName("highlightOutputConfirm2");
       let output = PluralForm.get(args.selector.length, highlightText)
                              .replace("%1$S", args.selector.length);
       if (args.selector.length > i) {
         output = l10n.lookupFormat("highlightOutputMaxReached",
           ["" + args.selector.length, "" + i]);
--- a/toolkit/devtools/gcli/commands/rulers.js
+++ b/toolkit/devtools/gcli/commands/rulers.js
@@ -3,89 +3,93 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const EventEmitter = require("devtools/toolkit/event-emitter");
 const eventEmitter = new EventEmitter();
 const events = require("sdk/event/core");
 
-const gcli = require("gcli/index");
 const l10n = require("gcli/l10n");
 require("devtools/server/actors/inspector");
-const { RulersHighlighter } = require("devtools/server/actors/highlighter");
+const { RulersHighlighter, HighlighterEnvironment } =
+  require("devtools/server/actors/highlighter");
 
 const highlighters = new WeakMap();
+let isRulersVisible = false;
 
 exports.items = [
+  // The client rulers command is used to maintain the toolbar button state only
+  // and redirects to the server command to actually toggle the rulers (see
+  // rulers_server below).
   {
     name: "rulers",
+    runAt: "client",
     description: l10n.lookup("rulersDesc"),
     manual: l10n.lookup("rulersManual"),
     buttonId: "command-button-rulers",
     buttonClass: "command-button command-button-invertable",
     tooltipText: l10n.lookup("rulersTooltip"),
     state: {
-      isChecked: function(aTarget) {
-        if (aTarget.isLocalTab) {
-          let window = aTarget.tab.linkedBrowser.contentWindow;
+      isChecked: () => isRulersVisible,
+      onChange: (target, handler) => eventEmitter.on("changed", handler),
+      offChange: (target, handler) => eventEmitter.off("changed", handler)
+    },
+    exec: function*(args, context) {
+      let { target } = context.environment;
 
-          if (window) {
-            return highlighters.has(window.document);
-          }
+      // Pipe the call to the server command.
+      let response = yield context.updateExec("rulers_server");
+      isRulersVisible = response.data;
+      eventEmitter.emit("changed", { target });
 
-          return false;
-        } else {
-          throw new Error("Unsupported target");
-        }
-      },
-      onChange: function(aTarget, aChangeHandler) {
-        eventEmitter.on("changed", aChangeHandler);
-      },
-      offChange: function(aTarget, aChangeHandler) {
-        eventEmitter.off("changed", aChangeHandler);
-      },
-    },
+      // Toggle off the button when the page navigates because the rulers are
+      // removed automatically by the RulersHighlighter on the server then.
+      let onNavigate = () => {
+        isRulersVisible = false;
+        eventEmitter.emit("changed", { target });
+      };
+      target.off("will-navigate", onNavigate);
+      target.once("will-navigate", onNavigate);
+    }
+  },
+  // The server rulers command is hidden by default, it's just used by the
+  // client command.
+  {
+    name: "rulers_server",
+    runAt: "server",
+    hidden: true,
     exec: function(args, context) {
       let env = context.environment;
-      let { target } = env;
 
+      // Calling the command again after the rulers have been shown once hides
+      // them.
       if (highlighters.has(env.document)) {
-        highlighters.get(env.document).highlighter.destroy();
-        return;
+        let { highlighter, environment } = highlighters.get(env.document);
+        highlighter.destroy();
+        environment.destroy();
+        return false;
       }
 
-      // Build a tab context for the highlighter (which normally takes a
-      // TabActor as parameter to its constructor)
-      let tabContext = {
-        browser: env.chromeWindow.gBrowser.getBrowserForDocument(env.document),
-        window: env.window
-      };
+      // Otherwise, display the rulers.
+      let environment = new HighlighterEnvironment();
+      environment.initFromWindow(env.window);
+      let highlighter = new RulersHighlighter(environment);
 
-      let emitToContext = (type, data) =>
-        events.emit(tabContext, type, Object.assign({isTopLevel: true}, data))
+      // Store the instance of the rulers highlighter for this document so we
+      // can hide it later.
+      highlighters.set(env.document, { highlighter, environment });
 
-      target.once("navigate", emitToContext);
-      target.once("will-navigate", emitToContext);
-
-      let highlighter = new RulersHighlighter(tabContext);
-
-      highlighters.set(env.document, { highlighter, listener: emitToContext });
-
+      // Listen to the highlighter's destroy event which may happen if the
+      // window is refreshed or closed with the rulers shown.
       events.once(highlighter, "destroy", () => {
         if (highlighters.has(env.document)) {
-          let { highlighter, listener } = highlighters.get(env.document);
-
-          target.off("navigate", listener);
-          target.off("will-navigate", listener);
-
+          let { environment } = highlighters.get(env.document);
+          environment.destroy();
           highlighters.delete(env.document);
         }
-
-        eventEmitter.emit("changed", { target });
       });
 
       highlighter.show();
-
-      eventEmitter.emit("changed", { target });
+      return true;
     }
   }
 ];
--- a/toolkit/devtools/server/actors/gcli.js
+++ b/toolkit/devtools/server/actors/gcli.js
@@ -256,18 +256,17 @@ const GcliActor = ActorClass({
           throw new Error("environment.chromeWindow is not available in runAt:server commands");
         },
 
         get chromeDocument() {
           throw new Error("environment.chromeDocument is not available in runAt:server commands");
         },
 
         get window() tabActor.window,
-        get document() tabActor.window.document,
-        get __deprecatedTabActor() tabActor,
+        get document() tabActor.window.document
       };
 
       return new Requisition(this._system, { environment: environment });
     });
 
     return this._requisitionPromise;
   },
 
--- a/toolkit/devtools/server/actors/highlighter.js
+++ b/toolkit/devtools/server/actors/highlighter.js
@@ -125,41 +125,43 @@ let HighlighterActor = exports.Highlight
 
   initialize: function(inspector, autohide) {
     protocol.Actor.prototype.initialize.call(this, null);
 
     this._autohide = autohide;
     this._inspector = inspector;
     this._walker = this._inspector.walker;
     this._tabActor = this._inspector.tabActor;
+    this._highlighterEnv = new HighlighterEnvironment();
+    this._highlighterEnv.initFromTabActor(this._tabActor);
 
     this._highlighterReady = this._highlighterReady.bind(this);
     this._highlighterHidden = this._highlighterHidden.bind(this);
     this._onNavigate = this._onNavigate.bind(this);
 
     this._layoutHelpers = new LayoutHelpers(this._tabActor.window);
     this._createHighlighter();
 
     // Listen to navigation events to switch from the BoxModelHighlighter to the
     // SimpleOutlineHighlighter, and back, if the top level window changes.
     events.on(this._tabActor, "navigate", this._onNavigate);
   },
 
   get conn() this._inspector && this._inspector.conn,
 
   _createHighlighter: function() {
-    this._isPreviousWindowXUL = isXUL(this._tabActor);
+    this._isPreviousWindowXUL = isXUL(this._tabActor.window);
 
     if (!this._isPreviousWindowXUL) {
-      this._highlighter = new BoxModelHighlighter(this._tabActor,
-                                                          this._inspector);
+      this._highlighter = new BoxModelHighlighter(this._highlighterEnv,
+                                                  this._inspector);
       this._highlighter.on("ready", this._highlighterReady);
       this._highlighter.on("hide", this._highlighterHidden);
     } else {
-      this._highlighter = new SimpleOutlineHighlighter(this._tabActor);
+      this._highlighter = new SimpleOutlineHighlighter(this._highlighterEnv);
     }
   },
 
   _destroyHighlighter: function() {
     if (this._highlighter) {
       if (!this._isPreviousWindowXUL) {
         this._highlighter.off("ready", this._highlighterReady);
         this._highlighter.off("hide", this._highlighterHidden);
@@ -172,27 +174,31 @@ let HighlighterActor = exports.Highlight
   _onNavigate: function({isTopLevel}) {
     // Skip navigation events for non top-level windows, or if the document
     // doesn't exist anymore.
     if (!isTopLevel || !this._tabActor.window.document.documentElement) {
       return;
     }
 
     // Only rebuild the highlighter if the window type changed.
-    if (isXUL(this._tabActor) !== this._isPreviousWindowXUL) {
+    if (isXUL(this._tabActor.window) !== this._isPreviousWindowXUL) {
       this._destroyHighlighter();
       this._createHighlighter();
     }
   },
 
   destroy: function() {
     protocol.Actor.prototype.destroy.call(this);
 
     this._destroyHighlighter();
     events.off(this._tabActor, "navigate", this._onNavigate);
+
+    this._highlighterEnv.destroy();
+    this._highlighterEnv = null;
+
     this._autohide = null;
     this._inspector = null;
     this._walker = null;
     this._tabActor = null;
     this._layoutHelpers = null;
   },
 
   /**
@@ -352,28 +358,28 @@ let HighlighterActor = exports.Highlight
     let x = event.clientX;
     let y = event.clientY;
 
     let node = doc.elementFromPoint(x, y);
     return this._walker.attachElement(node);
   },
 
   _startPickerListeners: function() {
-    let target = getPageListenerTarget(this._tabActor);
+    let target = this._highlighterEnv.pageListenerTarget;
     target.addEventListener("mousemove", this._onHovered, true);
     target.addEventListener("click", this._onPick, true);
     target.addEventListener("mousedown", this._preventContentEvent, true);
     target.addEventListener("mouseup", this._preventContentEvent, true);
     target.addEventListener("dblclick", this._preventContentEvent, true);
     target.addEventListener("keydown", this._onKey, true);
     target.addEventListener("keyup", this._preventContentEvent, true);
   },
 
   _stopPickerListeners: function() {
-    let target = getPageListenerTarget(this._tabActor);
+    let target = this._highlighterEnv.pageListenerTarget;
     target.removeEventListener("mousemove", this._onHovered, true);
     target.removeEventListener("click", this._onPick, true);
     target.removeEventListener("mousedown", this._preventContentEvent, true);
     target.removeEventListener("mouseup", this._preventContentEvent, true);
     target.removeEventListener("dblclick", this._preventContentEvent, true);
     target.removeEventListener("keydown", this._onKey, true);
     target.removeEventListener("keyup", this._preventContentEvent, true);
   },
@@ -420,30 +426,33 @@ let CustomHighlighterActor = exports.Cus
       let list = [...highlighterTypes.keys()];
 
       throw new Error(`${typeName} isn't a valid highlighter class (${list})`);
       return;
     }
 
     // The assumption is that all custom highlighters need the canvasframe
     // container to append their elements, so if this is a XUL window, bail out.
-    if (!isXUL(this._inspector.tabActor)) {
-      this._highlighter = new constructor(inspector.tabActor);
+    if (!isXUL(this._inspector.tabActor.window)) {
+      this._highlighterEnv = new HighlighterEnvironment();
+      this._highlighterEnv.initFromTabActor(inspector.tabActor);
+      this._highlighter = new constructor(this._highlighterEnv);
     } else {
       throw new Error("Custom " + typeName +
         "highlighter cannot be created in a XUL window");
       return;
     }
   },
 
   get conn() this._inspector && this._inspector.conn,
 
   destroy: function() {
     protocol.Actor.prototype.destroy.call(this);
     this.finalize();
+    this._inspector = null;
   },
 
   /**
    * Show the highlighter.
    * This calls through to the highlighter instance's |show(node, options)|
    * method.
    *
    * Most custom highlighters are made to highlight DOM nodes, hence the first
@@ -480,20 +489,26 @@ let CustomHighlighterActor = exports.Cus
     request: {}
   }),
 
   /**
    * Kill this actor. This method is called automatically just before the actor
    * is destroyed.
    */
   finalize: method(function() {
+    if (this._highlighterEnv) {
+      this._highlighterEnv.destroy();
+      this._highlighterEnv = null;
+    }
+
     if (this._highlighter) {
       this._highlighter.destroy();
       this._highlighter = null;
     }
+
   }, {
     oneway: true
   })
 });
 
 let CustomHighlighterFront = protocol.FrontClass(CustomHighlighterActor, {});
 
 /**
@@ -504,63 +519,63 @@ let CustomHighlighterFront = protocol.Fr
  * should use this helper to have their markup content automatically re-inserted
  * in the new document.
  *
  * Since the markup content is inserted in the canvasFrame using
  * insertAnonymousContent, this means that it can be modified using the API
  * described in AnonymousContent.webidl.
  * To retrieve the AnonymousContent instance, use the content getter.
  *
- * @param {TabActor} tabActor
- *        The tabactor which windows will be used to insert the node
+ * @param {HighlighterEnv} highlighterEnv
+ *        The environemnt which windows will be used to insert the node.
  * @param {Function} nodeBuilder
  *        A function that, when executed, returns a DOM node to be inserted into
- *        the canvasFrame
+ *        the canvasFrame.
  */
-function CanvasFrameAnonymousContentHelper(tabActor, nodeBuilder) {
-  this.tabActor = tabActor;
+function CanvasFrameAnonymousContentHelper(highlighterEnv, nodeBuilder) {
+  this.highlighterEnv = highlighterEnv;
   this.nodeBuilder = nodeBuilder;
-  this.anonymousContentDocument = this.tabActor.window.document;
+  this.anonymousContentDocument = this.highlighterEnv.document;
   // XXX the next line is a wallpaper for bug 1123362.
   this.anonymousContentGlobal = Cu.getGlobalForObject(this.anonymousContentDocument);
 
   this._insert();
 
   this._onNavigate = this._onNavigate.bind(this);
-  events.on(this.tabActor, "navigate", this._onNavigate);
+  this.highlighterEnv.on("navigate", this._onNavigate);
 
   this.listeners = new Map();
 }
 
 exports.CanvasFrameAnonymousContentHelper = CanvasFrameAnonymousContentHelper;
 
 CanvasFrameAnonymousContentHelper.prototype = {
   destroy: function() {
     // If the current window isn't the one the content was inserted into, this
     // will fail, but that's fine.
     try {
       let doc = this.anonymousContentDocument;
       doc.removeAnonymousContent(this._content);
     } catch (e) {}
-    events.off(this.tabActor, "navigate", this._onNavigate);
-    this.tabActor = this.nodeBuilder = this._content = null;
+    this.highlighterEnv.off("navigate", this._onNavigate);
+    this.highlighterEnv = this.nodeBuilder = this._content = null;
     this.anonymousContentDocument = null;
     this.anonymousContentGlobal = null;
 
     this._removeAllListeners();
   },
 
   _insert: function() {
     // Insert the content node only if the page isn't in a XUL window, and if
     // the document still exists.
-    if (!this.tabActor.window.document.documentElement ||
-        isXUL(this.tabActor)) {
+    if (!this.highlighterEnv.document.documentElement ||
+        isXUL(this.highlighterEnv.window)) {
       return;
     }
-    let doc = this.tabActor.window.document;
+    let doc = this.highlighterEnv.document;
 
     // On B2G, for example, when connecting to keyboard just after startup,
     // we connect to a hidden document, which doesn't accept
     // insertAnonymousContent call yet.
     if (doc.hidden) {
       // In such scenario, just wait for the document to be visible
       // before injecting anonymous content.
       let onVisibilityChange = () => {
@@ -570,27 +585,27 @@ CanvasFrameAnonymousContentHelper.protot
       doc.addEventListener("visibilitychange", onVisibilityChange);
       return;
     }
 
     // For now highlighter.css is injected in content as a ua sheet because
     // <style scoped> doesn't work inside anonymous content (see bug 1086532).
     // If it did, highlighter.css would be injected as an anonymous content
     // node using CanvasFrameAnonymousContentHelper instead.
-    installHelperSheet(this.tabActor.window,
+    installHelperSheet(this.highlighterEnv.window,
       "@import url('" + HIGHLIGHTER_STYLESHEET_URI + "');");
     let node = this.nodeBuilder();
     this._content = doc.insertAnonymousContent(node);
   },
 
-  _onNavigate: function({isTopLevel}) {
+  _onNavigate: function(e, {isTopLevel}) {
     if (isTopLevel) {
       this._removeAllListeners();
       this._insert();
-      this.anonymousContentDocument = this.tabActor.window.document;
+      this.anonymousContentDocument = this.highlighterEnv.document;
     }
   },
 
   getTextContentForElement: function(id) {
     if (!this.content) {
       return null;
     }
     return this.content.getTextContentForElement(id);
@@ -660,17 +675,17 @@ CanvasFrameAnonymousContentHelper.protot
   addEventListenerForElement: function(id, type, handler) {
     if (typeof id !== "string") {
       throw new Error("Expected a string ID in addEventListenerForElement but" +
         " got: " + id);
     }
 
     // If no one is listening for this type of event yet, add one listener.
     if (!this.listeners.has(type)) {
-      let target = getPageListenerTarget(this.tabActor);
+      let target = this.highlighterEnv.pageListenerTarget;
       target.addEventListener(type, this, true);
       // Each type entry in the map is a map of ids:handlers.
       this.listeners.set(type, new Map);
     }
 
     let listeners = this.listeners.get(type);
     listeners.set(id, handler);
   },
@@ -686,17 +701,17 @@ CanvasFrameAnonymousContentHelper.protot
     let listeners = this.listeners.get(type);
     if (!listeners) {
       return;
     }
     listeners.delete(id);
 
     // If no one is listening for event type anymore, remove the listener.
     if (!this.listeners.has(type)) {
-      let target = getPageListenerTarget(this.tabActor);
+      let target = this.highlighterEnv.pageListenerTarget;
       target.removeEventListener(type, this, true);
     }
   },
 
   handleEvent: function(event) {
     let listeners = this.listeners.get(event.type);
     if (!listeners) {
       return;
@@ -730,18 +745,18 @@ CanvasFrameAnonymousContentHelper.protot
           break;
         }
       }
       node = node.parentNode;
     }
   },
 
   _removeAllListeners: function() {
-    if (this.tabActor) {
-      let target = getPageListenerTarget(this.tabActor);
+    if (this.highlighterEnv) {
+      let target = this.highlighterEnv.pageListenerTarget;
       for (let [type] of this.listeners) {
         target.removeEventListener(type, this, true);
       }
     }
     this.listeners.clear();
   },
 
   getElement: function(id) {
@@ -818,21 +833,21 @@ CanvasFrameAnonymousContentHelper.protot
  * - this.currentQuads: all of the node's box model region quads
  * - this.win: the current window
  *
  * Emits the following events:
  * - shown
  * - hidden
  * - updated
  */
-function AutoRefreshHighlighter(tabActor) {
+function AutoRefreshHighlighter(highlighterEnv) {
   EventEmitter.decorate(this);
 
-  this.tabActor = tabActor;
-  this.win = tabActor.window;
+  this.highlighterEnv = highlighterEnv;
+  this.win = highlighterEnv.window;
 
   this.currentNode = null;
   this.currentQuads = {};
 
   this.layoutHelpers = new LayoutHelpers(this.win);
 
   this.update = this.update.bind(this);
 }
@@ -968,32 +983,32 @@ AutoRefreshHighlighter.prototype = {
       this.rafWin.cancelAnimationFrame(this.rafID);
     }
     this.rafID = this.rafWin = null;
   },
 
   destroy: function() {
     this.hide();
 
-    this.tabActor = null;
+    this.highlighterEnv = null;
     this.win = null;
     this.currentNode = null;
     this.layoutHelpers = null;
   }
 };
 
 /**
  * The BoxModelHighlighter draws the box model regions on top of a node.
  * If the node is a block box, then each region will be displayed as 1 polygon.
  * If the node is an inline box though, each region may be represented by 1 or
  * more polygons, depending on how many line boxes the inline element has.
  *
  * Usage example:
  *
- * let h = new BoxModelHighlighter(browser);
+ * let h = new BoxModelHighlighter(env);
  * h.show(node, options);
  * h.hide();
  * h.destroy();
  *
  * Available options:
  * - region {String}
  *   "content", "padding", "border" or "margin"
  *   This specifies the region that the guides should outline.
@@ -1031,20 +1046,20 @@ AutoRefreshHighlighter.prototype = {
  *           <span class="box-model-nodeinfobar-pseudo-classes">:hover</span>
  *         </div>
  *       </div>
  *       <div class="box-model-nodeinfobar-arrow box-model-nodeinfobar-arrow-bottom"/>
  *     </div>
  *   </div>
  * </div>
  */
-function BoxModelHighlighter(tabActor) {
-  AutoRefreshHighlighter.call(this, tabActor);
-
-  this.markup = new CanvasFrameAnonymousContentHelper(this.tabActor,
+function BoxModelHighlighter(highlighterEnv) {
+  AutoRefreshHighlighter.call(this, highlighterEnv);
+
+  this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
     this._buildMarkup.bind(this));
 
   /**
    * Optionally customize each region's fill color by adding an entry to the
    * regionFill property: `highlighter.regionFill.margin = "red";
    */
   this.regionFill = {};
 
@@ -1642,20 +1657,20 @@ BoxModelHighlighter.prototype = Heritage
 register(BoxModelHighlighter);
 exports.BoxModelHighlighter = BoxModelHighlighter;
 
 /**
  * The CssTransformHighlighter is the class that draws an outline around a
  * transformed element and an outline around where it would be if untransformed
  * as well as arrows connecting the 2 outlines' corners.
  */
-function CssTransformHighlighter(tabActor) {
-  AutoRefreshHighlighter.call(this, tabActor);
-
-  this.markup = new CanvasFrameAnonymousContentHelper(this.tabActor,
+function CssTransformHighlighter(highlighterEnv) {
+  AutoRefreshHighlighter.call(this, highlighterEnv);
+
+  this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
     this._buildMarkup.bind(this));
 }
 
 let MARKER_COUNTER = 1;
 
 CssTransformHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype, {
   typeName: "CssTransformHighlighter",
 
@@ -1872,18 +1887,18 @@ CssTransformHighlighter.prototype = Heri
 register(CssTransformHighlighter);
 exports.CssTransformHighlighter = CssTransformHighlighter;
 
 /**
  * The SelectorHighlighter runs a given selector through querySelectorAll on the
  * document of the provided context node and then uses the BoxModelHighlighter
  * to highlight the matching nodes
  */
-function SelectorHighlighter(tabActor) {
-  this.tabActor = tabActor;
+function SelectorHighlighter(highlighterEnv) {
+  this.highlighterEnv = highlighterEnv;
   this._highlighters = [];
 }
 
 SelectorHighlighter.prototype = {
   typeName: "SelectorHighlighter",
 
   /**
    * Show BoxModelHighlighter on each node that matches that provided selector.
@@ -1909,17 +1924,17 @@ SelectorHighlighter.prototype = {
     delete options.selector;
 
     let i = 0;
     for (let matchingNode of nodes) {
       if (i >= MAX_HIGHLIGHTED_ELEMENTS) {
         break;
       }
 
-      let highlighter = new BoxModelHighlighter(this.tabActor);
+      let highlighter = new BoxModelHighlighter(this.highlighterEnv);
       if (options.fill) {
         highlighter.regionFill[options.region || "border"] = options.fill;
       }
       highlighter.show(matchingNode, options);
       this._highlighters.push(highlighter);
       i ++;
     }
   },
@@ -1928,33 +1943,33 @@ SelectorHighlighter.prototype = {
     for (let highlighter of this._highlighters) {
       highlighter.destroy();
     }
     this._highlighters = [];
   },
 
   destroy: function() {
     this.hide();
-    this.tabActor = null;
+    this.highlighterEnv = null;
   }
 };
 register(SelectorHighlighter);
 exports.SelectorHighlighter = SelectorHighlighter;
 
 /**
  * The RectHighlighter is a class that draws a rectangle highlighter at specific
  * coordinates.
  * It does *not* highlight DOM nodes, but rects.
  * It also does *not* update dynamically, it only highlights a rect and remains
  * there as long as it is shown.
  */
-function RectHighlighter(tabActor) {
-  this.win = tabActor.window;
+function RectHighlighter(highlighterEnv) {
+  this.win = highlighterEnv.window;
   this.layoutHelpers = new LayoutHelpers(this.win);
-  this.markup = new CanvasFrameAnonymousContentHelper(tabActor,
+  this.markup = new CanvasFrameAnonymousContentHelper(highlighterEnv,
     this._buildMarkup.bind(this));
 }
 
 RectHighlighter.prototype = {
   typeName: "RectHighlighter",
 
   _buildMarkup: function() {
     let doc = this.win.document;
@@ -2108,23 +2123,23 @@ let GeoProp = {
  * The highlighter displays lines and labels for each of the defined properties
  * in and around the element (relative to the offset parent when one exists).
  * The highlighter also highlights the element itself and its offset parent if
  * there is one.
  *
  * Note that the class name contains the word Editor because the aim is for the
  * handles to be draggable in content to make the geometry editable.
  */
-function GeometryEditorHighlighter(tabActor) {
-  AutoRefreshHighlighter.call(this, tabActor);
+function GeometryEditorHighlighter(highlighterEnv) {
+  AutoRefreshHighlighter.call(this, highlighterEnv);
 
   // The list of element geometry properties that can be set.
   this.definedProperties = new Map();
 
-  this.markup = new CanvasFrameAnonymousContentHelper(tabActor,
+  this.markup = new CanvasFrameAnonymousContentHelper(highlighterEnv,
     this._buildMarkup.bind(this));
 }
 
 GeometryEditorHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype, {
   typeName: "GeometryEditorHighlighter",
 
   ID_CLASS_PREFIX: "geometry-editor-",
 
@@ -2622,19 +2637,19 @@ GeometryEditorHighlighter.prototype = He
 register(GeometryEditorHighlighter);
 exports.GeometryEditorHighlighter = GeometryEditorHighlighter;
 
 /**
  * The RulersHighlighter is a class that displays both horizontal and
  * vertical rules on the page, along the top and left edges, with pixel
  * graduations, useful for users to quickly check distances
  */
-function RulersHighlighter(tabActor) {
-  this.win = tabActor.window;
-  this.markup = new CanvasFrameAnonymousContentHelper(tabActor,
+function RulersHighlighter(highlighterEnv) {
+  this.win = highlighterEnv.window;
+  this.markup = new CanvasFrameAnonymousContentHelper(highlighterEnv,
     this._buildMarkup.bind(this));
 
   this.win.addEventListener("scroll", this, true);
   this.win.addEventListener("pagehide", this, true);
 }
 
 RulersHighlighter.prototype =  {
   typeName: "RulersHighlighter",
@@ -2845,18 +2860,18 @@ exports.RulersHighlighter = RulersHighli
 
 /**
  * The SimpleOutlineHighlighter is a class that has the same API than the
  * BoxModelHighlighter, but adds a pseudo-class on the target element itself
  * to draw a simple css outline around the element.
  * It is used by the HighlighterActor when canvasframe-based highlighters can't
  * be used. This is the case for XUL windows.
  */
-function SimpleOutlineHighlighter(tabActor) {
-  this.chromeDoc = tabActor.window.document;
+function SimpleOutlineHighlighter(highlighterEnv) {
+  this.chromeDoc = highlighterEnv.document;
 }
 
 SimpleOutlineHighlighter.prototype = {
   /**
    * Destroy the nodes. Remove listeners.
    */
   destroy: function() {
     this.hide();
@@ -2925,20 +2940,22 @@ function installHelperSheet(win, source,
   let {Style} = require("sdk/stylesheet/style");
   let {attach} = require("sdk/content/mod");
   let style = Style({source, type});
   attach(style, win);
   installedHelperSheets.set(win.document, style);
 }
 
 /**
- * Is the content window in this tabActor a XUL window
+ * Is this content window a XUL window?
+ * @param {Window} window
+ * @return {Boolean}
  */
-function isXUL(tabActor) {
-  return tabActor.window.document.documentElement.namespaceURI === XUL_NS;
+function isXUL(window) {
+  return window.document.documentElement.namespaceURI === XUL_NS;
 }
 
 /**
  * Helper function that creates SVG DOM nodes.
  * @param {Window} This window's document will be used to create the element
  * @param {Object} Options for the node include:
  * - nodeType: the type of node, defaults to "box".
  * - attributes: a {name:value} object to be used as attributes for the node.
@@ -2989,33 +3006,16 @@ function createNode(win, options) {
   if (options.parent) {
     options.parent.appendChild(node);
   }
 
   return node;
 }
 
 /**
- * Get the right target for listening to events on the page (while picking an
- * element for instance).
- * - On a firefox desktop content page: tabActor is a BrowserTabActor from
- *   which the browser property will give us a target we can use to listen to
- *   events, even in nested iframes.
- * - On B2G: tabActor is a ContentActor which doesn't have a browser but
- *   since it overrides BrowserTabActor, it does get a browser property
- *   anyway, which points to its window object.
- * - When using the Browser Toolbox (to inspect firefox desktop): tabActor is
- *   the RootActor, in which case, the window property can be used to listen
- *   to events
- */
-function getPageListenerTarget(tabActor) {
-  return tabActor.isRootActor ? tabActor.window : tabActor.chromeEventHandler;
-}
-
-/**
  * Get a node's owner window.
  */
 function getWindow(node) {
   return node.ownerDocument.defaultView;
 }
 
 /**
  * Get the provided node's offsetParent dimensions.
@@ -3046,11 +3046,162 @@ function getOffsetParent(node) {
   }
 
   return {
     element: offsetParent,
     dimension: {width, height}
   };
 }
 
-XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
-  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)
+/**
+ * The HighlighterEnvironment is an object that holds all the required data for
+ * highlighters to work: the window, docShell, event listener target, ...
+ * It also emits "will-navigate" and "navigate" events, similarly to the
+ * TabActor.
+ *
+ * It can be initialized either from a TabActor (which is the most frequent way
+ * of using it, since highlighters are usually initialized by the
+ * HighlighterActor or CustomHighlighterActor, which have a tabActor reference).
+ * It can also be initialized just with a window object (which is useful for
+ * when a highlighter is used outside of the debugger server context, for
+ * instance from a gcli command).
+ */
+function HighlighterEnvironment() {
+  this.relayTabActorNavigate = this.relayTabActorNavigate.bind(this);
+  this.relayTabActorWillNavigate = this.relayTabActorWillNavigate.bind(this);
+
+  EventEmitter.decorate(this);
+}
+
+exports.HighlighterEnvironment = HighlighterEnvironment;
+
+HighlighterEnvironment.prototype = {
+  initFromTabActor: function(tabActor) {
+    this._tabActor = tabActor;
+    events.on(this._tabActor, "navigate", this.relayTabActorNavigate);
+    events.on(this._tabActor, "will-navigate", this.relayTabActorWillNavigate);
+  },
+
+  initFromWindow: function(win) {
+    this._win = win;
+
+    // We need a progress listener to know when the window will navigate/has
+    // navigated.
+    let self = this;
+    this.listener = {
+      QueryInterface: XPCOMUtils.generateQI([
+        Ci.nsIWebProgressListener,
+        Ci.nsISupportsWeakReference,
+        Ci.nsISupports
+      ]),
+
+      onStateChange: function(progress, request, flag) {
+        let isStart = flag & Ci.nsIWebProgressListener.STATE_START;
+        let isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
+        let isWindow = flag & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
+        let isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
+
+        if (progress.DOMWindow !== win) {
+          return;
+        }
+
+        if (isDocument && isStart) {
+          // One of the earliest events that tells us a new URI is being loaded
+          // in this window.
+          self.emit("will-navigate", {
+            window: win,
+            isTopLevel: true
+          });
+        }
+        if (isWindow && isStop) {
+          self.emit("navigate", {
+            window: win,
+            isTopLevel: true
+          });
+        }
+      }
+    };
+
+    this.webProgress.addProgressListener(this.listener,
+      Ci.nsIWebProgress.NOTIFY_STATUS |
+      Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
+      Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+  },
+
+  get isInitialized() {
+    return this._win || this._tabActor;
+  },
+
+  get window() {
+    if (!this.isInitialized) {
+      throw new Error("Initialize HighlighterEnvironment with a tabActor " +
+        "or window first");
+    }
+    return this._tabActor ? this._tabActor.window : this._win;
+  },
+
+  get document() {
+    return this.window.document;
+  },
+
+  get docShell() {
+    return this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+                      .getInterface(Ci.nsIWebNavigation)
+                      .QueryInterface(Ci.nsIDocShell);
+  },
+
+  get webProgress() {
+    return this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                        .getInterface(Ci.nsIWebProgress);
+  },
+
+  /**
+   * Get the right target for listening to events on the page.
+   * - If the environment was initialized from a TabActor *and* if we're in the
+   *   Browser Toolbox (to inspect firefox desktop): the tabActor is the
+   *   RootActor, in which case, the window property can be used to listen to
+   *   events.
+   * - With firefox desktop, that tabActor is a BrowserTabActor, and with B2G,
+   *   a ContentActor (which overrides BrowserTabActor). In both cases we use
+   *   the chromeEventHandler which gives us a target we can use to listen to
+   *   events, even from nested iframes.
+   * - If the environment was initialized from a window, we also use the
+   *   chromeEventHandler.
+   */
+  get pageListenerTarget() {
+    if (this._tabActor && this._tabActor.isRootActor) {
+      return this.window;
+    }
+    return this.docShell.chromeEventHandler;
+  },
+
+  relayTabActorNavigate: function(data) {
+    this.emit("navigate", data);
+  },
+
+  relayTabActorWillNavigate: function(data) {
+    this.emit("will-navigate", data);
+  },
+
+  destroy: function() {
+    if (this._tabActor) {
+      events.off(this._tabActor, "navigate", this.relayTabActorNavigate);
+      events.off(this._tabActor, "will-navigate", this.relayTabActorWillNavigate);
+    }
+
+    // In case the environment was initialized from a window, we need to remove
+    // the progress listener.
+    if (this._win) {
+      try {
+        this.webProgress.removeProgressListener(this.listener);
+      } catch(e) {
+        // Which may fail in case the window was already destroyed.
+      }
+    }
+
+    this._tabActor = null;
+    this._win = null;
+  }
+};
+
+XPCOMUtils.defineLazyGetter(this, "DOMUtils", function() {
+  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
--- a/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_01.js
+++ b/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_01.js
@@ -4,17 +4,20 @@
 
 "use strict";
 
 // Simple CanvasFrameAnonymousContentHelper tests.
 
 // This makes sure the 'domnode' protocol actor type is known when importing
 // highlighter.
 require("devtools/server/actors/inspector");
-const {CanvasFrameAnonymousContentHelper} = require("devtools/server/actors/highlighter");
+const {
+  CanvasFrameAnonymousContentHelper,
+  HighlighterEnvironment
+} = require("devtools/server/actors/highlighter");
 const TEST_URL = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test";
 
 add_task(function*() {
   let doc = yield addTab(TEST_URL);
 
   let nodeBuilder = () => {
     let root = doc.createElement("div");
     let child = doc.createElement("div");
@@ -22,18 +25,19 @@ add_task(function*() {
     child.id = "child-element";
     child.className = "child-element";
     child.textContent = "test element";
     root.appendChild(child);
     return root;
   };
 
   info("Building the helper");
-  let helper = new CanvasFrameAnonymousContentHelper(
-    getMockTabActor(doc.defaultView), nodeBuilder);
+  let env = new HighlighterEnvironment();
+  env.initFromWindow(doc.defaultView);
+  let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
 
   ok(helper.content instanceof AnonymousContent,
     "The helper owns the AnonymousContent object");
   ok(helper.getTextContentForElement,
     "The helper has the getTextContentForElement method");
   ok(helper.setTextContentForElement,
     "The helper has the setTextContentForElement method");
   ok(helper.setAttributeForElement,
@@ -65,16 +69,17 @@ add_task(function*() {
     "The text content was retrieve correctly");
   is(el.getAttribute("id"), "child-element",
     "The ID attribute was retrieve correctly");
   is(el.getAttribute("class"), "child-element",
     "The class attribute was retrieve correctly");
 
   info("Destroying the helper");
   helper.destroy();
+  env.destroy();
 
   ok(!helper.getTextContentForElement("child-element"),
     "No text content was retrieved after the helper was destroyed");
   ok(!helper.getAttributeForElement("child-element", "id"),
     "No ID attribute was retrieved after the helper was destroyed");
   ok(!helper.getAttributeForElement("child-element", "class"),
     "No class attribute was retrieved after the helper was destroyed");
 
--- a/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_02.js
+++ b/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_02.js
@@ -5,34 +5,41 @@
 "use strict";
 
 // Test that the CanvasFrameAnonymousContentHelper does not insert content in
 // XUL windows.
 
 // This makes sure the 'domnode' protocol actor type is known when importing
 // highlighter.
 require("devtools/server/actors/inspector");
-const {CanvasFrameAnonymousContentHelper} = require("devtools/server/actors/highlighter");
+const {
+  CanvasFrameAnonymousContentHelper,
+  HighlighterEnvironment
+} = require("devtools/server/actors/highlighter");
 
 add_task(function*() {
   let doc = yield addTab("about:preferences");
 
   let nodeBuilder = () => {
     let root = doc.createElement("div");
     let child = doc.createElement("div");
     child.style = "width:200px;height:200px;background:red;";
     child.id = "child-element";
     child.className = "child-element";
     child.textContent = "test element";
     root.appendChild(child);
     return root;
   };
 
   info("Building the helper");
-  let helper = new CanvasFrameAnonymousContentHelper(
-    getMockTabActor(doc.defaultView), nodeBuilder);
+  let env = new HighlighterEnvironment();
+  env.initFromWindow(doc.defaultView);
+  let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
 
   ok(!helper.content, "The AnonymousContent was not inserted in the window");
   ok(!helper.getTextContentForElement("child-element"),
     "No text content is returned");
 
+  env.destroy();
+  helper.destroy();
+
   gBrowser.removeCurrentTab();
 });
--- a/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_03.js
+++ b/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_03.js
@@ -4,44 +4,48 @@
 
 "use strict";
 
 // Test the CanvasFrameAnonymousContentHelper event handling mechanism.
 
 // This makes sure the 'domnode' protocol actor type is known when importing
 // highlighter.
 require("devtools/server/actors/inspector");
-const {CanvasFrameAnonymousContentHelper} = require("devtools/server/actors/highlighter");
+const {
+  CanvasFrameAnonymousContentHelper,
+  HighlighterEnvironment
+} = require("devtools/server/actors/highlighter");
 const TEST_URL = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test";
 
 add_task(function*() {
   let doc = yield addTab(TEST_URL);
 
   let nodeBuilder = () => {
     let root = doc.createElement("div");
     let child = doc.createElement("div");
     child.style = "pointer-events:auto;width:200px;height:200px;background:red;";
     child.id = "child-element";
     child.className = "child-element";
     root.appendChild(child);
     return root;
   };
 
   info("Building the helper");
-  let helper = new CanvasFrameAnonymousContentHelper(
-    getMockTabActor(doc.defaultView), nodeBuilder);
+  let env = new HighlighterEnvironment();
+  env.initFromWindow(doc.defaultView);
+  let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
 
   let el = helper.getElement("child-element");
 
   info("Adding an event listener on the inserted element");
   let mouseDownHandled = 0;
   function onMouseDown(e, id) {
     is(id, "child-element", "The mousedown event was triggered on the element");
     ok(!e.originalTarget, "The originalTarget property isn't available");
-    mouseDownHandled ++;
+    mouseDownHandled++;
   }
   el.addEventListener("mousedown", onMouseDown);
 
   info("Synthesizing an event on the inserted element");
   let onDocMouseDown = once(doc, "mousedown");
   synthesizeMouseDown(100, 100, doc.defaultView);
   yield onDocMouseDown;
 
@@ -65,16 +69,17 @@ add_task(function*() {
 
   is(mouseDownHandled, 1,
     "The mousedown event hasn't been handled after the listener was removed");
 
   info("Adding again the event listener");
   el.addEventListener("mousedown", onMouseDown);
 
   info("Destroying the helper");
+  env.destroy();
   helper.destroy();
 
   info("Synthesizing another event after the helper has been destroyed");
   // Using a document event listener to know when the event has been synthesized.
   onDocMouseDown = once(doc, "mousedown");
   synthesizeMouseDown(100, 100, doc.defaultView);
   yield onDocMouseDown;
 
--- a/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_04.js
+++ b/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_04.js
@@ -5,39 +5,42 @@
 "use strict";
 
 // Test the CanvasFrameAnonymousContentHelper re-inserts the content when the
 // page reloads.
 
 // This makes sure the 'domnode' protocol actor type is known when importing
 // highlighter.
 require("devtools/server/actors/inspector");
-const {CanvasFrameAnonymousContentHelper} = require("devtools/server/actors/highlighter");
+const {
+  CanvasFrameAnonymousContentHelper,
+  HighlighterEnvironment
+} = require("devtools/server/actors/highlighter");
 const events = require("sdk/event/core");
 const TEST_URL_1 = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test 1";
 const TEST_URL_2 = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test 2";
 
 add_task(function*() {
   let doc = yield addTab(TEST_URL_2);
 
-  let tabActor = getMockTabActor(doc.defaultView);
-
   let nodeBuilder = () => {
     let root = doc.createElement("div");
     let child = doc.createElement("div");
     child.style = "pointer-events:auto;width:200px;height:200px;background:red;";
     child.id = "child-element";
     child.className = "child-element";
     child.textContent= "test content";
     root.appendChild(child);
     return root;
   };
 
   info("Building the helper");
-  let helper = new CanvasFrameAnonymousContentHelper(tabActor, nodeBuilder);
+  let env = new HighlighterEnvironment();
+  env.initFromWindow(doc.defaultView);
+  let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
 
   info("Get an element from the helper");
   let el = helper.getElement("child-element");
 
   info("Try to access the element");
   is(el.getAttribute("class"), "child-element",
     "The attribute is correct before navigation");
   is(el.getTextContent(), "test content",
@@ -58,32 +61,30 @@ add_task(function*() {
   is(mouseDownHandled, 1, "The mousedown event was handled once before navigation");
 
   info("Navigating to a new page");
   let loaded = once(gBrowser.selectedBrowser, "load", true);
   content.location = TEST_URL_2;
   yield loaded;
   doc = gBrowser.selectedBrowser.contentWindow.document;
 
-  info("And faking the 'navigate' event on the tabActor");
-  events.emit(tabActor, "navigate", tabActor);
-
   info("Try to access the element again");
   is(el.getAttribute("class"), "child-element",
     "The attribute is correct after navigation");
   is(el.getTextContent(), "test content",
     "The text content is correct after navigation");
 
   info("Synthesizing an event on the element again");
   onDocMouseDown = once(doc, "mousedown");
   synthesizeMouseDown(100, 100, doc.defaultView);
   yield onDocMouseDown;
   is(mouseDownHandled, 1, "The mousedown event was not handled after navigation");
 
   info("Destroying the helper");
+  env.destroy();
   helper.destroy();
 
   gBrowser.removeCurrentTab();
 });
 
 function synthesizeMouseDown(x, y, win) {
   // We need to make sure the inserted anonymous content can be targeted by the
   // event right after having been inserted, and so we need to force a sync
--- a/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_05.js
+++ b/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_05.js
@@ -5,17 +5,20 @@
 "use strict";
 
 // Test some edge cases of the CanvasFrameAnonymousContentHelper event handling
 // mechanism.
 
 // This makes sure the 'domnode' protocol actor type is known when importing
 // highlighter.
 require("devtools/server/actors/inspector");
-const {CanvasFrameAnonymousContentHelper} = require("devtools/server/actors/highlighter");
+const {
+  CanvasFrameAnonymousContentHelper,
+  HighlighterEnvironment
+} = require("devtools/server/actors/highlighter");
 const TEST_URL = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test";
 
 add_task(function*() {
   let doc = yield addTab(TEST_URL);
 
   let nodeBuilder = () => {
     let root = doc.createElement("div");
 
@@ -28,18 +31,19 @@ add_task(function*() {
     child.style = "pointer-events:auto;width:200px;height:200px;background:red;";
     child.id = "child-element";
     parent.appendChild(child);
 
     return root;
   };
 
   info("Building the helper");
-  let helper = new CanvasFrameAnonymousContentHelper(
-    getMockTabActor(doc.defaultView), nodeBuilder);
+  let env = new HighlighterEnvironment();
+  env.initFromWindow(doc.defaultView);
+  let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
 
   info("Getting the parent and child elements");
   let parentEl = helper.getElement("parent-element");
   let childEl = helper.getElement("child-element");
 
   info("Adding an event listener on both elements");
   let mouseDownHandled = [];
   function onMouseDown(e, id) {
@@ -84,16 +88,19 @@ add_task(function*() {
 
   is(mouseDownHandled.length, 1, "The mousedown event was handled once");
   is(mouseDownHandled[0], "parent-element",
     "The mousedown event did bubble to the parent element");
 
   info("Removing the parent listener");
   parentEl.removeEventListener("mousedown", onMouseDown);
 
+  env.destroy();
+  helper.destroy();
+
   gBrowser.removeCurrentTab();
 });
 
 function synthesizeMouseDown(x, y, win) {
   // We need to make sure the inserted anonymous content can be targeted by the
   // event right after having been inserted, and so we need to force a sync
   // reflow.
   let forceReflow = win.document.documentElement.offsetWidth;
--- a/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_06.js
+++ b/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_06.js
@@ -5,17 +5,20 @@
 "use strict";
 
 // Test support for event propagation stop in the
 // CanvasFrameAnonymousContentHelper event handling mechanism.
 
 // This makes sure the 'domnode' protocol actor type is known when importing
 // highlighter.
 require("devtools/server/actors/inspector");
-const {CanvasFrameAnonymousContentHelper} = require("devtools/server/actors/highlighter");
+const {
+  CanvasFrameAnonymousContentHelper,
+  HighlighterEnvironment
+} = require("devtools/server/actors/highlighter");
 const TEST_URL = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test";
 
 add_task(function*() {
   let doc = yield addTab(TEST_URL);
 
   let nodeBuilder = () => {
     let root = doc.createElement("div");
 
@@ -28,18 +31,19 @@ add_task(function*() {
     child.style = "pointer-events:auto;width:200px;height:200px;background:red;";
     child.id = "child-element";
     parent.appendChild(child);
 
     return root;
   };
 
   info("Building the helper");
-  let helper = new CanvasFrameAnonymousContentHelper(
-    getMockTabActor(doc.defaultView), nodeBuilder);
+  let env = new HighlighterEnvironment();
+  env.initFromWindow(doc.defaultView);
+  let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
 
   info("Getting the parent and child elements");
   let parentEl = helper.getElement("parent-element");
   let childEl = helper.getElement("child-element");
 
   info("Adding an event listener on both elements");
   let mouseDownHandled = [];
 
@@ -72,16 +76,19 @@ add_task(function*() {
   is(mouseDownHandled.length, 1, "The mousedown event was handled only once");
   is(mouseDownHandled[0], "parent-element",
     "The mousedown event was handled on the parent element");
 
   info("Removing the event listener");
   parentEl.removeEventListener("mousedown", onParentMouseDown);
   childEl.removeEventListener("mousedown", onChildMouseDown);
 
+  env.destroy();
+  helper.destroy();
+
   gBrowser.removeCurrentTab();
 });
 
 function synthesizeMouseDown(x, y, win) {
   // We need to make sure the inserted anonymous content can be targeted by the
   // event right after having been inserted, and so we need to force a sync
   // reflow.
   let forceReflow = win.document.documentElement.offsetWidth;