Bug 1014547 - Add a css transform highlighter to the style-inspector; r=bgrins
authorPatrick Brosset <pbrosset@mozilla.com>
Fri, 13 Jun 2014 16:27:10 +0200
changeset 188725 141d06692291148a4f961042cade7f24bcb30239
parent 188724 9fd9c035a76ab6e1cc923d0f95e1855552173230
child 188726 e81fb1468aba8cc0bc991181cc41a6cbf94f9995
push id44892
push userkwierso@gmail.com
push dateSat, 14 Jun 2014 00:48:20 +0000
treeherdermozilla-inbound@e9f6e6ec3cde [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1014547
milestone33.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 1014547 - Add a css transform highlighter to the style-inspector; r=bgrins
browser/base/content/highlighter.css
browser/devtools/framework/toolbox-highlighter-utils.js
browser/devtools/framework/toolbox.js
browser/devtools/inspector/test/browser_inspector_highlighter.js
browser/devtools/inspector/test/browser_inspector_invalidate.js
browser/devtools/inspector/test/head.js
browser/devtools/layoutview/test/browser_editablemodel.js
browser/devtools/layoutview/test/browser_editablemodel_allproperties.js
browser/devtools/layoutview/test/browser_editablemodel_border.js
browser/devtools/layoutview/test/browser_editablemodel_stylerules.js
browser/devtools/markupview/markup-view.js
browser/devtools/shared/test/browser.ini
browser/devtools/shared/test/browser_csstransformpreview.js
browser/devtools/shared/test/browser_layoutHelpers-getBoxQuads.html
browser/devtools/shared/test/browser_layoutHelpers-getBoxQuads.js
browser/devtools/shared/test/browser_layoutHelpers.js
browser/devtools/shared/test/browser_templater_basic.js
browser/devtools/shared/test/browser_toolbar_basic.js
browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js
browser/devtools/shared/test/head.js
browser/devtools/shared/widgets/CSSTransformPreviewer.js
browser/devtools/shared/widgets/Tooltip.js
browser/devtools/styleinspector/computed-view.js
browser/devtools/styleinspector/rule-view.js
browser/devtools/styleinspector/test/browser.ini
browser/devtools/styleinspector/test/browser_ruleview_colorpicker-hides-on-tooltip.js
browser/devtools/styleinspector/test/browser_styleinspector_tooltip-size.js
browser/devtools/styleinspector/test/browser_styleinspector_tooltip-transform.js
browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-01.js
browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-02.js
browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-03.js
browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-04.js
browser/themes/shared/devtools/highlighter.inc.css
toolkit/devtools/LayoutHelpers.jsm
toolkit/devtools/server/actors/highlighter.js
toolkit/devtools/server/actors/inspector.js
toolkit/devtools/server/actors/root.js
toolkit/devtools/server/tests/mochitest/chrome.ini
toolkit/devtools/server/tests/mochitest/test_highlighter-csstransform_01.html
toolkit/devtools/server/tests/mochitest/test_highlighter-csstransform_02.html
toolkit/devtools/server/tests/mochitest/test_highlighter-csstransform_03.html
--- a/browser/base/content/highlighter.css
+++ b/browser/base/content/highlighter.css
@@ -66,8 +66,15 @@ html|*.highlighter-nodeinfobar-tagname {
 
 .highlighter-nodeinfobar-positioner[disabled] {
   visibility: hidden;
 }
 
 html|*.highlighter-nodeinfobar-tagname {
   text-transform: lowercase;
 }
+
+/*
+ * Css transform highlighter
+ */
+svg|svg.css-transform-root[hidden] {
+  display: none;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/framework/toolbox-highlighter-utils.js
@@ -0,0 +1,286 @@
+/* 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/. */
+
+"use strict";
+
+const {Cc, Ci, Cu} = require("chrome");
+const {Promise: promise} = require("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
+
+/**
+ * Client-side highlighter shared module.
+ * To be used by toolbox panels that need to highlight DOM elements.
+ *
+ * Highlighting and selecting elements is common enough that it needs to be at
+ * toolbox level, accessible by any panel that needs it.
+ * That's why the toolbox is the one that initializes the inspector and
+ * highlighter. It's also why the API returned by this module needs a reference
+ * to the toolbox which should be set once only.
+ */
+
+/**
+ * Get the highighterUtils instance for a given toolbox.
+ * This should be done once only by the toolbox itself and stored there so that
+ * panels can get it from there. That's because the API returned has a stateful
+ * scope that would be different for another instance returned by this function.
+ *
+ * @param {Toolbox} toolbox
+ * @return {Object} the highlighterUtils public API
+ */
+exports.getHighlighterUtils = function(toolbox) {
+  if (!toolbox || !toolbox.target) {
+    throw new Error("Missing or invalid toolbox passed to getHighlighterUtils");
+    return;
+  }
+
+  // Exported API properties will go here
+  let exported = {};
+
+  // The current toolbox target
+  let target = toolbox.target;
+
+  // Is the highlighter currently in pick mode
+  let isPicking = false;
+
+  /**
+   * Release this utils, nullifying the references to the toolbox
+   */
+  exported.release = function() {
+    toolbox = target = null;
+  }
+
+  /**
+   * Does the target have the highlighter actor.
+   * The devtools must be backwards compatible with at least B2G 1.3 (28),
+   * which doesn't have the highlighter actor. This can be removed as soon as
+   * the minimal supported version becomes 1.4 (29)
+   */
+  let isRemoteHighlightable = exported.isRemoteHighlightable = function() {
+    return target.client.traits.highlightable;
+  }
+
+  /**
+   * Does the target support custom highlighters.
+   */
+  let supportsCustomHighlighters = function() {
+    return !!target.client.traits.customHighlighters;
+  }
+
+  /**
+   * Is typeName a known custom highlighter
+   * @param {String} typeName
+   * @return {Boolean}
+   */
+  let hasCustomHighlighter = exported.hasCustomHighlighter = function(typeName) {
+    return supportsCustomHighlighters() &&
+           target.client.traits.customHighlighters.indexOf(typeName) !== -1;
+  }
+
+  /**
+   * Make a function that initializes the inspector before it runs.
+   * Since the init of the inspector is asynchronous, the return value will be
+   * produced by Task.async and the argument should be a generator
+   * @param {Function*} generator A generator function
+   * @return {Function} A function
+   */
+  let isInspectorInitialized = false;
+  let requireInspector = generator => {
+    return Task.async(function*(...args) {
+      if (!isInspectorInitialized) {
+        yield toolbox.initInspector();
+        isInspectorInitialized = true;
+      }
+      return yield generator.apply(null, args);
+    });
+  };
+
+  /**
+   * Start/stop the element picker on the debuggee target.
+   * @return A promise that resolves when done
+   */
+  let togglePicker = exported.togglePicker = function() {
+    if (isPicking) {
+      return stopPicker();
+    } else {
+      return startPicker();
+    }
+  }
+
+  /**
+   * Start the element picker on the debuggee target.
+   * This will request the inspector actor to start listening for mouse events
+   * on the target page to highlight the hovered/picked element.
+   * Depending on the server-side capabilities, this may fire events when nodes
+   * are hovered.
+   * @return A promise that resolves when the picker has started or immediately
+   * if it is already started
+   */
+  let startPicker = exported.startPicker = requireInspector(function*() {
+    if (isPicking) {
+      return;
+    }
+    isPicking = true;
+
+    toolbox.pickerButtonChecked = true;
+    yield toolbox.selectTool("inspector");
+    toolbox.on("select", stopPicker);
+
+    if (isRemoteHighlightable()) {
+      toolbox.walker.on("picker-node-hovered", onPickerNodeHovered);
+      toolbox.walker.on("picker-node-picked", onPickerNodePicked);
+
+      yield toolbox.highlighter.pick();
+      toolbox.emit("picker-started");
+    } else {
+      // If the target doesn't have the highlighter actor, we can use the
+      // walker's pick method instead, knowing that it only responds when a node
+      // is picked (instead of emitting events)
+      toolbox.emit("picker-started");
+      let node = yield toolbox.walker.pick();
+      onPickerNodePicked({node: node});
+    }
+  });
+
+  /**
+   * Stop the element picker. Note that the picker is automatically stopped when
+   * an element is picked
+   * @return A promise that resolves when the picker has stopped or immediately
+   * if it is already stopped
+   */
+  let stopPicker = exported.stopPicker = requireInspector(function*() {
+    if (!isPicking) {
+      return;
+    }
+    isPicking = false;
+
+    toolbox.pickerButtonChecked = false;
+
+    if (isRemoteHighlightable()) {
+      yield toolbox.highlighter.cancelPick();
+      toolbox.walker.off("picker-node-hovered", onPickerNodeHovered);
+      toolbox.walker.off("picker-node-picked", onPickerNodePicked);
+    } else {
+      // If the target doesn't have the highlighter actor, use the walker's
+      // cancelPick method instead
+      yield toolbox.walker.cancelPick();
+    }
+
+    toolbox.off("select", stopPicker);
+    toolbox.emit("picker-stopped");
+  });
+
+  /**
+   * When a node is hovered by the mouse when the highlighter is in picker mode
+   * @param {Object} data Information about the node being hovered
+   */
+  function onPickerNodeHovered(data) {
+    toolbox.emit("picker-node-hovered", data.node);
+  }
+
+  /**
+   * When a node has been picked while the highlighter is in picker mode
+   * @param {Object} data Information about the picked node
+   */
+  function onPickerNodePicked(data) {
+    toolbox.selection.setNodeFront(data.node, "picker-node-picked");
+    stopPicker();
+  }
+
+  /**
+   * Show the box model highlighter on a node in the content page.
+   * The node needs to be a NodeFront, as defined by the inspector actor
+   * @see toolkit/devtools/server/actors/inspector.js
+   * @param {NodeFront} nodeFront The node to highlight
+   * @param {Object} options
+   * @return A promise that resolves when the node has been highlighted
+   */
+  let highlightNodeFront = exported.highlightNodeFront = requireInspector(
+  function*(nodeFront, options={}) {
+    if (!nodeFront) {
+      return;
+    }
+
+    if (isRemoteHighlightable()) {
+      yield toolbox.highlighter.showBoxModel(nodeFront, options);
+    } else {
+      // If the target doesn't have the highlighter actor, revert to the
+      // walker's highlight method, which draws a simple outline
+      yield toolbox.walker.highlight(nodeFront);
+    }
+
+    toolbox.emit("node-highlight", nodeFront);
+  });
+
+  /**
+   * This is a convenience method in case you don't have a nodeFront but a
+   * valueGrip. This is often the case with VariablesView properties.
+   * This method will simply translate the grip into a nodeFront and call
+   * highlightNodeFront, so it has the same signature.
+   * @see highlightNodeFront
+   */
+  let highlightDomValueGrip = exported.highlightDomValueGrip = requireInspector(
+  function*(valueGrip, options={}) {
+    let nodeFront = yield gripToNodeFront(valueGrip);
+    if (nodeFront) {
+      yield highlightNodeFront(nodeFront, options);
+    } else {
+      throw new Error("The ValueGrip passed could not be translated to a NodeFront");
+    }
+  });
+
+  /**
+   * Translate a debugger value grip into a node front usable by the inspector
+   * @param {ValueGrip}
+   * @return a promise that resolves to the node front when done
+   */
+  let gripToNodeFront = exported.gripToNodeFront = requireInspector(
+  function*(grip) {
+    return yield toolbox.walker.getNodeActorFromObjectActor(grip.actor);
+  });
+
+  /**
+   * Hide the highlighter.
+   * @param {Boolean} forceHide Only really matters in test mode (when
+   * gDevTools.testing is true). In test mode, hovering over several nodes in
+   * the markup view doesn't hide/show the highlighter to ease testing. The
+   * highlighter stays visible at all times, except when the mouse leaves the
+   * markup view, which is when this param is passed to true
+   * @return a promise that resolves when the highlighter is hidden
+   */
+  let unhighlight = exported.unhighlight = Task.async(
+  function*(forceHide=false) {
+    forceHide = forceHide || !gDevTools.testing;
+
+    // Note that if isRemoteHighlightable is true, there's no need to hide the
+    // highlighter as the walker uses setTimeout to hide it after some time
+    if (forceHide && toolbox.highlighter && isRemoteHighlightable()) {
+      yield toolbox.highlighter.hideBoxModel();
+    }
+
+    toolbox.emit("node-unhighlight");
+  });
+
+  /**
+   * If the main, box-model, highlighter isn't enough, or if multiple
+   * highlighters are needed in parallel, this method can be used to return a
+   * new instance of a highlighter actor, given a type.
+   * The type of the highlighter passed must be known by the server.
+   * The highlighter actor returned will have the show(nodeFront) and hide()
+   * methods and needs to be released by the consumer when not needed anymore.
+   * @return a promise that resolves to the highlighter
+   */
+  let getHighlighterByType = exported.getHighlighterByType = requireInspector(
+  function*(typeName) {
+    if (hasCustomHighlighter(typeName)) {
+      return yield toolbox.inspector.getHighlighterByType(typeName);
+    } else {
+      throw "The target doesn't support creating highlighters by types or " +
+        typeName + " is unknown";
+    }
+  });
+
+  // Return the public API
+  return exported;
+};
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -8,16 +8,17 @@ const MAX_ORDINAL = 99;
 const ZOOM_PREF = "devtools.toolbox.zoomValue";
 const MIN_ZOOM = 0.5;
 const MAX_ZOOM = 2;
 
 let {Cc, Ci, Cu} = require("chrome");
 let {Promise: promise} = require("resource://gre/modules/Promise.jsm");
 let EventEmitter = require("devtools/toolkit/event-emitter");
 let Telemetry = require("devtools/shared/telemetry");
+let {getHighlighterUtils} = require("devtools/framework/toolbox-highlighter-utils");
 let HUDService = require("devtools/webconsole/hudservice");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/gDevTools.jsm");
 Cu.import("resource:///modules/devtools/scratchpad-manager.jsm");
 Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
@@ -63,17 +64,17 @@ function Toolbox(target, selectedTool, h
   this._toolPanels = new Map();
   this._telemetry = new Telemetry();
 
   this._toolRegistered = this._toolRegistered.bind(this);
   this._toolUnregistered = this._toolUnregistered.bind(this);
   this._refreshHostTitle = this._refreshHostTitle.bind(this);
   this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this)
   this.destroy = this.destroy.bind(this);
-  this.highlighterUtils = new ToolboxHighlighterUtils(this);
+  this.highlighterUtils = getHighlighterUtils(this);
   this._highlighterReady = this._highlighterReady.bind(this);
   this._highlighterHidden = this._highlighterHidden.bind(this);
 
   this._target.on("close", this.destroy);
 
   if (!hostType) {
     hostType = Services.prefs.getCharPref(this._prefs.LAST_HOST);
   }
@@ -182,23 +183,21 @@ Toolbox.prototype = {
    */
   get zoomValue() {
     return parseFloat(Services.prefs.getCharPref(ZOOM_PREF));
   },
 
   /**
    * Get the toolbox highlighter front. Note that it may not always have been
    * initialized first. Use `initInspector()` if needed.
+   * Consider using highlighterUtils instead, it exposes the highlighter API in
+   * a useful way for the toolbox panels
    */
   get highlighter() {
-    if (this.highlighterUtils.isRemoteHighlightable) {
-      return this._highlighter;
-    } else {
-      return null;
-    }
+    return this._highlighter;
   },
 
   /**
    * Get the toolbox's inspector front. Note that it may not always have been
    * initialized first. Use `initInspector()` if needed.
    */
   get inspector() {
     return this._inspector;
@@ -577,16 +576,28 @@ Toolbox.prototype = {
     let container = this.doc.querySelector("#toolbox-picker-container");
     container.appendChild(this._pickerButton);
 
     this._togglePicker = this.highlighterUtils.togglePicker.bind(this.highlighterUtils);
     this._pickerButton.addEventListener("command", this._togglePicker, false);
   },
 
   /**
+   * Setter for the checked state of the picker button in the toolbar
+   * @param {Boolean} isChecked
+   */
+  set pickerButtonChecked(isChecked) {
+    if (isChecked) {
+      this._pickerButton.setAttribute("checked", "true");
+    } else {
+      this._pickerButton.removeAttribute("checked");
+    }
+  },
+
+  /**
    * Return all toolbox buttons (command buttons, plus any others that were
    * added manually).
    */
   get toolboxButtons() {
     // White-list buttons that can be toggled to prevent adding prefs for
     // addons that have manually inserted toolbarbuttons into DOM.
     return [
       "command-button-pick",
@@ -1137,22 +1148,21 @@ Toolbox.prototype = {
    */
   initInspector: function() {
     if (!this._initInspector) {
       this._initInspector = Task.spawn(function*() {
         this._inspector = InspectorFront(this._target.client, this._target.form);
         this._walker = yield this._inspector.getWalker();
         this._selection = new Selection(this._walker);
 
-        if (this.highlighterUtils.isRemoteHighlightable) {
-          let autohide = !gDevTools.testing;
-
+        if (this.highlighterUtils.isRemoteHighlightable()) {
           this.walker.on("highlighter-ready", this._highlighterReady);
           this.walker.on("highlighter-hide", this._highlighterHidden);
 
+          let autohide = !gDevTools.testing;
           this._highlighter = yield this._inspector.getHighlighter(autohide);
         }
       }.bind(this));
     }
     return this._initInspector;
   },
 
   /**
@@ -1272,16 +1282,17 @@ Toolbox.prototype = {
       // This is done after other destruction tasks since it may tear down
       // fronts and the debugger transport which earlier destroy methods may
       // require to complete.
       if (!this._target) {
         return null;
       }
       let target = this._target;
       this._target = null;
+      this.highlighterUtils.release();
       target.off("close", this.destroy);
       return target.destroy();
     }).then(() => {
       this.emit("destroyed");
       // Free _host after the call to destroyed in order to let a chance
       // to destroyed listeners to still query toolbox attributes
       this._host = null;
       this._toolPanels.clear();
@@ -1289,207 +1300,10 @@ Toolbox.prototype = {
   },
 
   _highlighterReady: function() {
     this.emit("highlighter-ready");
   },
 
   _highlighterHidden: function() {
     this.emit("highlighter-hide");
-  },
-};
-
-/**
- * The ToolboxHighlighterUtils is what you should use for anything related to
- * node highlighting and picking.
- * It encapsulates the logic to connecting to the HighlighterActor.
- */
-function ToolboxHighlighterUtils(toolbox) {
-  this.toolbox = toolbox;
-  this._onPickerNodeHovered = this._onPickerNodeHovered.bind(this);
-  this._onPickerNodePicked = this._onPickerNodePicked.bind(this);
-  this.stopPicker = this.stopPicker.bind(this);
-}
-
-ToolboxHighlighterUtils.prototype = {
-  /**
-   * Indicates whether the highlighter actor exists on the server.
-   */
-  get isRemoteHighlightable() {
-    return this.toolbox._target.client.traits.highlightable;
-  },
-
-  /**
-   * Start/stop the element picker on the debuggee target.
-   */
-  togglePicker: function() {
-    if (this._isPicking) {
-      return this.stopPicker();
-    } else {
-      return this.startPicker();
-    }
-  },
-
-  _onPickerNodeHovered: function(res) {
-    this.toolbox.emit("picker-node-hovered", res.node);
-  },
-
-  _onPickerNodePicked: function(res) {
-    this.toolbox.selection.setNodeFront(res.node, "picker-node-picked");
-    this.stopPicker();
-  },
-
-  /**
-   * Start the element picker on the debuggee target.
-   * This will request the inspector actor to start listening for mouse/touch
-   * events on the target to highlight the hovered/picked element.
-   * Depending on the server-side capabilities, this may fire events when nodes
-   * are hovered.
-   * @return A promise that resolves when the picker has started or immediately
-   * if it is already started
-   */
-  startPicker: function() {
-    if (this._isPicking) {
-      return promise.resolve();
-    }
-
-    let deferred = promise.defer();
-
-    let done = () => {
-      this._isPicking = true;
-      this.toolbox.emit("picker-started");
-      this.toolbox.on("select", this.stopPicker);
-      deferred.resolve();
-    };
-
-    promise.all([
-      this.toolbox.initInspector(),
-      this.toolbox.selectTool("inspector")
-    ]).then(() => {
-      this.toolbox._pickerButton.setAttribute("checked", "true");
-
-      if (this.isRemoteHighlightable) {
-        this.toolbox.walker.on("picker-node-hovered", this._onPickerNodeHovered);
-        this.toolbox.walker.on("picker-node-picked", this._onPickerNodePicked);
-
-        this.toolbox.highlighter.pick().then(done);
-      } else {
-        return this.toolbox.walker.pick().then(node => {
-          this.toolbox.selection.setNodeFront(node, "picker-node-picked").then(() => {
-            this.stopPicker();
-            done();
-          });
-        });
-      }
-    });
-
-    return deferred.promise;
-  },
-
-  /**
-   * Stop the element picker
-   * @return A promise that resolves when the picker has stopped or immediately
-   * if it is already stopped
-   */
-  stopPicker: function() {
-    if (!this._isPicking) {
-      return promise.resolve();
-    }
-
-    let deferred = promise.defer();
-
-    let done = () => {
-      this.toolbox.emit("picker-stopped");
-      this.toolbox.off("select", this.stopPicker);
-      deferred.resolve();
-    };
-
-    this.toolbox.initInspector().then(() => {
-      this._isPicking = false;
-      this.toolbox._pickerButton.removeAttribute("checked");
-      if (this.isRemoteHighlightable) {
-        this.toolbox.highlighter.cancelPick().then(done);
-        this.toolbox.walker.off("picker-node-hovered", this._onPickerNodeHovered);
-        this.toolbox.walker.off("picker-node-picked", this._onPickerNodePicked);
-      } else {
-        this.toolbox.walker.cancelPick().then(done);
-      }
-    });
-
-    return deferred.promise;
-  },
-
-  /**
-   * Show the box model highlighter on a node, given its NodeFront (this type
-   * of front is normally returned by the WalkerActor).
-   * @return a promise that resolves to the nodeFront when the node has been
-   * highlit
-   */
-  highlightNodeFront: function(nodeFront, options={}) {
-    let deferred = promise.defer();
-
-    // If the remote highlighter exists on the target, use it
-    if (this.isRemoteHighlightable) {
-      this.toolbox.initInspector().then(() => {
-        this.toolbox.highlighter.showBoxModel(nodeFront, options).then(() => {
-          this.toolbox.emit("node-highlight", nodeFront);
-          deferred.resolve(nodeFront);
-        });
-      });
-    }
-    // Else, revert to the "older" version of the highlighter in the walker
-    // actor
-    else {
-      this.toolbox.walker.highlight(nodeFront).then(() => {
-        this.toolbox.emit("node-highlight", nodeFront);
-        deferred.resolve(nodeFront);
-      });
-    }
-
-    return deferred.promise;
-  },
-
-  /**
-   * This is a convenience method in case you don't have a nodeFront but a
-   * valueGrip. This is often the case with VariablesView properties.
-   * This method will simply translate the grip into a nodeFront and call
-   * highlightNodeFront
-   * @return a promise that resolves to the nodeFront when the node has been
-   * highlit
-   */
-  highlightDomValueGrip: function(valueGrip, options={}) {
-    return this._translateGripToNodeFront(valueGrip).then(nodeFront => {
-      if (nodeFront) {
-        return this.highlightNodeFront(nodeFront, options);
-      } else {
-        return promise.reject();
-      }
-    });
-  },
-
-  _translateGripToNodeFront: function(grip) {
-    return this.toolbox.initInspector().then(() => {
-      return this.toolbox.walker.getNodeActorFromObjectActor(grip.actor);
-    });
-  },
-
-  /**
-   * Hide the highlighter.
-   * @return a promise that resolves when the highlighter is hidden
-   */
-  unhighlight: function(forceHide=false) {
-    let unhighlightPromise;
-    forceHide = forceHide || !gDevTools.testing;
-
-    if (forceHide && this.isRemoteHighlightable && this.toolbox.highlighter) {
-      // If the remote highlighter exists on the target, use it
-      unhighlightPromise = this.toolbox.highlighter.hideBoxModel();
-    } else {
-      // If not, no need to unhighlight as the older highlight method uses a
-      // setTimeout to hide itself
-      unhighlightPromise = promise.resolve();
-    }
-
-    return unhighlightPromise.then(() => {
-      this.toolbox.emit("node-unhighlight");
-    });
   }
 };
--- a/browser/devtools/inspector/test/browser_inspector_highlighter.js
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter.js
@@ -37,39 +37,42 @@ function testMouseOverDivHighlights() {
   is(getHighlitNode(), div, "Highlighter's outline correspond to the non-rotated div");
   testNonTransformedBoxModelDimensionsNoZoom();
 }
 
 function testNonTransformedBoxModelDimensionsNoZoom() {
   info("Highlighted the non-rotated div");
   isNodeCorrectlyHighlighted(div, "non-zoomed");
 
-  inspector.toolbox.once("highlighter-ready", testNonTransformedBoxModelDimensionsZoomed);
+  waitForBoxModelUpdate().then(testNonTransformedBoxModelDimensionsZoomed);
   contentViewer = gBrowser.selectedBrowser.docShell.contentViewer
                           .QueryInterface(Ci.nsIMarkupDocumentViewer);
   contentViewer.fullZoom = 2;
 }
 
 function testNonTransformedBoxModelDimensionsZoomed() {
   info("Highlighted the zoomed, non-rotated div");
   isNodeCorrectlyHighlighted(div, "zoomed");
 
-  inspector.toolbox.once("highlighter-ready", testMouseOverRotatedHighlights);
+  waitForBoxModelUpdate().then(testMouseOverRotatedHighlights);
   contentViewer.fullZoom = 1;
 }
 
 function testMouseOverRotatedHighlights() {
-  inspector.toolbox.once("highlighter-ready", () => {
-    ok(isHighlighting(), "Highlighter is shown");
-    info("Highlighted the rotated div");
-    isNodeCorrectlyHighlighted(rotated, "rotated");
+  let onBoxModelUpdate = waitForBoxModelUpdate();
+  inspector.selection.setNode(rotated);
+  inspector.once("inspector-updated", () => {
+    onBoxModelUpdate.then(() => {
+      ok(isHighlighting(), "Highlighter is shown");
+      info("Highlighted the rotated div");
+      isNodeCorrectlyHighlighted(rotated, "rotated");
 
-    executeSoon(finishUp);
+      executeSoon(finishUp);
+    });
   });
-  inspector.selection.setNode(rotated);
 }
 
 function finishUp() {
   inspector.toolbox.highlighterUtils.stopPicker().then(() => {
     doc = div = rotated = inspector = contentViewer = null;
     let target = TargetFactory.forTab(gBrowser.selectedTab);
     gDevTools.closeToolbox(target);
     gBrowser.removeCurrentTab();
--- a/browser/devtools/inspector/test/browser_inspector_invalidate.js
+++ b/browser/devtools/inspector/test/browser_inspector_invalidate.js
@@ -13,24 +13,28 @@ function test() {
 
     openInspector(aInspector => {
       inspector = aInspector;
       inspector.toolbox.highlighter.showBoxModel(getNodeFront(div)).then(runTest);
     });
   }
 
   function runTest() {
+    info("Checking that the highlighter has the right size");
     let rect = getSimpleBorderRect();
     is(rect.width, 100, "outline has the right width");
 
+    waitForBoxModelUpdate().then(testRectWidth);
+
+    info("Changing the test element's size");
     div.style.width = "200px";
-    inspector.toolbox.once("highlighter-ready", testRectWidth);
   }
 
   function testRectWidth() {
+    info("Checking that the highlighter has the right size after update");
     let rect = getSimpleBorderRect();
     is(rect.width, 200, "outline updated");
     finishUp();
   }
 
   function finishUp() {
     inspector.toolbox.highlighter.hideBoxModel().then(() => {
       doc = div = inspector = null;
--- a/browser/devtools/inspector/test/head.js
+++ b/browser/devtools/inspector/test/head.js
@@ -304,16 +304,36 @@ function isRegionHidden(region) {
 }
 
 function isHighlighting()
 {
   let root = getBoxModelRoot();
   return !root.hasAttribute("hidden");
 }
 
+/**
+ * Observes mutation changes on the box-model highlighter and returns a promise
+ * that resolves when one of the attributes changes.
+ * If an attribute changes in the box-model, it means its position/dimensions
+ * got updated
+ */
+function waitForBoxModelUpdate() {
+  let def = promise.defer();
+
+  let root = getBoxModelRoot();
+  let polygon = root.querySelector(".box-model-content");
+  let observer = new polygon.ownerDocument.defaultView.MutationObserver(() => {
+    observer.disconnect();
+    def.resolve();
+  });
+  observer.observe(polygon, {attributes: true});
+
+  return def.promise;
+}
+
 function getHighlitNode()
 {
   if (isHighlighting()) {
     let helper = new LayoutHelpers(window.content);
     let points = getBoxModelStatus().content.points;
     let x = (points.p1.x + points.p2.x + points.p3.x + points.p4.x) / 4;
     let y = (points.p1.y + points.p2.y + points.p3.y + points.p4.y) / 4;
 
@@ -432,59 +452,25 @@ function isNodeCorrectlyHighlighted(node
   let boxModel = getBoxModelStatus();
   let helper = new LayoutHelpers(window.content);
 
   prefix += (prefix ? " " : "") + node.nodeName;
   prefix += (node.id ? "#" + node.id : "");
   prefix += (node.classList.length ? "." + [...node.classList].join(".") : "");
   prefix += " ";
 
-  let quads = helper.getAdjustedQuads(node, "content");
-  let {p1:cp1, p2:cp2, p3:cp3, p4:cp4} = boxModel.content.points;
-  is(cp1.x, quads.p1.x, prefix + "content point 1 x co-ordinate is correct");
-  is(cp1.y, quads.p1.y, prefix + "content point 1 y co-ordinate is correct");
-  is(cp2.x, quads.p2.x, prefix + "content point 2 x co-ordinate is correct");
-  is(cp2.y, quads.p2.y, prefix + "content point 2 y co-ordinate is correct");
-  is(cp3.x, quads.p3.x, prefix + "content point 3 x co-ordinate is correct");
-  is(cp3.y, quads.p3.y, prefix + "content point 3 y co-ordinate is correct");
-  is(cp4.x, quads.p4.x, prefix + "content point 4 x co-ordinate is correct");
-  is(cp4.y, quads.p4.y, prefix + "content point 4 y co-ordinate is correct");
-
-  quads = helper.getAdjustedQuads(node, "padding");
-  let {p1:pp1, p2:pp2, p3:pp3, p4:pp4} = boxModel.padding.points;
-  is(pp1.x, quads.p1.x, prefix + "padding point 1 x co-ordinate is correct");
-  is(pp1.y, quads.p1.y, prefix + "padding point 1 y co-ordinate is correct");
-  is(pp2.x, quads.p2.x, prefix + "padding point 2 x co-ordinate is correct");
-  is(pp2.y, quads.p2.y, prefix + "padding point 2 y co-ordinate is correct");
-  is(pp3.x, quads.p3.x, prefix + "padding point 3 x co-ordinate is correct");
-  is(pp3.y, quads.p3.y, prefix + "padding point 3 y co-ordinate is correct");
-  is(pp4.x, quads.p4.x, prefix + "padding point 4 x co-ordinate is correct");
-  is(pp4.y, quads.p4.y, prefix + "padding point 4 y co-ordinate is correct");
-
-  quads = helper.getAdjustedQuads(node, "border");
-  let {p1:bp1, p2:bp2, p3:bp3, p4:bp4} = boxModel.border.points;
-  is(bp1.x, quads.p1.x, prefix + "border point 1 x co-ordinate is correct");
-  is(bp1.y, quads.p1.y, prefix + "border point 1 y co-ordinate is correct");
-  is(bp2.x, quads.p2.x, prefix + "border point 2 x co-ordinate is correct");
-  is(bp2.y, quads.p2.y, prefix + "border point 2 y co-ordinate is correct");
-  is(bp3.x, quads.p3.x, prefix + "border point 3 x co-ordinate is correct");
-  is(bp3.y, quads.p3.y, prefix + "border point 3 y co-ordinate is correct");
-  is(bp4.x, quads.p4.x, prefix + "border point 4 x co-ordinate is correct");
-  is(bp4.y, quads.p4.y, prefix + "border point 4 y co-ordinate is correct");
-
-  quads = helper.getAdjustedQuads(node, "margin");
-  let {p1:mp1, p2:mp2, p3:mp3, p4:mp4} = boxModel.margin.points;
-  is(mp1.x, quads.p1.x, prefix + "margin point 1 x co-ordinate is correct");
-  is(mp1.y, quads.p1.y, prefix + "margin point 1 y co-ordinate is correct");
-  is(mp2.x, quads.p2.x, prefix + "margin point 2 x co-ordinate is correct");
-  is(mp2.y, quads.p2.y, prefix + "margin point 2 y co-ordinate is correct");
-  is(mp3.x, quads.p3.x, prefix + "margin point 3 x co-ordinate is correct");
-  is(mp3.y, quads.p3.y, prefix + "margin point 3 y co-ordinate is correct");
-  is(mp4.x, quads.p4.x, prefix + "margin point 4 x co-ordinate is correct");
-  is(mp4.y, quads.p4.y, prefix + "margin point 4 y co-ordinate is correct");
+  for (let boxType of ["content", "padding", "border", "margin"]) {
+    let quads = helper.getAdjustedQuads(node, boxType);
+    for (let point in boxModel[boxType].points) {
+      is(boxModel[boxType].points[point].x, quads[point].x,
+        prefix + boxType + " point " + point + " x coordinate is correct");
+      is(boxModel[boxType].points[point].y, quads[point].y,
+        prefix + boxType + " point " + point + " y coordinate is correct");
+    }
+  }
 }
 
 function getContainerForRawNode(markupView, rawNode)
 {
   let front = markupView.walker.frontForRawNode(rawNode);
   let container = markupView.getContainer(front);
   return container;
 }
--- a/browser/devtools/layoutview/test/browser_editablemodel.js
+++ b/browser/devtools/layoutview/test/browser_editablemodel.js
@@ -19,18 +19,17 @@ function getStyle(node, property) {
   return node.style.getPropertyValue(property);
 }
 
 let test = asyncTest(function*() {
   yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
   let {toolbox, inspector, view} = yield openLayoutView();
 
   yield runTests(inspector, view);
-  // TODO: Closing the toolbox in this test leaks - bug 994314
-  // yield destroyToolbox(inspector);
+  yield destroyToolbox(inspector);
 });
 
 addTest("Test that editing margin dynamically updates the document, pressing escape cancels the changes",
 function*(inspector, view) {
   let node = content.document.getElementById("div1");
   is(getStyle(node, "margin-top"), "", "Should be no margin-top on the element.")
   yield selectNode(node, inspector);
 
--- a/browser/devtools/layoutview/test/browser_editablemodel_allproperties.js
+++ b/browser/devtools/layoutview/test/browser_editablemodel_allproperties.js
@@ -18,18 +18,17 @@ function getStyle(node, property) {
   return node.style.getPropertyValue(property);
 }
 
 let test = asyncTest(function*() {
   yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
   let {toolbox, inspector, view} = yield openLayoutView();
 
   yield runTests(inspector, view);
-  // TODO: Closing the toolbox in this test leaks - bug 994314
-  // yield destroyToolbox(inspector);
+  yield destroyToolbox(inspector);
 });
 
 addTest("When all properties are set on the node editing one should work",
 function*(inspector, view) {
   let node = content.document.getElementById("div1");
 
   node.style.padding = "5px";
   yield waitForUpdate(inspector);
--- a/browser/devtools/layoutview/test/browser_editablemodel_border.js
+++ b/browser/devtools/layoutview/test/browser_editablemodel_border.js
@@ -18,18 +18,17 @@ function getStyle(node, property) {
   return node.style.getPropertyValue(property);
 }
 
 let test = asyncTest(function*() {
   yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
   let {toolbox, inspector, view} = yield openLayoutView();
 
   yield runTests(inspector, view);
-  // TODO: Closing the toolbox in this test leaks - bug 994314
-  // yield destroyToolbox(inspector);
+  yield destroyToolbox(inspector);
 });
 
 addTest("Test that adding a border applies a border style when necessary",
 function*(inspector, view) {
   let node = content.document.getElementById("div1");
   is(getStyle(node, "border-top-width"), "", "Should have the right border");
   is(getStyle(node, "border-top-style"), "", "Should have the right border");
   yield selectNode(node, inspector);
--- a/browser/devtools/layoutview/test/browser_editablemodel_stylerules.js
+++ b/browser/devtools/layoutview/test/browser_editablemodel_stylerules.js
@@ -19,18 +19,17 @@ function getStyle(node, property) {
   return node.style.getPropertyValue(property);
 }
 
 let test = asyncTest(function*() {
   yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
   let {toolbox, inspector, view} = yield openLayoutView();
 
   yield runTests(inspector, view);
-  // TODO: Closing the toolbox in this test leaks - bug 994314
-  // yield destroyToolbox(inspector);
+  yield destroyToolbox(inspector);
 });
 
 addTest("Test that entering units works",
 function*(inspector, view) {
   let node = content.document.getElementById("div1");
   is(getStyle(node, "padding-top"), "", "Should have the right padding");
   yield selectNode(node, inspector);
 
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -170,20 +170,36 @@ MarkupView.prototype = {
   _onMouseLeave: function() {
     this._hideBoxModel(true);
     if (this._hoveredNode) {
       this._containers.get(this._hoveredNode).hovered = false;
     }
     this._hoveredNode = null;
   },
 
+  /**
+   * Show the box model highlighter on a given node front
+   * @param {NodeFront} nodeFront The node to show the highlighter for
+   * @param {Object} options Options for the highlighter
+   * @return a promise that resolves when the highlighter for this nodeFront is
+   * shown, taking into account that there could already be highlighter requests
+   * queued up
+   */
   _showBoxModel: function(nodeFront, options={}) {
-    this._inspector.toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
+    return this._inspector.toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
   },
 
+  /**
+   * Hide the box model highlighter on a given node front
+   * @param {NodeFront} nodeFront The node to hide the highlighter for
+   * @param {Boolean} forceHide See toolbox-highlighter-utils/unhighlight
+   * @return a promise that resolves when the highlighter for this nodeFront is
+   * hidden, taking into account that there could already be highlighter requests
+   * queued up
+   */
   _hideBoxModel: function(forceHide) {
     return this._inspector.toolbox.highlighterUtils.unhighlight(forceHide);
   },
 
   _briefBoxModelTimer: null,
   _brieflyShowBoxModel: function(nodeFront, options) {
     let win = this._frame.contentWindow;
 
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -1,13 +1,14 @@
 [DEFAULT]
 skip-if = e10s # Bug ?????? - devtools tests disabled with e10s
 subsuite = devtools
 support-files =
   browser_layoutHelpers.html
+  browser_layoutHelpers-getBoxQuads.html
   browser_layoutHelpers_iframe.html
   browser_templater_basic.html
   browser_toolbar_basic.html
   browser_toolbar_webconsole_errors_count.html
   head.js
   leakhunt.js
 
 [browser_css_color.js]
@@ -17,16 +18,17 @@ support-files =
 [browser_graphs-04.js]
 [browser_graphs-05.js]
 [browser_graphs-06.js]
 [browser_graphs-07.js]
 [browser_graphs-08.js]
 [browser_graphs-09.js]
 [browser_graphs-10.js]
 [browser_layoutHelpers.js]
+[browser_layoutHelpers-getBoxQuads.js]
 [browser_observableobject.js]
 [browser_outputparser.js]
 [browser_require_basic.js]
 [browser_telemetry_button_paintflashing.js]
 [browser_telemetry_button_responsive.js]
 [browser_telemetry_button_scratchpad.js]
 [browser_telemetry_button_tilt.js]
 [browser_telemetry_sidebar.js]
@@ -44,9 +46,8 @@ support-files =
 [browser_toolbar_webconsole_errors_count.js]
 [browser_treeWidget_basic.js]
 [browser_treeWidget_keyboard_interaction.js]
 [browser_treeWidget_mouse_interaction.js]
 [browser_tableWidget_basic.js]
 [browser_tableWidget_keyboard_interaction.js]
 [browser_tableWidget_mouse_interaction.js]
 [browser_spectrum.js]
-[browser_csstransformpreview.js]
deleted file mode 100644
--- a/browser/devtools/shared/test/browser_csstransformpreview.js
+++ /dev/null
@@ -1,139 +0,0 @@
-/* vim: set ts=2 et sw=2 tw=80: */
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-// Tests that the spectrum color picker works correctly
-
-const TEST_URI = "data:text/html;charset=utf-8,<div></div>";
-const {CSSTransformPreviewer} = devtools.require("devtools/shared/widgets/CSSTransformPreviewer");
-
-let doc, root;
-
-function test() {
-  waitForExplicitFinish();
-  addTab(TEST_URI, () => {
-    doc = content.document;
-    root = doc.querySelector("div");
-    startTests();
-  });
-}
-
-function endTests() {
-  doc = root = null;
-  gBrowser.removeCurrentTab();
-  finish();
-}
-
-function startTests() {
-  testCreateAndDestroyShouldAppendAndRemoveElements();
-}
-
-function testCreateAndDestroyShouldAppendAndRemoveElements() {
-  ok(root, "We have the root node to append the preview to");
-  is(root.childElementCount, 0, "Root node is empty");
-
-  let p = new CSSTransformPreviewer(root);
-  p.preview("matrix(1, -0.2, 0, 1, 0, 0)");
-  ok(root.childElementCount > 0, "Preview has appended elements");
-  ok(root.querySelector("canvas"), "Canvas preview element is here");
-
-  p.destroy();
-  is(root.childElementCount, 0, "Destroying preview removed all nodes");
-
-  testCanvasDimensionIsConstrainedByMaxDim();
-}
-
-function testCanvasDimensionIsConstrainedByMaxDim() {
-  let p = new CSSTransformPreviewer(root);
-  p.MAX_DIM = 500;
-  p.preview("scale(1)", "center", 1000, 1000);
-
-  let canvas = root.querySelector("canvas");
-  is(canvas.width, 500, "Canvas width is correct");
-  is(canvas.height, 500, "Canvas height is correct");
-
-  p.destroy();
-
-  testCallingPreviewSeveralTimesReusesTheSameCanvas();
-}
-
-function testCallingPreviewSeveralTimesReusesTheSameCanvas() {
-  let p = new CSSTransformPreviewer(root);
-
-  p.preview("scale(1)", "center", 1000, 1000);
-  let canvas = root.querySelector("canvas");
-
-  p.preview("rotate(90deg)");
-  let canvases = root.querySelectorAll("canvas");
-  is(canvases.length, 1, "Still one canvas element");
-  is(canvases[0], canvas, "Still the same canvas element");
-  p.destroy();
-
-  testCanvasDimensionAreCorrect();
-}
-
-function testCanvasDimensionAreCorrect() {
-  // Only test a few simple transformations
-  let p = new CSSTransformPreviewer(root);
-
-  // Make sure we have a square
-  let w = 200, h = w;
-  p.MAX_DIM = w;
-
-  // We can't test the content of the canvas here, just that, given a max width
-  // the aspect ratio of the canvas seems correct.
-
-  // Translate a square by its width, should be a rectangle
-  p.preview("translateX(200px)", "center", w, h);
-  let canvas = root.querySelector("canvas");
-  is(canvas.width, w, "width is correct");
-  is(canvas.height, h/2, "height is half of the width");
-
-  // Rotate on the top right corner, should be a rectangle
-  p.preview("rotate(-90deg)", "top right", w, h);
-  is(canvas.width, w, "width is correct");
-  is(canvas.height, h/2, "height is half of the width");
-
-  // Rotate on the bottom left corner, should be a rectangle
-  p.preview("rotate(90deg)", "top right", w, h);
-  is(canvas.width, w/2, "width is half of the height");
-  is(canvas.height, h, "height is correct");
-
-  // Scale from center, should still be a square
-  p.preview("scale(2)", "center", w, h);
-  is(canvas.width, w, "width is correct");
-  is(canvas.height, h, "height is correct");
-
-  // Skew from center, 45deg, should be a rectangle
-  p.preview("skew(45deg)", "center", w, h);
-  is(canvas.width, w, "width is correct");
-  is(canvas.height, h/2, "height is half of the height");
-
-  p.destroy();
-
-  testPreviewingInvalidTransformReturnsFalse();
-}
-
-function testPreviewingInvalidTransformReturnsFalse() {
-  let p = new CSSTransformPreviewer(root);
-  ok(!p.preview("veryWow(muchPx) suchTransform(soDeg)"), "Returned false for invalid transform");
-  ok(!p.preview("rotae(3deg)"), "Returned false for invalid transform");
-
-  // Verify the canvas is empty by checking the image data
-  let canvas = root.querySelector("canvas"), ctx = canvas.getContext("2d");
-  let data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
-  for (let i = 0, n = data.length; i < n; i += 4) {
-    // Let's not log 250*250*4 asserts! Instead, just log when it fails
-    let red = data[i];
-    let green = data[i + 1];
-    let blue = data[i + 2];
-    let alpha = data[i + 3];
-    if (red !== 0 || green !== 0 || blue !== 0 || alpha !== 0) {
-      ok(false, "Image data is not empty after an invalid transformed was previewed");
-      break;
-    }
-  }
-
-  is(p.preview("translateX(30px)"), true, "Returned true for a valid transform");
-  endTests();
-}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_layoutHelpers-getBoxQuads.html
@@ -0,0 +1,62 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Layout Helpers</title>
+<style id="styles">
+  body {
+    margin: 0;
+    padding: 0;
+  }
+
+  #hidden-node {
+    display: none;
+  }
+
+  #simple-node-with-margin-padding-border {
+    width: 200px;
+    height: 200px;
+    background: #f06;
+
+    padding: 20px;
+    margin: 50px;
+    border: 10px solid black;
+  }
+
+  #scrolled-node {
+    position: absolute;
+    top: 0;
+    left: 0;
+
+    width: 300px;
+    height: 100px;
+    overflow: scroll;
+    background: linear-gradient(red, pink);
+  }
+
+  #sub-scrolled-node {
+    width: 200px;
+    height: 200px;
+    overflow: scroll;
+    background: linear-gradient(yellow, green);
+  }
+
+  #inner-scrolled-node {
+    width: 100px;
+    height: 400px;
+    background: linear-gradient(black, white);
+  }
+</style>
+<div id="hidden-node"></div>
+<div id="simple-node-with-margin-padding-border"></div>
+<!-- The inline encoded code below corresponds to:
+<iframe style="margin:10px;border:0;width:300px;height:300px;">
+  <iframe style="margin:10px;border:0;width:200px;height:200px;">
+    <div id="inner-node" style="width:100px;height:100px;border:10px solid red;margin:10px;padding:10px;"></div>
+  </iframe>
+</iframe>
+ -->
+<iframe src="data:text/html,%3Cstyle%3Ebody%7Bmargin:0;padding:0;%7D%3C/style%3E%3Ciframe%20src=%22data:text/html,%253Cstyle%253Ebody%257Bmargin:0;padding:0;%257D%253C/style%253E%253Cdiv%2520id='inner-node'%2520style='width:100px;height:100px;border:10px%2520solid%2520red;margin:10px;padding:10px;'%253E%253C/div%253E%22%20style=%22margin:10px;border:0;width:200px;height:200px;%22%3E%3C/iframe%3E" style="margin:10px;border:0;width:300px;height:300px;"></iframe>
+<div id="scrolled-node">
+  <div id="sub-scrolled-node">
+    <div id="inner-scrolled-node"></div>
+  </div>
+</div>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_layoutHelpers-getBoxQuads.js
@@ -0,0 +1,209 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that LayoutHelpers.getAdjustedQuads works properly in a variety of use
+// cases including iframes, scroll and zoom
+
+const {utils: Cu} = Components;
+const {LayoutHelpers} = Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm", {});
+
+const TEST_URI = TEST_URI_ROOT + "browser_layoutHelpers-getBoxQuads.html";
+
+function test() {
+  addTab(TEST_URI, function(browser, tab) {
+    let doc = browser.contentDocument;
+    let win = doc.defaultView;
+
+    info("Creating a new LayoutHelpers instance for the test window");
+    let helper = new LayoutHelpers(win);
+    ok(helper.getAdjustedQuads, "getAdjustedQuads is defined");
+
+    info("Running tests");
+
+    returnsTheRightDataStructure(doc, helper);
+    returnsNullForMissingNode(doc, helper);
+    returnsNullForHiddenNodes(doc, helper);
+    defaultsToBorderBoxIfNoneProvided(doc, helper);
+    returnsLikeGetBoxQuadsInSimpleCase(doc, helper);
+    takesIframesOffsetsIntoAccount(doc, helper);
+    takesScrollingIntoAccount(doc, helper);
+    takesZoomIntoAccount(doc, helper);
+
+    gBrowser.removeCurrentTab();
+    finish();
+  });
+}
+
+function returnsTheRightDataStructure(doc, helper) {
+  info("Checks that the returned data contains bounds and 4 points");
+
+  let node = doc.querySelector("body");
+  let res = helper.getAdjustedQuads(node, "content");
+
+  ok("bounds" in res, "The returned data has a bounds property");
+  ok("p1" in res, "The returned data has a p1 property");
+  ok("p2" in res, "The returned data has a p2 property");
+  ok("p3" in res, "The returned data has a p3 property");
+  ok("p4" in res, "The returned data has a p4 property");
+
+  for (let boundProp of
+    ["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
+    ok(boundProp in res.bounds, "The bounds has a " + boundProp + " property");
+  }
+
+  for (let point of ["p1", "p2", "p3", "p4"]) {
+    for (let pointProp of ["x", "y", "z", "w"]) {
+      ok(pointProp in res[point], point + " has a " + pointProp + " property");
+    }
+  }
+}
+
+function returnsNullForMissingNode(doc, helper) {
+  info("Checks that null is returned for invalid nodes");
+
+  for (let input of [null, undefined, "", 0]) {
+    ok(helper.getAdjustedQuads(input) === null, "null is returned for input " +
+      input);
+  }
+}
+
+function returnsNullForHiddenNodes(doc, helper) {
+  info("Checks that null is returned for nodes that aren't rendered");
+
+  let style = doc.querySelector("#styles");
+  ok(helper.getAdjustedQuads(style) === null,
+    "null is returned for a <style> node");
+
+  let hidden = doc.querySelector("#hidden-node");
+  ok(helper.getAdjustedQuads(hidden) === null,
+    "null is returned for a hidden node");
+}
+
+function defaultsToBorderBoxIfNoneProvided(doc, helper) {
+  info("Checks that if no boxtype is passed, then border is the default one");
+
+  let node = doc.querySelector("#simple-node-with-margin-padding-border");
+  let withBoxType = helper.getAdjustedQuads(node, "border");
+  let withoutBoxType = helper.getAdjustedQuads(node);
+
+  for (let boundProp of
+    ["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
+    is(withBoxType.bounds[boundProp], withoutBoxType.bounds[boundProp],
+      boundProp + " bound is equal with or without the border box type");
+  }
+
+  for (let point of ["p1", "p2", "p3", "p4"]) {
+    for (let pointProp of ["x", "y", "z", "w"]) {
+      is(withBoxType[point][pointProp], withoutBoxType[point][pointProp],
+        point + "." + pointProp +
+        " is equal with or without the border box type");
+    }
+  }
+}
+
+function returnsLikeGetBoxQuadsInSimpleCase(doc, helper) {
+  info("Checks that for an element in the main frame, without scroll nor zoom" +
+    "that the returned value is similar to the returned value of getBoxQuads");
+
+  let node = doc.querySelector("#simple-node-with-margin-padding-border");
+
+  for (let region of ["content", "padding", "border", "margin"]) {
+    let expected = node.getBoxQuads({
+      box: region
+    })[0];
+    let actual = helper.getAdjustedQuads(node, region);
+
+    for (let boundProp of
+      ["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
+      is(actual.bounds[boundProp], expected.bounds[boundProp],
+        boundProp + " bound is equal to the one returned by getBoxQuads for " +
+        region + " box");
+    }
+
+    for (let point of ["p1", "p2", "p3", "p4"]) {
+      for (let pointProp of ["x", "y", "z", "w"]) {
+        is(actual[point][pointProp], expected[point][pointProp],
+          point + "." + pointProp +
+          " is equal to the one returned by getBoxQuads for " + region + " box");
+      }
+    }
+  }
+}
+
+function takesIframesOffsetsIntoAccount(doc, helper) {
+  info("Checks that the quad returned for a node inside iframes that have " +
+    "margins takes those offsets into account");
+
+  let rootIframe = doc.querySelector("iframe");
+  let subIframe = rootIframe.contentDocument.querySelector("iframe");
+  let innerNode = subIframe.contentDocument.querySelector("#inner-node");
+
+  let quad = helper.getAdjustedQuads(innerNode, "content");
+
+  //rootIframe margin + subIframe margin + node margin + node border + node padding
+  let p1x = 10 + 10 + 10 + 10 + 10;
+  is(quad.p1.x, p1x, "The inner node's p1 x position is correct");
+
+  // Same as p1x + the inner node width
+  let p2x = p1x + 100;
+  is(quad.p2.x, p2x, "The inner node's p2 x position is correct");
+}
+
+function takesScrollingIntoAccount(doc, helper) {
+  info("Checks that the quad returned for a node inside multiple scrolled " +
+    "containers takes the scroll values into account");
+
+  // For info, the container being tested here is absolutely positioned at 0 0
+  // to simplify asserting the coordinates
+
+  info("Scroll the container nodes down");
+  let scrolledNode = doc.querySelector("#scrolled-node");
+  scrolledNode.scrollTop = 100;
+  let subScrolledNode = doc.querySelector("#sub-scrolled-node");
+  subScrolledNode.scrollTop = 200;
+  let innerNode = doc.querySelector("#inner-scrolled-node");
+
+  let quad = helper.getAdjustedQuads(innerNode, "content");
+  is(quad.p1.x, 0, "p1.x of the scrolled node is correct after scrolling down");
+  is(quad.p1.y, -300, "p1.y of the scrolled node is correct after scrolling down");
+
+  info("Scrolling back up");
+  scrolledNode.scrollTop = 0;
+  subScrolledNode.scrollTop = 0;
+
+  let quad = helper.getAdjustedQuads(innerNode, "content");
+  is(quad.p1.x, 0, "p1.x of the scrolled node is correct after scrolling up");
+  is(quad.p1.y, 0, "p1.y of the scrolled node is correct after scrolling up");
+}
+
+function takesZoomIntoAccount(doc, helper) {
+  info("Checks that if the page is zoomed in/out, the quad returned is correct");
+
+  // Hard-coding coordinates in this zoom test is a bad idea as it can vary
+  // depending on the platform, so we simply test that zooming in produces a
+  // bigger quad and zooming out produces a smaller quad
+
+  let node = doc.querySelector("#simple-node-with-margin-padding-border");
+  let defaultQuad = helper.getAdjustedQuads(node);
+
+  info("Zoom in");
+  window.FullZoom.enlarge();
+  let zoomedInQuad = helper.getAdjustedQuads(node);
+
+  ok(zoomedInQuad.bounds.width > defaultQuad.bounds.width,
+    "The zoomed in quad is bigger than the default one");
+  ok(zoomedInQuad.bounds.height > defaultQuad.bounds.height,
+    "The zoomed in quad is bigger than the default one");
+
+  info("Zoom out");
+  window.FullZoom.reset();
+  window.FullZoom.reduce();
+  let zoomedOutQuad = helper.getAdjustedQuads(node);
+
+  ok(zoomedOutQuad.bounds.width < defaultQuad.bounds.width,
+    "The zoomed out quad is smaller than the default one");
+  ok(zoomedOutQuad.bounds.height < defaultQuad.bounds.height,
+    "The zoomed out quad is smaller than the default one");
+
+  window.FullZoom.reset();
+}
--- a/browser/devtools/shared/test/browser_layoutHelpers.js
+++ b/browser/devtools/shared/test/browser_layoutHelpers.js
@@ -7,17 +7,17 @@ let imported = {};
 Components.utils.import("resource://gre/modules/devtools/LayoutHelpers.jsm",
     imported);
 registerCleanupFunction(function () {
   imported = {};
 });
 
 let LayoutHelpers = imported.LayoutHelpers;
 
-const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_layoutHelpers.html";
+const TEST_URI = TEST_URI_ROOT + "browser_layoutHelpers.html";
 
 function test() {
   addTab(TEST_URI, function(browser, tab) {
     info("Starting browser_layoutHelpers.js");
     let doc = browser.contentDocument;
     runTest(doc.defaultView, doc.getElementById('some'));
     gBrowser.removeCurrentTab();
     finish();
--- a/browser/devtools/shared/test/browser_templater_basic.js
+++ b/browser/devtools/shared/test/browser_templater_basic.js
@@ -7,17 +7,17 @@
  * These tests run both in Mozilla/Mochitest and plain browsers (as does
  * domtemplate)
  * We should endevour to keep the source in sync.
  */
 
 var promise = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {}).Promise;
 var template = Cu.import("resource://gre/modules/devtools/Templater.jsm", {}).template;
 
-const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_templater_basic.html";
+const TEST_URI = TEST_URI_ROOT + "browser_templater_basic.html";
 
 function test() {
   addTab(TEST_URI, function() {
     info("Starting DOM Templater Tests");
     runTest(0);
   });
 }
 
--- a/browser/devtools/shared/test/browser_toolbar_basic.js
+++ b/browser/devtools/shared/test/browser_toolbar_basic.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that the developer toolbar works properly
 
-const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_toolbar_basic.html";
+const TEST_URI = TEST_URI_ROOT + "browser_toolbar_basic.html";
 
 function test() {
   addTab(TEST_URI, function(browser, tab) {
     info("Starting browser_toolbar_basic.js");
     runTest();
   });
 }
 
--- a/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js
+++ b/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js
@@ -1,16 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that the developer toolbar errors count works properly.
 
 function test() {
-  const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/" +
-                   "browser_toolbar_webconsole_errors_count.html";
+  const TEST_URI = TEST_URI_ROOT + "browser_toolbar_webconsole_errors_count.html";
 
   let gDevTools = Cu.import("resource:///modules/devtools/gDevTools.jsm",
                              {}).gDevTools;
 
   let webconsole = document.getElementById("developer-toolbar-toolbox-button");
   let tab1, tab2;
 
   Services.prefs.setBoolPref("javascript.options.strict", true);
--- a/browser/devtools/shared/test/head.js
+++ b/browser/devtools/shared/test/head.js
@@ -6,16 +6,18 @@ let {devtools} = Cu.import("resource://g
 let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
 let TargetFactory = devtools.TargetFactory;
 
 gDevTools.testing = true;
 SimpleTest.registerCleanupFunction(() => {
   gDevTools.testing = false;
 });
 
+const TEST_URI_ROOT = "http://example.com/browser/browser/devtools/shared/test/";
+
 /**
  * Open a new tab at a URL and call a callback on load
  */
 function addTab(aURL, aCallback)
 {
   waitForExplicitFinish();
 
   gBrowser.selectedTab = gBrowser.addTab();
deleted file mode 100644
--- a/browser/devtools/shared/widgets/CSSTransformPreviewer.js
+++ /dev/null
@@ -1,389 +0,0 @@
-/* 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/. */
-
-"use strict";
-
-/**
- * The CSSTransformPreview module displays, using a <canvas> a rectangle, with
- * a given width and height and its transformed version, given a css transform
- * property and origin. It also displays arrows from/to each corner.
- *
- * It is useful to visualize how a css transform affected an element. It can
- * help debug tricky transformations. It is used today in a tooltip, and this
- * tooltip is shown when hovering over a css transform declaration in the rule
- * and computed view panels.
- *
- * TODO: For now, it multiplies matrices itself to calculate the coordinates of
- * the transformed box, but that should be removed as soon as we can get access
- * to getQuads().
- */
-
-const HTML_NS = "http://www.w3.org/1999/xhtml";
-
-/**
- * The TransformPreview needs an element to output a canvas tag.
- *
- * Usage example:
- *
- * let t = new CSSTransformPreviewer(myRootElement);
- * t.preview("rotate(45deg)", "top left", 200, 400);
- * t.preview("skew(19deg)", "center", 100, 500);
- * t.preview("matrix(1, -0.2, 0, 1, 0, 0)");
- * t.destroy();
- *
- * @param {nsIDOMElement} parentEl
- *        Where the canvas will go
- */
-function CSSTransformPreviewer(parentEl) {
-  this.parentEl = parentEl;
-  this.doc = this.parentEl.ownerDocument;
-  this.canvas = null;
-  this.ctx = null;
-}
-
-module.exports.CSSTransformPreviewer = CSSTransformPreviewer;
-
-CSSTransformPreviewer.prototype = {
-  /**
-   * The preview look-and-feel can be changed using these properties
-   */
-  MAX_DIM: 250,
-  PAD: 5,
-  ORIGINAL_FILL: "#1F303F",
-  ORIGINAL_STROKE: "#B2D8FF",
-  TRANSFORMED_FILL: "rgba(200, 200, 200, .5)",
-  TRANSFORMED_STROKE: "#B2D8FF",
-  ARROW_STROKE: "#329AFF",
-  ORIGIN_STROKE: "#329AFF",
-  ARROW_TIP_HEIGHT: 10,
-  ARROW_TIP_WIDTH: 8,
-  CORNER_SIZE_RATIO: 6,
-
-  /**
-   * Destroy removes the canvas from the parentelement passed in the constructor
-   */
-  destroy: function() {
-    if (this.canvas) {
-      this.parentEl.removeChild(this.canvas);
-    }
-    if (this._hiddenDiv) {
-      this.parentEl.removeChild(this._hiddenDiv);
-    }
-    this.parentEl = this.canvas = this.ctx = this.doc = null;
-  },
-
-  _createMarkup: function() {
-    this.canvas = this.doc.createElementNS(HTML_NS, "canvas");
-
-    this.canvas.setAttribute("id", "canvas");
-    this.canvas.setAttribute("width", this.MAX_DIM);
-    this.canvas.setAttribute("height", this.MAX_DIM);
-    this.canvas.style.position = "relative";
-    this.parentEl.appendChild(this.canvas);
-
-    this.ctx = this.canvas.getContext("2d");
-  },
-
-  _getComputed: function(name, value, width, height) {
-    if (!this._hiddenDiv) {
-      // Create a hidden element to apply the style to
-      this._hiddenDiv = this.doc.createElementNS(HTML_NS, "div");
-      this._hiddenDiv.style.visibility = "hidden";
-      this._hiddenDiv.style.position = "absolute";
-      this.parentEl.appendChild(this._hiddenDiv);
-    }
-
-    // Camelcase the name
-    name = name.replace(/-([a-z]{1})/g, (m, letter) => letter.toUpperCase());
-
-    // Apply width and height to make sure computation is made correctly
-    this._hiddenDiv.style.width = width + "px";
-    this._hiddenDiv.style.height = height + "px";
-
-    // Show the hidden div, apply the style, read the computed style, hide the
-    // hidden div again
-    this._hiddenDiv.style.display = "block";
-    this._hiddenDiv.style[name] = value;
-    let computed = this.doc.defaultView.getComputedStyle(this._hiddenDiv);
-    let computedValue = computed[name];
-    this._hiddenDiv.style.display = "none";
-
-    return computedValue;
-  },
-
-  _getMatrixFromTransformString: function(transformStr) {
-    let matrix = transformStr.substring(0, transformStr.length - 1).
-      substring(transformStr.indexOf("(") + 1).split(",");
-
-    matrix.forEach(function(value, index) {
-      matrix[index] = parseFloat(value, 10);
-    });
-
-    let transformMatrix = null;
-
-    if (matrix.length === 6) {
-      // 2d transform
-      transformMatrix = [
-        [matrix[0], matrix[2], matrix[4], 0],
-        [matrix[1], matrix[3], matrix[5], 0],
-        [0,     0,     1,     0],
-        [0,     0,     0,     1]
-      ];
-    } else {
-      // 3d transform
-      transformMatrix = [
-        [matrix[0], matrix[4], matrix[8],  matrix[12]],
-        [matrix[1], matrix[5], matrix[9],  matrix[13]],
-        [matrix[2], matrix[6], matrix[10], matrix[14]],
-        [matrix[3], matrix[7], matrix[11], matrix[15]]
-      ];
-    }
-
-    return transformMatrix;
-  },
-
-  _getOriginFromOriginString: function(originStr) {
-    let offsets = originStr.split(" ");
-    offsets.forEach(function(item, index) {
-      offsets[index] = parseInt(item, 10);
-    });
-
-    return offsets;
-  },
-
-  _multiply: function(m1, m2) {
-    let m = [];
-    for (let m1Line = 0; m1Line < m1.length; m1Line++) {
-      m[m1Line] = 0;
-      for (let m2Col = 0; m2Col < m2.length; m2Col++) {
-        m[m1Line] += m1[m1Line][m2Col] * m2[m2Col];
-      }
-    }
-    return [m[0], m[1]];
-  },
-
-  _getTransformedPoint: function(matrix, point, origin) {
-    let pointMatrix = [point[0] - origin[0], point[1] - origin[1], 1, 1];
-    return this._multiply(matrix, pointMatrix);
-  },
-
-  _getTransformedPoints: function(matrix, rect, origin) {
-    return rect.map(point => {
-      let tPoint = this._getTransformedPoint(matrix, [point[0], point[1]], origin);
-      return [tPoint[0] + origin[0], tPoint[1] + origin[1]];
-    });
-  },
-
-  /**
-   * For canvas to avoid anti-aliasing
-   */
-  _round: x => Math.round(x) + .5,
-
-  _drawShape: function(points, fillStyle, strokeStyle) {
-    this.ctx.save();
-
-    this.ctx.lineWidth = 1;
-    this.ctx.strokeStyle = strokeStyle;
-    this.ctx.fillStyle = fillStyle;
-
-    this.ctx.beginPath();
-    this.ctx.moveTo(this._round(points[0][0]), this._round(points[0][1]));
-    for (var i = 1; i < points.length; i++) {
-      this.ctx.lineTo(this._round(points[i][0]), this._round(points[i][1]));
-    }
-    this.ctx.lineTo(this._round(points[0][0]), this._round(points[0][1]));
-    this.ctx.fill();
-    this.ctx.stroke();
-
-    this.ctx.restore();
-  },
-
-  _drawArrow: function(x1, y1, x2, y2) {
-    // do not draw if the line is too small
-    if (Math.abs(x2-x1) < 20 && Math.abs(y2-y1) < 20) {
-      return;
-    }
-
-    this.ctx.save();
-
-    this.ctx.strokeStyle = this.ARROW_STROKE;
-    this.ctx.fillStyle = this.ARROW_STROKE;
-    this.ctx.lineWidth = 1;
-
-    this.ctx.beginPath();
-    this.ctx.moveTo(this._round(x1), this._round(y1));
-    this.ctx.lineTo(this._round(x2), this._round(y2));
-    this.ctx.stroke();
-
-    this.ctx.beginPath();
-    this.ctx.translate(x2, y2);
-    let radians = Math.atan((y1 - y2) / (x1 - x2));
-    radians += ((x1 >= x2) ? -90 : 90) * Math.PI / 180;
-    this.ctx.rotate(radians);
-    this.ctx.moveTo(0, 0);
-    this.ctx.lineTo(this.ARROW_TIP_WIDTH / 2, this.ARROW_TIP_HEIGHT);
-    this.ctx.lineTo(-this.ARROW_TIP_WIDTH / 2, this.ARROW_TIP_HEIGHT);
-    this.ctx.closePath();
-    this.ctx.fill();
-
-    this.ctx.restore();
-  },
-
-  _drawOrigin: function(x, y) {
-    this.ctx.save();
-
-    this.ctx.strokeStyle = this.ORIGIN_STROKE;
-    this.ctx.fillStyle = this.ORIGIN_STROKE;
-
-    this.ctx.beginPath();
-    this.ctx.arc(x, y, 4, 0, 2 * Math.PI, false);
-    this.ctx.stroke();
-    this.ctx.fill();
-
-    this.ctx.restore();
-  },
-
-  /**
-   * Computes the largest width and height of all the given shapes and changes
-   * all of the shapes' points (by reference) so they fit into the configured
-   * MAX_DIM - 2*PAD area.
-   * @return {Object} A {w, h} giving the size the canvas should be
-   */
-  _fitAllShapes: function(allShapes) {
-    let allXs = [], allYs = [];
-    for (let shape of allShapes) {
-      for (let point of shape) {
-        allXs.push(point[0]);
-        allYs.push(point[1]);
-      }
-    }
-    let minX = Math.min.apply(Math, allXs);
-    let maxX = Math.max.apply(Math, allXs);
-    let minY = Math.min.apply(Math, allYs);
-    let maxY = Math.max.apply(Math, allYs);
-
-    let spanX = maxX - minX;
-    let spanY = maxY - minY;
-    let isWide = spanX > spanY;
-
-    let cw = isWide ? this.MAX_DIM :
-      this.MAX_DIM * Math.min(spanX, spanY) / Math.max(spanX, spanY);
-    let ch = !isWide ? this.MAX_DIM :
-      this.MAX_DIM * Math.min(spanX, spanY) / Math.max(spanX, spanY);
-
-    let mapX = x => this.PAD + ((cw - 2 * this.PAD) / (maxX - minX)) * (x - minX);
-    let mapY = y => this.PAD + ((ch - 2 * this.PAD) / (maxY - minY)) * (y - minY);
-
-    for (let shape of allShapes) {
-      for (let point of shape) {
-        point[0] = mapX(point[0]);
-        point[1] = mapY(point[1]);
-      }
-    }
-
-    return {w: cw, h: ch};
-  },
-
-  _drawShapes: function(shape, corner, transformed, transformedCorner) {
-    this._drawOriginal(shape);
-    this._drawOriginalCorner(corner);
-    this._drawTransformed(transformed);
-    this._drawTransformedCorner(transformedCorner);
-  },
-
-  _drawOriginal: function(points) {
-    this._drawShape(points, this.ORIGINAL_FILL, this.ORIGINAL_STROKE);
-  },
-
-  _drawTransformed: function(points) {
-    this._drawShape(points, this.TRANSFORMED_FILL, this.TRANSFORMED_STROKE);
-  },
-
-  _drawOriginalCorner: function(points) {
-    this._drawShape(points, this.ORIGINAL_STROKE, this.ORIGINAL_STROKE);
-  },
-
-  _drawTransformedCorner: function(points) {
-    this._drawShape(points, this.TRANSFORMED_STROKE, this.TRANSFORMED_STROKE);
-  },
-
-  _drawArrows: function(shape, transformed) {
-    this._drawArrow(shape[0][0], shape[0][1], transformed[0][0], transformed[0][1]);
-    this._drawArrow(shape[1][0], shape[1][1], transformed[1][0], transformed[1][1]);
-    this._drawArrow(shape[2][0], shape[2][1], transformed[2][0], transformed[2][1]);
-    this._drawArrow(shape[3][0], shape[3][1], transformed[3][0], transformed[3][1]);
-  },
-
-  /**
-   * Draw a transform preview
-   *
-   * @param {String} transform
-   *        The css transform value as a string, as typed by the user, as long
-   *        as it can be computed by the browser
-   * @param {String} origin
-   *        Same as above for the transform-origin value. Defaults to "center"
-   * @param {Number} width
-   *        The width of the container. Defaults to 200
-   * @param {Number} height
-   *        The height of the container. Defaults to 200
-   * @return {Boolean} Whether or not the preview could be created. Will return
-   *         false for instance if the transform is invalid
-   */
-  preview: function(transform, origin="center", width=200, height=200) {
-    // Create/clear the canvas
-    if (!this.canvas) {
-      this._createMarkup();
-    }
-    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
-
-    // Get computed versions of transform and origin
-    transform = this._getComputed("transform", transform, width, height);
-    if (transform && transform !== "none") {
-      origin = this._getComputed("transform-origin", origin, width, height);
-
-      // Get the matrix, origin and width height data for the previewed element
-      let originData = this._getOriginFromOriginString(origin);
-      let matrixData = this._getMatrixFromTransformString(transform);
-
-      // Compute the original box rect and transformed box rect
-      let shapePoints = [
-        [0, 0],
-        [width, 0],
-        [width, height],
-        [0, height]
-      ];
-      let transformedPoints = this._getTransformedPoints(matrixData, shapePoints, originData);
-
-      // Do the same for the corner triangle shape
-      let cornerSize = Math.min(shapePoints[2][1] - shapePoints[1][1],
-        shapePoints[1][0] - shapePoints[0][0]) / this.CORNER_SIZE_RATIO;
-      let cornerPoints = [
-        [shapePoints[1][0], shapePoints[1][1]],
-        [shapePoints[1][0], shapePoints[1][1] + cornerSize],
-        [shapePoints[1][0] - cornerSize, shapePoints[1][1]]
-      ];
-      let transformedCornerPoints = this._getTransformedPoints(matrixData, cornerPoints, originData);
-
-      // Resize points to fit everything in the canvas
-      let {w, h} = this._fitAllShapes([
-        shapePoints,
-        transformedPoints,
-        cornerPoints,
-        transformedCornerPoints,
-        [originData]
-      ]);
-
-      this.canvas.setAttribute("width", w);
-      this.canvas.setAttribute("height", h);
-
-      this._drawShapes(shapePoints, cornerPoints, transformedPoints, transformedCornerPoints)
-      this._drawArrows(shapePoints, transformedPoints);
-      this._drawOrigin(originData[0], originData[1]);
-
-      return true;
-    } else {
-      return false;
-    }
-  }
-};
--- a/browser/devtools/shared/widgets/Tooltip.js
+++ b/browser/devtools/shared/widgets/Tooltip.js
@@ -7,17 +7,16 @@
 const {Cc, Cu, Ci} = require("chrome");
 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 const IOService = Cc["@mozilla.org/network/io-service;1"]
   .getService(Ci.nsIIOService);
 const {Spectrum} = require("devtools/shared/widgets/Spectrum");
 const EventEmitter = require("devtools/toolkit/event-emitter");
 const {colorUtils} = require("devtools/css-color");
 const Heritage = require("sdk/core/heritage");
-const {CSSTransformPreviewer} = require("devtools/shared/widgets/CSSTransformPreviewer");
 const {Eyedropper} = require("devtools/eyedropper/eyedropper");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "setNamedTimeout",
   "resource:///modules/devtools/ViewHelpers.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "clearNamedTimeout",
@@ -733,55 +732,16 @@ Tooltip.prototype = {
 
     // Put the iframe in the tooltip
     this.content = iframe;
 
     return def.promise;
   },
 
   /**
-   * Set the content of the tooltip to be the result of CSSTransformPreviewer.
-   * Meaning a canvas previewing a css transformation.
-   *
-   * @param {String} transform
-   *        The CSS transform value (e.g. "rotate(45deg) translateX(50px)")
-   * @param {PageStyleActor} pageStyle
-   *        An instance of the PageStyleActor that will be used to retrieve
-   *        computed styles
-   * @param {NodeActor} node
-   *        The NodeActor for the currently selected node
-   * @return A promise that resolves when the tooltip content is ready, or
-   *         rejects if no transform is provided or the transform is invalid
-   */
-  setCssTransformContent: Task.async(function*(transform, pageStyle, node) {
-    if (!transform) {
-      throw "Missing transform";
-    }
-
-    // Look into the computed styles to find the width and height and possibly
-    // the origin if it hadn't been provided
-    let styles = yield pageStyle.getComputed(node, {
-      filter: "user",
-      markMatched: false,
-      onlyMatched: false
-    });
-
-    let origin = styles["transform-origin"].value;
-    let width = parseInt(styles["width"].value);
-    let height = parseInt(styles["height"].value);
-
-    let root = this.doc.createElementNS(XHTML_NS, "div");
-    let previewer = new CSSTransformPreviewer(root);
-    this.content = root;
-    if (!previewer.preview(transform, origin, width, height)) {
-      throw "Invalid transform";
-    }
-  }),
-
-  /**
    * Set the content of the tooltip to display a font family preview.
    * This is based on Lea Verou's Dablet. See https://github.com/LeaVerou/dabblet
    * for more info.
    * @param {String} font The font family value.
    * @param {object} nodeFront
    *        The NodeActor that will used to retrieve the dataURL for the font
    *        family tooltip contents.
    * @return A promise that resolves when the font tooltip content is ready, or
--- a/browser/devtools/styleinspector/computed-view.js
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -21,16 +21,17 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/devtools/Templater.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                   "resource://gre/modules/PluralForm.jsm");
 
 const FILTER_CHANGED_TIMEOUT = 300;
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const TRANSFORM_HIGHLIGHTER_TYPE = "CssTransformHighlighter";
 
 /**
  * Helper for long-running processes that should yield occasionally to
  * the mainloop.
  *
  * @param {Window} aWin
  *        Timeouts will be set on this window when appropriate.
  * @param {Generator} aGenerator
@@ -182,16 +183,22 @@ function CssHtmlTree(aStyleInspector, aP
 
   // Properties preview tooltip
   this.tooltip = new Tooltip(this.styleInspector.inspector.panelDoc);
   this.tooltip.startTogglingOnHover(this.propertyContainer,
     this._onTooltipTargetHover.bind(this));
 
   this._buildContextMenu();
   this.createStyleViews();
+
+  // Initialize the css transform highlighter if the target supports it
+  let hUtils = this.styleInspector.inspector.toolbox.highlighterUtils;
+  if (hUtils.hasCustomHighlighter(TRANSFORM_HIGHLIGHTER_TYPE)) {
+    this._initTransformHighlighter();
+  }
 }
 
 /**
  * Memoized lookup of a l10n string from a string bundle.
  * @param {string} aName The key to lookup.
  * @returns A localized version of the given key.
  */
 CssHtmlTree.l10n = function CssHtmlTree_l10n(aName)
@@ -509,16 +516,75 @@ CssHtmlTree.prototype = {
    */
   focusWindow: function(aEvent)
   {
     let win = this.styleDocument.defaultView;
     win.focus();
   },
 
   /**
+   * Get the css transform highlighter front, initializing it if needed
+   * @param a promise that resolves to the highlighter
+   */
+  getTransformHighlighter: function() {
+    if (this.transformHighlighterPromise) {
+      return this.transformHighlighterPromise;
+    }
+
+    let utils = this.styleInspector.inspector.toolbox.highlighterUtils;
+    this.transformHighlighterPromise =
+      utils.getHighlighterByType(TRANSFORM_HIGHLIGHTER_TYPE).then(highlighter => {
+        this.transformHighlighter = highlighter;
+        return this.transformHighlighter;
+      });
+
+    return this.transformHighlighterPromise;
+  },
+
+  _initTransformHighlighter: function() {
+    this.isTransformHighlighterShown = false;
+
+    this._onMouseMove = this._onMouseMove.bind(this);
+    this._onMouseLeave = this._onMouseLeave.bind(this);
+
+    this.propertyContainer.addEventListener("mousemove", this._onMouseMove, false);
+    this.propertyContainer.addEventListener("mouseleave", this._onMouseLeave, false);
+  },
+
+  _onMouseMove: function(event) {
+    if (event.target === this._lastHovered) {
+      return;
+    }
+
+    if (this.isTransformHighlighterShown) {
+      this.isTransformHighlighterShown = false;
+      this.getTransformHighlighter().then(highlighter => highlighter.hide());
+    }
+
+    this._lastHovered = event.target;
+    if (this._lastHovered.classList.contains("property-value")) {
+      let propName = this._lastHovered.parentNode.querySelector(".property-name");
+
+      if (propName.textContent === "transform") {
+        this.isTransformHighlighterShown = true;
+        let node = this.styleInspector.inspector.selection.nodeFront;
+        this.getTransformHighlighter().then(highlighter => highlighter.show(node));
+      }
+    }
+  },
+
+  _onMouseLeave: function(event) {
+    this._lastHovered = null;
+    if (this.isTransformHighlighterShown) {
+      this.isTransformHighlighterShown = false;
+      this.getTransformHighlighter().then(highlighter => highlighter.hide());
+    }
+  },
+
+  /**
    * Executed by the tooltip when the pointer hovers over an element of the view.
    * Used to decide whether the tooltip should be shown or not and to actually
    * put content in it.
    * Checks if the hovered target is a css value we support tooltips for.
    */
   _onTooltipTargetHover: function(target)
   {
     let inspector = this.styleInspector.inspector;
@@ -533,22 +599,16 @@ CssHtmlTree.prototype = {
         return this.tooltip.setRelativeImageContent(uri, inspector.inspector, maxDim);
       }
     }
 
     if (target.classList.contains("property-value")) {
       let propValue = target;
       let propName = target.parentNode.querySelector(".property-name");
 
-      // Test for css transform
-      if (propName.textContent === "transform") {
-        return this.tooltip.setCssTransformContent(propValue.textContent,
-          this.pageStyle, this.viewedElement);
-      }
-
       // Test for font family
       if (propName.textContent === "font-family") {
         let prop = propValue.textContent.toLowerCase();
 
         if (prop !== "inherit" && prop !== "unset" && prop !== "initial") {
           return this.tooltip.setFontFamilyContent(propValue.textContent,
             inspector.selection.nodeFront);
         }
@@ -807,16 +867,26 @@ CssHtmlTree.prototype = {
       this._contextmenu = null;
     }
 
     this.popupNode = null;
 
     this.tooltip.stopTogglingOnHover(this.propertyContainer);
     this.tooltip.destroy();
 
+    if (this.transformHighlighter) {
+      this.transformHighlighter.finalize();
+      this.transformHighlighter = null;
+
+      this.propertyContainer.removeEventListener("mousemove", this._onMouseMove, false);
+      this.propertyContainer.removeEventListener("mouseleave", this._onMouseLeave, false);
+
+      this._lastHovered = null;
+    }
+
     // Remove bound listeners
     this.styleDocument.removeEventListener("contextmenu", this._onContextMenu);
     this.styleDocument.removeEventListener("copy", this._onCopy);
     this.styleDocument.removeEventListener("mousedown", this.focusWindow);
 
     // Nodes used in templating
     delete this.root;
     delete this.propertyContainer;
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -19,16 +19,17 @@ const {parseSingleValue, parseDeclaratio
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
 const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit";
+const TRANSFORM_HIGHLIGHTER_TYPE = "CssTransformHighlighter";
 
 /**
  * These regular expressions are adapted from firebug's css.js, and are
  * used to parse CSSStyleDeclaration's cssText attribute.
  */
 
 // Used to split on css line separators
 const CSS_LINE_RE = /(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g;
@@ -1025,17 +1026,16 @@ TextProperty.prototype = {
     this.updateEditor();
   },
 
   remove: function() {
     this.rule.removeProperty(this);
   }
 };
 
-
 /**
  * View hierarchy mostly follows the model hierarchy.
  *
  * CssRuleView:
  *   Owns an ElementStyle and creates a list of RuleEditors for its
  *    Rules.
  * RuleEditor:
  *   Owns a Rule object and creates a list of TextPropertyEditors
@@ -1106,16 +1106,22 @@ function CssRuleView(aInspector, aDoc, a
     this._onTooltipTargetHover.bind(this));
 
   // Also create a more complex tooltip for editing colors with the spectrum
   // color picker
   this.colorPicker = new SwatchColorPickerTooltip(this.inspector.panelDoc);
 
   this._buildContextMenu();
   this._showEmpty();
+
+  // Initialize the css transform highlighter if the target supports it
+  let hUtils = this.inspector.toolbox.highlighterUtils;
+  if (hUtils.hasCustomHighlighter(TRANSFORM_HIGHLIGHTER_TYPE)) {
+    this._initTransformHighlighter();
+  }
 }
 
 exports.CssRuleView = CssRuleView;
 
 CssRuleView.prototype = {
   // The element that we're inspecting.
   _viewedElement: null,
 
@@ -1155,30 +1161,84 @@ CssRuleView.prototype = {
       popupset = doc.createElementNS(XUL_NS, "popupset");
       doc.documentElement.appendChild(popupset);
     }
 
     popupset.appendChild(this._contextmenu);
   },
 
   /**
+   * Get the css transform highlighter front, initializing it if needed
+   * @param a promise that resolves to the highlighter
+   */
+  getTransformHighlighter: function() {
+    if (this.transformHighlighterPromise) {
+      return this.transformHighlighterPromise;
+    }
+
+    let utils = this.inspector.toolbox.highlighterUtils;
+    this.transformHighlighterPromise =
+    utils.getHighlighterByType(TRANSFORM_HIGHLIGHTER_TYPE).then(highlighter => {
+      this.transformHighlighter = highlighter;
+      return this.transformHighlighter;
+    });
+
+    return this.transformHighlighterPromise;
+  },
+
+  _initTransformHighlighter: function() {
+    this.isTransformHighlighterShown = false;
+
+    this._onMouseMove = this._onMouseMove.bind(this);
+    this._onMouseLeave = this._onMouseLeave.bind(this);
+
+    this.element.addEventListener("mousemove", this._onMouseMove, false);
+    this.element.addEventListener("mouseleave", this._onMouseLeave, false);
+  },
+
+  _onMouseMove: function(event) {
+    if (event.target === this._lastHovered) {
+      return;
+    }
+
+    if (this.isTransformHighlighterShown) {
+      this.isTransformHighlighterShown = false;
+      this.getTransformHighlighter().then(highlighter => highlighter.hide());
+    }
+
+    this._lastHovered = event.target;
+    let prop = event.target.textProperty;
+    let isHighlightable = prop && prop.name === "transform" &&
+                          prop.enabled && !prop.overridden &&
+                          !prop.rule.pseudoElement;
+
+    if (isHighlightable) {
+      this.isTransformHighlighterShown = true;
+      let node = this.inspector.selection.nodeFront;
+      this.getTransformHighlighter().then(highlighter => highlighter.show(node));
+    }
+  },
+
+  _onMouseLeave: function(event) {
+    this._lastHovered = null;
+    if (this.isTransformHighlighterShown) {
+      this.isTransformHighlighterShown = false;
+      this.getTransformHighlighter().then(highlighter => highlighter.hide());
+    }
+  },
+
+  /**
    * Which type of hover-tooltip should be shown for the given element?
-   * This depends on the element: does it contain an image URL, a CSS transform,
-   * a font-family, ...
+   * This depends on the element: does it contain a URL, a font-family, ...
    * @param {DOMNode} el The element to test
    * @return {String} The type of hover-tooltip
    */
   _getHoverTooltipTypeForTarget: function(el) {
     let prop = el.textProperty;
 
-    // Test for css transform
-    if (prop && prop.name === "transform") {
-      return "transform";
-    }
-
     // Test for image
     let isUrl = el.classList.contains("theme-link") &&
                 el.parentNode.classList.contains("ruleview-propertyvalue");
     if (this.inspector.hasUrlToImageDataResolver && isUrl) {
       return "image";
     }
 
     // Test for font-family
@@ -1213,20 +1273,16 @@ CssRuleView.prototype = {
       return false;
     }
 
     if (this.colorPicker.tooltip.isShown()) {
       this.colorPicker.revert();
       this.colorPicker.hide();
     }
 
-    if (tooltipType === "transform") {
-      return this.previewTooltip.setCssTransformContent(target.textProperty.value,
-        this.pageStyle, this._viewedElement);
-    }
     if (tooltipType === "image") {
       let prop = target.parentNode.textProperty;
       let dim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
       let uri = CssLogic.getBackgroundImageUriFromProperty(prop.value, prop.rule.domRule.href);
       return this.previewTooltip.setRelativeImageContent(uri, this.inspector.inspector, dim);
     }
     if (tooltipType === "font") {
       let prop = target.textContent.toLowerCase();
@@ -1460,16 +1516,26 @@ CssRuleView.prototype = {
 
     // We manage the popupNode ourselves so we also need to destroy it.
     this.doc.popupNode = null;
 
     this.previewTooltip.stopTogglingOnHover(this.element);
     this.previewTooltip.destroy();
     this.colorPicker.destroy();
 
+    if (this.transformHighlighter) {
+      this.transformHighlighter.finalize();
+      this.transformHighlighter = null;
+
+      this.element.removeEventListener("mousemove", this._onMouseMove, false);
+      this.element.removeEventListener("mouseleave", this._onMouseLeave, false);
+
+      this._lastHovered = null;
+    }
+
     if (this.element.parentNode) {
       this.element.parentNode.removeChild(this.element);
     }
 
     if (this.elementStyle) {
       this.elementStyle.destroy();
     }
 
--- a/browser/devtools/styleinspector/test/browser.ini
+++ b/browser/devtools/styleinspector/test/browser.ini
@@ -94,9 +94,12 @@ skip-if = os == "win" && debug # bug 963
 [browser_styleinspector_csslogic-specificity.js]
 [browser_styleinspector_inplace-editor.js]
 [browser_styleinspector_output-parser.js]
 [browser_styleinspector_tooltip-background-image.js]
 [browser_styleinspector_tooltip-closes-on-new-selection.js]
 [browser_styleinspector_tooltip-longhand-fontfamily.js]
 [browser_styleinspector_tooltip-shorthand-fontfamily.js]
 [browser_styleinspector_tooltip-size.js]
-[browser_styleinspector_tooltip-transform.js]
+[browser_styleinspector_transform-highlighter-01.js]
+[browser_styleinspector_transform-highlighter-02.js]
+[browser_styleinspector_transform-highlighter-03.js]
+[browser_styleinspector_transform-highlighter-04.js]
--- a/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-hides-on-tooltip.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-hides-on-tooltip.js
@@ -1,40 +1,37 @@
 /* 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 color picker tooltip hides when an image or transform
-// tooltip appears
+// Test that the color picker tooltip hides when an image tooltip appears
 
 const PAGE_CONTENT = [
   '<style type="text/css">',
   '  body {',
   '    color: red;',
   '    background-color: #ededed;',
   '    background-image: url(chrome://global/skin/icons/warning-64.png);',
   '    border: 2em solid rgba(120, 120, 120, .5);',
-  '    transform: skew(-16deg);',
   '  }',
   '</style>',
   'Testing the color picker tooltip!'
 ].join("\n");
 
 let test = asyncTest(function*() {
   yield addTab("data:text/html,rule view color picker tooltip test");
   content.document.body.innerHTML = PAGE_CONTENT;
   let {toolbox, inspector, view} = yield openRuleView();
 
   let swatch = getRuleViewProperty(view, "body", "color").valueSpan
     .querySelector(".ruleview-colorswatch");
 
   yield testColorPickerHidesWhenImageTooltipAppears(view, swatch);
-  yield testColorPickerHidesWhenTransformTooltipAppears(view, swatch);
 });
 
 function* testColorPickerHidesWhenImageTooltipAppears(view, swatch) {
   let bgImageSpan = getRuleViewProperty(view, "body", "background-image").valueSpan;
   let uriSpan = bgImageSpan.querySelector(".theme-link");
   let tooltip = view.colorPicker.tooltip;
 
   info("Showing the color picker tooltip by clicking on the color swatch");
@@ -44,25 +41,8 @@ function* testColorPickerHidesWhenImageT
 
   info("Now showing the image preview tooltip to hide the color picker");
   let onHidden = tooltip.once("hidden");
   yield assertHoverTooltipOn(view.previewTooltip, uriSpan);
   yield onHidden;
 
   ok(true, "The color picker closed when the image preview tooltip appeared");
 }
-
-function* testColorPickerHidesWhenTransformTooltipAppears(view, swatch) {
-  let transformSpan = getRuleViewProperty(view, "body", "transform").valueSpan;
-  let tooltip = view.colorPicker.tooltip;
-
-  info("Showing the color picker tooltip by clicking on the color swatch");
-  let onShown = tooltip.once("shown");
-  swatch.click();
-  yield onShown;
-
-  info("Now showing the transform preview tooltip to hide the color picker");
-  let onHidden = tooltip.once("hidden");
-  yield assertHoverTooltipOn(view.previewTooltip, transformSpan);
-  yield onHidden;
-
-  ok(true, "The color picker closed when the transform preview tooltip appeared");
-}
--- a/browser/devtools/styleinspector/test/browser_styleinspector_tooltip-size.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_tooltip-size.js
@@ -7,65 +7,32 @@
 // Checking tooltips dimensions, to make sure their big enough to display their
 // content
 
 const TEST_PAGE = [
   'data:text/html;charset=utf-8,',
   '<style type="text/css">',
   '  div {',
   '    width: 300px;height: 300px;border-radius: 50%;',
-  '    transform: skew(45deg);',
   '    background: red url(chrome://global/skin/icons/warning-64.png);',
   '  }',
   '</style>',
   '<div></div>'
 ].join("\n");
 
 let test = asyncTest(function*() {
   yield addTab(TEST_PAGE);
   let {toolbox, inspector, view} = yield openRuleView();
 
   yield selectNode("div", inspector);
 
-  yield testTransformDimension(view);
   yield testImageDimension(view);
   yield testPickerDimension(view);
 });
 
-function* testTransformDimension(ruleView) {
-  info("Testing css transform tooltip dimensions");
-
-  let tooltip = ruleView.previewTooltip;
-  let panel = tooltip.panel;
-  let {valueSpan} = getRuleViewProperty(ruleView, "div", "transform");
-
-  // Make sure there is a hover tooltip for this property, this also will fill
-  // the tooltip with its content
-  yield assertHoverTooltipOn(tooltip, valueSpan);
-
-  info("Showing the tooltip");
-  let onShown = tooltip.once("shown");
-  tooltip.show();
-  yield onShown;
-
-  // Let's not test for a specific size, but instead let's make sure it's at
-  // least as big as the preview canvas
-  let canvas = panel.querySelector("canvas");
-  let w = canvas.width;
-  let h = canvas.height;
-  let panelRect = panel.getBoundingClientRect();
-
-  ok(panelRect.width >= w, "The panel is wide enough to show the canvas");
-  ok(panelRect.height >= h, "The panel is high enough to show the canvas");
-
-  let onHidden = tooltip.once("hidden");
-  tooltip.hide();
-  yield onHidden;
-}
-
 function* testImageDimension(ruleView) {
   info("Testing background-image tooltip dimensions");
 
   let tooltip = ruleView.previewTooltip;
   let panel = tooltip.panel;
   let {valueSpan} = getRuleViewProperty(ruleView, "div", "background");
   let uriSpan = valueSpan.querySelector(".theme-link");
 
deleted file mode 100644
--- a/browser/devtools/styleinspector/test/browser_styleinspector_tooltip-transform.js
+++ /dev/null
@@ -1,87 +0,0 @@
-/* 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 css transform preview tooltip is shown on transform properties
-
-const PAGE_CONTENT = [
-  '<style type="text/css">',
-  '  #testElement {',
-  '    width: 500px;',
-  '    height: 300px;',
-  '    background: red;',
-  '    transform: skew(16deg);',
-  '  }',
-  '  .test-element {',
-  '    transform-origin: top left;',
-  '    transform: rotate(45deg);',
-  '  }',
-  '  div {',
-  '    transform: scaleX(1.5);',
-  '    transform-origin: bottom right;',
-  '  }',
-  '  [attr] {',
-  '  }',
-  '</style>',
-  '<div id="testElement" class="test-element" attr="value">transformed element</div>'
-].join("\n");
-
-let test = asyncTest(function*() {
-  yield addTab("data:text/html,rule view css transform tooltip test");
-  content.document.body.innerHTML = PAGE_CONTENT;
-  let {toolbox, inspector, view} = yield openRuleView();
-
-  info("Selecting the test node");
-  yield selectNode("#testElement", inspector);
-
-  info("Checking that transforms tooltips are shown in various rule-view properties");
-  for (let selector of [".test-element", "div", "#testElement"]) {
-    yield testTransformTooltipOnSelector(view, selector);
-  }
-
-  info("Checking that the transform tooltip doesn't appear for invalid transforms");
-  yield testTransformTooltipNotShownOnInvalidTransform(view);
-
-  info("Checking transforms in the computed-view");
-  let {view} = yield openComputedView();
-  yield testTransformTooltipOnComputedView(view);
-});
-
-function* testTransformTooltipOnSelector(view, selector) {
-  info("Testing that a transform tooltip appears on transform in " + selector);
-
-  let {valueSpan} = getRuleViewProperty(view, selector, "transform");
-  ok(valueSpan, "The transform property was found");
-  yield assertHoverTooltipOn(view.previewTooltip, valueSpan);
-
-  // The transform preview is canvas, so there's not much we can test, so for
-  // now, let's just be happy with the fact that the tooltips is shown!
-  ok(true, "Tooltip shown on the transform property in " + selector);
-}
-
-function* testTransformTooltipNotShownOnInvalidTransform(view) {
-  let ruleEditor;
-  for (let rule of view._elementStyle.rules) {
-    if (rule.matchedSelectors[0] === "[attr]") {
-      ruleEditor = rule.editor;
-    }
-  }
-  ruleEditor.addProperty("transform", "muchTransform(suchAngle)", "");
-
-  let {valueSpan} = getRuleViewProperty(view, "[attr]", "transform");
-  let isValid = yield isHoverTooltipTarget(view.previewTooltip, valueSpan);
-  ok(!isValid, "The tooltip did not appear on hover of an invalid transform value");
-}
-
-function* testTransformTooltipOnComputedView(view) {
-  info("Testing that a transform tooltip appears in the computed view too");
-
-  let {valueSpan} = getComputedViewProperty(view, "transform");
-  yield assertHoverTooltipOn(view.tooltip, valueSpan);
-
-  // The transform preview is canvas, so there's not much we can test, so for
-  // now, let's just be happy with the fact that the tooltips is shown!
-  ok(true, "Tooltip shown on the computed transform property");
-}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-01.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 css transform highlighter is created only when asked
+
+const PAGE_CONTENT = [
+  '<style type="text/css">',
+  '  body {',
+  '    transform: skew(16deg);',
+  '  }',
+  '</style>',
+  'Test the css transform highlighter'
+].join("\n");
+
+let test = asyncTest(function*() {
+  yield addTab("data:text/html," + PAGE_CONTENT);
+
+  let {view: rView} = yield openRuleView();
+
+  ok(!rView.transformHighlighter, "No highlighter exists in the rule-view");
+  let h = yield rView.getTransformHighlighter();
+  ok(rView.transformHighlighter, "The highlighter has been created in the rule-view");
+  is(h, rView.transformHighlighter, "The right highlighter has been created");
+  let h2 = yield rView.getTransformHighlighter();
+  is(h, h2, "The same instance of highlighter is returned everytime in the rule-view");
+
+  let {view: cView} = yield openComputedView();
+
+  ok(!cView.transformHighlighter, "No highlighter exists in the computed-view");
+  let h = yield cView.getTransformHighlighter();
+  ok(cView.transformHighlighter, "The highlighter has been created in the computed-view");
+  is(h, cView.transformHighlighter, "The right highlighter has been created");
+  let h2 = yield cView.getTransformHighlighter();
+  is(h, h2, "The same instance of highlighter is returned everytime in the computed-view");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-02.js
@@ -0,0 +1,54 @@
+/* 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 css transform highlighter is created when hovering over a
+// transform property
+
+const PAGE_CONTENT = [
+  '<style type="text/css">',
+  '  body {',
+  '    transform: skew(16deg);',
+  '    color: yellow;',
+  '  }',
+  '</style>',
+  'Test the css transform highlighter'
+].join("\n");
+
+let test = asyncTest(function*() {
+  yield addTab("data:text/html," + PAGE_CONTENT);
+
+  let {view: rView} = yield openRuleView();
+  ok(!rView.transformHighlighter, "No highlighter exists in the rule-view (1)");
+
+  info("Faking a mousemove on a non-transform property");
+  let {valueSpan} = getRuleViewProperty(rView, "body", "color");
+  rView._onMouseMove({target: valueSpan});
+  ok(!rView.transformHighlighter, "No highlighter exists in the rule-view (2)");
+  ok(!rView.transformHighlighterPromise, "No highlighter is being initialized");
+
+  info("Faking a mousemove on a transform property");
+  let {valueSpan} = getRuleViewProperty(rView, "body", "transform");
+  rView._onMouseMove({target: valueSpan});
+  ok(rView.transformHighlighterPromise, "The highlighter is being initialized");
+  let h = yield rView.transformHighlighterPromise;
+  is(h, rView.transformHighlighter, "The initialized highlighter is the right one");
+
+  let {view: cView} = yield openComputedView();
+  ok(!cView.transformHighlighter, "No highlighter exists in the computed-view (1)");
+
+  info("Faking a mousemove on a non-transform property");
+  let {valueSpan} = getComputedViewProperty(cView, "color");
+  cView._onMouseMove({target: valueSpan});
+  ok(!cView.transformHighlighter, "No highlighter exists in the computed-view (2)");
+  ok(!cView.transformHighlighterPromise, "No highlighter is being initialized");
+
+  info("Faking a mousemove on a transform property");
+  let {valueSpan} = getComputedViewProperty(cView, "transform");
+  cView._onMouseMove({target: valueSpan});
+  ok(cView.transformHighlighterPromise, "The highlighter is being initialized");
+  let h = yield cView.transformHighlighterPromise;
+  is(h, cView.transformHighlighter, "The initialized highlighter is the right one");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-03.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 that the css transform highlighter is shown when hovering over transform
+// properties
+
+// Note that in this test, we mock the highlighter front, merely testing the
+// behavior of the style-inspector UI for now
+
+const PAGE_CONTENT = [
+  '<style type="text/css">',
+  '  html {',
+  '    transform: scale(.9);',
+  '  }',
+  '  body {',
+  '    transform: skew(16deg);',
+  '    color: purple;',
+  '  }',
+  '</style>',
+  'Test the css transform highlighter'
+].join("\n");
+
+let test = asyncTest(function*() {
+  yield addTab("data:text/html," + PAGE_CONTENT);
+
+  let {inspector, view: rView} = yield openRuleView();
+
+  // Mock the highlighter front to get the reference of the NodeFront
+  let HighlighterFront = {
+    isShown: false,
+    nodeFront: null,
+    nbOfTimesShown: 0,
+    show: function(nodeFront) {
+      this.nodeFront = nodeFront;
+      this.isShown = true;
+      this.nbOfTimesShown ++;
+    },
+    hide: function() {
+      this.nodeFront = null;
+      this.isShown = false;
+    }
+  };
+
+  // Inject the mock highlighter in the rule-view
+  rView.transformHighlighterPromise = {
+    then: function(cb) {
+      cb(HighlighterFront);
+    }
+  };
+
+  let {valueSpan} = getRuleViewProperty(rView, "body", "transform");
+
+  info("Checking that the HighlighterFront's show/hide methods are called");
+  rView._onMouseMove({target: valueSpan});
+  ok(HighlighterFront.isShown, "The highlighter is shown");
+  rView._onMouseLeave();
+  ok(!HighlighterFront.isShown, "The highlighter is hidden");
+
+  info("Checking that hovering several times over the same property doesn't" +
+    " show the highlighter several times");
+  let nb = HighlighterFront.nbOfTimesShown;
+  rView._onMouseMove({target: valueSpan});
+  is(HighlighterFront.nbOfTimesShown, nb + 1, "The highlighter was shown once");
+  rView._onMouseMove({target: valueSpan});
+  rView._onMouseMove({target: valueSpan});
+  is(HighlighterFront.nbOfTimesShown, nb + 1,
+    "The highlighter was shown once, after several mousemove");
+
+  info("Checking that the right NodeFront reference is passed");
+  yield selectNode(content.document.documentElement, inspector);
+  let {valueSpan} = getRuleViewProperty(rView, "html", "transform");
+  rView._onMouseMove({target: valueSpan});
+  is(HighlighterFront.nodeFront.tagName, "HTML",
+    "The right NodeFront is passed to the highlighter (1)");
+
+  yield selectNode("body", inspector);
+  let {valueSpan} = getRuleViewProperty(rView, "body", "transform");
+  rView._onMouseMove({target: valueSpan});
+  is(HighlighterFront.nodeFront.tagName, "BODY",
+    "The right NodeFront is passed to the highlighter (2)");
+
+  info("Checking that the highlighter gets hidden when hovering a non-transform property");
+  let {valueSpan} = getRuleViewProperty(rView, "body", "color");
+  rView._onMouseMove({target: valueSpan});
+  ok(!HighlighterFront.isShown, "The highlighter is hidden");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-04.js
@@ -0,0 +1,58 @@
+/* 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 css transform highlighter is shown only when hovering over a
+// transform declaration that isn't overriden or disabled
+
+// Note that unlike the other browser_styleinspector_transform-highlighter-N.js
+// tests, this one only tests the rule-view as only this view features disabled
+// and overriden properties
+
+const PAGE_CONTENT = [
+  '<style type="text/css">',
+  '  div {',
+  '    background: purple;',
+  '    width:300px;height:300px;',
+  '    transform: rotate(16deg);',
+  '  }',
+  '  .test {',
+  '    transform: skew(25deg);',
+  '  }',
+  '</style>',
+  '<div class="test"></div>'
+].join("\n");
+
+let test = asyncTest(function*() {
+  yield addTab("data:text/html," + PAGE_CONTENT);
+
+  let {view: rView, inspector} = yield openRuleView();
+  yield selectNode(".test", inspector);
+
+  info("Faking a mousemove on the overriden property");
+  let {valueSpan} = getRuleViewProperty(rView, "div", "transform");
+  rView._onMouseMove({target: valueSpan});
+  ok(!rView.transformHighlighter, "No highlighter was created for the overriden property");
+  ok(!rView.transformHighlighterPromise, "And no highlighter is being initialized either");
+
+  info("Disabling the applied property");
+  let classRuleEditor = rView.element.children[1]._ruleEditor;
+  let propEditor = classRuleEditor.rule.textProps[0].editor;
+  propEditor.enable.click();
+  yield classRuleEditor.rule._applyingModifications;
+
+  info("Faking a mousemove on the disabled property");
+  let {valueSpan} = getRuleViewProperty(rView, ".test", "transform");
+  rView._onMouseMove({target: valueSpan});
+  ok(!rView.transformHighlighter, "No highlighter was created for the disabled property");
+  ok(!rView.transformHighlighterPromise, "And no highlighter is being initialized either");
+
+  info("Faking a mousemove on the now unoverriden property");
+  let {valueSpan} = getRuleViewProperty(rView, "div", "transform");
+  rView._onMouseMove({target: valueSpan});
+  ok(rView.transformHighlighterPromise, "The highlighter is being initialized now");
+  let h = yield rView.transformHighlighterPromise;
+  is(h, rView.transformHighlighter, "The initialized highlighter is the right one");
+});
--- a/browser/themes/shared/devtools/highlighter.inc.css
+++ b/browser/themes/shared/devtools/highlighter.inc.css
@@ -1,15 +1,16 @@
 %if 0
 /* 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/. */
 %endif
 
 /* Box model highlighter */
+
 svg|g.box-model-container {
   opacity: 0.4;
 }
 
 svg|polygon.box-model-content {
   fill: #80d4ff;
 }
 
@@ -102,8 +103,28 @@ html|*.highlighter-nodeinfobar-dimension
   margin-top: -8px;
   margin-bottom: 8px;
   background-image: linear-gradient(to bottom left, transparent 50%, hsl(210,2%,22%) 50%);
 }
 
 .highlighter-nodeinfobar-container[hide-arrow] > .highlighter-nodeinfobar {
   margin: 7px 0;
 }
+
+/* Css transform highlighter */
+
+svg|polygon.css-transform-transformed {
+  fill: #80d4ff;
+  opacity: 0.8;
+}
+
+svg|polygon.css-transform-untransformed {
+  fill: #66cc52;
+  opacity: 0.8;
+}
+
+svg|polygon.css-transform-transformed,
+svg|polygon.css-transform-untransformed,
+svg|line.css-transform-line {
+  stroke: #08C;
+  stroke-dasharray: 5 3;
+  stroke-width: 2;
+}
--- a/toolkit/devtools/LayoutHelpers.jsm
+++ b/toolkit/devtools/LayoutHelpers.jsm
@@ -20,37 +20,39 @@ this.LayoutHelpers = LayoutHelpers = fun
                                      .getInterface(Ci.nsIWebNavigation)
                                      .QueryInterface(Ci.nsIDocShell);
 };
 
 LayoutHelpers.prototype = {
 
   /**
    * Get box quads adjusted for iframes and zoom level.
-   *
+
    * @param {DOMNode} node
    *        The node for which we are to get the box model region quads
    * @param  {String} region
    *         The box model region to return:
    *         "content", "padding", "border" or "margin"
+   * @return {Object} An object that has the same structure as one quad returned
+   *         by getBoxQuads
    */
   getAdjustedQuads: function(node, region) {
-    if (!node) {
-      return;
+    if (!node || !node.getBoxQuads) {
+      return null;
     }
 
     let [quads] = node.getBoxQuads({
       box: region
     });
 
     if (!quads) {
-      return;
+      return null;
     }
 
-    let [xOffset, yOffset] = this._getNodeOffsets(node);
+    let [xOffset, yOffset] = this.getFrameOffsets(node);
     let scale = this.calculateScale(node);
 
     return {
       p1: {
         w: quads.p1.w * scale,
         x: quads.p1.x * scale + xOffset,
         y: quads.p1.y * scale + yOffset,
         z: quads.p1.z * scale
@@ -81,46 +83,53 @@ LayoutHelpers.prototype = {
         top: quads.bounds.top * scale + yOffset,
         width: quads.bounds.width * scale,
         x: quads.bounds.x * scale + xOffset,
         y: quads.bounds.y * scale + yOffset
       }
     };
   },
 
+  /**
+   * Get the current zoom factor applied to the container window of a given node
+   * @param {DOMNode}
+   *        The node for which the zoom factor should be calculated
+   * @return {Number}
+   */
   calculateScale: function(node) {
     let win = node.ownerDocument.defaultView;
     let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
                       .getInterface(Ci.nsIDOMWindowUtils);
     return winUtils.fullZoom;
   },
 
   /**
    * Compute the absolute position and the dimensions of a node, relativalely
    * to the root window.
    *
-   * @param nsIDOMNode aNode
+   * @param {DOMNode} aNode
    *        a DOM element to get the bounds for
-   * @param nsIWindow aContentWindow
+   * @param {DOMWindow} aContentWindow
    *        the content window holding the node
+   * @return {Object}
+   *         A rect object with the {top, left, width, height} properties
    */
-  getRect: function LH_getRect(aNode, aContentWindow) {
+  getRect: function(aNode, aContentWindow) {
     let frameWin = aNode.ownerDocument.defaultView;
     let clientRect = aNode.getBoundingClientRect();
 
     // Go up in the tree of frames to determine the correct rectangle.
     // clientRect is read-only, we need to be able to change properties.
     let rect = {top: clientRect.top + aContentWindow.pageYOffset,
             left: clientRect.left + aContentWindow.pageXOffset,
             width: clientRect.width,
             height: clientRect.height};
 
     // We iterate through all the parent windows.
     while (true) {
-
       // Are we in the top-level window?
       if (this.isTopLevelWindow(frameWin)) {
         break;
       }
 
       let frameElement = this.getFrameElement(frameWin);
       if (!frameElement) {
         break;
@@ -144,25 +153,25 @@ LayoutHelpers.prototype = {
   },
 
   /**
    * Returns iframe content offset (iframe border + padding).
    * Note: this function shouldn't need to exist, had the platform provided a
    * suitable API for determining the offset between the iframe's content and
    * its bounding client rect. Bug 626359 should provide us with such an API.
    *
-   * @param aIframe
+   * @param {DOMNode} aIframe
    *        The iframe.
-   * @returns array [offsetTop, offsetLeft]
-   *          offsetTop is the distance from the top of the iframe and the
-   *            top of the content document.
-   *          offsetLeft is the distance from the left of the iframe and the
-   *            left of the content document.
+   * @return {Array} [offsetTop, offsetLeft]
+   *         offsetTop is the distance from the top of the iframe and the top of
+   *         the content document.
+   *         offsetLeft is the distance from the left of the iframe and the left
+   *         of the content document.
    */
-  getIframeContentOffset: function LH_getIframeContentOffset(aIframe) {
+  getIframeContentOffset: function(aIframe) {
     let style = aIframe.contentWindow.getComputedStyle(aIframe, null);
 
     // In some cases, the computed style is null
     if (!style) {
       return [0, 0];
     }
 
     let paddingTop = parseInt(style.getPropertyValue("padding-top"));
@@ -173,22 +182,24 @@ LayoutHelpers.prototype = {
 
     return [borderTop + paddingTop, borderLeft + paddingLeft];
   },
 
   /**
    * Find an element from the given coordinates. This method descends through
    * frames to find the element the user clicked inside frames.
    *
-   * @param DOMDocument aDocument the document to look into.
-   * @param integer aX
-   * @param integer aY
-   * @returns Node|null the element node found at the given coordinates.
+   * @param {DOMDocument} aDocument the document to look into.
+   * @param {Number} aX
+   * @param {Number} aY
+   * @return {DOMNode}
+   *         the element node found at the given coordinates, or null if no node
+   *         was found
    */
-  getElementFromPoint: function LH_elementFromPoint(aDocument, aX, aY) {
+  getElementFromPoint: function(aDocument, aX, aY) {
     let node = aDocument.elementFromPoint(aX, aY);
     if (node && node.contentDocument) {
       if (node instanceof Ci.nsIDOMHTMLIFrameElement) {
         let rect = node.getBoundingClientRect();
 
         // Gap between the iframe and its content window.
         let [offsetTop, offsetLeft] = this.getIframeContentOffset(node);
 
@@ -209,20 +220,22 @@ LayoutHelpers.prototype = {
       }
     }
     return node;
   },
 
   /**
    * Scroll the document so that the element "elem" appears in the viewport.
    *
-   * @param Element elem the element that needs to appear in the viewport.
-   * @param bool centered true if you want it centered, false if you want it to
-   * appear on the top of the viewport. It is true by default, and that is
-   * usually what you want.
+   * @param {DOMNode} elem
+   *        The element that needs to appear in the viewport.
+   * @param {Boolean} centered
+   *        true if you want it centered, false if you want it to appear on the
+   *        top of the viewport. It is true by default, and that is usually what
+   *        you want.
    */
   scrollIntoViewIfNeeded: function(elem, centered) {
     // We want to default to centering the element in the page,
     // so as to keep the context of the element.
     centered = centered === undefined? true: !!centered;
 
     let win = elem.ownerDocument.defaultView;
     let clientRect = elem.getBoundingClientRect();
@@ -288,62 +301,71 @@ LayoutHelpers.prototype = {
       this.scrollIntoViewIfNeeded(frameElement, centered);
     }
   },
 
   /**
    * Check if a node and its document are still alive
    * and attached to the window.
    *
-   * @param aNode
+   * @param {DOMNode} aNode
+   * @return {Boolean}
    */
-  isNodeConnected: function LH_isNodeConnected(aNode)
-  {
+  isNodeConnected: function(aNode) {
     try {
       let connected = (aNode.ownerDocument && aNode.ownerDocument.defaultView &&
                       !(aNode.compareDocumentPosition(aNode.ownerDocument.documentElement) &
                       aNode.DOCUMENT_POSITION_DISCONNECTED));
       return connected;
     } catch (e) {
       // "can't access dead object" error
       return false;
     }
   },
 
   /**
    * like win.parent === win, but goes through mozbrowsers and mozapps iframes.
+   *
+   * @param {DOMWindow} win
+   * @return {Boolean}
    */
-  isTopLevelWindow: function LH_isTopLevelWindow(win) {
+  isTopLevelWindow: function(win) {
     let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.nsIWebNavigation)
                    .QueryInterface(Ci.nsIDocShell);
 
     return docShell === this._topDocShell;
   },
 
   /**
    * Check a window is part of the top level window.
+   *
+   * @param {DOMWindow} win
+   * @return {Boolean}
    */
   isIncludedInTopLevelWindow: function LH_isIncludedInTopLevelWindow(win) {
     if (this.isTopLevelWindow(win)) {
       return true;
     }
 
     let parent = this.getParentWindow(win);
     if (!parent || parent === win) {
       return false;
     }
 
     return this.isIncludedInTopLevelWindow(parent);
   },
 
   /**
    * like win.parent, but goes through mozbrowsers and mozapps iframes.
+   *
+   * @param {DOMWindow} win
+   * @return {DOMWindow}
    */
-  getParentWindow: function LH_getParentWindow(win) {
+  getParentWindow: function(win) {
     if (this.isTopLevelWindow(win)) {
       return null;
     }
 
     let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.nsIWebNavigation)
                    .QueryInterface(Ci.nsIDocShell);
 
@@ -353,38 +375,42 @@ LayoutHelpers.prototype = {
     } else {
       return win.parent;
     }
   },
 
   /**
    * like win.frameElement, but goes through mozbrowsers and mozapps iframes.
    *
-   * @param DOMWindow win The window to get the frame for
-   * @return DOMElement The element in which the window is embedded.
+   * @param {DOMWindow} win
+   *        The window to get the frame for
+   * @return {DOMNode}
+   *         The element in which the window is embedded.
    */
-  getFrameElement: function LH_getFrameElement(win) {
+  getFrameElement: function(win) {
     if (this.isTopLevelWindow(win)) {
       return null;
     }
 
     let winUtils = win.
       QueryInterface(Components.interfaces.nsIInterfaceRequestor).
       getInterface(Components.interfaces.nsIDOMWindowUtils);
 
     return winUtils.containerElement;
   },
 
   /**
-   * Get the x and y offsets for a node taking iframes into account.
+   * Get the x/y offsets for of all the parent frames of a given node
    *
    * @param {DOMNode} node
    *        The node for which we are to get the offset
+   * @return {Array}
+   *         The frame offset [x, y]
    */
-  _getNodeOffsets: function(node) {
+  getFrameOffsets: function(node) {
     let xOffset = 0;
     let yOffset = 0;
     let frameWin = node.ownerDocument.defaultView;
     let scale = this.calculateScale(node);
 
     while (true) {
       // Are we in the top-level window?
       if (this.isTopLevelWindow(frameWin)) {
@@ -407,9 +433,66 @@ LayoutHelpers.prototype = {
       xOffset += frameRect.left + offsetLeft;
       yOffset += frameRect.top + offsetTop;
 
       frameWin = this.getParentWindow(frameWin);
     }
 
     return [xOffset * scale, yOffset * scale];
   },
+
+  /**
+   * Get the 4 bounding points for a node taking iframes into account.
+   * Note that for transformed nodes, this will return the untransformed bound.
+   *
+   * @param {DOMNode} node
+   * @return {Object}
+   *         An object with p1,p2,p3,p4 properties being {x,y} objects
+   */
+  getNodeBounds: function(node) {
+    if (!node) {
+      return;
+    }
+
+    let scale = this.calculateScale(node);
+
+    // Find out the offset of the node in its current frame
+    let offsetLeft = 0;
+    let offsetTop = 0;
+    let el = node;
+    while (el && el.parentNode) {
+      offsetLeft += el.offsetLeft;
+      offsetTop += el.offsetTop;
+      el = el.offsetParent;
+    }
+
+    // Also take scrolled containers into account
+    let el = node;
+    while (el && el.parentNode) {
+      if (el.scrollTop) {
+        offsetTop -= el.scrollTop;
+      }
+      if (el.scrollLeft) {
+        offsetLeft -= el.scrollLeft;
+      }
+      el = el.parentNode;
+    }
+
+    // And add the potential frame offset if the node is nested
+    let [xOffset, yOffset] = this.getFrameOffsets(node);
+    xOffset += offsetLeft;
+    yOffset += offsetTop;
+
+    xOffset *= scale;
+    yOffset *= scale;
+
+    // Get the width and height
+    let width = node.offsetWidth * scale;
+    let height = node.offsetHeight * scale;
+
+    return {
+      p1: {x: xOffset, y: yOffset},
+      p2: {x: xOffset + width, y: yOffset},
+      p3: {x: xOffset + width, y: yOffset + height},
+      p4: {x: xOffset, y: yOffset + height}
+    };
+  }
 };
--- a/toolkit/devtools/server/actors/highlighter.js
+++ b/toolkit/devtools/server/actors/highlighter.js
@@ -4,16 +4,17 @@
 
 "use strict";
 
 const {Cu, Cc, Ci} = require("chrome");
 const Services = require("Services");
 const protocol = require("devtools/server/protocol");
 const {Arg, Option, method} = protocol;
 const events = require("sdk/event/core");
+const Heritage = require("sdk/core/heritage");
 
 const EventEmitter = require("devtools/toolkit/event-emitter");
 const GUIDE_STROKE_WIDTH = 1;
 
 // Make sure the domnode type is known here
 require("devtools/server/actors/inspector");
 
 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
@@ -23,66 +24,78 @@ Cu.import("resource://gre/modules/XPCOMU
 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
 const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted";
 let HELPER_SHEET = ".__fx-devtools-hide-shortcut__ { visibility: hidden !important } ";
 HELPER_SHEET += ":-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px!important } ";
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
 const SVG_NS = "http://www.w3.org/2000/svg";
 const HIGHLIGHTER_PICKED_TIMER = 1000;
 const INFO_BAR_OFFSET = 5;
+// The minimum distance a line should be before it has an arrow marker-end
+const ARROW_LINE_MIN_DISTANCE = 10;
+
+// All possible highlighter classes
+let HIGHLIGHTER_CLASSES = exports.HIGHLIGHTER_CLASSES = {
+  "BoxModelHighlighter": BoxModelHighlighter,
+  "CssTransformHighlighter": CssTransformHighlighter
+};
 
 /**
- * The HighlighterActor is the server-side entry points for any tool that wishes
- * to highlight elements in the content document.
+ * The Highlighter is the server-side entry points for any tool that wishes to
+ * highlight elements in some way in the content document.
  *
- * The highlighter can be retrieved via the inspector's getHighlighter method.
+ * A little bit of vocabulary:
+ * - <something>HighlighterActor classes are the actors that can be used from
+ *   the client. They do very little else than instantiate a given
+ *   <something>Highlighter and use it to highlight elements.
+ * - <something>Highlighter classes aren't actors, they're just JS classes that
+ *   know how to create and attach the actual highlighter elements on top of the
+ *   content
+ *
+ * The most used highlighter actor is the HighlighterActor which can be
+ * conveniently retrieved via the InspectorActor's 'getHighlighter' method.
+ * The InspectorActor will always return the same instance of
+ * HighlighterActor if asked several times and this instance is used in the
+ * toolbox to highlighter elements's box-model from the markup-view, layout-view,
+ * console, debugger, ... as well as select elements with the pointer (pick).
+ *
+ * Other types of highlighter actors exist and can be accessed via the
+ * InspectorActor's 'getHighlighterByType' method.
  */
 
 /**
  * The HighlighterActor class
  */
-let HighlighterActor = protocol.ActorClass({
+let HighlighterActor = exports.HighlighterActor = protocol.ActorClass({
   typeName: "highlighter",
 
   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._highlighterReady = this._highlighterReady.bind(this);
     this._highlighterHidden = this._highlighterHidden.bind(this);
 
-    if (this._supportsBoxModelHighlighter()) {
+    if (supportXULBasedHighlighter(this._tabActor)) {
       this._boxModelHighlighter =
         new BoxModelHighlighter(this._tabActor, this._inspector);
 
         this._boxModelHighlighter.on("ready", this._highlighterReady);
         this._boxModelHighlighter.on("hide", this._highlighterHidden);
     } else {
       this._boxModelHighlighter = new SimpleOutlineHighlighter(this._tabActor);
     }
   },
 
   get conn() this._inspector && this._inspector.conn,
 
-  /**
-   * Can the host support the box model highlighter which requires a parent
-   * XUL node to attach itself.
-   */
-  _supportsBoxModelHighlighter: function() {
-    // Note that <browser>s on Fennec also have a XUL parentNode but the box
-    // model highlighter doesn't display correctly on Fennec (bug 993190)
-    return this._tabActor.browser &&
-           !!this._tabActor.browser.parentNode &&
-           Services.appinfo.ID !== "{aa3c5121-dab2-40e2-81ca-7ea25febc110}";
-  },
-
   destroy: function() {
     protocol.Actor.prototype.destroy.call(this);
     if (this._boxModelHighlighter) {
       this._boxModelHighlighter.off("ready", this._highlighterReady);
       this._boxModelHighlighter.off("hide", this._highlighterHidden);
       this._boxModelHighlighter.destroy();
       this._boxModelHighlighter = null;
     }
@@ -98,47 +111,28 @@ let HighlighterActor = protocol.ActorCla
    * method several times won't display several highlighters, it will just move
    * the highlighter instance to these nodes.
    *
    * @param NodeActor The node to be highlighted
    * @param Options See the request part for existing options. Note that not
    * all options may be supported by all types of highlighters.
    */
   showBoxModel: method(function(node, options={}) {
-    if (node && this._isNodeValidForHighlighting(node.rawNode)) {
+    if (node && isNodeValid(node.rawNode)) {
       this._boxModelHighlighter.show(node.rawNode, options);
     } else {
       this._boxModelHighlighter.hide();
     }
   }, {
     request: {
       node: Arg(0, "domnode"),
       region: Option(1)
     }
   }),
 
-  _isNodeValidForHighlighting: function(node) {
-    // Is it null or dead?
-    let isNotDead = node && !Cu.isDeadWrapper(node);
-
-    // Is it connected to the document?
-    let isConnected = false;
-    try {
-      let doc = node.ownerDocument;
-      isConnected = (doc && doc.defaultView && doc.documentElement.contains(node));
-    } catch (e) {
-      // "can't access dead object" error
-    }
-
-    // Is it an element node
-    let isElementNode = node.nodeType === Ci.nsIDOMNode.ELEMENT_NODE;
-
-    return isNotDead && isConnected && isElementNode;
-  },
-
   /**
    * Hide the box model highlighting if it was shown before
    */
   hideBoxModel: method(function() {
     this._boxModelHighlighter.hide();
   }, {
     request: {}
   }),
@@ -253,22 +247,200 @@ let HighlighterActor = protocol.ActorCla
       this._boxModelHighlighter.hide();
       this._stopPickerListeners();
       this._isPicking = false;
       this._hoveredNode = null;
     }
   })
 });
 
-exports.HighlighterActor = HighlighterActor;
+let HighlighterFront = protocol.FrontClass(HighlighterActor, {});
+
+/**
+ * A generic highlighter actor class that instantiate a highlighter given its
+ * type name and allows to show/hide it.
+ */
+let CustomHighlighterActor = exports.CustomHighlighterActor = protocol.ActorClass({
+  typeName: "customhighlighter",
+
+  /**
+   * Create a highlighter instance given its typename
+   * The typename must be one of HIGHLIGHTER_CLASSES and the class must
+   * implement constructor(tab, inspector), show(node), hide(), destroy()
+   */
+  initialize: function(inspector, typeName) {
+    protocol.Actor.prototype.initialize.call(this, null);
+
+    this._inspector = inspector;
+
+    let constructor = HIGHLIGHTER_CLASSES[typeName];
+    if (!constructor) {
+      throw new Error(typeName + " isn't a valid highlighter class (" +
+        Object.keys(HIGHLIGHTER_CLASSES) + ")");
+      return;
+    }
+
+    // The assumption is that all custom highlighters need a XUL parent in the
+    // browser to append their elements
+    if (supportXULBasedHighlighter(inspector.tabActor)) {
+      this._highlighter = new constructor(inspector.tabActor, inspector);
+    }
+  },
+
+  get conn() this._inspector && this._inspector.conn,
+
+  destroy: function() {
+    protocol.Actor.prototype.destroy.call(this);
+    this.finalize();
+  },
+
+  /**
+   * Display the highlighter on a given NodeActor.
+   * @param NodeActor The node to be highlighted
+   */
+  show: method(function(node) {
+    if (!node || !isNodeValid(node.rawNode) || !this._highlighter) {
+      return;
+    }
+
+    this._highlighter.show(node.rawNode);
+  }, {
+    request: {
+      node: Arg(0, "domnode")
+    }
+  }),
+
+  /**
+   * Hide the highlighter if it was shown before
+   */
+  hide: method(function() {
+    if (this._highlighter) {
+      this._highlighter.hide();
+    }
+  }, {
+    request: {}
+  }),
+
+  /**
+   * Kill this actor. This method is called automatically just before the actor
+   * is destroyed.
+   */
+  finalize: method(function() {
+    if (this._highlighter) {
+      this._highlighter.destroy();
+      this._highlighter = null;
+    }
+  }, {
+    oneway: true
+  })
+});
+
+let CustomHighlighterFront = protocol.FrontClass(CustomHighlighterActor, {});
 
 /**
- * The HighlighterFront class
+ * Parent class for XUL-based complex highlighter that are inserted in the
+ * parent browser structure
  */
-let HighlighterFront = protocol.FrontClass(HighlighterActor, {});
+function XULBasedHighlighter(tabActor, inspector) {
+  this._inspector = inspector;
+
+  this.browser = tabActor.browser;
+  this.win = tabActor.window;
+  this.chromeDoc = this.browser.ownerDocument;
+  this.currentNode = null;
+
+  this.update = this.update.bind(this);
+}
+
+XULBasedHighlighter.prototype = {
+  /**
+   * Show the highlighter on a given node
+   * @param {DOMNode} node
+   */
+  show: function(node) {
+    if (!isNodeValid(node) || node === this.currentNode) {
+      return;
+    }
+
+    this._detachPageListeners();
+    this.currentNode = node;
+    this._attachPageListeners();
+    this._show();
+  },
+
+  /**
+   * Hide the highlighter
+   */
+  hide: function() {
+    if (!isNodeValid(this.currentNode)) {
+      return;
+    }
+
+    this._hide();
+    this._detachPageListeners();
+    this.currentNode = null;
+  },
+
+  /**
+   * Update the highlighter while shown
+   */
+  update: function() {
+    if (isNodeValid(this.currentNode)) {
+      this._update();
+    }
+  },
+
+  _show: function() {
+    // To be implemented by sub classes
+    // When called, sub classes should actually show the highlighter for
+    // this.currentNode
+  },
+
+  _update: function() {
+    // To be implemented by sub classes
+    // When called, sub classes should update the highlighter shown for
+    // this.currentNode
+    // This is called as a result of a page scroll, zoom or repaint
+  },
+
+  _hide: function() {
+    // To be implemented by sub classes
+    // When called, sub classes should actually hide the highlighter
+  },
+
+  /**
+   * Listen to changes on the content page to update the highlighter
+   */
+  _attachPageListeners: function() {
+    if (isNodeValid(this.currentNode)) {
+      let win = this.currentNode.ownerDocument.defaultView;
+      this.browser.addEventListener("MozAfterPaint", this.update);
+    }
+  },
+
+  /**
+   * Stop listening to page changes
+   */
+  _detachPageListeners: function() {
+    if (isNodeValid(this.currentNode)) {
+      let win = this.currentNode.ownerDocument.defaultView;
+      this.browser.removeEventListener("MozAfterPaint", this.update);
+    }
+  },
+
+  destroy: function() {
+    this.hide();
+
+    this.win = null;
+    this.browser = null;
+    this.chromeDoc = null;
+    this._inspector = null;
+    this.currentNode = null;
+  }
+};
 
 /**
  * The BoxModelHighlighter is the class that actually draws the the box model
  * regions on top of a node.
  * It is used by the HighlighterActor.
  *
  * Usage example:
  *
@@ -303,36 +475,23 @@ let HighlighterFront = protocol.FrontCla
  *         </hbox>
  *       </hbox>
  *       <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom"/>
  *     </box>
  *   </box>
  * </stack>
  */
 function BoxModelHighlighter(tabActor, inspector) {
-  this.browser = tabActor.browser;
-  this.win = tabActor.window;
-  this.chromeDoc = this.browser.ownerDocument;
-  this.chromeWin = this.chromeDoc.defaultView;
-  this._inspector = inspector;
-
+  XULBasedHighlighter.call(this, tabActor, inspector);
   this.layoutHelpers = new LayoutHelpers(this.win);
-  this.chromeLayoutHelper = new LayoutHelpers(this.chromeWin);
-
-  this.transitionDisabler = null;
-  this.pageEventsMuter = null;
-  this._update = this._update.bind(this);
-  this.handleEvent = this.handleEvent.bind(this);
-  this.currentNode = null;
-
+  this._initMarkup();
   EventEmitter.decorate(this);
-  this._initMarkup();
 }
 
-BoxModelHighlighter.prototype = {
+BoxModelHighlighter.prototype = Heritage.extend(XULBasedHighlighter.prototype, {
   get zoom() {
     return this.win.QueryInterface(Ci.nsIInterfaceRequestor)
                .getInterface(Ci.nsIDOMWindowUtils).fullZoom;
   },
 
   _initMarkup: function() {
     let stack = this.browser.parentNode;
 
@@ -445,107 +604,81 @@ BoxModelHighlighter.prototype = {
 
     return node;
   },
 
   /**
    * Destroy the nodes. Remove listeners.
    */
   destroy: function() {
-    this.hide();
-
-    this.chromeWin.clearTimeout(this.transitionDisabler);
-    this.chromeWin.clearTimeout(this.pageEventsMuter);
-
-    this.nodeInfo = null;
+    XULBasedHighlighter.prototype.destroy.call(this);
 
     this._highlighterContainer.remove();
     this._highlighterContainer = null;
 
+    this.nodeInfo = null;
     this.rect = null;
-    this.win = null;
-    this.browser = null;
-    this.chromeDoc = null;
-    this.chromeWin = null;
-    this.currentNode = null;
   },
 
   /**
    * Show the highlighter on a given node
-   *
-   * @param {DOMNode} node
    * @param {Object} options
    *        Object used for passing options
    */
-  show: function(node, options={}) {
-    this.currentNode = node;
-
-    this._showInfobar();
-    this._detachPageListeners();
-    this._attachPageListeners();
+  _show: function(options={}) {
     this._update();
     this._trackMutations();
+    this.emit("ready");
   },
 
+  /**
+   * Track the current node markup mutations so that the node info bar can be
+   * updated to reflects the node's attributes
+   */
   _trackMutations: function() {
-    if (this.currentNode) {
+    if (isNodeValid(this.currentNode)) {
       let win = this.currentNode.ownerDocument.defaultView;
-      this.currentNodeObserver = new win.MutationObserver(() => {
-        this._update();
-      });
+      this.currentNodeObserver = new win.MutationObserver(this.update);
       this.currentNodeObserver.observe(this.currentNode, {attributes: true});
     }
   },
 
   _untrackMutations: function() {
-    if (this.currentNode) {
-      if (this.currentNodeObserver) {
-        // The following may fail with a "can't access dead object" exception
-        // when the actor is being destroyed
-        try {
-          this.currentNodeObserver.disconnect();
-        } catch (e) {}
-        this.currentNodeObserver = null;
-      }
+    if (isNodeValid(this.currentNode) && this.currentNodeObserver) {
+      this.currentNodeObserver.disconnect();
+      this.currentNodeObserver = null;
     }
   },
 
   /**
    * Update the highlighter on the current highlighted node (the one that was
    * passed as an argument to show(node)).
    * Should be called whenever node size or attributes change
    * @param {Object} options
    *        Object used for passing options. Valid options are:
    *          - box: "content", "padding", "border" or "margin." This specifies
    *            the box that the guides should outline. Default is content.
    */
   _update: function(options={}) {
-    if (this.currentNode) {
-      if (this._highlightBoxModel(options)) {
-        this._showInfobar();
-      } else {
-        // Nothing to highlight (0px rectangle like a <script> tag for instance)
-        this.hide();
-      }
-      this.emit("ready");
+    if (this._updateBoxModel(options)) {
+      this._showInfobar();
+      this._showBoxModel();
+    } else {
+      // Nothing to highlight (0px rectangle like a <script> tag for instance)
+      this._hide();
     }
   },
 
   /**
    * Hide the highlighter, the outline and the infobar.
    */
-  hide: function() {
-    if (this.currentNode) {
-      this._untrackMutations();
-      this.currentNode = null;
-      this._hideBoxModel();
-      this._hideInfobar();
-      this._detachPageListeners();
-    }
-    this.emit("hide");
+  _hide: function() {
+    this._untrackMutations();
+    this._hideBoxModel();
+    this._hideInfobar();
   },
 
   /**
    * Hide the infobar
    */
   _hideInfobar: function() {
     this.nodeInfo.positioner.setAttribute("hidden", "true");
   },
@@ -568,65 +701,50 @@ BoxModelHighlighter.prototype = {
   /**
    * Show the box model
    */
   _showBoxModel: function() {
     this._svgRoot.removeAttribute("hidden");
   },
 
   /**
-   * Highlight the box model.
+   * Update the box model as per the current node.
    *
    * @param {Object} options
    *        Object used for passing options. Valid options are:
    *          - region: "content", "padding", "border" or "margin." This specifies
    *            the region that the guides should outline. Default is content.
    * @return {boolean}
-   *         True if the rectangle was highlighted, false otherwise.
+   *         True if the current node has a box model to be highlighted
    */
-  _highlightBoxModel: function(options) {
-    let isShown = false;
-
+  _updateBoxModel: function(options) {
     options.region = options.region || "content";
-
     this.rect = this.layoutHelpers.getAdjustedQuads(this.currentNode, "margin");
 
-    if (!this.rect) {
-      return null;
+    if (!this.rect || (this.rect.bounds.width <= 0 && this.rect.bounds.height <= 0)) {
+      return false;
     }
 
-    if (this.rect.bounds.width > 0 && this.rect.bounds.height > 0) {
-      for (let boxType in this._boxModelNodes) {
-        let {p1, p2, p3, p4} = boxType === "margin" ? this.rect :
-          this.layoutHelpers.getAdjustedQuads(this.currentNode, boxType);
-
-        let boxNode = this._boxModelNodes[boxType];
-        boxNode.setAttribute("points",
-                             p1.x + "," + p1.y + " " +
-                             p2.x + "," + p2.y + " " +
-                             p3.x + "," + p3.y + " " +
-                             p4.x + "," + p4.y);
+    for (let boxType in this._boxModelNodes) {
+      let {p1, p2, p3, p4} = boxType === "margin" ? this.rect :
+        this.layoutHelpers.getAdjustedQuads(this.currentNode, boxType);
 
-        if (boxType === options.region) {
-          this._showGuides(p1, p2, p3, p4);
-        }
-      }
+      let boxNode = this._boxModelNodes[boxType];
+      boxNode.setAttribute("points",
+                           p1.x + "," + p1.y + " " +
+                           p2.x + "," + p2.y + " " +
+                           p3.x + "," + p3.y + " " +
+                           p4.x + "," + p4.y);
 
-      isShown = true;
-      this._showBoxModel();
-    } else {
-      // Only return false if the element really is invisible.
-      // A height of 0 and a non-0 width corresponds to a visible element that
-      // is below the fold for instance
-      if (this.rect.width > 0 || this.rect.height > 0) {
-        isShown = true;
-        this._hideBoxModel();
+      if (boxType === options.region) {
+        this._showGuides(p1, p2, p3, p4);
       }
     }
-    return isShown;
+
+    return true;
   },
 
   /**
    * We only want to show guides for horizontal and vertical edges as this helps
    * to line them up. This method finds these edges and displays a guide there.
    *
    * @param  {DOMPoint} p1
    *                    Point 1
@@ -706,41 +824,50 @@ BoxModelHighlighter.prototype = {
   /**
    * Update node information (tagName#id.class)
    */
   _updateInfobar: function() {
     if (!this.currentNode) {
       return;
     }
 
-    // Tag name
-    this.nodeInfo.tagNameLabel.textContent = this.currentNode.tagName;
+    let node = this.currentNode;
+    let info = this.nodeInfo;
 
-    // ID
-    this.nodeInfo.idLabel.textContent = this.currentNode.id ? "#" + this.currentNode.id : "";
+    // Update the tag, id, classes, pseudo-classes and dimensions only if they
+    // changed to avoid triggering paint events
 
-    // Classes
-    let classes = this.nodeInfo.classesBox;
+    let tagName = node.tagName;
+    if (info.tagNameLabel.textContent !== tagName) {
+      info.tagNameLabel.textContent = tagName;
+    }
 
-    classes.textContent = this.currentNode.classList.length ?
-                            "." + Array.join(this.currentNode.classList, ".") : "";
+    let id = node.id ? "#" + node.id : "";
+    if (info.idLabel.textContent !== id) {
+      info.idLabel.textContent = id;
+    }
 
-    // Pseudo-classes
-    let pseudos = PSEUDO_CLASSES.filter(pseudo => {
-      return DOMUtils.hasPseudoClassLock(this.currentNode, pseudo);
-    }, this);
+    let classList = node.classList.length ? "." + [...node.classList].join(".") : "";
+    if (info.classesBox.textContent !== classList) {
+      info.classesBox.textContent = classList;
+    }
 
-    let pseudoBox = this.nodeInfo.pseudoClassesBox;
-    pseudoBox.textContent = pseudos.join("");
+    let pseudos = PSEUDO_CLASSES.filter(pseudo => {
+      return DOMUtils.hasPseudoClassLock(node, pseudo);
+    }, this).join("");
+    if (info.pseudoClassesBox.textContent !== pseudos) {
+      info.pseudoClassesBox.textContent = pseudos;
+    }
 
-    // Dimensions
-    let dimensionBox = this.nodeInfo.dimensionBox;
-    let rect = this.currentNode.getBoundingClientRect();
-    dimensionBox.textContent = Math.ceil(rect.width) + " x " +
-                               Math.ceil(rect.height);
+    let rect = node.getBoundingClientRect();
+    let dim = Math.ceil(rect.width) + " x " + Math.ceil(rect.height);
+    if (info.dimensionBox.textContent !== dim) {
+      info.dimensionBox.textContent = dim;
+    }
+
     this._moveInfobar();
   },
 
   /**
    * Move the Infobar to the right place in the highlighter.
    */
   _moveInfobar: function() {
     if (this.rect) {
@@ -785,54 +912,183 @@ BoxModelHighlighter.prototype = {
       }
       this.nodeInfo.positioner.style.left = left + "px";
     } else {
       this.nodeInfo.positioner.style.left = "0";
       this.nodeInfo.positioner.style.top = "0";
       this.nodeInfo.positioner.setAttribute("position", "top");
       this.nodeInfo.positioner.setAttribute("hide-arrow", "true");
     }
+  }
+});
+
+/**
+ * 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, inspector) {
+  XULBasedHighlighter.call(this, tabActor, inspector);
+
+  this.layoutHelpers = new LayoutHelpers(tabActor.window);
+  this._initMarkup();
+}
+
+let MARKER_COUNTER = 1;
+
+CssTransformHighlighter.prototype = Heritage.extend(XULBasedHighlighter.prototype, {
+  _initMarkup: function() {
+    let stack = this.browser.parentNode;
+
+    this._container = this.chromeDoc.createElement("stack");
+    this._container.className = "highlighter-container";
+
+    this._svgRoot = this._createSVGNode("root", "svg", this._container);
+    this._svgRoot.setAttribute("hidden", "true");
+
+    // Add a marker tag to the svg root for the arrow tip
+    let marker = this.chromeDoc.createElementNS(SVG_NS, "marker");
+    this.markerId = "css-transform-arrow-marker-" + MARKER_COUNTER;
+    MARKER_COUNTER ++;
+    marker.setAttribute("id", this.markerId);
+    marker.setAttribute("markerWidth", "10");
+    marker.setAttribute("markerHeight", "5");
+    marker.setAttribute("orient", "auto");
+    marker.setAttribute("markerUnits", "strokeWidth");
+    marker.setAttribute("refX", "10");
+    marker.setAttribute("refY", "5");
+    marker.setAttribute("viewBox", "0 0 10 10");
+    let path = this.chromeDoc.createElementNS(SVG_NS, "path");
+    path.setAttribute("d", "M 0 0 L 10 5 L 0 10 z");
+    path.setAttribute("fill", "#08C");
+    marker.appendChild(path);
+    this._svgRoot.appendChild(marker);
+
+    // Create the 2 polygons (transformed and untransformed)
+    let shapesGroup = this._createSVGNode("container", "g", this._svgRoot);
+    this._shapes = {
+      untransformed: this._createSVGNode("untransformed", "polygon", shapesGroup),
+      transformed: this._createSVGNode("transformed", "polygon", shapesGroup)
+    };
+
+    // Create the arrows
+    for (let nb of ["1", "2", "3", "4"]) {
+      let line = this._createSVGNode("line", "line", shapesGroup);
+      line.setAttribute("marker-end", "url(#" + this.markerId + ")");
+      this._shapes["line" + nb] = line;
+    }
+
+    this._container.appendChild(this._svgRoot);
+
+    // Insert the highlighter right after the browser
+    stack.insertBefore(this._container, stack.childNodes[1]);
   },
 
-  _attachPageListeners: function() {
-    if (this.currentNode) {
-      let win = this.currentNode.ownerGlobal;
+  _createSVGNode: function(classPostfix, nodeType, parent) {
+    let node = this.chromeDoc.createElementNS(SVG_NS, nodeType);
+    node.setAttribute("class", "css-transform-" + classPostfix);
+
+    parent.appendChild(node);
+    return node;
+  },
 
-      win.addEventListener("scroll", this, false);
-      win.addEventListener("resize", this, false);
-      win.addEventListener("MozAfterPaint", this, false);
-    }
+  /**
+   * Destroy the nodes. Remove listeners.
+   */
+  destroy: function() {
+    XULBasedHighlighter.prototype.destroy.call(this);
+
+    this._container.remove();
+    this._container = null;
   },
 
-  _detachPageListeners: function() {
-    if (this.currentNode) {
-      let win = this.currentNode.ownerGlobal;
+  /**
+   * Show the highlighter on a given node
+   * @param {DOMNode} node
+   */
+  _show: function() {
+    if (!this._isTransformed(this.currentNode)) {
+      this.hide();
+      return;
+    }
+
+    this._update();
+  },
 
-      win.removeEventListener("scroll", this, false);
-      win.removeEventListener("resize", this, false);
-      win.removeEventListener("MozAfterPaint", this, false);
+  /**
+   * Checks if the supplied node is transformed and not inline
+   */
+  _isTransformed: function(node) {
+    let style = node.ownerDocument.defaultView.getComputedStyle(node);
+    return style.transform !== "none" && style.display !== "inline";
+  },
+
+  _setPolygonPoints: function(quad, poly) {
+    let points = [];
+    for (let point of ["p1","p2", "p3", "p4"]) {
+      points.push(quad[point].x + "," + quad[point].y);
+    }
+    poly.setAttribute("points", points.join(" "));
+  },
+
+  _setLinePoints: function(p1, p2, line) {
+    line.setAttribute("x1", p1.x);
+    line.setAttribute("y1", p1.y);
+    line.setAttribute("x2", p2.x);
+    line.setAttribute("y2", p2.y);
+
+    let dist = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
+    if (dist < ARROW_LINE_MIN_DISTANCE) {
+      line.removeAttribute("marker-end");
+    } else {
+      line.setAttribute("marker-end", "url(#" + this.markerId + ")");
     }
   },
 
   /**
-   * Generic event handler.
-   *
-   * @param nsIDOMEvent aEvent
-   *        The DOM event object.
+   * Update the highlighter on the current highlighted node (the one that was
+   * passed as an argument to show(node)).
+   * Should be called whenever node size or attributes change
    */
-  handleEvent: function(event) {
-    switch (event.type) {
-      case "resize":
-      case "MozAfterPaint":
-      case "scroll":
-        this._update();
-        break;
+  _update: function() {
+    // Getting the points for the transformed shape
+    let quad = this.layoutHelpers.getAdjustedQuads(this.currentNode, "border");
+    if (!quad || quad.bounds.width <= 0 || quad.bounds.height <= 0) {
+      this._hideShapes();
+      return null;
+    }
+
+    // Getting the points for the untransformed shape
+    let untransformedQuad = this.layoutHelpers.getNodeBounds(this.currentNode);
+
+    this._setPolygonPoints(quad, this._shapes.transformed);
+    this._setPolygonPoints(untransformedQuad, this._shapes.untransformed);
+    for (let nb of ["1", "2", "3", "4"]) {
+      this._setLinePoints(untransformedQuad["p" + nb], quad["p" + nb],
+        this._shapes["line" + nb]);
     }
+
+    this._showShapes();
   },
-};
+
+  /**
+   * Hide the highlighter, the outline and the infobar.
+   */
+  _hide: function() {
+    this._hideShapes();
+  },
+
+  _hideShapes: function() {
+    this._svgRoot.setAttribute("hidden", "true");
+  },
+
+  _showShapes: function() {
+    this._svgRoot.removeAttribute("hidden");
+  }
+});
 
 /**
  * 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 outline.
  * It is used by the HighlighterActor too, but in case the more complex
  * BoxModelHighlighter can't be attached (which is the case for FirefoxOS and
  * Fennec targets for instance).
@@ -886,11 +1142,45 @@ SimpleOutlineHighlighter.prototype = {
   hide: function() {
     if (this.currentNode) {
       DOMUtils.removePseudoClassLock(this.currentNode, HIGHLIGHTED_PSEUDO_CLASS);
       this.currentNode = null;
     }
   }
 };
 
+/**
+ * Can the host support the XUL-based highlighters which require a parent
+ * XUL node to get attached.
+ * @param {TabActor}
+ * @return {Boolean}
+ */
+function supportXULBasedHighlighter(tabActor) {
+  // Note that <browser>s on Fennec also have a XUL parentNode but the box
+  // model highlighter doesn't display correctly on Fennec (bug 993190)
+  return tabActor.browser &&
+         !!tabActor.browser.parentNode &&
+         Services.appinfo.ID !== "{aa3c5121-dab2-40e2-81ca-7ea25febc110}";
+}
+
+function isNodeValid(node) {
+  // Is it null or dead?
+  if(!node || Cu.isDeadWrapper(node)) {
+    return false;
+  }
+
+  // Is it an element node
+  if (node.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
+    return false;
+  }
+
+  // Is it connected to the document?
+  let doc = node.ownerDocument;
+  if (!doc || !doc.defaultView || !doc.documentElement.contains(node)) {
+    return false;
+  }
+
+  return true;
+}
+
 XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)
 });
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -56,17 +56,21 @@ const protocol = require("devtools/serve
 const {Arg, Option, method, RetVal, types} = protocol;
 const {LongStringActor, ShortLongString} = require("devtools/server/actors/string");
 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 const object = require("sdk/util/object");
 const events = require("sdk/event/core");
 const {Unknown} = require("sdk/platform/xpcom");
 const {Class} = require("sdk/core/heritage");
 const {PageStyleActor} = require("devtools/server/actors/styles");
-const {HighlighterActor} = require("devtools/server/actors/highlighter");
+const {
+  HighlighterActor,
+  CustomHighlighterActor,
+  HIGHLIGHTER_CLASSES
+} = require("devtools/server/actors/highlighter");
 const {getLayoutChangesObserver, releaseLayoutChangesObserver} =
   require("devtools/server/actors/layout");
 
 const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog";
 const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20;
 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
 const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
@@ -2596,33 +2600,74 @@ var InspectorActor = protocol.ActorClass
     return this._pageStylePromise;
   }, {
     request: {},
     response: {
       pageStyle: RetVal("pagestyle")
     }
   }),
 
+  /**
+   * The most used highlighter actor is the HighlighterActor which can be
+   * conveniently retrieved by this method.
+   * The same instance will always be returned by this method when called
+   * several times.
+   * The highlighter actor returned here is used to highlighter elements's
+   * box-models from the markup-view, layout-view, console, debugger, ... as
+   * well as select elements with the pointer (pick).
+   *
+   * @param {Boolean} autohide Optionally autohide the highlighter after an
+   * element has been picked
+   * @return {HighlighterActor}
+   */
   getHighlighter: method(function (autohide) {
     if (this._highlighterPromise) {
       return this._highlighterPromise;
     }
 
     this._highlighterPromise = this.getWalker().then(walker => {
       return HighlighterActor(this, autohide);
     });
     return this._highlighterPromise;
   }, {
-    request: { autohide: Arg(0, "boolean") },
+    request: {
+      autohide: Arg(0, "boolean")
+    },
     response: {
       highligter: RetVal("highlighter")
     }
   }),
 
   /**
+   * If consumers need to display several highlighters at the same time or
+   * different types of highlighters, then this method should be used, passing
+   * the type name of the highlighter needed as argument.
+   * A new instance will be created everytime the method is called, so it's up
+   * to the consumer to release it when it is not needed anymore
+   *
+   * @param {String} type The type of highlighter to create
+   * @return {Highlighter} The highlighter actor instance or null if the
+   * typeName passed doesn't match any available highlighter
+   */
+  getHighlighterByType: method(function (typeName) {
+    if (HIGHLIGHTER_CLASSES[typeName]) {
+      return CustomHighlighterActor(this, typeName);
+    } else {
+      return null;
+    }
+  }, {
+    request: {
+      typeName: Arg(0)
+    },
+    response: {
+      highlighter: RetVal("nullable:customhighlighter")
+    }
+  }),
+
+  /**
    * Get the node's image data if any (for canvas and img nodes).
    * Returns an imageData object with the actual data being a LongStringActor
    * and a size json object.
    * The image data is transmitted as a base64 encoded png data-uri.
    * The method rejects if the node isn't an image or if the image is missing
    *
    * Accepts a maxDim request parameter to resize images that are larger. This
    * is important as the resizing occurs server-side so that image-data being
--- a/toolkit/devtools/server/actors/root.js
+++ b/toolkit/devtools/server/actors/root.js
@@ -99,16 +99,22 @@ RootActor.prototype = {
   applicationType: "browser",
 
   traits: {
     sources: true,
     editOuterHTML: true,
     // Whether the server-side highlighter actor exists and can be used to
     // remotely highlight nodes (see server/actors/highlighter.js)
     highlightable: true,
+    // Which custom highlighter does the server-side highlighter actor supports?
+    // (see server/actors/highlighter.js)
+    customHighlighters: [
+      "BoxModelHighlighter",
+      "CssTransformHighlighter"
+    ],
     // Whether the inspector actor implements the getImageDataFromURL
     // method that returns data-uris for image URLs. This is used for image
     // tooltips for instance
     urlToImageDataResolver: true,
     networkMonitor: true,
     // Whether the storage inspector actor to inspect cookies, etc.
     storageInspector: true,
     // Whether storage inspector is read only
--- a/toolkit/devtools/server/tests/mochitest/chrome.ini
+++ b/toolkit/devtools/server/tests/mochitest/chrome.ini
@@ -18,16 +18,19 @@ support-files =
 [test_Debugger.Script.prototype.global.html]
 [test_connection-manager.html]
 [test_css-logic.html]
 [test_device.html]
 [test_framerate_01.html]
 [test_framerate_02.html]
 [test_framerate_03.html]
 [test_framerate_04.html]
+[test_highlighter-csstransform_01.html]
+[test_highlighter-csstransform_02.html]
+[test_highlighter-csstransform_03.html]
 [test_inspector-changeattrs.html]
 [test_inspector-changevalue.html]
 [test_inspector-hide.html]
 [test_inspector-insert.html]
 [test_inspector-mutations-attr.html]
 [test_inspector-mutations-childlist.html]
 [test_inspector-mutations-frameload.html]
 [test_inspector-mutations-value.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_highlighter-csstransform_01.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1014547 - CSS transforms highlighter
+Test the high level API of the highlighters
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Framerate actor test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript;version=1.8">
+
+window.onload = function() {
+  var Cu = Components.utils;
+  var Cc = Components.classes;
+  var Ci = Components.interfaces;
+
+  Cu.import("resource://gre/modules/Services.jsm");
+  Cu.import("resource://gre/modules/devtools/Loader.jsm");
+  Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
+  Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+  Cu.import("resource://gre/modules/Task.jsm");
+
+  SimpleTest.waitForExplicitFinish();
+
+  var {InspectorFront} = devtools.require("devtools/server/actors/inspector");
+
+  DebuggerServer.init(() => true);
+  DebuggerServer.addBrowserActors();
+
+  var client = new DebuggerClient(DebuggerServer.connectPipe());
+  client.connect(() => {
+    client.listTabs(response => {
+      var form = response.tabs[response.selected];
+      var front = InspectorFront(client, form);
+
+      Task.spawn(function*() {
+        yield onlyOneInstanceOfMainHighlighter(front);
+        yield manyInstancesOfCustomHighlighters(front);
+        yield showHideMethodsAreAvailable(front);
+        yield unknownHighlighterTypeShouldntBeAccepted(front);
+        yield rootActorTraitsShouldContainKnownTypes(client);
+      }).then(null, ok.bind(null, false)).then(() => {
+        client.close(() => {
+          DebuggerServer.destroy();
+          SimpleTest.finish();
+        });
+      });
+    });
+  });
+
+  function* onlyOneInstanceOfMainHighlighter(inspectorFront) {
+    info("Check that the inspector always sends back the same main highlighter");
+
+    let h1 = yield inspectorFront.getHighlighter(false);
+    let h2 = yield inspectorFront.getHighlighter(false);
+    is(h1, h2, "The same highlighter front was returned");
+
+    is(h1.typeName, "highlighter", "The right front type was returned");
+  }
+
+  function* manyInstancesOfCustomHighlighters(inspectorFront) {
+    let h1 = yield inspectorFront.getHighlighterByType("BoxModelHighlighter");
+    let h2 = yield inspectorFront.getHighlighterByType("BoxModelHighlighter");
+    ok(h1 !== h2, "getHighlighterByType returns new instances every time (1)");
+
+    let h3 = yield inspectorFront.getHighlighterByType("CssTransformHighlighter");
+    let h4 = yield inspectorFront.getHighlighterByType("CssTransformHighlighter");
+    ok(h3 !== h4, "getHighlighterByType returns new instances every time (2)");
+    ok(h3 !== h1 && h3 !== h2,
+      "getHighlighterByType returns new instances every time (3)");
+    ok(h4 !== h1 && h4 !== h2,
+      "getHighlighterByType returns new instances every time (4)");
+
+    yield h1.finalize();
+    yield h2.finalize();
+    yield h3.finalize();
+    yield h4.finalize();
+  }
+
+  function* showHideMethodsAreAvailable(inspectorFront) {
+    let h1 = yield inspectorFront.getHighlighterByType("BoxModelHighlighter");
+    let h2 = yield inspectorFront.getHighlighterByType("CssTransformHighlighter");
+
+    ok("show" in h1, "Show method is present on the front API");
+    ok("show" in h2, "Show method is present on the front API");
+    ok("hide" in h1, "Hide method is present on the front API");
+    ok("hide" in h2, "Hide method is present on the front API");
+
+    yield h1.finalize();
+    yield h2.finalize();
+  }
+
+  function* unknownHighlighterTypeShouldntBeAccepted(inspectorFront) {
+    let h = yield inspectorFront.getHighlighterByType("whatever");
+    ok(!h, "No highlighter was returned for the invalid type");
+  }
+
+  function* rootActorTraitsShouldContainKnownTypes(client) {
+    ok(client.traits.customHighlighters.indexOf("BoxModelHighlighter") !== -1,
+      "The root actor's trait contains BoxModelHighlighter as a known type");
+    ok(client.traits.customHighlighters.indexOf("CssTransformHighlighter") !== -1,
+      "The root actor's trait contains CssTransformHighlighter as a known type");
+  }
+}
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_highlighter-csstransform_02.html
@@ -0,0 +1,156 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1014547 - CSS transforms highlighter
+Test the creation of the SVG highlighter elements in the browser
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Framerate actor test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+  <div id="transformed" style="border:1px solid red;width:100px;height:100px;transform:skew(13deg);"></div>
+  <div id="untransformed" style="border:1px solid blue;width:100px;height:100px;"></div>
+  <span id="inline" style="transform:rotate(90deg);">this is an inline transformed element</span>
+<pre id="test">
+<script type="application/javascript;version=1.8">
+
+window.onload = function() {
+  var Cu = Components.utils;
+  var Cc = Components.classes;
+  var Ci = Components.interfaces;
+
+  Cu.import("resource://gre/modules/Services.jsm");
+  Cu.import("resource://gre/modules/devtools/Loader.jsm");
+  Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
+  Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+  Cu.import("resource://gre/modules/Task.jsm");
+  const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
+
+  SimpleTest.waitForExplicitFinish();
+
+  var {InspectorFront} = devtools.require("devtools/server/actors/inspector");
+
+  DebuggerServer.init(() => true);
+  DebuggerServer.addBrowserActors();
+
+  var client = new DebuggerClient(DebuggerServer.connectPipe());
+  client.connect(() => {
+    client.listTabs(response => {
+      var form = response.tabs[response.selected];
+      var front = InspectorFront(client, form);
+
+      Task.spawn(function*() {
+        let walkerFront = yield front.getWalker();
+        let highlighterFront = yield front.getHighlighterByType(
+          "CssTransformHighlighter");
+
+        let gBrowser = Services.wm.getMostRecentWindow("navigator:browser").gBrowser;
+        let container =
+          gBrowser.selectedBrowser.parentNode.querySelector(".highlighter-container");
+        ok(container, "The highlighter container was found");
+
+        yield isHiddenByDefault(container);
+        yield has2PolygonsAnd4Lines(container);
+        yield isNotShownForUntransformed(highlighterFront, walkerFront, container);
+        yield isNotShownForInline(highlighterFront, walkerFront, container);
+        yield isVisibleWhenShown(highlighterFront, walkerFront, container);
+        yield linesLinkThePolygons(highlighterFront, walkerFront, container);
+
+        yield highlighterFront.finalize();
+      }).then(null, ok.bind(null, false)).then(() => {
+        client.close(() => {
+          DebuggerServer.destroy();
+          SimpleTest.finish();
+        });
+      });
+    });
+  });
+
+  function* isHiddenByDefault(container) {
+    let svg = container.querySelector("svg");
+    ok(svg.hasAttribute("hidden"), "The highlighter is hidden by default");
+  }
+
+  function* has2PolygonsAnd4Lines(container) {
+    is(container.querySelectorAll("polygon").length, 2, "Found 2 polygons");
+    is(container.querySelectorAll("line").length, 4, "Found 4 lines");
+  }
+
+  function* isNotShownForUntransformed(highlighterFront, walkerFront, container) {
+    let rawNode = document.getElementById("untransformed");
+    let node = walkerFront.frontForRawNode(rawNode);
+
+    info("Asking to show the highlighter on the untransformed test node");
+    yield highlighterFront.show(node);
+    let svg = container.querySelector("svg");
+    ok(svg.hasAttribute("hidden"), "The highlighter is still hidden");
+  }
+
+  function* isNotShownForInline(highlighterFront, walkerFront, container) {
+    let rawNode = document.getElementById("inline");
+    let node = walkerFront.frontForRawNode(rawNode);
+
+    info("Asking to show the highlighter on the inline test node");
+    yield highlighterFront.show(node);
+    let svg = container.querySelector("svg");
+    ok(svg.hasAttribute("hidden"), "The highlighter is still hidden");
+  }
+
+  function* isVisibleWhenShown(highlighterFront, walkerFront, container) {
+    let rawNode = document.getElementById("transformed");
+    let node = walkerFront.frontForRawNode(rawNode);
+
+    info("Asking to show the highlighter on the test node");
+    yield highlighterFront.show(node);
+    let svg = container.querySelector("svg");
+    ok(!svg.hasAttribute("hidden"), "The highlighter is visible");
+
+    info("Hiding the highlighter");
+    yield highlighterFront.hide();
+    ok(svg.hasAttribute("hidden"), "The highlighter is hidden");
+  }
+
+  function* linesLinkThePolygons(highlighterFront, walkerFront, container) {
+    let rawNode = document.getElementById("transformed");
+    let node = walkerFront.frontForRawNode(rawNode);
+
+    info("Showing the highlighter on the transformed node");
+    yield highlighterFront.show(node);
+
+    info("Checking that the 4 lines do link the 2 shape's corners");
+    let lines = [...container.querySelectorAll("line")];
+
+    let polygon1 = container.querySelector(".css-transform-untransformed");
+    let points1 = polygon1.getAttribute("points").split(" ");
+
+    let polygon2 = container.querySelector(".css-transform-transformed");
+    let points2 = polygon2.getAttribute("points").split(" ");
+
+    for (let i = 0; i < lines.length; i++) {
+      info("Checking line nb " + i);
+      let line = lines[i];
+
+      let p1 = points1[i].split(",");
+      let x1 = line.getAttribute("x1");
+      let y1 = line.getAttribute("y1");
+      is(p1[0], x1, "line " + i + "'s first point matches the untransformed x coordinate");
+      is(p1[1], y1, "line " + i + "'s first point matches the untransformed y coordinate");
+
+      let p2 = points2[i].split(",");
+      let x2 = line.getAttribute("x2");
+      let y2 = line.getAttribute("y2");
+      is(p2[0], x2, "line " + i + "'s first point matches the transformed x coordinate");
+      is(p2[1], y2, "line " + i + "'s first point matches the transformed y coordinate");
+    }
+
+    yield highlighterFront.hide();
+  }
+
+}
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_highlighter-csstransform_03.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1014547 - CSS transforms highlighter
+Test that the highlighter elements created have the right size and coordinates.
+
+Note that instead of hard-coding values here, the assertions are made by
+comparing with the result of LayoutHelpers.getAdjustedQuads.
+
+There's a separate test for checking that getAdjustedQuads actually returns
+sensible values
+(browser/devtools/shared/test/browser_layoutHelpers-getBoxQuads.js),
+so the present test doesn't care about that, it just verifies that the css
+transform highlighter applies those values correctly to the SVG elements
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Framerate actor test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  <style type="text/css">
+    #test-node {
+      position: absolute;
+      top: 0;
+      left: 0;
+
+      width: 300px;
+      height: 300px;
+
+      transform: rotate(90deg) skew(13deg) scale(.8) translateX(50px);
+      transform-origin: 50%;
+
+      background: linear-gradient(green, yellow);
+    }
+  </style>
+</head>
+<body>
+  <div id="test-node"></div>
+<pre id="test">
+<script type="application/javascript;version=1.8">
+
+window.onload = function() {
+  var Cu = Components.utils;
+  var Cc = Components.classes;
+  var Ci = Components.interfaces;
+
+  Cu.import("resource://gre/modules/Services.jsm");
+  Cu.import("resource://gre/modules/devtools/Loader.jsm");
+  Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
+  Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+  Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
+  Cu.import("resource://gre/modules/Task.jsm");
+  const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
+
+  SimpleTest.waitForExplicitFinish();
+
+  var {InspectorFront} = devtools.require("devtools/server/actors/inspector");
+
+  DebuggerServer.init(() => true);
+  DebuggerServer.addBrowserActors();
+
+  var client = new DebuggerClient(DebuggerServer.connectPipe());
+  client.connect(() => {
+    client.listTabs(response => {
+      var form = response.tabs[response.selected];
+      var front = InspectorFront(client, form);
+
+      Task.spawn(function*() {
+        let walker = yield front.getWalker();
+        let highlighter = yield front.getHighlighterByType(
+          "CssTransformHighlighter");
+
+        let browser = Services.wm.getMostRecentWindow("navigator:browser")
+          .gBrowser.selectedBrowser;
+
+        let container = browser.parentNode.querySelector(".highlighter-container");
+
+        let node = document.querySelector("#test-node");
+        let helper = new LayoutHelpers(browser.docShell
+          .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow));
+
+        info("Displaying the transform highlighter on test node " +
+          node.tagName);
+        yield highlighter.show(walker.frontForRawNode(node));
+
+        let expected = helper.getAdjustedQuads(node, "border");
+        let polygon = container.querySelector(".css-transform-transformed");
+        let polygonPoints = polygon.getAttribute("points").split(" ").map(p => {
+          return {
+            x: +p.substring(0, p.indexOf(",")),
+            y: +p.substring(p.indexOf(",")+1)
+          };
+        });
+
+        for (let i = 1; i < 5; i ++) {
+          is(polygonPoints[i - 1].x, expected["p" + i].x,
+            "p" + i + " x coordinate is correct");
+          is(polygonPoints[i - 1].y, expected["p" + i].y,
+            "p" + i + " y coordinate is correct");
+        }
+
+        info("Hiding the transform highlighter");
+        yield highlighter.hide();
+
+        yield highlighter.finalize();
+      }).then(null, ok.bind(null, false)).then(() => {
+        client.close(() => {
+          DebuggerServer.destroy();
+          SimpleTest.finish();
+        });
+      });
+    });
+  });
+
+}
+</script>
+</pre>
+</body>
+</html>