Bug 886037 - Add a styles actor for the style inspectors. r=jwalker
☠☠ backed out by 678dd0508c82 ☠ ☠
authorDave Camp <dcamp@mozilla.com>
Tue, 23 Jul 2013 15:51:58 -0700
changeset 142055 986f7c642b9f207f8e88633d92c7a0d4360b7a24
parent 142054 53a0468d797ff6a5c446bc32da63c68dd176a869
child 142056 678dd0508c822e467eea8e5348ebfa2aa92429f4
push id25078
push userryanvm@gmail.com
push dateFri, 09 Aug 2013 23:28:36 +0000
treeherdermozilla-central@c5946a8bcd5b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker
bugs886037
milestone26.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 886037 - Add a styles actor for the style inspectors. r=jwalker
toolkit/devtools/server/actors/inspector.js
toolkit/devtools/server/actors/styles.js
toolkit/devtools/server/tests/mochitest/Makefile.in
toolkit/devtools/server/tests/mochitest/inspector-styles-data.css
toolkit/devtools/server/tests/mochitest/inspector-styles-data.html
toolkit/devtools/server/tests/mochitest/inspector-traversal-data.html
toolkit/devtools/server/tests/mochitest/test_styles-applied.html
toolkit/devtools/server/tests/mochitest/test_styles-computed.html
toolkit/devtools/server/tests/mochitest/test_styles-matched.html
toolkit/devtools/server/tests/mochitest/test_styles-modify.html
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -55,16 +55,17 @@ const {Cc, Ci, Cu} = require("chrome");
 const protocol = require("devtools/server/protocol");
 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 {PageStyleActor} = require("devtools/server/actors/styles");
 
 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
 
 const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
 
 const HELPER_SHEET = "." + HIDDEN_CLASS + " { visibility: hidden !important }";
 
 Cu.import("resource://gre/modules/Services.jsm");
@@ -2075,48 +2076,87 @@ var InspectorActor = protocol.ActorClass
     let deferred = promise.defer();
     this._walkerPromise = deferred.promise;
 
     let window = this.window;
 
     var domReady = () => {
       let tabActor = this.tabActor;
       window.removeEventListener("DOMContentLoaded", domReady, true);
-      deferred.resolve(WalkerActor(this.conn, window.document, tabActor._tabbrowser, options));
+      this.walker = WalkerActor(this.conn, window.document, tabActor._tabbrowser, options);
+      deferred.resolve(this.walker);
     };
 
     if (window.document.readyState === "loading") {
       window.addEventListener("DOMContentLoaded", domReady, true);
     } else {
       domReady();
     }
 
     return this._walkerPromise;
   }, {
     request: {},
     response: {
       walker: RetVal("domwalker")
     }
+  }),
+
+  getPageStyle: method(function() {
+    if (this._pageStylePromise) {
+      return this._pageStylePromise;
+    }
+
+    this._pageStylePromise = this.getWalker().then(walker => {
+      return PageStyleActor(this);
+    });
+    return this._pageStylePromise;
+  }, {
+    request: {},
+    response: { pageStyle: RetVal("pagestyle") }
   })
 });
 
 /**
  * Client side of the inspector actor, which is used to create
  * inspector-related actors, including the walker.
  */
 var InspectorFront = exports.InspectorFront = protocol.FrontClass(InspectorActor, {
   initialize: function(client, tabForm) {
     protocol.Front.prototype.initialize.call(this, client);
     this.actorID = tabForm.inspectorActor;
 
     // XXX: This is the first actor type in its hierarchy to use the protocol
     // library, so we're going to self-own on the client side for now.
     client.addActorPool(this);
     this.manage(this);
-  }
+  },
+
+  getWalker: protocol.custom(function() {
+    return this._getWalker().then(walker => {
+      this.walker = walker;
+      return walker;
+    });
+  }, {
+    impl: "_getWalker"
+  }),
+
+  getPageStyle: protocol.custom(function() {
+    return this._getPageStyle().then(pageStyle => {
+      // We need a walker to understand node references from the
+      // node style.
+      if (this.walker) {
+        return pageStyle;
+      }
+      return this.getWalker().then(() => {
+        return pageStyle;
+      });
+    });
+  }, {
+    impl: "_getPageStyle"
+  })
 });
 
 function documentWalker(node, whatToShow=Ci.nsIDOMNodeFilter.SHOW_ALL) {
   return new DocumentWalker(node, whatToShow, whitespaceTextFilter, false);
 }
 
 // Exported for test purposes.
 exports._documentWalker = documentWalker;
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/styles.js
@@ -0,0 +1,757 @@
+/* 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} = require("chrome");
+const protocol = require("devtools/server/protocol");
+const {Arg, Option, method, RetVal, types} = protocol;
+const events = require("sdk/event/core");
+const object = require("sdk/util/object");
+const { Class } = require("sdk/core/heritage");
+
+loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic);
+loader.lazyGetter(this, "DOMUtils", () => Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils));
+
+// The PageStyle actor flattens the DOM CSS objects a little bit, merging
+// Rules and their Styles into one actor.  For elements (which have a style
+// but no associated rule) we fake a rule with the following style id.
+const ELEMENT_STYLE = 100;
+exports.ELEMENT_STYLE = ELEMENT_STYLE;
+
+// Predeclare the domnode actor type for use in requests.
+types.addActorType("domnode");
+
+/**
+ * DOM Nodes returned by the style actor will be owned by the DOM walker
+ * for the connection.
+  */
+types.addLifetime("walker", "walker");
+
+/**
+ * When asking for the styles applied to a node, we return a list of
+ * appliedstyle json objects that lists the rules that apply to the node
+ * and which element they were inherited from (if any).
+ */
+types.addDictType("appliedstyle", {
+  rule: "domstylerule#actorid",
+  inherited: "nullable:domnode#actorid"
+});
+
+types.addDictType("matchedselector", {
+  rule: "domstylerule#actorid",
+  selector: "string",
+  value: "string",
+  status: "number"
+});
+
+/**
+ * The PageStyle actor lets the client look at the styles on a page, as
+ * they are applied to a given node.
+ */
+var PageStyleActor = protocol.ActorClass({
+  typeName: "pagestyle",
+
+  /**
+   * Create a PageStyleActor.
+   *
+   * @param inspector
+   *    The InspectorActor that owns this PageStyleActor.
+   *
+   * @constructor
+   */
+  initialize: function(inspector) {
+    protocol.Actor.prototype.initialize.call(this, null);
+    this.inspector = inspector;
+    if (!this.inspector.walker) {
+      throw Error("The inspector's WalkerActor must be created before " +
+                   "creating a PageStyleActor.");
+    }
+    this.walker = inspector.walker;
+    this.cssLogic = new CssLogic;
+
+    // Stores the association of DOM objects -> actors
+    this.refMap = new Map;
+  },
+
+  get conn() this.inspector.conn,
+
+  /**
+   * Return or create a StyleRuleActor for the given item.
+   * @param item Either a CSSStyleRule or a DOM element.
+   */
+  _styleRef: function(item) {
+    if (this.refMap.has(item)) {
+      return this.refMap.get(item);
+    }
+    let actor = StyleRuleActor(this, item);
+    this.manage(actor);
+    this.refMap.set(item, actor);
+
+    return actor;
+  },
+
+  /**
+   * Return or create a StyleSheetActor for the given
+   * nsIDOMCSSStyleSheet
+   */
+  _sheetRef: function(sheet) {
+    if (this.refMap.has(sheet)) {
+      return this.refMap.get(sheet);
+    }
+    let actor = StyleSheetActor(this, sheet);
+    this.manage(actor);
+    this.refMap.set(sheet, actor);
+
+    return actor;
+  },
+
+  /**
+   * Get the computed style for a node.
+   *
+   * @param NodeActor node
+   * @param object options
+   *   `filter`: A string filter that affects the "matched" handling.
+   *     'user': Include properties from user style sheets.
+   *     'ua': Include properties from user and user-agent sheets.
+   *     Default value is 'ua'
+   *   `markMatched`: true if you want the 'matched' property to be added
+   *     when a computed property has been modified by a style included
+   *     by `filter`.
+   *   `onlyMatched`: true if unmatched properties shouldn't be included.
+   *
+   * @returns a JSON blob with the following form:
+   *   {
+   *     "property-name": {
+   *       value: "property-value",
+   *       priority: "!important" <optional>
+   *       matched: <true if there are matched selectors for this value>
+   *     },
+   *     ...
+   *   }
+   */
+  getComputed: method(function(node, options) {
+    let win = node.rawNode.ownerDocument.defaultView;
+    let ret = Object.create(null);
+
+    this.cssLogic.sourceFilter = options.filter || CssLogic.FILTER.UA;
+    this.cssLogic.highlight(node.rawNode);
+    let computed = this.cssLogic._computedStyle;
+
+    Array.prototype.forEach.call(computed, name => {
+      let matched = undefined;
+      ret[name] = {
+        value: computed.getPropertyValue(name),
+        priority: computed.getPropertyPriority(name) || undefined
+      };
+    });
+
+    if (options.markMatched || options.onlyMatched) {
+      let matched = this.cssLogic.hasMatchedSelectors(Object.keys(ret));
+      for (let key in ret) {
+        if (matched[key]) {
+          ret[key].matched = options.markMatched ? true : undefined
+        } else if (options.onlyMatched) {
+          delete ret[key];
+        }
+      }
+    }
+
+    return ret;
+  }, {
+    request: {
+      node: Arg(0, "domnode"),
+      markMatched: Option(1, "boolean"),
+      onlyMatched: Option(1, "boolean"),
+      filter: Option(1, "string"),
+    },
+    response: {
+      computed: RetVal("json")
+    }
+  }),
+
+  /**
+   * Get a list of selectors that match a given property for a node.
+   *
+   * @param NodeActor node
+   * @param string property
+   * @param object options
+   *   `filter`: A string filter that affects the "matched" handling.
+   *     'user': Include properties from user style sheets.
+   *     'ua': Include properties from user and user-agent sheets.
+   *     Default value is 'ua'
+   *
+   * @returns a JSON object with the following form:
+   *   {
+   *     // An ordered list of rules that apply
+   *     matched: [{
+   *       rule: <rule actorid>,
+   *       sourceText: <string>, // The source of the selector, relative
+   *                             // to the node in question.
+   *       selector: <string>, // the selector ID that matched
+   *       value: <string>, // the value of the property
+   *       status: <int>,
+   *         // The status of the match - high numbers are better placed
+   *         // to provide styling information:
+   *         // 3: Best match, was used.
+   *         // 2: Matched, but was overridden.
+   *         // 1: Rule from a parent matched.
+   *         // 0: Unmatched (never returned in this API)
+   *     }, ...],
+   *
+   *     // The full form of any domrule referenced.
+   *     rules: [ <domrule>, ... ], // The full form of any domrule referenced
+   *
+   *     // The full form of any sheets referenced.
+   *     sheets: [ <domsheet>, ... ]
+   *  }
+   */
+  getMatchedSelectors: method(function(node, property, options) {
+    this.cssLogic.sourceFilter = options.filter || CssLogic.FILTER.UA;
+    this.cssLogic.highlight(node.rawNode);
+
+    let walker = node.parent();
+
+    let rules = new Set;
+    let sheets = new Set;
+
+    let matched = [];
+    let propInfo = this.cssLogic.getPropertyInfo(property);
+    for (let selectorInfo of propInfo.matchedSelectors) {
+      let cssRule = selectorInfo.selector._cssRule;
+      let domRule = cssRule.sourceElement || cssRule._domRule;
+
+      let rule = this._styleRef(domRule);
+      rules.add(rule);
+
+      matched.push({
+        rule: rule,
+        sourceText: this.getSelectorSource(selectorInfo, node.rawNode),
+        selector: selectorInfo.selector.text,
+        value: selectorInfo.value,
+        status: selectorInfo.status
+      });
+    }
+
+    this.expandSets(rules, sheets);
+
+    return {
+      matched: matched,
+      rules: [...rules],
+      sheets: [...sheets],
+    }
+  }, {
+    request: {
+      node: Arg(0, "domnode"),
+      property: Arg(1, "string"),
+      filter: Option(2, "string")
+    },
+    response: RetVal(types.addDictType("matchedselectorresponse", {
+      rules: "array:domstylerule",
+      sheets: "array:domsheet",
+      matched: "array:matchedselector"
+    }))
+  }),
+
+  // Get a selector source for a CssSelectorInfo relative to a given
+  // node.
+  getSelectorSource: function(selectorInfo, relativeTo) {
+    let result = selectorInfo.selector.text;
+    if (selectorInfo.elementStyle) {
+      let source = selectorInfo.sourceElement;
+      if (source === relativeTo) {
+        result = "this";
+      } else {
+        result = CssLogic.getShortName(source);
+      }
+      result += ".style"
+    }
+    return result;
+  },
+
+  /**
+   * Get the set of styles that apply to a given node.
+   * @param NodeActor node
+   * @param string property
+   * @param object options
+   *   `filter`: A string filter that affects the "matched" handling.
+   *     'user': Include properties from user style sheets.
+   *     'ua': Include properties from user and user-agent sheets.
+   *     Default value is 'ua'
+   *   `inherited`: Include styles inherited from parent nodes.
+   *   `matchedSeletors`: Include an array of specific selectors that
+   *     caused this rule to match its node.
+   */
+  getApplied: method(function(node, options) {
+    let entries = [];
+
+    this.addElementRules(node.rawNode, undefined, options, entries);
+
+    if (options.inherited) {
+      let parent = this.walker.parentNode(node);
+      while (parent && parent.rawNode.nodeType != Ci.nsIDOMNode.DOCUMENT_NODE) {
+        this.addElementRules(parent.rawNode, parent, options, entries);
+        parent = this.walker.parentNode(parent);
+      }
+    }
+
+    if (options.matchedSelectors) {
+      for (let entry of entries) {
+        if (entry.rule.type === ELEMENT_STYLE) {
+          continue;
+        }
+
+        let domRule = entry.rule.rawRule;
+        let selectors = CssLogic.getSelectors(domRule);
+        let element = entry.inherited ? entry.inherited.rawNode : node.rawNode;
+        entry.matchedSelectors = [];
+        for (let i = 0; i < selectors.length; i++) {
+          if (DOMUtils.selectorMatchesElement(element, domRule, i)) {
+            entry.matchedSelectors.push(selectors[i]);
+          }
+        }
+
+      }
+    }
+
+    let rules = new Set;
+    let sheets = new Set;
+    entries.forEach(entry => rules.add(entry.rule));
+    this.expandSets(rules, sheets);
+
+    return {
+      entries: entries,
+      rules: [...rules],
+      sheets: [...sheets]
+    }
+  }, {
+    request: {
+      node: Arg(0, "domnode"),
+      inherited: Option(1, "boolean"),
+      matchedSelectors: Option(1, "boolean"),
+      filter: Option(1, "string")
+    },
+    response: RetVal(types.addDictType("appliedStylesReturn", {
+      entries: "array:appliedstyle",
+      rules: "array:domstylerule",
+      sheets: "array:domsheet"
+    }))
+  }),
+
+  _hasInheritedProps: function(style) {
+    return Array.prototype.some.call(style, prop => {
+      return DOMUtils.isInheritedProperty(prop);
+    });
+  },
+
+  /**
+   * Helper function for getApplied, adds all the rules from a given
+   * element.
+   */
+  addElementRules: function(element, inherited, options, rules)
+  {
+    let elementStyle = this._styleRef(element);
+
+    if (!inherited || this._hasInheritedProps(element.style)) {
+      rules.push({
+        rule: elementStyle,
+        inherited: inherited,
+      });
+    }
+
+    // Get the styles that apply to the element.
+    let domRules = DOMUtils.getCSSStyleRules(element);
+
+    // getCSSStyleRules returns ordered from least-specific to
+    // most-specific.
+    for (let i = domRules.Count() - 1; i >= 0; i--) {
+      let domRule = domRules.GetElementAt(i);
+
+      let isSystem = !CssLogic.isContentStylesheet(domRule.parentStyleSheet);
+
+      if (isSystem && options.filter != CssLogic.FILTER.UA) {
+        continue;
+      }
+
+      if (inherited) {
+        // Don't include inherited rules if none of its properties
+        // are inheritable.
+        let hasInherited = Array.prototype.some.call(domRule.style, prop => {
+          return DOMUtils.isInheritedProperty(prop);
+        });
+        if (!hasInherited) {
+          continue;
+        }
+      }
+
+      let ruleActor = this._styleRef(domRule);
+      rules.push({
+        rule: ruleActor,
+        inherited: inherited,
+      });
+    }
+  },
+
+  /**
+   * Expand Sets of rules and sheets to include all parent rules and sheets.
+   */
+  expandSets: function(ruleSet, sheetSet) {
+    // Sets include new items in their iteration
+    for (let rule of ruleSet) {
+      if (rule.rawRule.parentRule) {
+        let parent = this._styleRef(rule.rawRule.parentRule);
+        if (!ruleSet.has(parent)) {
+          ruleSet.add(parent);
+        }
+      }
+      if (rule.rawRule.parentStyleSheet) {
+        let parent = this._sheetRef(rule.rawRule.parentStyleSheet);
+        if (!sheetSet.has(parent)) {
+          sheetSet.add(parent);
+        }
+      }
+    }
+
+    for (let sheet of sheetSet) {
+      if (sheet.rawSheet.parentStyleSheet) {
+        let parent = this._sheetRef(sheet.rawSheet.parentStyleSheet);
+        if (!sheetSet.has(parent)) {
+          sheetSet.add(parent);
+        }
+      }
+    }
+  }
+});
+exports.PageStyleActor = PageStyleActor;
+
+/**
+ * Front object for the PageStyleActor
+ */
+var PageStyleFront = protocol.FrontClass(PageStyleActor, {
+  initialize: function(conn, form, ctx, detail) {
+    protocol.Front.prototype.initialize.call(this, conn, form, ctx, detail);
+    this.inspector = this.parent();
+  },
+
+  destroy: function() {
+    protocol.Front.prototype.destroy.call(this);
+  },
+
+  get walker() {
+    return this.inspector.walker;
+  },
+
+  getMatchedSelectors: protocol.custom(function(node, property, options) {
+    return this._getMatchedSelectors(node, property, options).then(ret => {
+      return ret.matched;
+    });
+  }, {
+    impl: "_getMatchedSelectors"
+  }),
+
+  getApplied: protocol.custom(function(node, options={}) {
+    return this._getApplied(node, options).then(ret => {
+      return ret.entries;
+    });
+  }, {
+    impl: "_getApplied"
+  })
+});
+
+/**
+ * Actor representing an nsIDOMCSSStyleSheet.
+ */
+var StyleSheetActor = protocol.ActorClass({
+  typeName: "domsheet",
+
+  initialize: function(pageStyle, sheet) {
+    protocol.Front.prototype.initialize.call(this);
+    this.pageStyle = pageStyle;
+    this.rawSheet = sheet;
+  },
+
+  get conn() this.pageStyle.conn,
+
+  form: function(detail) {
+    if (detail === "actorid") {
+      return this.actorID;
+    }
+
+    return {
+      actor: this.actorID,
+
+      // href stores the uri of the sheet
+      href: this.rawSheet.href,
+
+      // nodeHref stores the URI of the document that
+      // included the sheet.
+      nodeHref: this.rawSheet.ownerNode ? this.rawSheet.ownerNode.ownerDocument.location.href : undefined,
+
+      system: !CssLogic.isContentStylesheet(this.rawSheet),
+      disabled: this.rawSheet.disabled ? true : undefined
+    }
+  }
+});
+
+/**
+ * Front for the StyleSheetActor.
+ */
+var StyleSheetFront = protocol.FrontClass(StyleSheetActor, {
+  initialize: function(conn, form, ctx, detail) {
+    protocol.Front.prototype.initialize.call(this, conn, form, ctx, detail);
+  },
+
+  form: function(form, detail) {
+    if (detail === "actorid") {
+      this.actorID = form;
+      return;
+    }
+    this.actorID = form.actorID;
+    this._form = form;
+  },
+
+  get href() this._form.href,
+  get nodeHref() this._form.nodeHref,
+  get disabled() !!this._form.disabled,
+  get isSystem() this._form.system
+});
+
+
+// Predeclare the domstylerule actor type
+types.addActorType("domstylerule");
+
+/**
+ * An actor that represents a CSS style object on the protocol.
+ *
+ * We slightly flatten the CSSOM for this actor, it represents
+ * both the CSSRule and CSSStyle objects in one actor.  For nodes
+ * (which have a CSSStyle but no CSSRule) we create a StyleRuleActor
+ * with a special rule type (100).
+ */
+var StyleRuleActor = protocol.ActorClass({
+  typeName: "domstylerule",
+  initialize: function(pageStyle, item) {
+    protocol.Actor.prototype.initialize.call(this, null);
+    this.pageStyle = pageStyle;
+    this.rawStyle = item.style;
+
+    if (item instanceof (Ci.nsIDOMCSSRule)) {
+      this.type = item.type;
+      this.rawRule = item;
+      if (this.rawRule instanceof Ci.nsIDOMCSSStyleRule && this.rawRule.parentStyleSheet) {
+        this.line = DOMUtils.getRuleLine(this.rawRule);
+      }
+    } else {
+      // Fake a rule
+      this.type = ELEMENT_STYLE;
+      this.rawNode = item;
+      this.rawRule = {
+        style: item.style,
+        toString: function() "[element rule " + this.style + "]"
+      }
+    }
+  },
+
+  get conn() this.pageStyle.conn,
+
+  // Objects returned by this actor are owned by the PageStyleActor
+  // to which this rule belongs.
+  get marshallPool() this.pageStyle,
+
+  toString: function() "[StyleRuleActor for " + this.rawRule + "]",
+
+  form: function(detail) {
+    if (detail === "actorid") {
+      return this.actorID;
+    }
+
+    let form = {
+      actor: this.actorID,
+      type: this.type,
+      line: this.line || undefined,
+    };
+
+    if (this.rawRule.parentRule) {
+      form.parentRule = this.pageStyle._styleRef(this.rawRule.parentRule).actorID;
+    }
+    if (this.rawRule.parentStyleSheet) {
+      form.parentStyleSheet = this.pageStyle._sheetRef(this.rawRule.parentStyleSheet).actorID;
+    }
+
+    switch (this.type) {
+      case Ci.nsIDOMCSSRule.STYLE_RULE:
+        form.selectors = CssLogic.getSelectors(this.rawRule);
+        form.cssText = this.rawStyle.cssText || "";
+        break;
+      case ELEMENT_STYLE:
+        // Elements don't have a parent stylesheet, and therefore
+        // don't have an associated URI.  Provide a URI for
+        // those.
+        form.href = this.rawNode.ownerDocument.location.href;
+        form.cssText = this.rawStyle.cssText || "";
+        break;
+      case Ci.nsIDOMCSSRule.CHARSET_RULE:
+        form.encoding = this.rawRule.encoding;
+        break;
+      case Ci.nsIDOMCSSRule.IMPORT_RULE:
+        form.href = this.rawRule.href;
+        break;
+      case Ci.nsIDOMCSSRule.MEDIA_RULE:
+        form.media = [];
+        for (let i = 0, n = this.rawRule.media.length; i < n; i++) {
+          form.media.push(this.rawRule.media.item(i));
+        }
+        break;
+    }
+
+    return form;
+  },
+
+  /**
+   * Modify a rule's properties.  Passed an array of modifications:
+   * {
+   *   type: "set",
+   *   name: <string>,
+   *   value: <string>,
+   *   priority: <optional string>
+   * }
+   *  or
+   * {
+   *   type: "remove",
+   *   name: <string>,
+   * }
+   *
+   * @returns the rule with updated properties
+   */
+  modifyProperties: method(function(modifications) {
+    for (let mod of modifications) {
+      if (mod.type === "set") {
+        this.rawStyle.setProperty(mod.name, mod.value, mod.priority || "");
+      } else if (mod.type === "remove") {
+        this.rawStyle.removeProperty(mod.name);
+      }
+    }
+    return this;
+  }, {
+    request: { modifications: Arg(0, "array:json") },
+    response: { rule: RetVal("domstylerule") }
+  })
+});
+
+/**
+ * Front for the StyleRule actor.
+ */
+var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
+  initialize: function(client, form, ctx, detail) {
+    protocol.Front.prototype.initialize.call(this, client, form, ctx, detail);
+  },
+
+  destroy: function() {
+    protocol.Front.prototype.destroy.call(this);
+  },
+
+  form: function(form, detail) {
+    if (detail === "actorid") {
+      this.actorID = form;
+      return;
+    }
+    this.actorID = form.actor;
+    this._form = form;
+    if (this._mediaText) {
+      this._mediaText = null;
+    }
+  },
+
+  /**
+   * Return a new RuleModificationList for this node.
+   */
+  startModifyingProperties: function() {
+  return new RuleModificationList(this);
+  },
+
+  get type() this._form.type,
+  get line() this._form.line || -1,
+  get cssText() {
+    return this._form.cssText;
+  },
+  get selectors() {
+    return this._form.selectors;
+  },
+  get media() {
+    return this._form.media;
+  },
+  get mediaText() {
+    if (!this._form.media) {
+      return null;
+    }
+    if (this._mediaText) {
+      return this._mediaText;
+    }
+    this._mediaText = this.media.join(", ");
+    return this._mediaText;
+  },
+
+  get parentRule() {
+    return this.conn.getActor(this._form.parentRule);
+  },
+
+  get parentStyleSheet() {
+    return this.conn.getActor(this._form.parentStyleSheet);
+  },
+
+  get element() {
+    return this.conn.getActor(this._form.element);
+  },
+
+  get href() {
+    if (this._form.href) {
+      return this._form.href;
+    }
+    let sheet = this.parentStyleSheet;
+    return sheet.href || sheet.nodeHref;
+  },
+
+  // Only used for testing, please keep it that way.
+  _rawStyle: function() {
+    if (!this.conn._transport._serverConnection) {
+      console.warn("Tried to use rawNode on a remote connection.");
+      return null;
+    }
+    let actor = this.conn._transport._serverConnection.getActor(this.actorID);
+    if (!actor) {
+      return null;
+    }
+    return actor.rawStyle;
+  }
+});
+
+/**
+ * Convenience API for building a list of attribute modifications
+ * for the `modifyAttributes` request.
+ */
+var RuleModificationList = Class({
+  initialize: function(rule) {
+    this.rule = rule;
+    this.modifications = [];
+  },
+
+  apply: function() {
+    return this.rule.modifyProperties(this.modifications);
+  },
+  setProperty: function(name, value, priority) {
+    this.modifications.push({
+      type: "set",
+      name: name,
+      value: value,
+      priority: priority
+    });
+  },
+  removeProperty: function(name) {
+    this.modifications.push({
+      type: "remove",
+      name: name
+    });
+  }
+});
+
--- a/toolkit/devtools/server/tests/mochitest/Makefile.in
+++ b/toolkit/devtools/server/tests/mochitest/Makefile.in
@@ -9,27 +9,33 @@ srcdir		= @srcdir@
 VPATH		= @srcdir@
 relativesrcdir	= @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_CHROME_FILES	= \
 	inspector-helpers.js \
 	inspector-traversal-data.html \
+	inspector-styles-data.html \
+	inspector-styles-data.css \
 	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 \
 	test_inspector-release.html \
 	test_inspector-remove.html \
 	test_inspector-reload.html \
 	test_inspector-retain.html \
 	test_inspector-pseudoclass-lock.html \
 	test_inspector-traversal.html \
+	test_styles-applied.html \
+	test_styles-computed.html \
+	test_styles-matched.html \
+	test_styles-modify.html \
 	test_unsafeDereference.html \
 	nonchrome_unsafeDereference.html \
 	$(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/inspector-styles-data.css
@@ -0,0 +1,3 @@
+.external-rule {
+  cursor: crosshair;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/inspector-styles-data.html
@@ -0,0 +1,55 @@
+<html>
+<script>
+  window.onload = () => {
+    window.opener.postMessage('ready', '*')
+  }
+</script>
+<style>
+  .inheritable-rule {
+    font-size: 15px;
+  }
+  .uninheritable-rule {
+    background-color: #f06;
+  }
+  @media screen {
+    #mediaqueried {
+      background-color: #f06;
+    }
+  }
+</style>
+<link type="text/css" rel="stylesheet" href="inspector-styles-data.css"></link>
+<body>
+  <h1>Style Actor Tests</h1>
+  <!-- Inheritance checks -->
+  <div id="inheritable-rule-uninheritable-style" class="inheritable-rule" style="background-color: purple">
+    <div id="inheritable-rule-inheritable-style" class="inheritable-rule" style="color: blue">
+      <div id="uninheritable-rule-uninheritable-style" class="uninheritable-rule" style="background-color: green">
+        <div id="uninheritable-rule-inheritable-style" class="uninheritable-rule" style="color: red">
+          <div id="test-node">
+            Here is the test node.
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <!-- Computed checks -->
+  <div id="computed-parent" class="external-rule inheritable-rule uninheritable-rule" style="color: red;">
+    <div id="computed-test-node" class="external-rule">
+      Here is the test node.
+    </div>
+  </div>
+
+  <!-- Matched checks -->
+  <div id="matched-parent" class="external-rule inheritable-rule uninheritable-rule" style="color: red;">
+    <div id="matched-test-node" style="font-size: 10px" class="external-rule">
+      Here is the test node.
+    </div>
+  </div>
+
+  <div id="mediaqueried">
+    Screen mediaqueried.
+  </div>
+
+</body>
+</html>
--- a/toolkit/devtools/server/tests/mochitest/inspector-traversal-data.html
+++ b/toolkit/devtools/server/tests/mochitest/inspector-traversal-data.html
@@ -9,17 +9,17 @@
       iframe.setAttribute("id", "childFrame");
       iframe.onload = function() {
         window.opener.postMessage('ready', '*')
       };
       iframe.src = data;
       body.appendChild(iframe);
     }
   </script>
-<body>
+<body style="background-color:white">
   <h1>Inspector Actor Tests</h1>
   <span id="longstring">longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong</span>
   <span id="shortstring">short</span>
   <span id="empty"></span>
   <div id="longlist" data-test="exists">
     <div id="a">a</div>
     <div id="b">b</div>
     <div id="c">c</div>
@@ -46,9 +46,9 @@
     <div id="x">x</div>
     <div id="y">y</div>
     <div id="z">z</div>
   </div>
   <div id="longlist-sibling">
     <div id="longlist-sibling-firstchild"></div>
   </div>
 </body>
-</html>
\ No newline at end of file
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_styles-applied.html
@@ -0,0 +1,153 @@
+<!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");
+
+window.onload = function() {
+  SimpleTest.waitForExplicitFinish();
+  runNextTest();
+}
+
+var gWalker = null;
+var gStyles = null;
+var gClient = null;
+
+addTest(function setup() {
+  let url = document.getElementById("inspectorContent").href;
+  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 => {
+      ok(walker, "getWalker() should return an actor.");
+      gClient = client;
+      gWalker = walker;
+      return inspector.getPageStyle();
+    }).then(styles => {
+      gStyles = styles;
+    }).then(runNextTest));
+  });
+});
+
+addTest(function inheritedUserStyles() {
+  let node = node;
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#test-node").then(node => {
+    return gStyles.getApplied(node, { inherited: true, filter: "user" });
+  }).then(applied => {
+    ok(!applied[0].inherited, "Entry 0 should be uninherited");
+    is(applied[0].rule.type, 100, "Entry 0 should be an element style");
+    ok(!!applied[0].rule.href, "Element styles should have a URL");
+    is(applied[0].rule.cssText, "", "Entry 0 should be an empty style");
+
+    is(applied[1].inherited.id, "uninheritable-rule-inheritable-style",
+       "Entry 1 should be inherited from the parent");
+    is(applied[1].rule.type, 100, "Entry 1 should be an element style");
+    is(applied[1].rule.cssText, "color: red;", "Entry 1 should have the expected cssText");
+
+    is(applied[2].inherited.id, "inheritable-rule-inheritable-style",
+       "Entry 2 should be inherited from the parent's parent");
+    is(applied[2].rule.type, 100, "Entry 2 should be an element style");
+    is(applied[2].rule.cssText, "color: blue;", "Entry 2 should have the expected cssText");
+
+    is(applied[3].inherited.id, "inheritable-rule-inheritable-style",
+       "Entry 3 should be inherited from the parent's parent");
+    is(applied[3].rule.type, 1, "Entry 3 should be a rule style");
+    is(applied[3].rule.cssText, "font-size: 15px;", "Entry 3 should have the expected cssText");
+    ok(!applied[3].matchedSelectors, "Shouldn't get matchedSelectors with this request.");
+
+    is(applied[4].inherited.id, "inheritable-rule-uninheritable-style",
+       "Entry 4 should be inherited from the parent's parent");
+    is(applied[4].rule.type, 1, "Entry 4 should be an rule style");
+    is(applied[4].rule.cssText, "font-size: 15px;", "Entry 4 should have the expected cssText");
+    ok(!applied[4].matchedSelectors, "Shouldn't get matchedSelectors with this request.");
+
+    is(applied.length, 5, "Should have 5 rules.");
+  }).then(runNextTest));
+});
+
+addTest(function inheritedSystemStyles() {
+  let node = node;
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#test-node").then(node => {
+    return gStyles.getApplied(node, { inherited: true, filter: "ua" });
+  }).then(applied => {
+    // If our system stylesheets are prone to churn, this might be a fragile
+    // test.  If you're here because of that I apologize, file a bug
+    // and we can find a different way to test.
+
+    ok(!applied[1].inherited, "Entry 1 should not be inherited");
+    ok(!applied[1].rule.parentStyleSheet.system, "Entry 1 should be a system style");
+    is(applied[1].rule.type, 1, "Entry 1 should be a rule style");
+
+    is(applied.length, 7, "Should have 7 rules.");
+  }).then(runNextTest));
+});
+
+addTest(function noInheritedStyles() {
+  let node = node;
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#test-node").then(node => {
+    return gStyles.getApplied(node, { inherited: false, filter: "user" });
+  }).then(applied => {
+    ok(!applied[0].inherited, "Entry 0 should be uninherited");
+    is(applied[0].rule.type, 100, "Entry 0 should be an element style");
+    is(applied[0].rule.cssText, "", "Entry 0 should be an empty style");
+    is(applied.length, 1, "Should have 1 rule.");
+  }).then(runNextTest));
+});
+
+addTest(function matchedSelectors() {
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#test-node").then(node => {
+    return gStyles.getApplied(node, {
+      inherited: true, filter: "user", matchedSelectors: true
+    });
+  }).then(applied => {
+    is(applied[3].matchedSelectors[0], ".inheritable-rule", "Entry 3 should have a matched selector");
+    is(applied[4].matchedSelectors[0], ".inheritable-rule", "Entry 4 should have a matched selector");
+  }).then(runNextTest));
+});
+
+addTest(function testMediaQuery() {
+  let node = node;
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#mediaqueried").then(node => {
+    return gStyles.getApplied(node, {
+      inherited: false, filter: "user", matchedSelectors: true
+    });
+  }).then(applied => {
+    is(applied[1].rule.type, 1, "Entry 1 is a rule style");
+    is(applied[1].rule.parentRule.type, 4, "Entry 1's parent rule is a media rule");
+    is(applied[1].rule.parentRule.media[0], "screen", "Entry 1's parent rule has the expected medium");
+  }).then(runNextTest));
+});
+
+addTest(function cleanup() {
+  delete gStyles;
+  delete gWalker;
+  delete gClient;
+  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-styles-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_styles-computed.html
@@ -0,0 +1,142 @@
+<!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");
+
+window.onload = function() {
+  SimpleTest.waitForExplicitFinish();
+  runNextTest();
+}
+
+var gWalker = null;
+var gStyles = null;
+var gClient = null;
+
+addTest(function setup() {
+  let url = document.getElementById("inspectorContent").href;
+  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 => {
+      ok(walker, "getWalker() should return an actor.");
+      gClient = client;
+      gWalker = walker;
+      return inspector.getPageStyle();
+    }).then(styles => {
+      gStyles = styles;
+    }).then(runNextTest));
+  });
+});
+
+addTest(function testComputed() {
+  let localNode = gInspectee.querySelector("#computed-test-node");
+  let elementStyle = null;
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#computed-test-node").then(node => {
+    return gStyles.getComputed(node, {});
+  }).then(computed => {
+    // Test a smattering of properties that include some system-defined
+    // props, some props that were defined in this node's stylesheet,
+    // and some default props.
+    is(computed["white-space"].value, "normal", "Default value should appear");
+    is(computed["display"].value, "block", "System stylesheet item should appear");
+    is(computed["cursor"].value, "crosshair", "Included stylesheet rule should appear");
+    is(computed["color"].value, "rgb(255, 0, 0)", "Inherited style attribute should appear");
+    is(computed["font-size"].value, "15px", "Inherited inline rule should appear");
+
+    // We didn't request markMatched, so these shouldn't be set
+    ok(!computed["cursor"].matched, "Didn't ask for matched, shouldn't get it");
+    ok(!computed["color"].matched, "Didn't ask for matched, shouldn't get it");
+    ok(!computed["font-size"].matched, "Didn't ask for matched, shouldn't get it");
+  }).then(runNextTest));
+});
+
+addTest(function testComputedUserMatched() {
+  let localNode = gInspectee.querySelector("#computed-test-node");
+  let elementStyle = null;
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#computed-test-node").then(node => {
+    return gStyles.getComputed(node, { filter: "user", markMatched: true });
+  }).then(computed => {
+    ok(!computed["white-space"].matched, "Default style shouldn't match");
+    ok(!computed["display"].matched, "Only user styles should match");
+    ok(computed["cursor"].matched, "Asked for matched, should get it");
+    ok(computed["color"].matched, "Asked for matched, should get it");
+    ok(computed["font-size"].matched, "Asked for matched, should get it");
+  }).then(runNextTest));
+});
+
+addTest(function testComputedSystemMatched() {
+  let localNode = gInspectee.querySelector("#computed-test-node");
+  let elementStyle = null;
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#computed-test-node").then(node => {
+    return gStyles.getComputed(node, { filter: "ua", markMatched: true });
+  }).then(computed => {
+    ok(!computed["white-space"].matched, "Default style shouldn't match");
+    ok(computed["display"].matched, "System stylesheets should match");
+    ok(computed["cursor"].matched, "Asked for matched, should get it");
+    ok(computed["color"].matched, "Asked for matched, should get it");
+    ok(computed["font-size"].matched, "Asked for matched, should get it");
+  }).then(runNextTest));
+});
+
+addTest(function testComputedUserOnlyMatched() {
+  let localNode = gInspectee.querySelector("#computed-test-node");
+  let elementStyle = null;
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#computed-test-node").then(node => {
+    return gStyles.getComputed(node, { filter: "user", onlyMatched: true });
+  }).then(computed => {
+    ok(!("white-space" in computed), "Default style shouldn't exist");
+    ok(!("display" in computed), "System stylesheets shouldn't exist");
+    ok(("cursor" in computed), "User items should exist.");
+    ok(("color" in computed), "User items should exist.");
+    ok(("font-size" in computed), "User items should exist.");
+  }).then(runNextTest));
+});
+
+addTest(function testComputedSystemOnlyMatched() {
+  let localNode = gInspectee.querySelector("#computed-test-node");
+  let elementStyle = null;
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#computed-test-node").then(node => {
+    return gStyles.getComputed(node, { filter: "ua", onlyMatched: true });
+  }).then(computed => {
+    ok(!("white-space" in computed), "Default style shouldn't exist");
+    ok(("display" in computed), "System stylesheets should exist");
+    ok(("cursor" in computed), "User items should exist.");
+    ok(("color" in computed), "User items should exist.");
+    ok(("font-size" in computed), "User items should exist.");
+  }).then(runNextTest));
+});
+
+addTest(function cleanup() {
+  delete gStyles;
+  delete gWalker;
+  delete gClient;
+  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-styles-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_styles-matched.html
@@ -0,0 +1,101 @@
+<!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 {CssLogic} = devtools.require("devtools/styleinspector/css-logic");
+
+window.onload = function() {
+  SimpleTest.waitForExplicitFinish();
+  runNextTest();
+}
+
+var gWalker = null;
+var gStyles = null;
+var gClient = null;
+
+addTest(function setup() {
+  let url = document.getElementById("inspectorContent").href;
+  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 => {
+      ok(walker, "getWalker() should return an actor.");
+      gClient = client;
+      gWalker = walker;
+      return inspector.getPageStyle();
+    }).then(styles => {
+      gStyles = styles;
+    }).then(runNextTest));
+  });
+});
+
+addTest(function testMatchedStyles() {
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#matched-test-node").then(node => {
+    return gStyles.getMatchedSelectors(node, "font-size", {});
+  }).then(matched => {
+    is(matched[0].sourceText, "this.style", "First match comes from the element style");
+    is(matched[0].selector, "@element.style", "Element style has a special selector");
+    is(matched[0].value, "10px", "First match has the expected value");
+    is(matched[0].status, CssLogic.STATUS.BEST, "First match is the best match")
+    is(matched[0].rule.type, 100, "First match is an element style");
+    is(matched[0].rule.href, gInspectee.defaultView.location.href, "Node style comes from this document")
+
+    is(matched[1].sourceText, ".inheritable-rule", "Second match comes from a rule");
+    is(matched[1].selector, ".inheritable-rule", "Second style has a selector");
+    is(matched[1].value, "15px", "Second match has the expected value");
+    is(matched[1].status, CssLogic.STATUS.PARENT_MATCH, "Second match is from the parent")
+    is(matched[1].rule.parentStyleSheet.href, null, "Inline stylesheet shouldn't have an href");
+    is(matched[1].rule.parentStyleSheet.nodeHref, gInspectee.defaultView.location.href, "Inline stylesheet's nodeHref should match the current document");
+    ok(!matched[1].rule.parentStyleSheet.system, "Inline stylesheet shouldn't be a system stylesheet.");
+  }).then(runNextTest));
+});
+
+addTest(function testSystemStyles() {
+  let testNode = null;
+
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#matched-test-node").then(node => {
+    testNode = node;
+    return gStyles.getMatchedSelectors(testNode, "display", { filter: "user" });
+  }).then(matched => {
+    is(matched.length, 0, "No user selectors apply to this rule.");
+    return gStyles.getMatchedSelectors(testNode, "display", { filter: "ua" });
+  }).then(matched => {
+    is(matched[0].selector, "div", "Should match system div selector");
+    is(matched[0].value, "block");
+  }).then(runNextTest));
+});
+
+addTest(function cleanup() {
+  delete gStyles;
+  delete gWalker;
+  delete gClient;
+  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-styles-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_styles-modify.html
@@ -0,0 +1,103 @@
+<!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");
+
+window.onload = function() {
+  SimpleTest.waitForExplicitFinish();
+  runNextTest();
+}
+
+var gWalker = null;
+var gStyles = null;
+var gClient = null;
+
+addTest(function setup() {
+  let url = document.getElementById("inspectorContent").href;
+  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 => {
+      ok(walker, "getWalker() should return an actor.");
+      gClient = client;
+      gWalker = walker;
+      return inspector.getPageStyle();
+    }).then(styles => {
+      gStyles = styles;
+    }).then(runNextTest));
+  });
+});
+
+addTest(function modifyProperties() {
+  let localNode = gInspectee.querySelector("#inheritable-rule-inheritable-style");
+  let elementStyle = null;
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#inheritable-rule-inheritable-style").then(node => {
+    return gStyles.getApplied(node, { inherited: false, filter: "user" });
+  }).then(applied => {
+    elementStyle = applied[0].rule;
+    is(elementStyle.cssText, localNode.style.cssText, "Got expected css text");
+
+    // Will start with "color:blue"
+    let changes = elementStyle.startModifyingProperties();
+
+    // Change an existing property...
+    changes.setProperty("color", "black");
+    // Create a new property
+    changes.setProperty("background-color", "green");
+
+    // Create a new property and then change it immediately.
+    changes.setProperty("border", "1px solid black");
+    changes.setProperty("border", "2px solid black");
+
+    return changes.apply();
+  }).then(() => {
+    is(elementStyle.cssText, "color: black; background-color: green; border: 2px solid black;", "Should have expected cssText");
+    is(elementStyle.cssText, localNode.style.cssText, "Local node and style front match.");
+
+    // Remove all the properties
+    let changes = elementStyle.startModifyingProperties();
+    changes.removeProperty("color");
+    changes.removeProperty("background-color");
+    changes.removeProperty("border");
+
+    return changes.apply();
+  }).then(() => {
+    is(elementStyle.cssText, "", "Should have expected cssText");
+    is(elementStyle.cssText, localNode.style.cssText, "Local node and style front match.");
+  }).then(runNextTest));
+});
+
+addTest(function cleanup() {
+  delete gStyles;
+  delete gWalker;
+  delete gClient;
+  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-styles-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>