Bug 1069829 - 2 - Don't use cssPropertyIsValid in the rule-view UI, get the info from the server instead; r?tromey draft
authorPatrick Brosset <pbrosset@mozilla.com>
Fri, 29 Apr 2016 09:30:02 +0200
changeset 364837 9c4888c86bc6f28f53897f2f5482ca4cd6d61d93
parent 364836 e3fd704ec44049132e3ae9f67a2f4b43a38e869a
child 520397 ccc8bdbda4095e179d69c07d3bd95097498c326e
push id17574
push userpbrosset@mozilla.com
push dateMon, 09 May 2016 13:59:09 +0000
reviewerstromey
bugs1069829
milestone49.0a1
Bug 1069829 - 2 - Don't use cssPropertyIsValid in the rule-view UI, get the info from the server instead; r?tromey - StyleRuleActors now parse declarations and send them within the form - Each declaration also contains isValid flag - Parsing is still done client side for old backends (back compat) - Declarations are sent with the form, so updated every time the rule changes - Also made StyleRuleActors send the declarations array for element styles, and made canSetRuleText true for this, since we can simply set the style attribute MozReview-Commit-ID: 2nI4bRyvwwi
devtools/client/inspector/layout/layout.js
devtools/client/inspector/rules/models/rule.js
devtools/client/inspector/rules/models/text-property.js
devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js
devtools/client/inspector/rules/test/browser_rules_add-property_02.js
devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js
devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js
devtools/client/shared/css-parsing-utils.js
devtools/server/actors/styles.js
devtools/server/tests/mochitest/test_styles-modify.html
--- a/devtools/client/inspector/layout/layout.js
+++ b/devtools/client/inspector/layout/layout.js
@@ -34,22 +34,29 @@ function EditingSession(doc, rules) {
   this._rules = rules;
   this._modifications = new Map();
 }
 
 EditingSession.prototype = {
   /**
    * Gets the value of a single property from the CSS rule.
    *
-   * @param rule      The CSS rule
-   * @param property  The name of the property
+   * @param {StyleRuleFront} rule The CSS rule.
+   * @param {String} property The name of the property.
+   * @return {String} The value.
    */
   getPropertyFromRule: function (rule, property) {
+    // Use the parsed declarations in the StyleRuleFront object if available.
+    let index = this.getPropertyIndex(property, rule);
+    if (index !== -1) {
+      return rule.declarations[index].value;
+    }
+
+    // Fallback to parsing the cssText locally otherwise.
     let dummyStyle = this._element.style;
-
     dummyStyle.cssText = rule.cssText;
     return dummyStyle.getPropertyValue(property);
   },
 
   /**
    * Returns the current value for a property as a string or the empty string if
    * no style rules affect the property.
    *
@@ -72,59 +79,101 @@ EditingSession.prototype = {
         return value;
       }
     }
     div.remove();
     return "";
   },
 
   /**
-   * Sets a number of properties on the node. Returns a promise that will be
-   * resolved when the modifications are complete.
-   *
+   * Get the index of a given css property name in a CSS rule.
+   * Or -1, if there are no properties in the rule yet.
+   * @param {String} name The property name.
+   * @param {StyleRuleFront} rule Optional, defaults to the element style rule.
+   * @return {Number} The property index in the rule.
+   */
+  getPropertyIndex: function (name, rule = this._rules[0]) {
+    let elementStyleRule = this._rules[0];
+    if (!elementStyleRule.declarations.length) {
+      return -1;
+    }
+
+    return elementStyleRule.declarations.findIndex(p => p.name === name);
+  },
+
+  /**
+   * Sets a number of properties on the node.
    * @param properties  An array of properties, each is an object with name and
    *                    value properties. If the value is "" then the property
    *                    is removed.
+   * @return {Promise} Resolves when the modifications are complete.
    */
-  setProperties: function (properties) {
-    let modifications = this._rules[0].startModifyingProperties();
+  setProperties: Task.async(function* (properties) {
+    for (let property of properties) {
+      // Get a RuleModificationList or RuleRewriter helper object from the
+      // StyleRuleActor to make changes to CSS properties.
+      // Note that RuleRewriter doesn't support modifying several properties at
+      // once, so we do this in a sequence here.
+      let modifications = this._rules[0].startModifyingProperties();
 
-    for (let property of properties) {
+      // Remember the property so it can be reverted.
       if (!this._modifications.has(property.name)) {
         this._modifications.set(property.name,
           this.getPropertyFromRule(this._rules[0], property.name));
       }
 
-      if (property.value == "") {
-        modifications.removeProperty(-1, property.name);
-      } else {
-        modifications.setProperty(-1, property.name, property.value, "");
+      // Find the index of the property to be changed, or get the next index to
+      // insert the new property at.
+      let index = this.getPropertyIndex(property.name);
+      if (index === -1) {
+        index = this._rules[0].declarations.length;
       }
-    }
 
-    return modifications.apply().then(null, console.error);
-  },
+      if (property.value == "") {
+        modifications.removeProperty(index, property.name);
+      } else {
+        modifications.setProperty(index, property.name, property.value, "");
+      }
+
+      yield modifications.apply();
+    }
+  }),
 
   /**
-   * Reverts all of the property changes made by this instance. Returns a
-   * promise that will be resolved when complete.
+   * Reverts all of the property changes made by this instance.
+   * @return {Promise} Resolves when all properties have been reverted.
    */
-  revert: function () {
-    let modifications = this._rules[0].startModifyingProperties();
+  revert: Task.async(function* () {
+    // Revert each property that we modified previously, one by one. See
+    // setProperties for information about why.
+    for (let [property, value] of this._modifications) {
+      let modifications = this._rules[0].startModifyingProperties();
 
-    for (let [property, value] of this._modifications) {
+      // Find the index of the property to be reverted.
+      let index = this.getPropertyIndex(property);
+
       if (value != "") {
-        modifications.setProperty(-1, property, value, "");
+        // If the property doesn't exist anymore, insert at the beginning of the
+        // rule.
+        if (index === -1) {
+          index = 0;
+        }
+        modifications.setProperty(index, property, value, "");
       } else {
-        modifications.removeProperty(-1, property);
+        // If the property doesn't exist anymore, no need to remove it. It had
+        // not been added after all.
+        if (index === -1) {
+          continue;
+        }
+        modifications.removeProperty(index, property);
       }
+
+      yield modifications.apply();
     }
-
-    return modifications.apply().then(null, console.error);
-  },
+  }),
 
   destroy: function () {
     this._doc = null;
     this._rules = null;
     this._modifications.clear();
   }
 };
 
@@ -337,24 +386,25 @@ LayoutView.prototype = {
         if (property.substring(0, 7) == "border-") {
           let bprop = property.substring(0, property.length - 5) + "style";
           let style = session.getProperty(bprop);
           if (!style || style == "none" || style == "hidden") {
             properties.push({ name: bprop, value: "solid" });
           }
         }
 
-        session.setProperties(properties);
+        session.setProperties(properties).catch(e => console.error(e));
       },
 
       done: (value, commit) => {
         editor.elt.parentNode.classList.remove("layout-editing");
         if (!commit) {
-          session.revert();
-          session.destroy();
+          session.revert().then(() => {
+            session.destroy();
+          }, e => console.error(e));
         }
       }
     }, event);
   },
 
   /**
    * Is the layoutview visible in the sidebar.
    * @return {Boolean}
--- a/devtools/client/inspector/rules/models/rule.js
+++ b/devtools/client/inspector/rules/models/rule.js
@@ -191,17 +191,21 @@ Rule.prototype = {
       this.textProps.splice(ind, 0, prop);
     } else {
       ind = this.textProps.length;
       this.textProps.push(prop);
     }
 
     this.applyProperties((modifications) => {
       modifications.createProperty(ind, name, value, priority);
+      // Now that the rule has been updated, the server might have given us data
+      // that changes the state of the property. Update it now.
+      prop.updateEditor();
     });
+
     return prop;
   },
 
   /**
    * Helper function for applyProperties that is called when the actor
    * does not support as-authored styles.  Store disabled properties
    * in the element style's store.
    */
@@ -236,16 +240,19 @@ Rule.prototype = {
     if (disabledProps.length > 0) {
       disabled.set(this.style, disabledProps);
     } else {
       disabled.delete(this.style);
     }
 
     return modifications.apply().then(() => {
       let cssProps = {};
+      // Note that even though StyleRuleActors normally provide parsed
+      // declarations already, _applyPropertiesNoAuthored is only used when
+      // connected to older backend that do not provide them. So parse here.
       for (let cssProp of parseDeclarations(this.style.authoredText)) {
         cssProps[cssProp.name] = cssProp;
       }
 
       for (let textProp of this.textProps) {
         if (!textProp.enabled) {
           continue;
         }
@@ -428,17 +435,23 @@ Rule.prototype = {
 
   /**
    * Get the list of TextProperties from the style. Needs
    * to parse the style's authoredText.
    */
   _getTextProperties: function () {
     let textProps = [];
     let store = this.elementStyle.store;
-    let props = parseDeclarations(this.style.authoredText, true);
+
+    // Starting with FF49, StyleRuleActors provide parsed declarations.
+    let props = this.style.declarations;
+    if (!props) {
+      props = parseDeclarations(this.style.authoredText, true);
+    }
+
     for (let prop of props) {
       let name = prop.name;
       // If the authored text has an invalid property, it will show up
       // as nameless.  Skip these as we don't currently have a good
       // way to display them.
       if (!name) {
         continue;
       }
--- a/devtools/client/inspector/rules/models/text-property.js
+++ b/devtools/client/inspector/rules/models/text-property.js
@@ -194,18 +194,36 @@ TextProperty.prototype = {
       return true;
     } catch (e) {
       return false;
     }
   },
 
   /**
    * Validate this property. Does it make sense for this value to be assigned
-   * to this property name? This does not apply the property value
+   * to this property name?
    *
    * @return {Boolean} true if the property value is valid, false otherwise.
    */
   isValid: function () {
-    return domUtils.cssPropertyIsValid(this.name, this.value);
+    // Starting with FF49, StyleRuleActors provide a list of parsed
+    // declarations, with data about their validity, but if we don't have this,
+    // compute validity locally (which might not be correct, but better than
+    // nothing).
+    if (!this.rule.domRule.declarations) {
+      return domUtils.cssPropertyIsValid(this.name, this.value);
+    }
+
+    let selfIndex = this.rule.textProps.indexOf(this);
+
+    // When adding a new property in the rule-view, the TextProperty object is
+    // created right away before the rule gets updated on the server, so we're
+    // not going to find the corresponding declaration object yet. Default to
+    // true.
+    if (!this.rule.domRule.declarations[selfIndex]) {
+      return true;
+    }
+
+    return this.rule.domRule.declarations[selfIndex].isValid;
   }
 };
 
 exports.TextProperty = TextProperty;
--- a/devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js
@@ -10,29 +10,33 @@
 const TEST_URI = URL_ROOT + "doc_content_stylesheet.html";
 
 add_task(function* () {
   yield addTab(TEST_URI);
   let {inspector, view} = yield openRuleView();
   yield selectNode("#target", inspector);
 
   info("Setting a font-weight property on all rules");
-  setPropertyOnAllRules(view);
+  yield setPropertyOnAllRules(view);
 
   info("Reselecting the element");
   yield selectNode("body", inspector);
   yield selectNode("#target", inspector);
 
   checkPropertyOnAllRules(view);
 });
 
-function setPropertyOnAllRules(view) {
+function* setPropertyOnAllRules(view) {
+  // Wait for the properties to be properly created on the backend and for the
+  // view to be updated.
+  let onRefreshed = view.once("ruleview-refreshed");
   for (let rule of view._elementStyle.rules) {
     rule.editor.addProperty("font-weight", "bold", "");
   }
+  yield onRefreshed;
 }
 
 function checkPropertyOnAllRules(view) {
   for (let rule of view._elementStyle.rules) {
     let lastRule = rule.textProps[rule.textProps.length - 1];
 
     is(lastRule.name, "font-weight", "Last rule name is font-weight");
     is(lastRule.value, "bold", "Last rule value is bold");
--- a/devtools/client/inspector/rules/test/browser_rules_add-property_02.js
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property_02.js
@@ -1,69 +1,66 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-//
+// Test adding a valid property to a CSS rule, and navigating through the fields
+// by pressing ENTER.
 
 const TEST_URI = `
   <style type="text/css">
     #testid {
-      background-color: blue;
+      color: blue;
     }
-    .testclass, .unmatched {
-      background-color: green;
-    };
   </style>
-  <div id='testid' class='testclass'>Styled Node</div>
-  <div id='testid2'>Styled Node</div>
+  <div id='testid'>Styled Node</div>
 `;
 
 add_task(function* () {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
+  yield selectNode("#testid", inspector);
 
   info("Focus the new property name field");
-  let elementRuleEditor = getRuleViewRuleEditor(view, 0);
-  let editor = yield focusNewRuleViewProperty(elementRuleEditor);
+  let ruleEditor = getRuleViewRuleEditor(view, 1);
+  let editor = yield focusNewRuleViewProperty(ruleEditor);
   let input = editor.input;
 
-  is(inplaceEditor(elementRuleEditor.newPropSpan), editor,
+  is(inplaceEditor(ruleEditor.newPropSpan), editor,
     "Next focused editor should be the new property editor.");
   ok(input.selectionStart === 0 && input.selectionEnd === input.value.length,
     "Editor contents are selected.");
 
   // Try clicking on the editor's input again, shouldn't cause trouble
   // (see bug 761665).
   EventUtils.synthesizeMouse(input, 1, 1, {}, view.styleWindow);
   input.select();
 
   info("Entering the property name");
   editor.input.value = "background-color";
 
   info("Pressing RETURN and waiting for the value field focus");
-  let onNameAdded = view.once("ruleview-changed");
+  // Pressing ENTER triggeres 2 changed events, one for the new property, and
+  // one for the preview.
+  let onNameAdded = waitForNEvents(view, "ruleview-changed", 2);
   EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
   yield onNameAdded;
 
   editor = inplaceEditor(view.styleDocument.activeElement);
 
-  is(elementRuleEditor.rule.textProps.length, 1,
+  is(ruleEditor.rule.textProps.length, 2,
     "Should have created a new text property.");
-  is(elementRuleEditor.propertyList.children.length, 1,
+  is(ruleEditor.propertyList.children.length, 2,
     "Should have created a property editor.");
-  let textProp = elementRuleEditor.rule.textProps[0];
+  let textProp = ruleEditor.rule.textProps[1];
   is(editor, inplaceEditor(textProp.editor.valueSpan),
     "Should be editing the value span now.");
 
   info("Entering the property value");
-  // We're editing inline style, so expect a style attribute mutation.
-  let onMutated = inspector.once("markupmutation");
   let onValueAdded = view.once("ruleview-changed");
   editor.input.value = "purple";
   EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
   yield onValueAdded;
-  yield onMutated;
 
   is(textProp.value, "purple", "Text prop should have been changed.");
 });
--- a/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js
+++ b/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js
@@ -54,23 +54,22 @@ function* checkCopySelection(view) {
   let win = view.styleWindow;
   let prop = contentDoc.querySelector(".ruleview-property");
   let values = contentDoc.querySelectorAll(".ruleview-propertyvaluecontainer");
 
   let range = contentDoc.createRange();
   range.setStart(prop, 0);
   range.setEnd(values[4], 2);
   win.getSelection().addRange(range);
-
   info("Checking that _Copy() returns the correct clipboard value");
 
   let expectedPattern = "    margin: 10em;[\\r\\n]+" +
                         "    font-size: 14pt;[\\r\\n]+" +
-                        "    font-family: helvetica,sans-serif;[\\r\\n]+" +
-                        "    color: rgb\\(170, 170, 170\\);[\\r\\n]+" +
+                        "    font-family: helvetica, sans-serif;[\\r\\n]+" +
+                        "    color: #AAA;[\\r\\n]+" +
                         "}[\\r\\n]+" +
                         "html {[\\r\\n]+" +
                         "    color: #000000;[\\r\\n]*";
 
   let onPopup = once(view._contextmenu._menupopup, "popupshown");
   EventUtils.synthesizeMouseAtCenter(prop,
     {button: 2, type: "contextmenu"}, win);
   yield onPopup;
@@ -96,18 +95,18 @@ function* checkSelectAll(view) {
   let prop = contentDoc.querySelector(".ruleview-property");
 
   info("Checking that _SelectAll() then copy returns the correct " +
     "clipboard value");
   view._contextmenu._onSelectAll();
   let expectedPattern = "element {[\\r\\n]+" +
                         "    margin: 10em;[\\r\\n]+" +
                         "    font-size: 14pt;[\\r\\n]+" +
-                        "    font-family: helvetica,sans-serif;[\\r\\n]+" +
-                        "    color: rgb\\(170, 170, 170\\);[\\r\\n]+" +
+                        "    font-family: helvetica, sans-serif;[\\r\\n]+" +
+                        "    color: #AAA;[\\r\\n]+" +
                         "}[\\r\\n]+" +
                         "html {[\\r\\n]+" +
                         "    color: #000000;[\\r\\n]+" +
                         "}[\\r\\n]*";
 
   let onPopup = once(view._contextmenu._menupopup, "popupshown");
   EventUtils.synthesizeMouseAtCenter(prop,
     {button: 2, type: "contextmenu"}, win);
--- a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js
@@ -1,17 +1,17 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test "Copy color" item of the context menu #1: Test _isColorPopup.
 
 const TEST_URI = `
-  <div style="color: #123ABC; margin: 0px; background: span[data-color];">
+  <div style="color:rgb(18, 58, 188);margin:0px;background:span[data-color];">
     Test "Copy color" context menu option
   </div>
 `;
 
 add_task(function* () {
   // Test is slow on Linux EC2 instances - Bug 1137765
   requestLongerTimeout(2);
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
--- a/devtools/client/shared/css-parsing-utils.js
+++ b/devtools/client/shared/css-parsing-utils.js
@@ -903,16 +903,22 @@ RuleRewriter.prototype = {
   /**
    * Remove a declaration.
    *
    * @param {Number} index index of the property in the rule.
    * @param {String} name the name of the property to remove
    */
   removeProperty: function (index, name) {
     this.completeInitialization(index);
+
+    // If asked to remove a property that does not exist, bail out.
+    if (!this.decl) {
+      return;
+    }
+
     let copyOffset = this.decl.offsets[1];
     // Maybe removing this rule left us with a completely blank
     // line.  In this case, we'll delete the whole thing.  We only
     // bother with this if we're looking at sources that already
     // have a newline somewhere.
     if (this.hasNewLine) {
       let nlOffset = this.skipWhitespaceBackward(this.result,
                                                  this.decl.offsets[0]);
--- a/devtools/server/actors/styles.js
+++ b/devtools/server/actors/styles.js
@@ -6,36 +6,26 @@
 
 const {Cc, Ci} = require("chrome");
 const promise = require("promise");
 const protocol = require("devtools/server/protocol");
 const {Arg, Option, method, RetVal, types} = protocol;
 const events = require("sdk/event/core");
 const {Class} = require("sdk/core/heritage");
 const {LongStringActor} = require("devtools/server/actors/string");
-const {
-  getDefinedGeometryProperties
-} = require("devtools/server/actors/highlighters/geometry-editor");
+const {getDefinedGeometryProperties} = require("devtools/server/actors/highlighters/geometry-editor");
+const {parseDeclarations} = require("devtools/client/shared/css-parsing-utils");
 
 // This will also add the "stylesheet" actor type for protocol.js to recognize
-const {UPDATE_PRESERVING_RULES, UPDATE_GENERAL} =
-      require("devtools/server/actors/stylesheets");
+const {UPDATE_PRESERVING_RULES, UPDATE_GENERAL} = require("devtools/server/actors/stylesheets");
 
 loader.lazyRequireGetter(this, "CSS", "CSS");
-
-loader.lazyGetter(this, "CssLogic", () => {
-  return require("devtools/shared/inspector/css-logic").CssLogic;
-});
-loader.lazyGetter(this, "DOMUtils", () => {
-  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
-});
-
-loader.lazyGetter(this, "RuleRewriter", () => {
-  return require("devtools/client/shared/css-parsing-utils").RuleRewriter;
-});
+loader.lazyGetter(this, "CssLogic", () => require("devtools/shared/inspector/css-logic").CssLogic);
+loader.lazyGetter(this, "DOMUtils", () => Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils));
+loader.lazyGetter(this, "RuleRewriter", () => require("devtools/client/shared/css-parsing-utils").RuleRewriter);
 
 // 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;
 
 // When gathering rules to read for pseudo elements, we will skip
@@ -1203,27 +1193,26 @@ var StyleRuleActor = protocol.ActorClass
   // to which this rule belongs.
   get marshallPool() {
     return this.pageStyle;
   },
 
   // True if this rule supports as-authored styles, meaning that the
   // rule text can be rewritten using setRuleText.
   get canSetRuleText() {
-    // Special case about:PreferenceStyleSheet, as it is
-    // generated on the fly and the URI is not registered with the
-    // about: handler.
-    // https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37
-    return !!(this._parentSheet &&
-              // If a rule does not have source, then it has been
-              // modified via CSSOM; and we should fall back to
-              // non-authored editing.
-              // https://bugzilla.mozilla.org/show_bug.cgi?id=1224121
-              this.sheetActor.allRulesHaveSource() &&
-              this._parentSheet.href !== "about:PreferenceStyleSheet");
+    return this.type === ELEMENT_STYLE ||
+           (this._parentSheet &&
+            // If a rule does not have source, then it has been modified via
+            // CSSOM; and we should fall back to non-authored editing.
+            // https://bugzilla.mozilla.org/show_bug.cgi?id=1224121
+            this.sheetActor.allRulesHaveSource() &&
+            // Special case about:PreferenceStyleSheet, as it is generated on
+            // the fly and the URI is not registered with the about:handler
+            // https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37
+            this._parentSheet.href !== "about:PreferenceStyleSheet");
   },
 
   getDocument: function(sheet) {
     let document;
 
     if (sheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) {
       document = sheet.ownerNode;
     } else {
@@ -1291,16 +1280,17 @@ var StyleRuleActor = protocol.ActorClass
         break;
       case ELEMENT_STYLE:
         // Elements don't have a parent stylesheet, and therefore
         // don't have an associated URI.  Provide a URI for
         // those.
         let doc = this.rawNode.ownerDocument;
         form.href = doc.location ? doc.location.href : "";
         form.cssText = this.rawStyle.cssText || "";
+        form.authoredText = this.rawNode.getAttribute("style");
         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.KEYFRAMES_RULE:
@@ -1308,16 +1298,28 @@ var StyleRuleActor = protocol.ActorClass
         form.name = this.rawRule.name;
         break;
       case Ci.nsIDOMCSSRule.KEYFRAME_RULE:
         form.cssText = this.rawStyle.cssText || "";
         form.keyText = this.rawRule.keyText || "";
         break;
     }
 
+    // Parse the text into a list of declarations so the client doesn't have to
+    // and so that we can safely determine if a declaration is valid rather than
+    // have the client guess it.
+    if (form.authoredText || form.cssText) {
+      let declarations = parseDeclarations(form.authoredText ||
+                                           form.cssText, true);
+      form.declarations = declarations.map(decl => {
+        decl.isValid = DOMUtils.cssPropertyIsValid(decl.name, decl.value);
+        return decl;
+      });
+    }
+
     return form;
   },
 
   /**
    * Send an event notifying that the location of the rule has
    * changed.
    *
    * @param {Number} line the new line number
@@ -1450,31 +1452,36 @@ var StyleRuleActor = protocol.ActorClass
   /**
    * Set the contents of the rule.  This rewrites the rule in the
    * stylesheet and causes it to be re-evaluated.
    *
    * @param {String} newText the new text of the rule
    * @returns the rule with updated properties
    */
   setRuleText: method(Task.async(function* (newText) {
-    if (!this.canSetRuleText ||
-        (this.type !== Ci.nsIDOMCSSRule.STYLE_RULE &&
-         this.type !== Ci.nsIDOMCSSRule.KEYFRAME_RULE)) {
+    if (!this.canSetRuleText) {
       throw new Error("invalid call to setRuleText");
     }
 
-    let parentStyleSheet = this.pageStyle._sheetRef(this._parentSheet);
-    let {str: cssText} = yield parentStyleSheet.getText();
+    if (this.type === ELEMENT_STYLE) {
+      // For element style rules, set the node's style attribute.
+      this.rawNode.setAttribute("style", newText);
+    } else {
+      // For stylesheet rules, set the text in the stylesheet.
+      let parentStyleSheet = this.pageStyle._sheetRef(this._parentSheet);
+      let {str: cssText} = yield parentStyleSheet.getText();
 
-    let {offset, text} = getRuleText(cssText, this.line, this.column);
-    cssText = cssText.substring(0, offset) + newText +
-      cssText.substring(offset + text.length);
+      let {offset, text} = getRuleText(cssText, this.line, this.column);
+      cssText = cssText.substring(0, offset) + newText +
+        cssText.substring(offset + text.length);
+
+      yield parentStyleSheet.update(cssText, false, UPDATE_PRESERVING_RULES);
+    }
 
     this.authoredText = newText;
-    yield parentStyleSheet.update(cssText, false, UPDATE_PRESERVING_RULES);
 
     return this;
   }), {
     request: { modification: Arg(0, "string") },
     response: { rule: RetVal("domstylerule") }
   }),
 
   /**
@@ -1759,16 +1766,19 @@ protocol.FrontClass(StyleRuleActor, {
     return this._form.column || -1;
   },
   get cssText() {
     return this._form.cssText;
   },
   get authoredText() {
     return this._form.authoredText || this._form.cssText;
   },
+  get declarations() {
+    return this._form.declarations || [];
+  },
   get keyText() {
     return this._form.keyText;
   },
   get name() {
     return this._form.name;
   },
   get selectors() {
     return this._form.selectors;
--- a/devtools/server/tests/mochitest/test_styles-modify.html
+++ b/devtools/server/tests/mochitest/test_styles-modify.html
@@ -17,71 +17,86 @@ window.onload = function() {
   SimpleTest.waitForExplicitFinish();
   runNextTest();
 }
 
 var gWalker = null;
 var gStyles = null;
 var gClient = null;
 
-addTest(function setup() {
+addAsyncTest(function* setup() {
   let url = document.getElementById("inspectorContent").href;
-  attachURL(url, function(err, client, tab, doc) {
-    gInspectee = doc;
-    let {InspectorFront} = require("devtools/server/actors/inspector");
-    let inspector = InspectorFront(client, tab);
-    promiseDone(inspector.getWalker().then(walker => {
-      ok(walker, "getWalker() should return an actor.");
+  let inspector;
+
+  yield new Promise(resolve => {
+    attachURL(url, function(err, client, tab, doc) {
+      gInspectee = doc;
       gClient = client;
-      gWalker = walker;
-      return inspector.getPageStyle();
-    }).then(styles => {
-      gStyles = styles;
-    }).then(runNextTest));
+      let {InspectorFront} = require("devtools/server/actors/inspector");
+      inspector = InspectorFront(client, tab);
+      resolve();
+    });
   });
+
+  gWalker = yield inspector.getWalker();
+  gStyles = yield inspector.getPageStyle();
+
+  runNextTest();
 });
 
-addTest(function modifyProperties() {
+addAsyncTest(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");
+
+  let node = yield gWalker.querySelector(gWalker.rootNode,
+    "#inheritable-rule-inheritable-style");
+
+  let applied = yield gStyles.getApplied(node,
+    { inherited: false, filter: "user" });
 
-    // Will start with "color:blue"
-    let changes = elementStyle.startModifyingProperties();
+  let elementStyle = applied[0].rule;
+  is(elementStyle.cssText, localNode.style.cssText, "Got expected css text");
 
-    // Change an existing property...
-    changes.setProperty(-1, "color", "black");
-    // Create a new property
-    changes.setProperty(-1, "background-color", "green");
+  // Change an existing property...
+  yield setProperty(elementStyle, 0, "color", "black");
+  // Create a new property
+  yield setProperty(elementStyle, 1, "background-color", "green");
+
+  // Create a new property and then change it immediately.
+  yield setProperty(elementStyle, 2, "border", "1px solid black");
+  yield setProperty(elementStyle, 2, "border", "2px solid black");
 
-    // Create a new property and then change it immediately.
-    changes.setProperty(-1, "border", "1px solid black");
-    changes.setProperty(-1, "border", "2px solid black");
+  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.");
 
-    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
+  yield removeProperty(elementStyle, 0, "color");
+  yield removeProperty(elementStyle, 0, "background-color");
+  yield removeProperty(elementStyle, 0, "border");
+
+  is(elementStyle.cssText, "", "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(-1, "color");
-    changes.removeProperty(-1, "background-color");
-    changes.removeProperty(-1, "border");
+  runNextTest();
+});
 
-    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));
-});
+function* setProperty(rule, index, name, value) {
+  let changes = rule.startModifyingProperties();
+  changes.setProperty(index, name, value);
+  yield changes.apply();
+}
+
+function* removeProperty(rule, index, name) {
+  let changes = rule.startModifyingProperties();
+  changes.removeProperty(index, name);
+  yield changes.apply();
+}
 
 addTest(function cleanup() {
   delete gStyles;
   delete gWalker;
   delete gClient;
   runNextTest();
 });