Bug 1304679 - Box-model highlighter now highlights text nodes; r=gl draft
authorPatrick Brosset <pbrosset@mozilla.com>
Fri, 23 Sep 2016 09:57:37 +0200
changeset 416923 f038f4719387571ff8e0801008a5adb616c6c379
parent 416576 08abfe8a96a71b2f697e02c5086482216818d485
child 531985 0a24cd4a29fc04516736aa695b4f04e6421196ee
push id30285
push userbmo:pbrosset@mozilla.com
push dateFri, 23 Sep 2016 08:06:31 +0000
reviewersgl
bugs1304679
milestone52.0a1
Bug 1304679 - Box-model highlighter now highlights text nodes; r=gl MozReview-Commit-ID: 8gZvTE1tZuH
devtools/client/inspector/markup/markup.js
devtools/client/inspector/test/browser_inspector_highlighter-comments.js
devtools/client/inspector/test/doc_inspector_highlighter-comments.html
devtools/client/shared/test/test-actor.js
devtools/server/actors/highlighters.js
devtools/server/actors/highlighters/auto-refresh.js
devtools/server/actors/highlighters/box-model.js
devtools/server/actors/highlighters/geometry-editor.js
devtools/server/actors/highlighters/utils/markup.js
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -221,21 +221,17 @@ MarkupView.prototype = {
       if (target.tagName.toLowerCase() === "body") {
         return;
       }
       target = target.parentNode;
     }
 
     let container = target.container;
     if (this._hoveredNode !== container.node) {
-      if (container.node.nodeType !== nodeConstants.TEXT_NODE) {
-        this._showBoxModel(container.node);
-      } else {
-        this._hideBoxModel();
-      }
+      this._showBoxModel(container.node);
     }
     this._showContainerAsHovered(container.node);
 
     this.emit("node-hover");
   },
 
   /**
    * If focus is moved outside of the markup view document and there is a
--- a/devtools/client/inspector/test/browser_inspector_highlighter-comments.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-comments.js
@@ -46,42 +46,61 @@ add_task(function* () {
   info("Hovering over #id3 and waiting for highlighter to appear.");
   yield hoverElement("#id3");
   yield assertHighlighterShownOn("#id3");
 
   info("Hovering over hidden #id4 and ensuring highlighter doesn't appear.");
   yield hoverElement("#id4");
   yield assertHighlighterHidden();
 
+  info("Hovering over a text node and waiting for highlighter to appear.");
+  yield hoverTextNode("Visible text node");
+  yield assertHighlighterShownOnTextNode("body", 14);
+
   function hoverContainer(container) {
     let promise = inspector.toolbox.once("node-highlight");
+
     EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"},
         markupView.doc.defaultView);
 
     return promise;
   }
 
   function* hoverElement(selector) {
-    info("Hovering node " + selector + " in the markup view");
+    info(`Hovering node ${selector} in the markup view`);
     let container = yield getContainerForSelector(selector, inspector);
     return hoverContainer(container);
   }
 
   function hoverComment() {
     info("Hovering the comment node in the markup view");
     for (let [node, container] of markupView._containers) {
       if (node.nodeType === Ci.nsIDOMNode.COMMENT_NODE) {
         return hoverContainer(container);
       }
     }
     return null;
   }
 
+  function hoverTextNode(text) {
+    info(`Hovering the text node "${text}" in the markup view`);
+    let container = [...markupView._containers].filter(([nodeFront]) => {
+      return nodeFront.nodeType === Ci.nsIDOMNode.TEXT_NODE &&
+             nodeFront._form.nodeValue.trim() === text.trim();
+    })[0][1];
+    return hoverContainer(container);
+  }
+
   function* assertHighlighterShownOn(selector) {
     ok((yield testActor.assertHighlightedNode(selector)),
        "Highlighter is shown on the right node: " + selector);
   }
 
+  function* assertHighlighterShownOnTextNode(parentSelector, childNodeIndex) {
+    ok((yield testActor.assertHighlightedTextNode(parentSelector, childNodeIndex)),
+       "Highlighter is shown on the right text node");
+  }
+
   function* assertHighlighterHidden() {
     let isVisible = yield testActor.isHighlighting();
     ok(!isVisible, "Highlighter is hidden");
   }
 });
--- a/devtools/client/inspector/test/doc_inspector_highlighter-comments.html
+++ b/devtools/client/inspector/test/doc_inspector_highlighter-comments.html
@@ -9,10 +9,11 @@
   <div id="id1">Visible div 1</div>
   <!-- Invisible comment node -->
   <div id="id2">Visible div 2</div>
   <script type="text/javascript">
     /* Invisible script node */
   </script>
   <div id="id3">Visible div 3</div>
   <div id="id4" style="display:none;">Invisible div node</div>
+  Visible text node
 </body>
 </html>
--- a/devtools/client/shared/test/test-actor.js
+++ b/devtools/client/shared/test/test-actor.js
@@ -255,16 +255,25 @@ var testSpec = protocol.generateActorSpe
     getNodeRect: {
       request: {
         selector: Arg(0, "string")
       },
       response: {
         value: RetVal("json")
       }
     },
+    getTextNodeRect: {
+      request: {
+        parentSelector: Arg(0, "string"),
+        childNodeIndex: Arg(1, "number")
+      },
+      response: {
+        value: RetVal("json")
+      }
+    },
     getNodeInfo: {
       request: {
         selector: Arg(0, "string")
       },
       response: {
         value: RetVal("json")
       }
     },
@@ -712,16 +721,22 @@ var TestActor = exports.TestActor = prot
     return deferred.promise;
   },
 
   getNodeRect: Task.async(function* (selector) {
     let node = this._querySelector(selector);
     return getRect(this.content, node, this.content);
   }),
 
+  getTextNodeRect: Task.async(function* (parentSelector, childNodeIndex) {
+    let parentNode = this._querySelector(parentSelector);
+    let node = parentNode.childNodes[childNodeIndex];
+    return getAdjustedQuads(this.content, node)[0].bounds;
+  }),
+
   /**
    * Get information about a DOM element, identified by a selector.
    * @param {String} selector The CSS selector to get the node (can be an array
    * of selectors to get elements in an iframe).
    * @return {Object} data Null if selector didn't match any node, otherwise:
    * - {String} tagName.
    * - {String} namespaceURI.
    * - {Number} numChildren The number of children in the element.
@@ -886,85 +901,69 @@ var TestActorFront = exports.TestActorFr
     ret.guides = {};
     for (let guide of ["top", "right", "bottom", "left"]) {
       ret.guides[guide] = yield this._getGuideStatus(guide);
     }
 
     return ret;
   }),
 
+  /**
+   * Check that the box-model highlighter is currently highlighting the node matching the
+   * given selector.
+   * @param {String} selector
+   * @return {Boolean}
+   */
   assertHighlightedNode: Task.async(function* (selector) {
-    // Taken and tweaked from:
-    // https://github.com/iominh/point-in-polygon-extended/blob/master/src/index.js#L30-L85
-    function isLeft(p0, p1, p2) {
-      let l = ((p1[0] - p0[0]) * (p2[1] - p0[1])) -
-              ((p2[0] - p0[0]) * (p1[1] - p0[1]));
-      return l;
-    }
-    function isInside(point, polygon) {
-      if (polygon.length === 0) {
-        return false;
-      }
-
-      var n = polygon.length;
-      var newPoints = polygon.slice(0);
-      newPoints.push(polygon[0]);
-      var wn = 0; // wn counter
+    let rect = yield this.getNodeRect(selector);
+    return yield this.isNodeRectHighlighted(rect);
+  }),
 
-      // loop through all edges of the polygon
-      for (var i = 0; i < n; i++) {
-        // Accept points on the edges
-        let r = isLeft(newPoints[i], newPoints[i + 1], point);
-        if (r === 0) {
-          return true;
-        }
-        if (newPoints[i][1] <= point[1]) {
-          if (newPoints[i + 1][1] > point[1] && r > 0) {
-            wn++;
-          }
-        } else {
-          if (newPoints[i + 1][1] <= point[1] && r < 0) {
-            wn--;
-          }
-        }
-      }
-      if (wn === 0) {
-        dumpn(JSON.stringify(point) + " is outside of " + JSON.stringify(polygon));
-      }
-      // the point is outside only when this winding number wn===0, otherwise it's inside
-      return wn !== 0;
+  /**
+   * Check that the box-model highlighter is currently highlighting the text node that can
+   * be found at a given index within the list of childNodes of a parent element matching
+   * the given selector.
+   * @param {String} parentSelector
+   * @param {Number} childNodeIndex
+   * @return {Boolean}
+   */
+  assertHighlightedTextNode: Task.async(function* (parentSelector, childNodeIndex) {
+    let rect = yield this.getTextNodeRect(parentSelector, childNodeIndex);
+    return yield this.isNodeRectHighlighted(rect);
+  }),
+
+  /**
+   * Check that the box-model highlighter is currently highlighting the given rect.
+   * @param {Object} rect
+   * @return {Boolean}
+   */
+  isNodeRectHighlighted: Task.async(function* ({ left, top, width, height }) {
+    let {visible, border} = yield this._getBoxModelStatus();
+    let points = border.points;
+    if (!visible) {
+      return false;
     }
 
-    let {visible, border} = yield this._getBoxModelStatus();
-    let points = border.points;
-    if (visible) {
-      // Check that the node is within the box model
-      let { left, top, width, height } = yield this.getNodeRect(selector);
-      let right = left + width;
-      let bottom = top + height;
+    // Check that the node is within the box model
+    let right = left + width;
+    let bottom = top + height;
 
-      // Converts points dictionnary into an array
-      let list = [];
-      for (var i = 1; i <= 4; i++) {
-        let p = points["p" + i];
-        list.push([p.x, p.y]);
-      }
-      points = list;
+    // Converts points dictionnary into an array
+    let list = [];
+    for (let i = 1; i <= 4; i++) {
+      let p = points["p" + i];
+      list.push([p.x, p.y]);
+    }
+    points = list;
 
-      // Check that each point of the node is within the box model
-      if (!isInside([left, top], points) ||
-          !isInside([right, top], points) ||
-          !isInside([right, bottom], points) ||
-          !isInside([left, bottom], points)) {
-        return false;
-      }
-      return true;
-    } else {
-      return false;
-    }
+    // Check that each point of the node is within the box model
+    return isInside([left, top], points) &&
+           isInside([right, top], points) &&
+           isInside([right, bottom], points) &&
+           isInside([left, bottom], points);
   }),
 
   /**
    * Get the coordinate (points attribute) from one of the polygon elements in the
    * box model highlighter.
    */
   _getPointsForRegion: Task.async(function* (region) {
     let d = yield this.getHighlighterNodeAttribute("box-model-" + region, "d");
@@ -1081,8 +1080,54 @@ var TestActorFront = exports.TestActorFr
       points.push(polygon.trim().split(" ").map(i => {
         return i.replace(/M|L/, "").split(",");
       }));
     }
 
     return {d, points};
   })
 });
+
+/**
+ * Check whether a point is included in a polygon.
+ * Taken and tweaked from:
+ * https://github.com/iominh/point-in-polygon-extended/blob/master/src/index.js#L30-L85
+ * @param {Array} point [x,y] coordinates
+ * @param {Array} polygon An array of [x,y] points
+ * @return {Boolean}
+ */
+function isInside(point, polygon) {
+  if (polygon.length === 0) {
+    return false;
+  }
+
+  const n = polygon.length;
+  const newPoints = polygon.slice(0);
+  newPoints.push(polygon[0]);
+  let wn = 0;
+
+  // loop through all edges of the polygon
+  for (let i = 0; i < n; i++) {
+    // Accept points on the edges
+    let r = isLeft(newPoints[i], newPoints[i + 1], point);
+    if (r === 0) {
+      return true;
+    }
+    if (newPoints[i][1] <= point[1]) {
+      if (newPoints[i + 1][1] > point[1] && r > 0) {
+        wn++;
+      }
+    } else if (newPoints[i + 1][1] <= point[1] && r < 0) {
+      wn--;
+    }
+  }
+  if (wn === 0) {
+    dumpn(JSON.stringify(point) + " is outside of " + JSON.stringify(polygon));
+  }
+  // the point is outside only when this winding number wn===0, otherwise it's inside
+  return wn !== 0;
+}
+
+function isLeft(p0, p1, p2) {
+  let l = ((p1[0] - p0[0]) * (p2[1] - p0[1])) -
+          ((p2[0] - p0[0]) * (p1[1] - p0[1]));
+  return l;
+}
--- a/devtools/server/actors/highlighters.js
+++ b/devtools/server/actors/highlighters.js
@@ -8,17 +8,17 @@ const { Ci } = require("chrome");
 
 const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
 const EventEmitter = require("devtools/shared/event-emitter");
 const events = require("sdk/event/core");
 const protocol = require("devtools/shared/protocol");
 const Services = require("Services");
 const { isWindowIncluded } = require("devtools/shared/layout/utils");
 const { highlighterSpec, customHighlighterSpec } = require("devtools/shared/specs/highlighters");
-const { isXUL, isNodeValid } = require("./highlighters/utils/markup");
+const { isXUL } = require("./highlighters/utils/markup");
 const { SimpleOutlineHighlighter } = require("./highlighters/simple-outline");
 
 const HIGHLIGHTER_PICKED_TIMER = 1000;
 const IS_OSX = Services.appinfo.OS === "Darwin";
 
 /**
  * The registration mechanism for highlighters provide a quick way to
  * have modular highlighters, instead of a hard coded list.
@@ -177,19 +177,17 @@ var HighlighterActor = exports.Highlight
    * 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: function (node, options = {}) {
-    if (node && isNodeValid(node.rawNode)) {
-      this._highlighter.show(node.rawNode, options);
-    } else {
+    if (!node || !this._highlighter.show(node.rawNode, options)) {
       this._highlighter.hide();
     }
   },
 
   /**
    * Hide the box model highlighting if it was shown before
    */
   hideBoxModel: function () {
@@ -463,17 +461,17 @@ var CustomHighlighterActor = exports.Cus
    * to run the provided CSS selector on.
    *
    * @param {NodeActor} The node to be highlighted
    * @param {Object} Options for the custom highlighter
    * @return {Boolean} True, if the highlighter has been successfully shown
    * (FF41+)
    */
   show: function (node, options) {
-    if (!node || !isNodeValid(node.rawNode) || !this._highlighter) {
+    if (!node || !this._highlighter) {
       return false;
     }
 
     return this._highlighter.show(node.rawNode, options);
   },
 
   /**
    * Hide the highlighter if it was shown before
--- a/devtools/server/actors/highlighters/auto-refresh.js
+++ b/devtools/server/actors/highlighters/auto-refresh.js
@@ -60,17 +60,17 @@ AutoRefreshHighlighter.prototype = {
    * @param {DOMNode} node
    * @param {Object} options
    *        Object used for passing options
    */
   show: function (node, options = {}) {
     let isSameNode = node === this.currentNode;
     let isSameOptions = this._isSameOptions(options);
 
-    if (!isNodeValid(node) || (isSameNode && isSameOptions)) {
+    if (!this._isNodeValid(node) || (isSameNode && isSameOptions)) {
       return false;
     }
 
     this.options = options;
 
     this._stopRefreshLoop();
     this.currentNode = node;
     this._updateAdjustedQuads();
@@ -82,30 +82,41 @@ AutoRefreshHighlighter.prototype = {
     }
     return shown;
   },
 
   /**
    * Hide the highlighter
    */
   hide: function () {
-    if (!isNodeValid(this.currentNode)) {
+    if (!this._isNodeValid(this.currentNode)) {
       return;
     }
 
     this._hide();
     this._stopRefreshLoop();
     this.currentNode = null;
     this.currentQuads = {};
     this.options = null;
 
     this.emit("hidden");
   },
 
   /**
+   * Whether the current node is valid for this highlighter type.
+   * This is implemented by default to check if the node is an element node. Highlighter
+   * sub-classes should override this method if they want to highlight other node types.
+   * @param {DOMNode} node
+   * @return {Boolean}
+   */
+  _isNodeValid: function (node) {
+    return isNodeValid(node);
+  },
+
+  /**
    * Are the provided options the same as the currently stored options?
    * Returns false if there are no options stored currently.
    */
   _isSameOptions: function (options) {
     if (!this.options) {
       return false;
     }
 
@@ -146,17 +157,17 @@ AutoRefreshHighlighter.prototype = {
     let newQuads = JSON.stringify(this.currentQuads);
     return oldQuads !== newQuads;
   },
 
   /**
    * Update the highlighter if the node has moved since the last update.
    */
   update: function () {
-    if (!isNodeValid(this.currentNode) || !this._hasMoved()) {
+    if (!this._isNodeValid(this.currentNode) || !this._hasMoved()) {
       return;
     }
 
     this._update();
     this.emit("updated");
   },
 
   _show: function () {
--- a/devtools/server/actors/highlighters/box-model.js
+++ b/devtools/server/actors/highlighters/box-model.js
@@ -2,21 +2,27 @@
  * 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 { extend } = require("sdk/core/heritage");
 const { AutoRefreshHighlighter } = require("./auto-refresh");
 const {
-  CanvasFrameAnonymousContentHelper, moveInfobar,
-  getBindingElementAndPseudo, hasPseudoClassLock, getComputedStyle,
-  createSVGNode, createNode, isNodeValid } = require("./utils/markup");
+  CanvasFrameAnonymousContentHelper,
+  createNode,
+  createSVGNode,
+  getBindingElementAndPseudo,
+  hasPseudoClassLock,
+  isNodeValid,
+  moveInfobar,
+} = require("./utils/markup");
 const { setIgnoreLayoutChanges } = require("devtools/shared/layout/utils");
 const inspector = require("devtools/server/actors/inspector");
+const nodeConstants = require("devtools/shared/dom-node-constants");
 
 // Note that the order of items in this array is important because it is used
 // for drawing the BoxModelHighlighter's path elements correctly.
 const BOX_MODEL_REGIONS = ["margin", "border", "padding", "content"];
 const BOX_MODEL_SIDES = ["top", "right", "bottom", "left"];
 // Width of boxmodelhighlighter guides
 const GUIDE_STROKE_WIDTH = 1;
 // FIXME: add ":visited" and ":link" after bug 713106 is fixed
@@ -89,34 +95,23 @@ function BoxModelHighlighter(highlighter
   this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
     this._buildMarkup.bind(this));
 
   /**
    * Optionally customize each region's fill color by adding an entry to the
    * regionFill property: `highlighter.regionFill.margin = "red";
    */
   this.regionFill = {};
-
-  this._currentNode = null;
 }
 
 BoxModelHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
   typeName: "BoxModelHighlighter",
 
   ID_CLASS_PREFIX: "box-model-",
 
-  get currentNode() {
-    return this._currentNode;
-  },
-
-  set currentNode(node) {
-    this._currentNode = node;
-    this._computedStyle = null;
-  },
-
   _buildMarkup: function () {
     let doc = this.win.document;
 
     let highlighterContainer = doc.createElement("div");
     highlighterContainer.className = "highlighter-container";
 
     // Build the root wrapper, used to adapt to the page zoom.
     let rootWrapper = createNode(this.win, {
@@ -253,27 +248,34 @@ BoxModelHighlighter.prototype = extend(A
     return highlighterContainer;
   },
 
   /**
    * Destroy the nodes. Remove listeners.
    */
   destroy: function () {
     AutoRefreshHighlighter.prototype.destroy.call(this);
-
     this.markup.destroy();
-
-    this._currentNode = null;
   },
 
   getElement: function (id) {
     return this.markup.getElement(this.ID_CLASS_PREFIX + id);
   },
 
   /**
+   * Override the AutoRefreshHighlighter's _isNodeValid method to also return true for
+   * text nodes since these can also be highlighted.
+   * @param {DOMNode} node
+   * @return {Boolean}
+   */
+  _isNodeValid: function (node) {
+    return node && (isNodeValid(node) || isNodeValid(node, nodeConstants.TEXT_NODE));
+  },
+
+  /**
    * Show the highlighter on a given node
    */
   _show: function () {
     if (BOX_MODEL_REGIONS.indexOf(this.options.region) == -1) {
       this.options.region = "content";
     }
 
     let shown = this._update();
@@ -306,17 +308,19 @@ BoxModelHighlighter.prototype = extend(A
    * passed as an argument to show(node)).
    * Should be called whenever node size or attributes change
    */
   _update: function () {
     let shown = false;
     setIgnoreLayoutChanges(true);
 
     if (this._updateBoxModel()) {
-      if (!this.options.hideInfoBar) {
+      // Show the infobar only if configured to do so and the node is an element.
+      if (!this.options.hideInfoBar &&
+          this.currentNode.nodeType === this.currentNode.ELEMENT_NODE) {
         this._showInfobar();
       } else {
         this._hideInfobar();
       }
       this._showBoxModel();
       shown = true;
     } else {
       // Nothing to highlight (0px rectangle like a <script> tag for instance)
@@ -514,30 +518,25 @@ BoxModelHighlighter.prototype = extend(A
              "L" + np3.x + "," + np3.y + " " +
              "L" + np2.x + "," + np2.y + " " +
              "L" + np1.x + "," + np1.y;
     }
 
     return path;
   },
 
+  /**
+   * Can the current node be highlighted? Does it have quads.
+   * @return {Boolean}
+   */
   _nodeNeedsHighlighting: function () {
-    let hasNoQuads = !this.currentQuads.margin.length &&
-                     !this.currentQuads.border.length &&
-                     !this.currentQuads.padding.length &&
-                     !this.currentQuads.content.length;
-    if (!isNodeValid(this.currentNode) || hasNoQuads) {
-      return false;
-    }
-
-    if (!this._computedStyle) {
-      this._computedStyle = getComputedStyle(this.currentNode);
-    }
-
-    return this._computedStyle.getPropertyValue("display") !== "none";
+    return this.currentQuads.margin.length ||
+           this.currentQuads.border.length ||
+           this.currentQuads.padding.length ||
+           this.currentQuads.content.length;
   },
 
   _getOuterBounds: function () {
     for (let region of ["margin", "border", "padding", "content"]) {
       let quad = this._getOuterQuad(region);
 
       if (!quad) {
         // Invisible element such as a script tag.
--- a/devtools/server/actors/highlighters/geometry-editor.js
+++ b/devtools/server/actors/highlighters/geometry-editor.js
@@ -1,22 +1,19 @@
 /* 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 { extend } = require("sdk/core/heritage");
 const { AutoRefreshHighlighter } = require("./auto-refresh");
-const {
-  CanvasFrameAnonymousContentHelper, getCSSStyleRules, getComputedStyle,
-  createSVGNode, createNode } = require("./utils/markup");
-
-const { setIgnoreLayoutChanges,
-  getAdjustedQuads } = require("devtools/shared/layout/utils");
+const { CanvasFrameAnonymousContentHelper, getCSSStyleRules, getComputedStyle,
+        createSVGNode, createNode } = require("./utils/markup");
+const { setIgnoreLayoutChanges, getAdjustedQuads } = require("devtools/shared/layout/utils");
 
 const GEOMETRY_LABEL_SIZE = 6;
 
 // List of all DOM Events subscribed directly to the document from the
 // Geometry Editor highlighter
 const DOM_EVENTS = ["mousemove", "mouseup", "pagehide"];
 
 const _dragging = Symbol("geometry/dragging");
--- a/devtools/server/actors/highlighters/utils/markup.js
+++ b/devtools/server/actors/highlighters/utils/markup.js
@@ -116,28 +116,35 @@ function installHelperSheet(win, source,
   let {Style} = require("sdk/stylesheet/style");
   let {attach} = require("sdk/content/mod");
   let style = Style({source, type});
   attach(style, win);
   installedHelperSheets.set(win.document, style);
 }
 exports.installHelperSheet = installHelperSheet;
 
-function isNodeValid(node) {
-  // Is it null or dead?
+/**
+ * Returns true if a DOM node is "valid", where "valid" means that the node isn't a dead
+ * object wrapper, is still attached to a document, and is of a given type.
+ * @param {DOMNode} node
+ * @param {Number} nodeType Optional, defaults to ELEMENT_NODE
+ * @return {Boolean}
+ */
+function isNodeValid(node, nodeType = Ci.nsIDOMNode.ELEMENT_NODE) {
+  // Is it still alive?
   if (!node || Cu.isDeadWrapper(node)) {
     return false;
   }
 
-  // Is it an element node
-  if (node.nodeType !== node.ELEMENT_NODE) {
+  // Is it of the right type?
+  if (node.nodeType !== nodeType) {
     return false;
   }
 
-  // Is the document inaccessible?
+  // Is its document accessible?
   let doc = node.ownerDocument;
   if (!doc || !doc.defaultView) {
     return false;
   }
 
   // Is the node connected to the document? Using getBindingParent adds
   // support for anonymous elements generated by a node in the document.
   let bindingParent = getRootBindingParent(node);