Bug 1147826 - Remove deprecated TabActor reference in runAt:server GCLI commands; r=jwalker; r=miker
authorPatrick Brosset <pbrosset@mozilla.com>
Mon, 01 Jun 2015 14:41:12 +0200
changeset 247432 33cc16699f798e001d31fa94c6adde9a74e6878e
parent 247431 ead0c29ea6f255827258e172ea0bba23149332da
child 247433 d538a04610e29aa733632937dfdd1bc5895a9ef1
push id28865
push userkwierso@gmail.com
push dateFri, 05 Jun 2015 22:26:42 +0000
treeherdermozilla-central@49f13b251968 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker, miker
bugs1147826
milestone41.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 1147826 - Remove deprecated TabActor reference in runAt:server GCLI commands; r=jwalker; r=miker 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
@@ -261,20 +261,16 @@ const GcliActor = ActorClass({
         },
 
         get window() {
           return tabActor.window;
         },
 
         get document() {
           return tabActor.window.document;
-        },
-
-        get __deprecatedTabActor() {
-          return tabActor;
         }
       };
 
       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,16 +125,18 @@ 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();
 
@@ -143,25 +145,25 @@ let HighlighterActor = exports.Highlight
     events.on(this._tabActor, "navigate", this._onNavigate);
   },
 
   get conn() {
     return 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);
@@ -174,27 +176,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
     // originalTarget allows access to the "real" element before any retargeting
     // is applied, such as in the case of XBL anonymous elements.  See also
     // https://developer.mozilla.org/docs/XBL/XBL_1.0_Reference/Anonymous_Content#Event_Flow_and_Targeting
     let node = event.originalTarget || event.target;
     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,32 +426,35 @@ 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() {
     return 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
@@ -486,20 +495,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, {});
 
 /**
@@ -510,63 +525,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 = () => {
@@ -576,27 +591,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);
@@ -666,17 +681,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);
   },
@@ -692,17 +707,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;
@@ -736,18 +751,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) {
@@ -824,21 +839,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);
 }
@@ -980,32 +995,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.
@@ -1043,20 +1058,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 = {};
 
@@ -1659,20 +1674,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",
 
@@ -1889,18 +1904,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.
@@ -1926,17 +1941,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 ++;
     }
 
@@ -1947,33 +1962,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;
@@ -2129,23 +2144,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-",
 
@@ -2645,19 +2660,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",
@@ -2869,18 +2884,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();
@@ -2950,20 +2965,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.
@@ -3014,33 +3031,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.
@@ -3071,11 +3071,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;