Bug 877316 - Add pseudo-class lock functionality to the inspector actor. r=harth
authorDave Camp <dcamp@mozilla.com>
Mon, 10 Jun 2013 21:18:41 -0700
changeset 135970 4de2ded917f1aa191be6f1f7f3d8b13834a53b32
parent 135969 b2db738f75d8cae8cf236479d558e18f6ba4f5b4
child 135971 19a51caf7582121d8799d102d5ed604e4365b2cd
push id1779
push userdcamp@campd.org
push dateFri, 21 Jun 2013 22:07:18 +0000
treeherderfx-team@deb4d87d1684 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersharth
bugs877316
milestone24.0a1
Bug 877316 - Add pseudo-class lock functionality to the inspector actor. r=harth
toolkit/devtools/server/actors/inspector.js
toolkit/devtools/server/tests/mochitest/Makefile.in
toolkit/devtools/server/tests/mochitest/inspector-helpers.js
toolkit/devtools/server/tests/mochitest/test_inspector-pseudoclass-lock.html
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -47,16 +47,18 @@ const protocol = require("devtools/serve
 const {Arg, Option, method, RetVal, types} = protocol;
 const {LongStringActor, ShortLongString} = require("devtools/server/actors/string");
 const promise = require("sdk/core/promise");
 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 PSEUDO_CLASSES = [":hover", ":active", ":focus"];
+
 Cu.import("resource://gre/modules/Services.jsm");
 
 exports.register = function(handle) {
   handle.addTabActor(InspectorActor, "inspectorActor");
 };
 
 exports.unregister = function(handle) {
   handle.removeTabActor(InspectorActor);
@@ -133,17 +135,19 @@ var NodeActor = protocol.ActorClass({
       nodeName: this.rawNode.nodeName,
       numChildren: this.rawNode.childNodes.length,
 
       // doctype attributes
       name: this.rawNode.name,
       publicId: this.rawNode.publicId,
       systemId: this.rawNode.systemId,
 
-      attrs: this.writeAttrs()
+      attrs: this.writeAttrs(),
+
+      pseudoClassLocks: this.writePseudoClassLocks(),
     };
 
     if (this.rawNode.ownerDocument &&
         this.rawNode.ownerDocument.documentElement === this.rawNode) {
       form.isDocumentElement = true;
     }
 
     if (this.rawNode.nodeValue) {
@@ -163,16 +167,30 @@ var NodeActor = protocol.ActorClass({
   writeAttrs: function() {
     if (!this.rawNode.attributes) {
       return undefined;
     }
     return [{namespace: attr.namespace, name: attr.name, value: attr.value }
             for (attr of this.rawNode.attributes)];
   },
 
+  writePseudoClassLocks: function() {
+    if (this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
+      return undefined;
+    }
+    let ret = undefined;
+    for (let pseudo of PSEUDO_CLASSES) {
+      if (DOMUtils.hasPseudoClassLock(this.rawNode, pseudo)) {
+        ret = ret || [];
+        ret.push(pseudo);
+      }
+    }
+    return ret;
+  },
+
   /**
    * Returns a LongStringActor with the node's value.
    */
   getNodeValue: method(function() {
     return new LongStringActor(this.conn, this.rawNode.nodeValue || "");
   }, {
     request: {},
     response: {
@@ -284,16 +302,18 @@ let NodeFront = protocol.FrontClass(Node
           name: change.attributeName,
           namespace: change.attributeNamespace,
           value: change.newValue
         });
       }
     } else if (change.type === "characterData") {
       this._form.shortValue = change.newValue;
       this._form.incompleteValue = change.incompleteValue;
+    } else if (change.type === "pseudoClassLock") {
+      this._form.pseudoClassLocks = change.pseudoClassLocks;
     }
   },
 
   // Some accessors to make NodeFront feel more like an nsIDOMNode
 
   get id() this.getAttribute("id"),
 
   get nodeType() this._form.nodeType,
@@ -324,16 +344,21 @@ let NodeFront = protocol.FrontClass(Node
   },
   hasAttribute: function(name) {
     this._cacheAttributes();
     return (name in this._attrMap);
   },
 
   get attributes() this._form.attrs,
 
+  get pseudoClassLocks() this._form.pseudoClassLocks || [],
+  hasPseudoClassLock: function(pseudo) {
+    return this.pseudoClassLocks.some(locked => locked === pseudo);
+  },
+
   getNodeValue: protocol.custom(function() {
     if (!this.incompleteValue) {
       return delayedResolve(new ShortLongString(this.shortValue));
     } else {
       return this._getNodeValue();
     }
   }, {
     impl: "_getNodeValue"
@@ -643,16 +668,17 @@ var WalkerActor = protocol.ActorClass({
    * @param DebuggerServerConnection conn
    *    The server connection.
    */
   initialize: function(conn, document, webProgress, options) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this.rootDoc = document;
     this._refMap = new Map();
     this._pendingMutations = [];
+    this._activePseudoClassLocks = new Set();
 
     // Nodes which have been removed from the client's known
     // ownership tree are considered "orphaned", and stored in
     // this set.
     this._orphaned = new Set();
 
     // The client can tell the walker that it is interested in a node
     // even when it is orphaned with the `retainNode` method.  This
@@ -681,25 +707,31 @@ var WalkerActor = protocol.ActorClass({
     }
   },
 
   toString: function() {
     return "[WalkerActor " + this.actorID + "]";
   },
 
   destroy: function() {
-    protocol.Actor.prototype.destroy.call(this);
+    this.clearPseudoClassLocks();
+    this._activePseudoClassLocks = null;
     this.progressListener.destroy();
     this.rootDoc = null;
+    protocol.Actor.prototype.destroy.call(this);
   },
 
   release: method(function() {}, { release: true }),
 
   unmanage: function(actor) {
     if (actor instanceof NodeActor) {
+      if (this._activePseudoClassLocks &&
+          this._activePseudoClassLocks.has(actor)) {
+        this.clearPsuedoClassLocks(actor);
+      }
       this._refMap.delete(actor.rawNode);
     }
     protocol.Actor.prototype.unmanage.call(this, actor);
   },
 
   _ref: function(node) {
     let actor = this._refMap.get(node);
     if (actor) return actor;
@@ -1139,16 +1171,141 @@ var WalkerActor = protocol.ActorClass({
       selector: Arg(1)
     },
     response: {
       list: RetVal("domnodelist")
     }
   }),
 
   /**
+   * Add a pseudo-class lock to a node.
+   *
+   * @param NodeActor node
+   * @param string pseudo
+   *    A pseudoclass: ':hover', ':active', ':focus'
+   * @param options
+   *    Options object:
+   *    `parents`: True if the pseudo-class should be added
+   *      to parent nodes.
+   *
+   * @returns An empty packet.  A "pseudoClassLock" mutation will
+   *    be queued for any changed nodes.
+   */
+  addPseudoClassLock: method(function(node, pseudo, options={}) {
+    this._addPseudoClassLock(node, pseudo);
+
+    if (!options.parents) {
+      return;
+    }
+
+    let walker = documentWalker(node.rawNode);
+    let cur;
+    while ((cur = walker.parentNode())) {
+      let curNode = this._ref(cur);
+      this._addPseudoClassLock(curNode, pseudo);
+    }
+  }, {
+    request: {
+      node: Arg(0, "domnode"),
+      pseudoClass: Arg(1),
+      parents: Option(2)
+    },
+    response: {}
+  }),
+
+  _queuePseudoClassMutation: function(node) {
+    this.queueMutation({
+      target: node.actorID,
+      type: "pseudoClassLock",
+      pseudoClassLocks: node.writePseudoClassLocks()
+    });
+  },
+
+  _addPseudoClassLock: function(node, pseudo) {
+    if (node.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
+      return false;
+    }
+    DOMUtils.addPseudoClassLock(node.rawNode, pseudo);
+    this._activePseudoClassLocks.add(node);
+    this._queuePseudoClassMutation(node);
+    return true;
+  },
+
+  /**
+   * Remove a pseudo-class lock from a node.
+   *
+   * @param NodeActor node
+   * @param string pseudo
+   *    A pseudoclass: ':hover', ':active', ':focus'
+   * @param options
+   *    Options object:
+   *    `parents`: True if the pseudo-class should be removed
+   *      from parent nodes.
+   *
+   * @returns An empty response.  "pseudoClassLock" mutations
+   *    will be emitted for any changed nodes.
+   */
+  removePseudoClassLock: method(function(node, pseudo, options={}) {
+    this._removePseudoClassLock(node, pseudo);
+
+    if (!options.parents) {
+      return;
+    }
+
+    let walker = documentWalker(node.rawNode);
+    let cur;
+    while ((cur = walker.parentNode())) {
+      let curNode = this._ref(cur);
+      this._removePseudoClassLock(curNode, pseudo);
+    }
+  }, {
+    request: {
+      node: Arg(0, "domnode"),
+      pseudoClass: Arg(1),
+      parents: Option(2)
+    },
+    response: {}
+  }),
+
+  _removePseudoClassLock: function(node, pseudo) {
+    if (node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE) {
+      return false;
+    }
+    DOMUtils.removePseudoClassLock(node.rawNode, pseudo);
+    if (!node.writePseudoClassLocks()) {
+      this._activePseudoClassLocks.delete(node);
+    }
+    this._queuePseudoClassMutation(node);
+    return true;
+  },
+
+  /**
+   * Clear all the pseudo-classes on a given node
+   * or all nodes.
+   */
+  clearPseudoClassLocks: method(function(node) {
+    if (node) {
+      DOMUtils.clearPseudoClassLocks(node.rawNode);
+      this._activePseudoClassLocks.delete(node);
+      this._queuePseudoClassMutation(node);
+    } else {
+      for (let locked of this._activePseudoClassLocks) {
+        DOMUtils.clearPseudoClassLocks(locked.rawNode);
+        this._activePseudoClassLocks.delete(locked);
+        this._queuePseudoClassMutation(locked);
+      }
+    }
+  }, {
+    request: {
+      node: Arg(0, "domnode", { optional: true }),
+    },
+    response: {}
+  }),
+
+  /**
    * Get any pending mutation records.  Must be called by the client after
    * the `new-mutations` notification is received.  Returns an array of
    * mutation records.
    *
    * Mutation records have a basic structure:
    *
    * {
    *   type: attributes|characterData|childList,
--- a/toolkit/devtools/server/tests/mochitest/Makefile.in
+++ b/toolkit/devtools/server/tests/mochitest/Makefile.in
@@ -15,14 +15,15 @@ MOCHITEST_CHROME_FILES	= \
 	inspector-helpers.js \
 	inspector-traversal-data.html \
 	test_inspector-mutations-attr.html \
 	test_inspector-mutations-childlist.html \
 	test_inspector-mutations-frameload.html \
 	test_inspector-mutations-value.html \
 	test_inspector-release.html \
 	test_inspector-retain.html \
+	test_inspector-pseudoclass-lock.html \
 	test_inspector-traversal.html \
 	test_unsafeDereference.html \
 	nonchrome_unsafeDereference.html \
 	$(NULL)
 
 include $(topsrcdir)/config/rules.mk
--- a/toolkit/devtools/server/tests/mochitest/inspector-helpers.js
+++ b/toolkit/devtools/server/tests/mochitest/inspector-helpers.js
@@ -73,16 +73,28 @@ function attachURL(url, callback) {
         });
       });
     }
   }, false);
 
   return cleanup;
 }
 
+function promiseOnce(target, event) {
+  let deferred = Promise.defer();
+  target.on(event, (...args) => {
+    if (args.length === 1) {
+      deferred.resolve(args[0]);
+    } else {
+      deferred.resolve(args);
+    }
+  });
+  return deferred.promise;
+}
+
 function sortOwnershipChildren(children) {
   return children.sort((a, b) => a.name.localeCompare(b.name));
 }
 
 function serverOwnershipSubtree(walker, node) {
   let actor = walker._refMap.get(node);
   if (!actor) {
     return undefined;
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_inspector-pseudoclass-lock.html
@@ -0,0 +1,177 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug </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">
+  <script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
+  <script type="application/javascript;version=1.8">
+Components.utils.import("resource://gre/modules/devtools/Loader.jsm");
+
+const Promise = devtools.require("sdk/core/promise");
+const inspector = devtools.require("devtools/server/actors/inspector");
+const DOMUtils = Components.classes["@mozilla.org/inspector/dom-utils;1"].
+                   getService(Components.interfaces.inIDOMUtils);
+
+const KNOWN_PSEUDOCLASSES = [':hover', ':active', ':focus']
+
+window.onload = function() {
+  SimpleTest.waitForExplicitFinish();
+  runNextTest();
+}
+
+var gInspectee = null;
+var gWalker = null;
+var gClient = null;
+var gCleanupConnection = null;
+
+function setup(callback) {
+  let url = document.getElementById("inspectorContent").href;
+  gCleanupConnection = attachURL(url, function(err, client, tab, doc) {
+    gInspectee = doc;
+    let {InspectorFront} = devtools.require("devtools/server/actors/inspector");
+    let inspector = InspectorFront(client, tab);
+    promiseDone(inspector.getWalker().then(walker => {
+      gClient = client;
+      gWalker = walker;
+    }).then(callback));
+  });
+}
+
+function teardown() {
+  gWalker = null;
+  gClient = null;
+  gInspectee = null;
+  if (gCleanupConnection) {
+    gCleanupConnection();
+    gCleanupConnection = null;
+  }
+}
+
+function checkChange(change, expectation) {
+  is(change.type, "pseudoClassLock", "Expect a pseudoclass lock change.");
+  let target = change.target;
+  if (expectation.id)
+    is(target.id, expectation.id, "Expect a change on node id " + expectation.id);
+  if (expectation.nodeName)
+    is(target.nodeName, expectation.nodeName, "Expect a change on node name " + expectation.nodeName);
+
+  is(target.pseudoClassLocks.length, expectation.pseudos.length,
+     "Expect " + expectation.pseudos.length + " pseudoclass locks.");
+  for (let pseudo of expectation.pseudos) {
+    ok(target.hasPseudoClassLock(pseudo), "Expect lock: " + pseudo);
+    ok(DOMUtils.hasPseudoClassLock(target.rawNode(), pseudo), "Expect lock in dom: " + pseudo);
+  }
+
+  for (let pseudo of KNOWN_PSEUDOCLASSES) {
+    if (!expectation.pseudos.some(expected => pseudo === expected)) {
+      ok(!target.hasPseudoClassLock(pseudo), "Don't expect lock: " + pseudo);
+      ok(!DOMUtils.hasPseudoClassLock(target.rawNode(), pseudo), "Don't expect lock in dom: " + pseudo);
+
+    }
+  }
+}
+
+function checkMutations(mutations, expectations) {
+  is(mutations.length, expectations.length, "Should get the right number of mutations.");
+  for (let i = 0; i < mutations.length; i++) {
+    checkChange(mutations[i] , expectations[i]);
+  }
+}
+
+addTest(function testPseudoClassLock() {
+  let contentNode;
+  let nodeFront;
+  setup(() => {
+    contentNode = gInspectee.querySelector("#b");
+    return promiseDone(gWalker.querySelector(gWalker.rootNode, "#b").then(front => {
+      nodeFront = front;
+      // Lock the pseudoclass alone, no parents.
+      gWalker.addPseudoClassLock(nodeFront, ':active');
+      // Expect a single pseudoClassLock mutation.
+      return promiseOnce(gWalker, "mutations");
+    }).then(mutations => {
+      is(mutations.length, 1, "Should get one mutations");
+      is(mutations[0].target, nodeFront, "Should be the node we tried to apply to");
+      checkChange(mutations[0], {
+        id: "b",
+        nodeName: "DIV",
+        pseudos: [":active"]
+      });
+    }).then(() => {
+      // Now add :hover, this time with parents.
+      gWalker.addPseudoClassLock(nodeFront, ':hover', {parents: true});
+      return promiseOnce(gWalker, "mutations");
+    }).then(mutations => {
+      let expectedMutations = [{
+        id: 'b',
+        nodeName: 'DIV',
+        pseudos: [':hover', ':active'],
+      },
+      {
+        id: 'longlist',
+        nodeName: 'DIV',
+        pseudos: [':hover']
+      },
+      {
+        nodeName: 'BODY',
+        pseudos: [':hover']
+      },
+      {
+        nodeName: 'HTML',
+        pseudos: [':hover']
+      }];
+      checkMutations(mutations, expectedMutations);
+    }).then(() => {
+      // Now remove the :hover on all parents
+      gWalker.removePseudoClassLock(nodeFront, ':hover', {parents: true});
+      return promiseOnce(gWalker, "mutations");
+    }).then(mutations => {
+      let expectedMutations = [{
+        id: 'b',
+        nodeName: 'DIV',
+        // Should still have :active on the original node.
+        pseudos: [':active']
+      },
+      {
+        id: 'longlist',
+        nodeName: 'DIV',
+        pseudos: []
+      },
+      {
+        nodeName: 'BODY',
+        pseudos: []
+      },
+      {
+        nodeName: 'HTML',
+        pseudos: []
+      }];
+      checkMutations(mutations, expectedMutations);
+    }).then(() => {
+      // Now shut down the walker and make sure that clears up the remaining lock.
+      return gWalker.release();
+    }).then(() => {
+      ok(!DOMUtils.hasPseudoClassLock(contentNode, ':active'), "Pseudoclass should have been removed during destruction.");
+      teardown();
+    }).then(runNextTest));
+  });
+});
+
+  </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
+<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>