Bug 1323618 - Allow locking off of psuedo-classes through inIDOMUtils. r?heycam draft
authorJared Wein <jwein@mozilla.com>
Thu, 12 Jan 2017 12:10:07 -0500
changeset 459734 7599e042756956c500de47a76b5d1bbec72726cf
parent 459680 048240a074e841c425a4da4707cf8e353074ec1d
child 541997 6e91367688f1458542fb6117c9848499982b6619
push id41331
push userbmo:jaws@mozilla.com
push dateThu, 12 Jan 2017 17:52:34 +0000
reviewersheycam
bugs1323618
milestone53.0a1
Bug 1323618 - Allow locking off of psuedo-classes through inIDOMUtils. r?heycam MozReview-Commit-ID: DppYTmILpwy
devtools/server/actors/inspector.js
devtools/server/tests/mochitest/inspector-traversal-data.html
devtools/server/tests/mochitest/test_inspector-pseudoclass-lock.html
devtools/shared/specs/inspector.js
dom/base/Element.cpp
dom/base/Element.h
layout/inspector/inDOMUtils.cpp
layout/inspector/inIDOMUtils.idl
--- a/devtools/server/actors/inspector.js
+++ b/devtools/server/actors/inspector.js
@@ -1750,16 +1750,18 @@ var WalkerActor = protocol.ActorClassWit
    *
    * @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.
+   *    `enabled`: False if the pseudo-class should be locked
+   *      to 'off'. Defaults to true.
    *
    * @returns An empty packet.  A "pseudoClassLock" mutation will
    *    be queued for any changed nodes.
    */
   addPseudoClassLock: function (node, pseudo, options = {}) {
     if (isNodeDead(node)) {
       return;
     }
@@ -1767,43 +1769,45 @@ var WalkerActor = protocol.ActorClassWit
     // There can be only one node locked per pseudo, so dismiss all existing
     // ones
     for (let locked of this._activePseudoClassLocks) {
       if (DOMUtils.hasPseudoClassLock(locked.rawNode, pseudo)) {
         this._removePseudoClassLock(locked, pseudo);
       }
     }
 
-    this._addPseudoClassLock(node, pseudo);
+    let enabled = options.enabled === undefined ||
+                  options.enabled;
+    this._addPseudoClassLock(node, pseudo, enabled);
 
     if (!options.parents) {
       return;
     }
 
     let walker = this.getDocumentWalker(node.rawNode);
     let cur;
     while ((cur = walker.parentNode())) {
       let curNode = this._ref(cur);
-      this._addPseudoClassLock(curNode, pseudo);
+      this._addPseudoClassLock(curNode, pseudo, enabled);
     }
   },
 
   _queuePseudoClassMutation: function (node) {
     this.queueMutation({
       target: node.actorID,
       type: "pseudoClassLock",
       pseudoClassLocks: node.writePseudoClassLocks()
     });
   },
 
-  _addPseudoClassLock: function (node, pseudo) {
+  _addPseudoClassLock: function (node, pseudo, enabled) {
     if (node.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
       return false;
     }
-    DOMUtils.addPseudoClassLock(node.rawNode, pseudo);
+    DOMUtils.addPseudoClassLock(node.rawNode, pseudo, enabled);
     this._activePseudoClassLocks.add(node);
     this._queuePseudoClassMutation(node);
     return true;
   },
 
   _installHelperSheet: function (node) {
     if (!this.installedHelpers) {
       this.installedHelpers = new WeakMap();
--- a/devtools/server/tests/mochitest/inspector-traversal-data.html
+++ b/devtools/server/tests/mochitest/inspector-traversal-data.html
@@ -1,8 +1,9 @@
+<!DOCTYPE html>
 <html>
 <head>
   <meta charset="UTF-8">
   <title>Inspector Traversal Test Data</title>
   <style type="text/css">
     #pseudo::before {
       content: "before";
     }
--- a/devtools/server/tests/mochitest/test_inspector-pseudoclass-lock.html
+++ b/devtools/server/tests/mochitest/test_inspector-pseudoclass-lock.html
@@ -55,26 +55,31 @@ function checkChange(change, expectation
   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) {
+  for (let i = 0; i < expectation.pseudos.length; i++) {
+    let pseudo = expectation.pseudos[i];
+    let enabled = expectation.enabled === undefined ? true : expectation.enabled[i];
     ok(target.hasPseudoClassLock(pseudo), "Expect lock: " + pseudo);
-    ok(DOMUtils.hasPseudoClassLock(target.rawNode(), pseudo), "Expect lock in dom: " + pseudo);
+    let rawNode = target.rawNode();
+    ok(DOMUtils.hasPseudoClassLock(rawNode, pseudo), "Expect lock in dom: " + pseudo);
+
+    is(rawNode.matches(pseudo), enabled,
+       `Target should match pseudoclass, '${pseudo}', if enabled (with .matches())`);
   }
 
   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]);
@@ -88,17 +93,17 @@ addTest(function testPseudoClassLock() {
     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.length, 1, "Should get one mutation");
       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.
@@ -145,16 +150,28 @@ addTest(function testPseudoClassLock() {
         pseudos: []
       },
       {
         nodeName: 'HTML',
         pseudos: []
       }];
       checkMutations(mutations, expectedMutations);
     }).then(() => {
+      gWalker.addPseudoClassLock(nodeFront, ":hover", {enabled: false});
+      return promiseOnce(gWalker, "mutations");
+    }).then(mutations => {
+      is(mutations.length, 1, "Should get one mutation");
+      is(mutations[0].target, nodeFront, "Should be the node we tried to apply to");
+      checkChange(mutations[0], {
+        id: "b",
+        nodeName: "DIV",
+        pseudos: [":hover", ":active"],
+        enabled: [false, true]
+      });
+    }).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));
   });
 });
--- a/devtools/shared/specs/inspector.js
+++ b/devtools/shared/specs/inspector.js
@@ -229,17 +229,18 @@ const walkerSpec = generateActorSpec({
       response: {
         list: RetVal("array:array:string")
       }
     },
     addPseudoClassLock: {
       request: {
         node: Arg(0, "domnode"),
         pseudoClass: Arg(1),
-        parents: Option(2)
+        parents: Option(2),
+        enabled: Option(2, "boolean"),
       },
       response: {}
     },
     hideNode: {
       request: { node: Arg(0, "domnode") }
     },
     unhideNode: {
       request: { node: Arg(0, "domnode") }
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -341,102 +341,110 @@ Element::Blur(mozilla::ErrorResult& aErr
   if (win && fm) {
     aError = fm->ClearFocus(win);
   }
 }
 
 EventStates
 Element::StyleStateFromLocks() const
 {
-  EventStates locks = LockedStyleStates();
-  EventStates state = mState | locks;
-
-  if (locks.HasState(NS_EVENT_STATE_VISITED)) {
+  StyleStateLocks locksAndValues = LockedStyleStates();
+  EventStates locks = locksAndValues.mLocks;
+  EventStates values = locksAndValues.mValues;
+  EventStates state = (mState & ~locks) | (locks & values);
+
+  if (state.HasState(NS_EVENT_STATE_VISITED)) {
     return state & ~NS_EVENT_STATE_UNVISITED;
   }
-  if (locks.HasState(NS_EVENT_STATE_UNVISITED)) {
+  if (state.HasState(NS_EVENT_STATE_UNVISITED)) {
     return state & ~NS_EVENT_STATE_VISITED;
   }
+
   return state;
 }
 
-EventStates
+Element::StyleStateLocks
 Element::LockedStyleStates() const
 {
-  EventStates* locks =
-    static_cast<EventStates*>(GetProperty(nsGkAtoms::lockedStyleStates));
+  StyleStateLocks* locks =
+    static_cast<StyleStateLocks*>(GetProperty(nsGkAtoms::lockedStyleStates));
   if (locks) {
     return *locks;
   }
-  return EventStates();
+  return StyleStateLocks();
 }
 
 void
 Element::NotifyStyleStateChange(EventStates aStates)
 {
   nsIDocument* doc = GetComposedDoc();
   if (doc) {
     nsIPresShell *presShell = doc->GetShell();
     if (presShell) {
       nsAutoScriptBlocker scriptBlocker;
       presShell->ContentStateChanged(doc, this, aStates);
     }
   }
 }
 
 void
-Element::LockStyleStates(EventStates aStates)
+Element::LockStyleStates(EventStates aStates, bool aEnabled)
 {
-  EventStates* locks = new EventStates(LockedStyleStates());
-
-  *locks |= aStates;
+  StyleStateLocks* locks = new StyleStateLocks(LockedStyleStates());
+
+  locks->mLocks |= aStates;
+  if (aEnabled) {
+    locks->mValues |= aStates;
+  } else {
+    locks->mValues &= ~aStates;
+  }
 
   if (aStates.HasState(NS_EVENT_STATE_VISITED)) {
-    *locks &= ~NS_EVENT_STATE_UNVISITED;
+    locks->mLocks &= ~NS_EVENT_STATE_UNVISITED;
   }
   if (aStates.HasState(NS_EVENT_STATE_UNVISITED)) {
-    *locks &= ~NS_EVENT_STATE_VISITED;
+    locks->mLocks &= ~NS_EVENT_STATE_VISITED;
   }
 
   SetProperty(nsGkAtoms::lockedStyleStates, locks,
-              nsINode::DeleteProperty<EventStates>);
+              nsINode::DeleteProperty<StyleStateLocks>);
   SetHasLockedStyleStates();
 
   NotifyStyleStateChange(aStates);
 }
 
 void
 Element::UnlockStyleStates(EventStates aStates)
 {
-  EventStates* locks = new EventStates(LockedStyleStates());
-
-  *locks &= ~aStates;
-
-  if (locks->IsEmpty()) {
+  StyleStateLocks* locks = new StyleStateLocks(LockedStyleStates());
+
+  locks->mLocks &= ~aStates;
+
+  if (locks->mLocks.IsEmpty()) {
     DeleteProperty(nsGkAtoms::lockedStyleStates);
     ClearHasLockedStyleStates();
     delete locks;
   }
   else {
     SetProperty(nsGkAtoms::lockedStyleStates, locks,
-                nsINode::DeleteProperty<EventStates>);
+                nsINode::DeleteProperty<StyleStateLocks>);
   }
 
   NotifyStyleStateChange(aStates);
 }
 
 void
 Element::ClearStyleStateLocks()
 {
-  EventStates locks = LockedStyleStates();
+  StyleStateLocks locks = LockedStyleStates();
 
   DeleteProperty(nsGkAtoms::lockedStyleStates);
   ClearHasLockedStyleStates();
 
-  NotifyStyleStateChange(locks);
+  NotifyStyleStateChange(locks.mLocks);
 }
 
 bool
 Element::GetBindingURL(nsIDocument *aDocument, css::URLValue **aResult)
 {
   // If we have a frame the frame has already loaded the binding.  And
   // otherwise, don't do anything else here unless we're dealing with
   // XUL or an HTML element that may have a plugin-related overlay
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -240,24 +240,36 @@ public:
   {
     if (!HasLockedStyleStates()) {
       return mState;
     }
     return StyleStateFromLocks();
   }
 
   /**
+   * StyleStateLocks is used to specify which event states should be locked,
+   * and whether they should be locked to on or off.
+   */
+  struct StyleStateLocks {
+    // mLocks tracks which event states should be locked.
+    EventStates mLocks;
+    // mValues tracks if the locked state should be on or off.
+    EventStates mValues;
+  };
+
+  /**
    * The style state locks applied to this element.
    */
-  EventStates LockedStyleStates() const;
+  StyleStateLocks LockedStyleStates() const;
 
   /**
    * Add a style state lock on this element.
+   * aEnabled is the value to lock the given state bits to.
    */
-  void LockStyleStates(EventStates aStates);
+  void LockStyleStates(EventStates aStates, bool aEnabled);
 
   /**
    * Remove a style state lock on this element.
    */
   void UnlockStyleStates(EventStates aStates);
 
   /**
    * Clear all style state locks on this element.
--- a/layout/inspector/inDOMUtils.cpp
+++ b/layout/inspector/inDOMUtils.cpp
@@ -1249,27 +1249,29 @@ inDOMUtils::GetCSSPseudoElementNames(uin
     ret[i] = ToNewUnicode(nsDependentAtomString(array[i]));
   }
   *aNames = ret;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 inDOMUtils::AddPseudoClassLock(nsIDOMElement *aElement,
-                               const nsAString &aPseudoClass)
+                               const nsAString &aPseudoClass,
+                               bool aEnabled,
+                               uint8_t aArgc)
 {
   EventStates state = GetStatesForPseudoClass(aPseudoClass);
   if (state.IsEmpty()) {
     return NS_OK;
   }
 
   nsCOMPtr<mozilla::dom::Element> element = do_QueryInterface(aElement);
   NS_ENSURE_ARG_POINTER(element);
 
-  element->LockStyleStates(state);
+  element->LockStyleStates(state, aArgc > 0 ? aEnabled : true);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 inDOMUtils::RemovePseudoClassLock(nsIDOMElement *aElement,
                                   const nsAString &aPseudoClass)
 {
@@ -1295,17 +1297,17 @@ inDOMUtils::HasPseudoClassLock(nsIDOMEle
   if (state.IsEmpty()) {
     *_retval = false;
     return NS_OK;
   }
 
   nsCOMPtr<mozilla::dom::Element> element = do_QueryInterface(aElement);
   NS_ENSURE_ARG_POINTER(element);
 
-  EventStates locks = element->LockedStyleStates();
+  EventStates locks = element->LockedStyleStates().mLocks;
 
   *_retval = locks.HasAllStates(state);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 inDOMUtils::ClearPseudoClassLocks(nsIDOMElement *aElement)
 {
--- a/layout/inspector/inIDOMUtils.idl
+++ b/layout/inspector/inIDOMUtils.idl
@@ -180,18 +180,21 @@ interface inIDOMUtils : nsISupports
    * @param {unsigned long} aCount the number of items returned
    * @param {wstring[]} aNames the names
    */
   void getCSSPseudoElementNames([optional] out unsigned long aCount,
                                 [retval, array, size_is(aCount)] out wstring aNames);
 
   // pseudo-class style locking methods. aPseudoClass must be a valid pseudo-class
   // selector string, e.g. ":hover". ":any-link" and non-event-state
-  // pseudo-classes are ignored.
-  void addPseudoClassLock(in nsIDOMElement aElement, in DOMString aPseudoClass);
+  // pseudo-classes are ignored. aEnabled sets whether the psuedo-class
+  // should be locked to on or off.
+  [optional_argc] void addPseudoClassLock(in nsIDOMElement aElement,
+                                          in DOMString aPseudoClass,
+                                          [optional] in boolean aEnabled);
   void removePseudoClassLock(in nsIDOMElement aElement, in DOMString aPseudoClass);
   bool hasPseudoClassLock(in nsIDOMElement aElement, in DOMString aPseudoClass);
   void clearPseudoClassLocks(in nsIDOMElement aElement);
 
   /**
    * Parse CSS and update the style sheet in place.
    *
    * @param DOMCSSStyleSheet aSheet