Bug 1139186 - 2 - Add event handling support to CanvasFrameAnonymousContentHelper; r=bgrins
authorPatrick Brosset <pbrosset@mozilla.com>
Fri, 20 Mar 2015 16:58:50 +0100
changeset 263461 ad230a491176bd5e01c6c8fbb9fc41a45c584cbb
parent 263460 fe3a966d78e23cc9e5913ad7841f0cdf12e014fb
child 263462 a89f634ec7a45b0e2bb4c887a978870205f7fcdf
push id4718
push userraliiev@mozilla.com
push dateMon, 11 May 2015 18:39:53 +0000
treeherdermozilla-beta@c20c4ef55f08 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1139186
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1139186 - 2 - Add event handling support to CanvasFrameAnonymousContentHelper; r=bgrins
toolkit/devtools/LayoutHelpers.jsm
toolkit/devtools/server/actors/highlighter.css
toolkit/devtools/server/actors/highlighter.js
toolkit/devtools/server/tests/browser/browser.ini
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
toolkit/devtools/server/tests/browser/head.js
--- a/toolkit/devtools/LayoutHelpers.jsm
+++ b/toolkit/devtools/LayoutHelpers.jsm
@@ -599,22 +599,23 @@ LayoutHelpers.isShadowAnonymous = functi
   return parent.shadowRoot && parent.shadowRoot.contains(node);
 };
 
 /**
  * Get the current zoom factor applied to the container window of a given node.
  * Container windows are used as a weakmap key to store the corresponding
  * nsIDOMWindowUtils instance to avoid querying it every time.
  *
- * @param {DOMNode} The node for which the zoom factor should be calculated
+ * @param {DOMNode|DOMWindow} The node for which the zoom factor should be
+ * calculated, or its owner window.
  * @return {Number}
  */
 let windowUtils = new WeakMap;
-LayoutHelpers.getCurrentZoom = function(node, map = z=>z) {
-  let win = node.ownerDocument.defaultView;
+LayoutHelpers.getCurrentZoom = function(node) {
+  let win = node.self === node ? node : node.ownerDocument.defaultView;
   let utils = windowUtils.get(win);
   if (utils) {
     return utils.fullZoom;
   }
 
   utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
              .getInterface(Ci.nsIDOMWindowUtils);
   windowUtils.set(win, utils);
--- a/toolkit/devtools/server/actors/highlighter.css
+++ b/toolkit/devtools/server/actors/highlighter.css
@@ -9,20 +9,25 @@
   This stylesheet is loaded as a ua stylesheet via the addon sdk, so having this
   pseudo-class is important.
   Having bug 1086532 fixed would make it possible to load this stylesheet in a
   <style scoped> node instead, directly in the native anonymous container
   element.
 */
 
 :-moz-native-anonymous .highlighter-container {
-  pointer-events: none;
   position: absolute;
   width: 100%;
   height: 100%;
+  /* The container for all highlighters doesn't react to pointer-events by
+     default. This is because most highlighters cover the whole viewport but
+     don't contain UIs that need to be accessed.
+     If your highlighter has UI that needs to be interacted with, add
+     'pointer-events:auto;' on its container element. */
+  pointer-events: none;
 }
 
 :-moz-native-anonymous .highlighter-container [hidden] {
   display: none;
 }
 
 /* Box model highlighter */
 
@@ -176,16 +181,22 @@
 :-moz-native-anonymous .highlighted-rect {
   position: absolute;
   background: #80d4ff;
   opacity: 0.8;
 }
 
 /* Element geometry highlighter */
 
+:-moz-native-anonymous .geometry-editor-root {
+  /* The geometry editor can be interacted with, so it needs to react to
+     pointer events */
+  pointer-events: auto;
+}
+
 :-moz-native-anonymous .geometry-editor-offset-parent {
   stroke: #08c;
   shape-rendering: crispEdges;
   stroke-dasharray: 5 3;
   fill: transparent;
 }
 
 :-moz-native-anonymous .geometry-editor-current-node {
--- a/toolkit/devtools/server/actors/highlighter.js
+++ b/toolkit/devtools/server/actors/highlighter.js
@@ -120,16 +120,17 @@ let HighlighterActor = exports.Highlight
     this._inspector = inspector;
     this._walker = this._inspector.walker;
     this._tabActor = this._inspector.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,
@@ -174,16 +175,17 @@ let HighlighterActor = exports.Highlight
     protocol.Actor.prototype.destroy.call(this);
 
     this._destroyHighlighter();
     events.off(this._tabActor, "navigate", this._onNavigate);
     this._autohide = null;
     this._inspector = null;
     this._walker = null;
     this._tabActor = null;
+    this._layoutHelpers = null;
   },
 
   /**
    * Display the box model highlighting on a given NodeActor.
    * There is only one instance of the box model highlighter, so calling this
    * method several times won't display several highlighters, it will just move
    * the highlighter instance to these nodes.
    *
@@ -507,30 +509,36 @@ function CanvasFrameAnonymousContentHelp
   this.anonymousContentDocument = this.tabActor.window.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.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.anonymousContentDocument = null;
     this.anonymousContentGlobal = null;
+
+    this._removeAllListeners();
   },
 
   _insert: function() {
     // Re-insert the content node after page navigation only if the new page
     // isn't XUL.
     if (isXUL(this.tabActor)) {
       return;
     }
@@ -557,17 +565,19 @@ CanvasFrameAnonymousContentHelper.protot
     installHelperSheet(this.tabActor.window,
       "@import url('" + HIGHLIGHTER_STYLESHEET_URI + "');");
     let node = this.nodeBuilder();
     this._content = doc.insertAnonymousContent(node);
   },
 
   _onNavigate: function({isTopLevel}) {
     if (isTopLevel) {
+      this._removeAllListeners();
       this._insert();
+      this.anonymousContentDocument = this.tabActor.window.document;
     }
   },
 
   getTextContentForElement: function(id) {
     if (!this.content) {
       return null;
     }
     return this.content.getTextContentForElement(id);
@@ -593,24 +603,153 @@ CanvasFrameAnonymousContentHelper.protot
   },
 
   removeAttributeForElement: function(id, name) {
     if (this.content) {
       this.content.removeAttributeForElement(id, name);
     }
   },
 
+  /**
+   * Add an event listener to one of the elements inserted in the canvasFrame
+   * native anonymous container.
+   * Like other methods in this helper, this requires the ID of the element to
+   * be passed in.
+   *
+   * Note that if the content page navigates, the event listeners won't be
+   * added again.
+   *
+   * Also note that unlike traditional DOM events, the events handled by
+   * listeners added here will propagate through the document only through
+   * bubbling phase, so the useCapture parameter isn't supported.
+   * It is possible however to call e.stopPropagation() to stop the bubbling.
+   *
+   * IMPORTANT: the chrome-only canvasFrame insertion API takes great care of
+   * not leaking references to inserted elements to chrome JS code. That's
+   * because otherwise, chrome JS code could freely modify native anon elements
+   * inside the canvasFrame and probably change things that are assumed not to
+   * change by the C++ code managing this frame.
+   * See https://wiki.mozilla.org/DevTools/Highlighter#The_AnonymousContent_API
+   * Unfortunately, the inserted nodes are still available via
+   * event.originalTarget, and that's what the event handler here uses to check
+   * that the event actually occured on the right element, but that also means
+   * consumers of this code would be able to access the inserted elements.
+   * Therefore, the originalTarget property will be nullified before the event
+   * is passed to your handler.
+   *
+   * IMPL DETAIL: A single event listener is added per event types only, at
+   * browser level and if the event originalTarget is found to have the provided
+   * ID, the callback is executed (and then IDs of parent nodes of the
+   * originalTarget are checked too).
+   *
+   * @param {String} id
+   * @param {String} type
+   * @param {Function} handler
+   */
+  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);
+      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);
+  },
+
+  /**
+   * Remove an event listener from one of the elements inserted in the
+   * canvasFrame native anonymous container.
+   * @param {String} id
+   * @param {String} type
+   * @param {Function} handler
+   */
+  removeEventListenerForElement: function(id, type, handler) {
+    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);
+      target.removeEventListener(type, this, true);
+    }
+  },
+
+  handleEvent: function(event) {
+    let listeners = this.listeners.get(event.type);
+    if (!listeners) {
+      return;
+    }
+
+    // Hide the originalTarget property to avoid exposing references to native
+    // anonymous elements. See addEventListenerForElement's comment.
+    let isPropagationStopped = false;
+    let eventProxy = new Proxy(event, {
+      get: (obj, name) => {
+        if (name === "originalTarget") {
+          return null;
+        } else if (name === "stopPropagation") {
+          return () => {
+            isPropagationStopped = true;
+          };
+        } else {
+          return obj[name];
+        }
+      }
+    });
+
+    // Start at originalTarget, bubble through ancestors and call handlers when
+    // needed.
+    let node = event.originalTarget;
+    while (node) {
+      let handler = listeners.get(node.id);
+      if (handler) {
+        handler(eventProxy, node.id);
+        if (isPropagationStopped) {
+          break;
+        }
+      }
+      node = node.parentNode;
+    }
+  },
+
+  _removeAllListeners: function() {
+    if (this.tabActor) {
+      let target = getPageListenerTarget(this.tabActor);
+      for (let [type] of this.listeners) {
+        target.removeEventListener(type, this, true);
+      }
+    }
+    this.listeners.clear();
+  },
+
   getElement: function(id) {
     let self = this;
     return {
       getTextContent: () => self.getTextContentForElement(id),
       setTextContent: text => self.setTextContentForElement(id, text),
       setAttribute: (name, value) => self.setAttributeForElement(id, name, value),
       getAttribute: name => self.getAttributeForElement(id, name),
-      removeAttribute: name => self.removeAttributeForElement(id, name)
+      removeAttribute: name => self.removeAttributeForElement(id, name),
+      addEventListener: (type, handler) => {
+        return self.addEventListenerForElement(id, type, handler);
+      },
+      removeEventListener: (type, handler) => {
+        return self.removeEventListenerForElement(id, type, handler);
+      }
     };
   },
 
   get content() {
     if (!this._content || Cu.isDeadWrapper(this._content)) {
       return null;
     }
     return this._content;
@@ -899,21 +1038,16 @@ function BoxModelHighlighter(tabActor) {
   this._currentNode = null;
 }
 
 BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype, {
   typeName: "BoxModelHighlighter",
 
   ID_CLASS_PREFIX: "box-model-",
 
-  get zoom() {
-    return this.win.QueryInterface(Ci.nsIInterfaceRequestor)
-               .getInterface(Ci.nsIDOMWindowUtils).fullZoom;
-  },
-
   get currentNode() {
     return this._currentNode;
   },
 
   set currentNode(node) {
     this._currentNode = node;
     this._computedStyle = null;
   },
@@ -938,17 +1072,16 @@ BoxModelHighlighter.prototype = Heritage
 
     let svg = createSVGNode(this.win, {
       nodeType: "svg",
       parent: rootWrapper,
       attributes: {
         "id": "elements",
         "width": "100%",
         "height": "100%",
-        "style": "width:100%;height:100%;",
         "hidden": "true"
       },
       prefix: this.ID_CLASS_PREFIX
     });
 
     let regions = createSVGNode(this.win, {
       nodeType: "g",
       parent: svg,
@@ -1442,18 +1575,18 @@ BoxModelHighlighter.prototype = Heritage
     this._moveInfobar();
   },
 
   /**
    * Move the Infobar to the right place in the highlighter.
    */
   _moveInfobar: function() {
     let bounds = this._getOuterBounds();
-    let winHeight = this.win.innerHeight * this.zoom;
-    let winWidth = this.win.innerWidth * this.zoom;
+    let winHeight = this.win.innerHeight * LayoutHelpers.getCurrentZoom(this.win);
+    let winWidth = this.win.innerWidth * LayoutHelpers.getCurrentZoom(this.win);
 
     // Ensure that containerBottom and containerTop are at least zero to avoid
     // showing tooltips outside the viewport.
     let containerBottom = Math.max(0, bounds.bottom) + NODE_INFOBAR_ARROW_SIZE;
     let containerTop = Math.min(winHeight, bounds.top);
     let container = this.getElement("nodeinfobar-container");
 
     // Can the bar be above the node?
@@ -1721,29 +1854,29 @@ CssTransformHighlighter.prototype = Heri
 
   _showShapes: function() {
     this.getElement("elements").removeAttribute("hidden");
   }
 });
 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;
   this._highlighters = [];
 }
 
 SelectorHighlighter.prototype = {
   typeName: "SelectorHighlighter",
+
   /**
    * Show BoxModelHighlighter on each node that matches that provided selector.
    * @param {DOMNode} node A context node that is used to get the document on
    * which querySelectorAll should be executed. This node will NOT be
    * highlighted.
    * @param {Object} options Should at least contain the 'selector' option, a
    * string that will be used in querySelectorAll. On top of this, all of the
    * valid options to BoxModelHighlighter.show are also valid here.
@@ -1997,18 +2130,17 @@ GeometryEditorHighlighter.prototype = He
     });
 
     let svg = createSVGNode(this.win, {
       nodeType: "svg",
       parent: root,
       attributes: {
         "id": "elements",
         "width": "100%",
-        "height": "100%",
-        "style": "width:100%;height:100%;"
+        "height": "100%"
       },
       prefix: this.ID_CLASS_PREFIX
     });
 
     // Offset parent node highlighter.
     createSVGNode(this.win, {
       nodeType: "polygon",
       parent: svg,
--- a/toolkit/devtools/server/tests/browser/browser.ini
+++ b/toolkit/devtools/server/tests/browser/browser.ini
@@ -21,16 +21,22 @@ support-files =
 [browser_animation_actors_04.js]
 [browser_animation_actors_05.js]
 [browser_animation_actors_06.js]
 [browser_animation_actors_07.js]
 [browser_animation_actors_08.js]
 [browser_animation_actors_09.js]
 [browser_animation_actors_10.js]
 [browser_animation_actors_11.js]
+[browser_canvasframe_helper_01.js]
+[browser_canvasframe_helper_02.js]
+[browser_canvasframe_helper_03.js]
+[browser_canvasframe_helper_04.js]
+[browser_canvasframe_helper_05.js]
+[browser_canvasframe_helper_06.js]
 [browser_navigateEvents.js]
 [browser_storage_dynamic_windows.js]
 [browser_storage_listings.js]
 [browser_storage_updates.js]
 [browser_timeline.js]
 skip-if = buildapp == 'mulet'
 [browser_timeline_actors.js]
 skip-if = buildapp == 'mulet'
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_01.js
@@ -0,0 +1,82 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"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 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 = "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);
+
+  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,
+    "The helper has the setAttributeForElement method");
+  ok(helper.getAttributeForElement,
+    "The helper has the getAttributeForElement method");
+  ok(helper.removeAttributeForElement,
+    "The helper has the removeAttributeForElement method");
+  ok(helper.addEventListenerForElement,
+    "The helper has the addEventListenerForElement method");
+  ok(helper.removeEventListenerForElement,
+    "The helper has the removeEventListenerForElement method");
+  ok(helper.getElement,
+    "The helper has the getElement method");
+  ok(helper.scaleRootElement,
+    "The helper has the scaleRootElement method");
+
+  is(helper.getTextContentForElement("child-element"), "test element",
+    "The text content was retrieve correctly");
+  is(helper.getAttributeForElement("child-element", "id"), "child-element",
+    "The ID attribute was retrieve correctly");
+  is(helper.getAttributeForElement("child-element", "class"), "child-element",
+    "The class attribute was retrieve correctly");
+
+  let el = helper.getElement("child-element");
+  ok(el, "The DOMNode-like element was created");
+
+  is(el.getTextContent(), "test element",
+    "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();
+
+  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");
+
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_02.js
@@ -0,0 +1,38 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"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");
+
+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);
+
+  ok(!helper.content, "The AnonymousContent was not inserted in the window");
+  ok(!helper.getTextContentForElement("child-element"),
+    "No text content is returned");
+
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_03.js
@@ -0,0 +1,93 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"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 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 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 ++;
+  }
+  el.addEventListener("mousedown", onMouseDown);
+
+  info("Synthesizing an event on the inserted element");
+  let onDocMouseDown = once(doc, "mousedown");
+  synthesizeMouseDown(100, 100, doc.defaultView);
+  yield onDocMouseDown;
+
+  is(mouseDownHandled, 1, "The mousedown event was handled once on the element");
+
+  info("Synthesizing an event somewhere else");
+  onDocMouseDown = once(doc, "mousedown");
+  synthesizeMouseDown(400, 400, doc.defaultView);
+  yield onDocMouseDown;
+
+  is(mouseDownHandled, 1, "The mousedown event was not handled on the element");
+
+  info("Removing the event listener");
+  el.removeEventListener("mousedown", onMouseDown);
+
+  info("Synthesizing another event after the listener has been removed");
+  // Using a document event listener to know when the event has been synthesized.
+  onDocMouseDown = once(doc, "mousedown");
+  synthesizeMouseDown(100, 100, doc.defaultView);
+  yield onDocMouseDown;
+
+  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");
+  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;
+
+  is(mouseDownHandled, 1,
+    "The mousedown event hasn't been handled after the helper was destroyed");
+
+  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;
+  EventUtils.synthesizeMouseAtPoint(x, y, {type: "mousedown"}, win);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_04.js
@@ -0,0 +1,93 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"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 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);
+
+  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",
+    "The text content is correct before navigation");
+
+  info("Add an event listener on the element");
+  let mouseDownHandled = 0;
+  function onMouseDown(e, id) {
+    is(id, "child-element", "The mousedown event was triggered on the element");
+    mouseDownHandled ++;
+  }
+  el.addEventListener("mousedown", onMouseDown);
+
+  info("Synthesizing an event on the element");
+  let onDocMouseDown = once(doc, "mousedown");
+  synthesizeMouseDown(100, 100, doc.defaultView);
+  yield onDocMouseDown;
+  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");
+  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;
+  EventUtils.synthesizeMouseAtPoint(x, y, {type: "mousedown"}, win);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_05.js
@@ -0,0 +1,101 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"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 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 parent = doc.createElement("div");
+    parent.style = "pointer-events:auto;width:300px;height:300px;background:yellow;";
+    parent.id = "parent-element";
+    root.appendChild(parent);
+
+    let child = doc.createElement("div");
+    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);
+
+  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) {
+    mouseDownHandled.push(id);
+  }
+  parentEl.addEventListener("mousedown", onMouseDown);
+  childEl.addEventListener("mousedown", onMouseDown);
+
+  info("Synthesizing an event on the child element");
+  let onDocMouseDown = once(doc, "mousedown");
+  synthesizeMouseDown(100, 100, doc.defaultView);
+  yield onDocMouseDown;
+
+  is(mouseDownHandled.length, 2, "The mousedown event was handled twice");
+  is(mouseDownHandled[0], "child-element",
+    "The mousedown event was handled on the child element");
+  is(mouseDownHandled[1], "parent-element",
+    "The mousedown event was handled on the parent element");
+
+  info("Synthesizing an event on the parent, outside of the child element");
+  mouseDownHandled = [];
+  onDocMouseDown = once(doc, "mousedown");
+  synthesizeMouseDown(250, 250, doc.defaultView);
+  yield onDocMouseDown;
+
+  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", onMouseDown);
+  childEl.removeEventListener("mousedown", onMouseDown);
+
+  info("Adding an event listener on the parent element only");
+  mouseDownHandled = [];
+  parentEl.addEventListener("mousedown", onMouseDown);
+
+  info("Synthesizing an event on the child element");
+  onDocMouseDown = once(doc, "mousedown");
+  synthesizeMouseDown(100, 100, doc.defaultView);
+  yield onDocMouseDown;
+
+  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);
+
+  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;
+  EventUtils.synthesizeMouseAtPoint(x, y, {type: "mousedown"}, win);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_canvasframe_helper_06.js
@@ -0,0 +1,89 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"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 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 parent = doc.createElement("div");
+    parent.style = "pointer-events:auto;width:300px;height:300px;background:yellow;";
+    parent.id = "parent-element";
+    root.appendChild(parent);
+
+    let child = doc.createElement("div");
+    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);
+
+  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 onParentMouseDown(e, id) {
+    mouseDownHandled.push(id);
+  }
+  parentEl.addEventListener("mousedown", onParentMouseDown);
+
+  function onChildMouseDown(e, id) {
+    mouseDownHandled.push(id);
+    e.stopPropagation();
+  }
+  childEl.addEventListener("mousedown", onChildMouseDown);
+
+  info("Synthesizing an event on the child element");
+  let onDocMouseDown = once(doc, "mousedown");
+  synthesizeMouseDown(100, 100, doc.defaultView);
+  yield onDocMouseDown;
+
+  is(mouseDownHandled.length, 1, "The mousedown event was handled only once");
+  is(mouseDownHandled[0], "child-element",
+    "The mousedown event was handled on the child element");
+
+  info("Synthesizing an event on the parent, outside of the child element");
+  mouseDownHandled = [];
+  onDocMouseDown = once(doc, "mousedown");
+  synthesizeMouseDown(250, 250, doc.defaultView);
+  yield onDocMouseDown;
+
+  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);
+
+  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;
+  EventUtils.synthesizeMouseAtPoint(x, y, {type: "mousedown"}, win);
+}
--- a/toolkit/devtools/server/tests/browser/head.js
+++ b/toolkit/devtools/server/tests/browser/head.js
@@ -113,13 +113,27 @@ function once(target, eventName, useCapt
  * windows.
  */
 function forceCollections() {
   Cu.forceGC();
   Cu.forceCC();
   Cu.forceShrinkingGC();
 }
 
+/**
+ * Get a mock tabActor from a given window.
+ * This is sometimes useful to test actors or classes that use the tabActor in
+ * isolation.
+ * @param {DOMWindow} win
+ * @return {Object}
+ */
+function getMockTabActor(win) {
+  return {
+    window: win,
+    isRootActor: true
+  };
+}
+
 registerCleanupFunction(function tearDown() {
   while (gBrowser.tabs.length > 1) {
     gBrowser.removeCurrentTab();
   }
 });