Bug 1030889 - [rule view] Add keyframe rules with its associated element. r=pbrosset
☠☠ backed out by b69b7c847c56 ☠ ☠
authorGabriel Luong <gabriel.luong@gmail.com>
Fri, 18 Jul 2014 11:13:00 -0400
changeset 216922 a1778d8e2e382f970154fe4bb81ffaf6c3ed7cf9
parent 216921 c6f27c773336b2bef38a4d4e70af579ae9425ad5
child 216923 85d5040f1ad2bccff75bfeaf6912791bfca527f0
push id515
push userraliiev@mozilla.com
push dateMon, 06 Oct 2014 12:51:51 +0000
treeherdermozilla-release@267c7a481bef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbrosset
bugs1030889
milestone33.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 1030889 - [rule view] Add keyframe rules with its associated element. r=pbrosset
browser/devtools/styleinspector/rule-view.js
browser/devtools/styleinspector/ruleview.css
browser/devtools/styleinspector/test/browser.ini
browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js
browser/devtools/styleinspector/test/browser_ruleview_keyframes-rule_01.js
browser/devtools/styleinspector/test/browser_ruleview_keyframes-rule_02.js
browser/devtools/styleinspector/test/browser_ruleview_pseudo-element.js
browser/devtools/styleinspector/test/doc_keyframeanimation.css
browser/devtools/styleinspector/test/doc_keyframeanimation.html
browser/devtools/styleinspector/test/head.js
toolkit/devtools/server/actors/styles.js
toolkit/devtools/styleinspector/css-logic.js
toolkit/locales/en-US/chrome/global/devtools/styleinspector.properties
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -297,20 +297,23 @@ ElementStyle.prototype = {
    * Mark the properties listed in this.rules for a given pseudo element
    * with an overridden flag if an earlier property overrides it.
    * @param {string} pseudo
    *        Which pseudo element to flag as overridden.
    *        Empty string or undefined will default to no pseudo element.
    */
   markOverridden: function(pseudo="") {
     // Gather all the text properties applied by these rules, ordered
-    // from more- to less-specific.
+    // from more- to less-specific. Text properties from keyframes rule are
+    // excluded from being marked as overridden since a number of criteria such
+    // as time, and animation overlay are required to be check in order to
+    // determine if the property is overridden.
     let textProps = [];
     for (let rule of this.rules) {
-      if (rule.pseudoElement == pseudo) {
+      if (rule.pseudoElement == pseudo && !rule.keyframes) {
         textProps = textProps.concat(rule.textProps.slice(0).reverse());
       }
     }
 
     // Gather all the computed properties applied by those text
     // properties.
     let computedProps = [];
     for (let textProp of textProps) {
@@ -414,16 +417,17 @@ function Rule(aElementStyle, aOptions) {
   this.elementStyle = aElementStyle;
   this.domRule = aOptions.rule || null;
   this.style = aOptions.rule;
   this.matchedSelectors = aOptions.matchedSelectors || [];
   this.pseudoElement = aOptions.pseudoElement || "";
 
   this.isSystem = aOptions.isSystem;
   this.inherited = aOptions.inherited || null;
+  this.keyframes = aOptions.keyframes || null;
   this._modificationDepth = 0;
 
   if (this.domRule) {
     let parentRule = this.domRule.parentRule;
     if (parentRule && parentRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE) {
       this.mediaText = parentRule.mediaText;
     }
   }
@@ -461,16 +465,28 @@ Rule.prototype = {
         eltText += "#" + this.inherited.id;
       }
       this._inheritedSource =
         CssLogic._strings.formatStringFromName("rule.inheritedFrom", [eltText], 1);
     }
     return this._inheritedSource;
   },
 
+  get keyframesName() {
+    if (this._keyframesName) {
+      return this._keyframesName;
+    }
+    this._keyframesName = "";
+    if (this.keyframes) {
+      this._keyframesName =
+        CssLogic._strings.formatStringFromName("rule.keyframe", [this.keyframes.name], 1);
+    }
+    return this._keyframesName;
+  },
+
   get selectorText() {
     return this.domRule.selectors ? this.domRule.selectors.join(", ") : CssLogic.l10n("rule.sourceElement");
   },
 
   /**
    * The rule's stylesheet.
    */
   get sheet() {
@@ -1622,57 +1638,102 @@ CssRuleView.prototype = {
   get pseudoElementLabel() {
     if (this._pseudoElementLabel) {
       return this._pseudoElementLabel;
     }
     this._pseudoElementLabel = CssLogic.l10n("rule.pseudoElement");
     return this._pseudoElementLabel;
   },
 
-  togglePseudoElementVisibility: function(value) {
-    this._showPseudoElements = !!value;
-    let isOpen = this.showPseudoElements;
-
-    Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements",
-      isOpen);
-
-    this.element.classList.toggle("show-pseudo-elements", isOpen);
-
-    if (this.pseudoElementTwisty) {
-      if (isOpen) {
-        this.pseudoElementTwisty.setAttribute("open", "true");
-      }
-      else {
-        this.pseudoElementTwisty.removeAttribute("open");
-      }
-    }
-  },
-
   get showPseudoElements() {
     if (this._showPseudoElements === undefined) {
       this._showPseudoElements =
         Services.prefs.getBoolPref("devtools.inspector.show_pseudo_elements");
     }
     return this._showPseudoElements;
   },
 
+  /**
+   * Creates an expandable container in the rule view
+   * @param  {String}  aLabel The label for the container header
+   * @param  {Boolean} isPseudo Whether or not the container will hold
+   *                            pseudo element rules
+   * @return {DOMNode} The container element
+   */
+  createExpandableContainer: function(aLabel, isPseudo = false) {
+    let header = this.doc.createElementNS(HTML_NS, "div");
+    header.className = this._getRuleViewHeaderClassName(true);
+    header.classList.add("show-expandable-container");
+    header.textContent = aLabel;
+
+    let twisty = this.doc.createElementNS(HTML_NS, "span");
+    twisty.className = "ruleview-expander theme-twisty";
+    twisty.setAttribute("open", "true");
+
+    header.insertBefore(twisty, header.firstChild);
+    this.element.appendChild(header);
+
+    let container = this.doc.createElementNS(HTML_NS, "div");
+    container.classList.add("ruleview-expandable-container");
+    this.element.appendChild(container);
+
+    let toggleContainerVisibility = (isPseudo, showPseudo) => {
+      let isOpen = twisty.getAttribute("open");
+
+      if (isPseudo) {
+        this._showPseudoElements = !!showPseudo;
+
+        Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements",
+          this.showPseudoElements);
+
+        header.classList.toggle("show-expandable-container",
+          this.showPseudoElements);
+
+        isOpen = !this.showPseudoElements;
+      } else {
+        header.classList.toggle("show-expandable-container");
+      }
+
+      if (isOpen) {
+        twisty.removeAttribute("open");
+      } else {
+        twisty.setAttribute("open", "true");
+      }
+    };
+
+    header.addEventListener("dblclick", () => {
+      toggleContainerVisibility(isPseudo, !this.showPseudoElements);
+    }, false);
+    twisty.addEventListener("click", () => {
+      toggleContainerVisibility(isPseudo, !this.showPseudoElements);
+    }, false);
+
+    if (isPseudo) {
+      toggleContainerVisibility(isPseudo, this.showPseudoElements);
+    }
+
+    return container;
+  },
+
   _getRuleViewHeaderClassName: function(isPseudo) {
     let baseClassName = "theme-gutter ruleview-header";
     return isPseudo ? baseClassName + " ruleview-expandable-header" : baseClassName;
   },
 
   /**
    * Creates editor UI for each of the rules in _elementStyle.
    */
   _createEditors: function() {
     // Run through the current list of rules, attaching
     // their editors in order.  Create editors if needed.
     let lastInheritedSource = "";
+    let lastKeyframes = null;
     let seenPseudoElement = false;
     let seenNormalElement = false;
+    let container = null;
 
     if (!this._elementStyle.rules) {
       return;
     }
 
     for (let rule of this._elementStyle.rules) {
       if (rule.domRule.system) {
         continue;
@@ -1683,53 +1744,45 @@ CssRuleView.prototype = {
         seenNormalElement = true;
         let div = this.doc.createElementNS(HTML_NS, "div");
         div.className = this._getRuleViewHeaderClassName();
         div.textContent = this.selectedElementLabel;
         this.element.appendChild(div);
       }
 
       let inheritedSource = rule.inheritedSource;
-      if (inheritedSource != lastInheritedSource) {
+      if (inheritedSource && inheritedSource != lastInheritedSource) {
         let div = this.doc.createElementNS(HTML_NS, "div");
         div.className = this._getRuleViewHeaderClassName();
         div.textContent = inheritedSource;
         lastInheritedSource = inheritedSource;
         this.element.appendChild(div);
       }
 
       if (!seenPseudoElement && rule.pseudoElement) {
         seenPseudoElement = true;
-
-        let div = this.doc.createElementNS(HTML_NS, "div");
-        div.className = this._getRuleViewHeaderClassName(true);
-        div.textContent = this.pseudoElementLabel;
-        div.addEventListener("dblclick", () => {
-          this.togglePseudoElementVisibility(!this.showPseudoElements);
-        }, false);
-
-        let twisty = this.pseudoElementTwisty =
-          this.doc.createElementNS(HTML_NS, "span");
-        twisty.className = "ruleview-expander theme-twisty";
-        twisty.addEventListener("click", () => {
-          this.togglePseudoElementVisibility(!this.showPseudoElements);
-        }, false);
-
-        div.insertBefore(twisty, div.firstChild);
-        this.element.appendChild(div);
+        container = this.createExpandableContainer(this.pseudoElementLabel, true);
+      }
+
+      let keyframes = rule.keyframes;
+      if (keyframes && keyframes != lastKeyframes) {
+        lastKeyframes = keyframes;
+        container = this.createExpandableContainer(rule.keyframesName);
       }
 
       if (!rule.editor) {
         rule.editor = new RuleEditor(this, rule);
       }
 
-      this.element.appendChild(rule.editor.element);
+      if (container && (rule.pseudoElement || keyframes)) {
+        container.appendChild(rule.editor.element);
+      } else {
+        this.element.appendChild(rule.editor.element);
+      }
     }
-
-    this.togglePseudoElementVisibility(this.showPseudoElements);
   }
 };
 
 /**
  * Create a RuleEditor.
  *
  * @param {CssRuleView} aRuleView
  *        The CssRuleView containg the document holding this rule editor.
@@ -1751,27 +1804,27 @@ function RuleEditor(aRuleView, aRule) {
   this._onSelectorDone = this._onSelectorDone.bind(this);
 
   this._create();
 }
 
 RuleEditor.prototype = {
   get isSelectorEditable() {
     let toolbox = this.ruleView.inspector.toolbox;
-    return toolbox.target.client.traits.selectorEditable;
+    return this.isEditable &&
+      toolbox.target.client.traits.selectorEditable &&
+      this.rule.domRule.type !== ELEMENT_STYLE &&
+      this.rule.domRule.type !== Ci.nsIDOMCSSRule.KEYFRAME_RULE
   },
 
   _create: function() {
     this.element = this.doc.createElementNS(HTML_NS, "div");
     this.element.className = "ruleview-rule theme-separator";
     this.element.setAttribute("uneditable", !this.isEditable);
     this.element._ruleEditor = this;
-    if (this.rule.pseudoElement) {
-      this.element.classList.add("ruleview-rule-pseudo-element");
-    }
 
     // Give a relative position for the inplace editor's measurement
     // span to be placed absolutely against.
     this.element.style.position = "relative";
 
     // Add the source link.
     let source = createChild(this.element, "div", {
       class: "ruleview-rule-source theme-link"
@@ -1803,18 +1856,17 @@ RuleEditor.prototype = {
     this.selectorContainer = createChild(header, "span", {
       class: "ruleview-selectorcontainer"
     });
 
     this.selectorText = createChild(this.selectorContainer, "span", {
       class: "ruleview-selector theme-fg-color3"
     });
 
-    if (this.isEditable && this.rule.domRule.type !== ELEMENT_STYLE &&
-        this.isSelectorEditable) {
+    if (this.isSelectorEditable) {
       this.selectorContainer.addEventListener("click", aEvent => {
         // Clicks within the selector shouldn't propagate any further.
         aEvent.stopPropagation();
       }, false);
 
       editableField({
         element: this.selectorText,
         done: this._onSelectorDone,
@@ -1920,16 +1972,18 @@ RuleEditor.prototype = {
       this.selectorText.removeChild(this.selectorText.lastChild);
     }
 
     // If selector text comes from a css rule, highlight selectors that
     // actually match.  For custom selector text (such as for the 'element'
     // style, just show the text directly.
     if (this.rule.domRule.type === ELEMENT_STYLE) {
       this.selectorText.textContent = this.rule.selectorText;
+    } else if (this.rule.domRule.type === Ci.nsIDOMCSSRule.KEYFRAME_RULE) {
+      this.selectorText.textContent = this.rule.domRule.keyText;
     } else {
       this.rule.domRule.selectors.forEach((selector, i) => {
         if (i != 0) {
           createChild(this.selectorText, "span", {
             class: "ruleview-selector-separator",
             textContent: ", "
           });
         }
--- a/browser/devtools/styleinspector/ruleview.css
+++ b/browser/devtools/styleinspector/ruleview.css
@@ -33,21 +33,21 @@
   cursor: pointer;
 }
 
 .ruleview-computedlist:not(.styleinspector-open),
 .ruleview-warning[hidden] {
   display: none;
 }
 
-.ruleview-rule-pseudo-element {
+.ruleview-expandable-container {
   display: none;
 }
 
-.show-pseudo-elements .ruleview-rule-pseudo-element {
+.show-expandable-container + .ruleview-expandable-container {
   display: block;
 }
 
 .ruleview .ruleview-expander {
   vertical-align: middle;
 }
 
 .ruleview-header {
--- a/browser/devtools/styleinspector/test/browser.ini
+++ b/browser/devtools/styleinspector/test/browser.ini
@@ -4,16 +4,18 @@ subsuite = devtools
 support-files =
   doc_content_stylesheet.html
   doc_content_stylesheet.xul
   doc_content_stylesheet_imported.css
   doc_content_stylesheet_imported2.css
   doc_content_stylesheet_linked.css
   doc_content_stylesheet_script.css
   doc_content_stylesheet_xul.css
+  doc_keyframeanimation.html
+  doc_keyframeanimation.css
   doc_matched_selectors.html
   doc_media_queries.html
   doc_pseudoelement.html
   doc_sourcemaps.css
   doc_sourcemaps.css.map
   doc_sourcemaps.html
   doc_sourcemaps.scss
   doc_style_editor_link.css
@@ -70,16 +72,18 @@ support-files =
 [browser_ruleview_edit-property_02.js]
 [browser_ruleview_edit-selector-commit.js]
 [browser_ruleview_edit-selector_01.js]
 [browser_ruleview_edit-selector_02.js]
 [browser_ruleview_eyedropper.js]
 skip-if = os == "win" && debug # bug 963492
 [browser_ruleview_inherit.js]
 [browser_ruleview_keybindings.js]
+[browser_ruleview_keyframes-rule_01.js]
+[browser_ruleview_keyframes-rule_02.js]
 [browser_ruleview_livepreview.js]
 [browser_ruleview_mathml-element.js]
 [browser_ruleview_media-queries.js]
 [browser_ruleview_multiple-properties-duplicates.js]
 [browser_ruleview_multiple-properties-priority.js]
 [browser_ruleview_multiple-properties-unfinished_01.js]
 [browser_ruleview_multiple-properties-unfinished_02.js]
 [browser_ruleview_multiple_properties_01.js]
--- a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js
@@ -46,17 +46,18 @@ let test = asyncTest(function*() {
   info("Selecting the modified element");
   yield selectNode(".testclass2", inspector);
   yield checkModifiedElement(view, ".testclass2:after");
 });
 
 function* testEditSelector(view, name) {
   info("Test editing existing selector fields");
 
-  let idRuleEditor = getRuleViewRuleEditor(view, 1);
+  let idRuleEditor = getRuleViewRuleEditor(view, 1) ||
+    getRuleViewRuleEditor(view, 1, 0);
 
   info("Focusing an existing selector name in the rule-view");
   let editor = yield focusEditableField(idRuleEditor.selectorText);
 
   is(inplaceEditor(idRuleEditor.selectorText), editor,
     "The selector editor got focused");
 
   info("Entering a new selector name: " + name);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_keyframes-rule_01.js
@@ -0,0 +1,127 @@
+/* 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 that keyframe rules and gutters are displayed correctly in the rule view
+
+const TEST_URI = TEST_URL_ROOT + "doc_keyframeanimation.html";
+
+let test = asyncTest(function*() {
+  yield addTab(TEST_URI);
+
+  let {toolbox, inspector, view} = yield openRuleView();
+
+  yield testPacman(inspector, view);
+  yield testBoxy(inspector, view);
+  yield testMoxy(inspector, view);
+});
+
+function* testPacman(inspector, view) {
+  info("Test content and gutter in the keyframes rule of #pacman");
+
+  let {
+    rules,
+    element,
+    elementStyle
+  } = yield assertKeyframeRules("#pacman", inspector, view, {
+    elementRulesNb: 2,
+    keyframeRulesNb: 2,
+    keyframesRules: ["pacman", "pacman"],
+    keyframeRules: ["100%", "100%"]
+  });
+
+  let gutters = assertGutters(view, {
+    guttersNbs: 2,
+    gutterHeading: ["Keyframes pacman", "Keyframes pacman"]
+  });
+}
+
+function* testBoxy(inspector, view) {
+  info("Test content and gutter in the keyframes rule of #boxy");
+
+  let {
+    rules,
+    element,
+    elementStyle
+  } = yield assertKeyframeRules("#boxy", inspector, view, {
+    elementRulesNb: 3,
+    keyframeRulesNb: 3,
+    keyframesRules: ["boxy", "boxy", "boxy"],
+    keyframeRules: ["10%", "20%", "100%"]
+  });
+
+  let gutters = assertGutters(view, {
+    guttersNbs: 1,
+    gutterHeading: ["Keyframes boxy"]
+  });
+}
+
+function testMoxy(inspector, view) {
+  info("Test content and gutter in the keyframes rule of #moxy");
+
+  let {
+    rules,
+    element,
+    elementStyle
+  } = yield assertKeyframeRules("#moxy", inspector, view, {
+    elementRulesNb: 3,
+    keyframeRulesNb: 4,
+    keyframesRules: ["boxy", "boxy", "boxy", "moxy"],
+    keyframeRules: ["10%", "20%", "100%", "100%"]
+  });
+
+  let gutters = assertGutters(view, {
+    guttersNbs: 2,
+    gutterHeading: ["Keyframes boxy", "Keyframes moxy"]
+  });
+}
+
+function* testNode(selector, inspector, view) {
+  let element = getNode(selector);
+  yield selectNode(element, inspector);
+  let elementStyle = view._elementStyle;
+  return {element, elementStyle};
+}
+
+function* assertKeyframeRules(selector, inspector, view, expected) {
+  let {element, elementStyle} = yield testNode(selector, inspector, view);
+
+  let rules = {
+    elementRules: elementStyle.rules.filter(rule => !rule.keyframes),
+    keyframeRules: elementStyle.rules.filter(rule => rule.keyframes)
+  };
+
+  is(rules.elementRules.length, expected.elementRulesNb, selector +
+    " has the correct number of non keyframe element rules");
+  is(rules.keyframeRules.length, expected.keyframeRulesNb, selector +
+    " has the correct number of keyframe rules");
+
+  let i = 0;
+  for (let keyframeRule of rules.keyframeRules) {
+    ok(keyframeRule.keyframes.name == expected.keyframesRules[i],
+      keyframeRule.keyframes.name + " has the correct keyframes name");
+    ok(keyframeRule.domRule.keyText == expected.keyframeRules[i],
+      keyframeRule.domRule.keyText + " selector heading is correct");
+    i++;
+  }
+
+  return {rules, element, elementStyle};
+}
+
+function assertGutters(view, expected) {
+  let gutters = view.element.querySelectorAll(".theme-gutter");
+
+  is(gutters.length, expected.guttersNbs,
+    "There are " + gutters.length + " gutter headings");
+
+  let i = 0;
+  for (let gutter of gutters) {
+    is(gutter.textContent, expected.gutterHeading[i],
+      "Correct " + gutter.textContent + " gutter headings");
+    i++;
+  }
+
+  return gutters;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_keyframes-rule_02.js
@@ -0,0 +1,111 @@
+/* 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 that verifies the content of the keyframes rule and property changes
+// to keyframe rules
+
+const TEST_URI = TEST_URL_ROOT + "doc_keyframeanimation.html";
+
+let test = asyncTest(function*() {
+  yield addTab(TEST_URI);
+
+  let {toolbox, inspector, view} = yield openRuleView();
+
+  yield testPacman(inspector, view);
+  yield testBoxy(inspector, view);
+});
+
+function* testPacman(inspector, view) {
+  info("Test content in the keyframes rule of #pacman");
+
+  let {
+    rules,
+    element,
+    elementStyle
+  } = yield getKeyframeRules("#pacman", inspector, view);
+
+  info("Test text properties for Keyframes #pacman");
+
+  is
+  (
+    convertTextPropsToString(rules.keyframeRules[0].textProps),
+    "left: 750px",
+    "Keyframe pacman (100%) property is correct"
+  );
+
+  info("Test dynamic changes to keyframe rule for #pacman");
+
+  let defaultView = element.ownerDocument.defaultView;
+  let ruleEditor = view.element.children[5].childNodes[0]._ruleEditor;
+  ruleEditor.addProperty("opacity", "0");
+  ruleEditor.addProperty("top", "750px");
+
+  yield ruleEditor._applyingModifications;
+  yield once(element, "animationend");
+
+  is
+  (
+    convertTextPropsToString(rules.keyframeRules[1].textProps),
+    "left: 750px; opacity: 0; top: 750px",
+    "Keyframe pacman (100%) property is correct"
+  );
+
+  is(defaultView.getComputedStyle(element).getPropertyValue("opacity"), "0",
+    "Added opacity property should have been used.");
+  is(defaultView.getComputedStyle(element).getPropertyValue("top"), "750px",
+    "Added top property should have been used.");
+}
+
+function* testBoxy(inspector, view) {
+  info("Test content in the keyframes rule of #boxy");
+
+  let {
+    rules,
+    element,
+    elementStyle
+  } = yield getKeyframeRules("#boxy", inspector, view);
+
+  info("Test text properties for Keyframes #boxy");
+
+  is
+  (
+    convertTextPropsToString(rules.keyframeRules[0].textProps),
+    "background-color: blue",
+    "Keyframe boxy (10%) property is correct"
+  );
+
+  is
+  (
+    convertTextPropsToString(rules.keyframeRules[1].textProps),
+    "background-color: green",
+    "Keyframe boxy (20%) property is correct"
+  );
+
+  is
+  (
+    convertTextPropsToString(rules.keyframeRules[2].textProps),
+    "opacity: 0",
+    "Keyframe boxy (100%) property is correct"
+  );
+}
+
+function convertTextPropsToString(textProps) {
+  return textProps.map(t => t.name + ": " + t.value).join("; ");
+}
+
+function* getKeyframeRules(selector, inspector, view) {
+  let element = getNode(selector);
+
+  yield selectNode(element, inspector);
+  let elementStyle = view._elementStyle;
+
+  let rules = {
+    elementRules: elementStyle.rules.filter(rule => !rule.keyframes),
+    keyframeRules: elementStyle.rules.filter(rule => rule.keyframes)
+  };
+
+  return {rules, element, elementStyle};
+}
--- a/browser/devtools/styleinspector/test/browser_ruleview_pseudo-element.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_pseudo-element.js
@@ -33,47 +33,45 @@ function* testTopLeft(inspector, view) {
     firstLetterRulesNb: 0,
     selectionRulesNb: 0
   });
 
   let gutters = assertGutters(view);
 
   // Make sure that clicking on the twisty hides pseudo elements
   let expander = gutters[0].querySelector(".ruleview-expander");
-  ok (view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are expanded");
+  ok (view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are expanded");
   expander.click();
-  ok (!view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are collapsed by twisty");
+  ok (!view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are collapsed by twisty");
   expander.click();
-  ok (view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are expanded again");
+  ok (view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are expanded again");
 
   // Make sure that dblclicking on the header container also toggles the pseudo elements
   EventUtils.synthesizeMouseAtCenter(gutters[0], {clickCount: 2}, inspector.sidebar.getWindowForTab("ruleview"));
-  ok (!view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are collapsed by dblclicking");
+  ok (!view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are collapsed by dblclicking");
 
   let defaultView = element.ownerDocument.defaultView;
   let elementRule = rules.elementRules[0];
-  let elementRuleView = [].filter.call(view.element.children, e => {
-    return e._ruleEditor && e._ruleEditor.rule === elementRule;
-  })[0]._ruleEditor;
+  let elementRuleView = getRuleViewRuleEditor(view, 3);
 
   let elementAfterRule = rules.afterRules[0];
-  let elementAfterRuleView = [].filter.call(view.element.children, (e) => {
+  let elementAfterRuleView = [].filter.call(view.element.children[1].children, (e) => {
     return e._ruleEditor && e._ruleEditor.rule === elementAfterRule;
   })[0]._ruleEditor;
 
   is
   (
     convertTextPropsToString(elementAfterRule.textProps),
     "background: none repeat scroll 0% 0% red; content: \" \"; position: absolute; " +
     "border-radius: 50%; height: 32px; width: 32px; top: 50%; left: 50%; margin-top: -16px; margin-left: -16px",
     "TopLeft after properties are correct"
   );
 
   let elementBeforeRule = rules.beforeRules[0];
-  let elementBeforeRuleView = [].filter.call(view.element.children, (e) => {
+  let elementBeforeRuleView = [].filter.call(view.element.children[1].children, (e) => {
     return e._ruleEditor && e._ruleEditor.rule === elementBeforeRule;
   })[0]._ruleEditor;
 
   is
   (
     convertTextPropsToString(elementBeforeRule.textProps),
     "top: 0px; left: 0px",
     "TopLeft before properties are correct"
@@ -133,20 +131,20 @@ function* testTopRight(inspector, view) 
     firstLineRulesNb: 0,
     firstLetterRulesNb: 0,
     selectionRulesNb: 0
   });
 
   let gutters = assertGutters(view);
 
   let expander = gutters[0].querySelector(".ruleview-expander");
-  ok (!view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements remain collapsed after switching element");
+  ok (!view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements remain collapsed after switching element");
   expander.scrollIntoView();
   expander.click();
-  ok (view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are shown again after clicking twisty");
+  ok (view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are shown again after clicking twisty");
 }
 
 function* testBottomRight(inspector, view) {
   yield assertPseudoElementRulesNumbers("#bottomright", inspector, view, {
     elementRulesNb: 4,
     afterRulesNb: 1,
     beforeRulesNb: 3,
     firstLineRulesNb: 0,
@@ -178,41 +176,41 @@ function* testParagraph(inspector, view)
     firstLineRulesNb: 1,
     firstLetterRulesNb: 1,
     selectionRulesNb: 1
   });
 
   let gutters = assertGutters(view);
 
   let elementFirstLineRule = rules.firstLineRules[0];
-  let elementFirstLineRuleView = [].filter.call(view.element.children, (e) => {
+  let elementFirstLineRuleView = [].filter.call(view.element.children[1].children, (e) => {
     return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule;
   })[0]._ruleEditor;
 
   is
   (
     convertTextPropsToString(elementFirstLineRule.textProps),
     "background: none repeat scroll 0% 0% blue",
     "Paragraph first-line properties are correct"
   );
 
   let elementFirstLetterRule = rules.firstLetterRules[0];
-  let elementFirstLetterRuleView = [].filter.call(view.element.children, (e) => {
+  let elementFirstLetterRuleView = [].filter.call(view.element.children[1].children, (e) => {
     return e._ruleEditor && e._ruleEditor.rule === elementFirstLetterRule;
   })[0]._ruleEditor;
 
   is
   (
     convertTextPropsToString(elementFirstLetterRule.textProps),
     "color: red; font-size: 130%",
     "Paragraph first-letter properties are correct"
   );
 
   let elementSelectionRule = rules.selectionRules[0];
-  let elementSelectionRuleView = [].filter.call(view.element.children, (e) => {
+  let elementSelectionRuleView = [].filter.call(view.element.children[1].children, (e) => {
     return e._ruleEditor && e._ruleEditor.rule === elementSelectionRule;
   })[0]._ruleEditor;
 
   is
   (
     convertTextPropsToString(elementSelectionRule.textProps),
     "color: white; background: none repeat scroll 0% 0% black",
     "Paragraph first-letter properties are correct"
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/doc_keyframeanimation.css
@@ -0,0 +1,84 @@
+/* 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/. */
+
+.box {
+  height: 50px;
+  width: 50px;
+}
+
+.circle {
+  width: 20px;
+  height: 20px;
+  border-radius: 10px;
+  background-color: #FFCB01;
+}
+
+#pacman {
+  width: 0px;
+  height: 0px;
+  border-right: 60px solid transparent;
+  border-top: 60px solid #FFCB01;
+  border-left: 60px solid #FFCB01;
+  border-bottom: 60px solid #FFCB01;
+  border-top-left-radius: 60px;
+  border-bottom-left-radius: 60px;
+  border-top-right-radius: 60px;
+  border-bottom-right-radius: 60px;
+  top: 120px;
+  left: 150px;
+  position: absolute;
+  animation-name: pacman;
+  animation-fill-mode: forwards;
+  animation-timing-function: linear;
+  animation-duration: 15s;
+}
+
+#boxy {
+  top: 170px;
+  left: 450px;
+  position: absolute;
+  animation: 4s linear 0s normal none infinite boxy;
+}
+
+
+#moxy {
+  animation-name: moxy, boxy;
+  animation-delay: 3.5s;
+  animation-duration: 2s;
+  top: 170px;
+  left: 650px;
+  position: absolute;
+}
+
+@-moz-keyframes pacman {
+  100% {
+    left: 750px;
+  }
+}
+
+@keyframes pacman {
+  100% {
+    left: 750px;
+  }
+}
+
+@keyframes boxy {
+  10% {
+    background-color: blue;
+  }
+
+  20% {
+    background-color: green;
+  }
+
+  100% {
+    opacity: 0;
+  }
+}
+
+@keyframes moxy {
+  to {
+    opacity: 0;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/doc_keyframeanimation.html
@@ -0,0 +1,13 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+  <head>
+    <title>test case for keyframes rule in rule-view</title>
+    <link rel="stylesheet" type="text/css" href="doc_keyframeanimation.css"/>
+  </head>
+  <body>
+    <div id="pacman"></div>
+    <div id="boxy" class="circle"></div>
+    <div id="moxy" class="circle"></div>
+  </body>
+</html>
--- a/browser/devtools/styleinspector/test/head.js
+++ b/browser/devtools/styleinspector/test/head.js
@@ -614,21 +614,24 @@ let simulateColorPickerChange = Task.asy
 function getRuleViewLinkByIndex(view, index) {
   let links = view.doc.querySelectorAll(".ruleview-rule-source");
   return links[index];
 }
 
 /**
  * Get the rule editor from the rule-view given its index
  * @param {CssRuleView} view The instance of the rule-view panel
- * @param {Number} index The index of the link to get
+ * @param {Number} childrenIndex The children index of the element to get
+ * @param {Number} nodeIndex The child node index of the element to get
  * @return {DOMNode} The rule editor if any at this index
  */
-function getRuleViewRuleEditor(view, index) {
-  return view.element.children[index]._ruleEditor;
+function getRuleViewRuleEditor(view, childrenIndex, nodeIndex) {
+  return nodeIndex !== undefined ?
+    view.element.children[childrenIndex].childNodes[nodeIndex]._ruleEditor :
+    view.element.children[childrenIndex]._ruleEditor;
 }
 
 /**
  * Click on a rule-view's close brace to focus a new property name editor
  * @param {RuleEditor} ruleEditor An instance of RuleEditor that will receive
  * the new property
  * @return a promise that resolves to the newly created editor when ready and
  * focused
--- a/toolkit/devtools/server/actors/styles.js
+++ b/toolkit/devtools/server/actors/styles.js
@@ -40,17 +40,18 @@ 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"
+  inherited: "nullable:domnode#actorid",
+  keyframes: "nullable:domstylerule#actorid"
 });
 
 types.addDictType("matchedselector", {
   rule: "domstylerule#actorid",
   selector: "string",
   value: "string",
   status: "number"
 });
@@ -149,17 +150,17 @@ var PageStyleActor = protocol.ActorClass
    *     ...
    *   }
    */
   getComputed: method(function(node, options) {
     let ret = Object.create(null);
 
     this.cssLogic.sourceFilter = options.filter || CssLogic.FILTER.UA;
     this.cssLogic.highlight(node.rawNode);
-    let computed = this.cssLogic._computedStyle || [];
+    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
       };
     });
@@ -319,18 +320,17 @@ var PageStyleActor = protocol.ActorClass
       return DOMUtils.isInheritedProperty(prop);
     });
   },
 
   /**
    * Helper function for getApplied, adds all the rules from a given
    * element.
    */
-  addElementRules: function(element, inherited, options, rules)
-  {
+  addElementRules: function(element, inherited, options, rules) {
     if (!element.style) {
       return;
     }
 
     let elementStyle = this._styleRef(element);
 
     if (!inherited || this._hasInheritedProps(element.style)) {
       rules.push({
@@ -374,17 +374,16 @@ var PageStyleActor = protocol.ActorClass
         let ruleActor = this._styleRef(domRule);
         rules.push({
           rule: ruleActor,
           inherited: inherited,
           pseudoElement: pseudo,
           isSystem: isSystem
         });
       }
-
     }
   },
 
   /**
    * Helper function for getApplied and addNewRule that fetches a set of
    * style properties that apply to the given node and associated rules
    * @param NodeActor node
    * @param object options
@@ -426,16 +425,34 @@ var PageStyleActor = protocol.ActorClass
         for (let i = 0; i < selectors.length; i++) {
           if (DOMUtils.selectorMatchesElement(element, domRule, i)) {
             entry.matchedSelectors.push(selectors[i]);
           }
         }
       }
     }
 
+    // Add all the keyframes rule associated with the element
+    let animationNames = this.cssLogic.computedStyle.animationName.split(",");
+    animationNames = animationNames.map(name => name.trim())
+    if (animationNames) {
+      // Traverse through all the available keyframes rule and add
+      // the keyframes rule that matches the computed animation name
+      for (let keyframesRule of this.cssLogic.keyframesRules) {
+        if (animationNames.indexOf(keyframesRule.name) > -1) {
+          for (let rule of keyframesRule.cssRules) {
+            entries.push({
+              rule: this._styleRef(rule),
+              keyframes: this._styleRef(keyframesRule)
+            });
+          }
+        }
+      }
+    }
+
     let rules = new Set;
     let sheets = new Set;
     entries.forEach(entry => rules.add(entry.rule));
     this.expandSets(rules, sheets);
 
     return {
       entries: entries,
       rules: [...rules],
@@ -656,17 +673,19 @@ var StyleRuleActor = protocol.ActorClass
   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) {
+      if ((this.rawRule instanceof Ci.nsIDOMCSSStyleRule ||
+           this.rawRule instanceof Ci.nsIDOMMozCSSKeyframeRule) &&
+           this.rawRule.parentStyleSheet) {
         this.line = DOMUtils.getRuleLine(this.rawRule);
         this.column = DOMUtils.getRuleColumn(this.rawRule);
       }
     } else {
       // Fake a rule
       this.type = ELEMENT_STYLE;
       this.rawNode = item;
       this.rawRule = {
@@ -734,16 +753,24 @@ var StyleRuleActor = protocol.ActorClass
         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;
+      case Ci.nsIDOMCSSRule.KEYFRAMES_RULE:
+        form.cssText = this.rawRule.cssText;
+        form.name = this.rawRule.name;
+        break;
+      case Ci.nsIDOMCSSRule.KEYFRAME_RULE:
+        form.cssText = this.rawStyle.cssText || "";
+        form.keyText = this.rawRule.keyText || "";
+        break;
     }
 
     return form;
   },
 
   /**
    * Modify a rule's properties.  Passed an array of modifications:
    * {
@@ -879,25 +906,31 @@ var StyleRuleFront = protocol.FrontClass
       this._mediaText = null;
     }
   },
 
   /**
    * Return a new RuleModificationList for this node.
    */
   startModifyingProperties: function() {
-  return new RuleModificationList(this);
+    return new RuleModificationList(this);
   },
 
   get type() this._form.type,
   get line() this._form.line || -1,
   get column() this._form.column || -1,
   get cssText() {
     return this._form.cssText;
   },
+  get keyText() {
+    return this._form.keyText;
+  },
+  get name() {
+    return this._form.name;
+  },
   get selectors() {
     return this._form.selectors;
   },
   get media() {
     return this._form.media;
   },
   get mediaText() {
     if (!this._form.media) {
--- a/toolkit/devtools/styleinspector/css-logic.js
+++ b/toolkit/devtools/styleinspector/css-logic.js
@@ -124,28 +124,32 @@ CssLogic.prototype = {
   _passId: 0,
 
   // Used for tracking matched CssSelector objects.
   _matchId: 0,
 
   _matchedRules: null,
   _matchedSelectors: null,
 
+  // Cached keyframes rules in all stylesheets
+  _keyframesRules: null,
+
   /**
    * Reset various properties
    */
   reset: function CssLogic_reset()
   {
     this._propertyInfos = {};
     this._ruleCount = 0;
     this._sheetIndex = 0;
     this._sheets = {};
     this._sheetsCached = false;
     this._matchedRules = null;
     this._matchedSelectors = null;
+    this._keyframesRules = [];
   },
 
   /**
    * Focus on a new element - remove the style caches.
    *
    * @param {nsIDOMElement} aViewedElement the element the user has highlighted
    * in the Inspector.
    */
@@ -175,16 +179,25 @@ CssLogic.prototype = {
 
     this._matchedRules = null;
     this._matchedSelectors = null;
     let win = this.viewedDocument.defaultView;
     this._computedStyle = win.getComputedStyle(this.viewedElement, "");
   },
 
   /**
+   * Get the values of all the computed CSS properties for the highlighted
+   * element.
+   * @returns {object} The computed CSS properties for a selected element
+   */
+  get computedStyle() {
+    return this._computedStyle;
+  },
+
+  /**
    * Get the source filter.
    * @returns {string} The source filter being used.
    */
   get sourceFilter() {
     return this._sourceFilter;
   },
 
   /**
@@ -265,17 +278,17 @@ CssLogic.prototype = {
 
     this._sheetsCached = true;
   },
 
   /**
    * Cache a stylesheet if it falls within the requirements: if it's enabled,
    * and if the @media is allowed. This method also walks through the stylesheet
    * cssRules to find @imported rules, to cache the stylesheets of those rules
-   * as well.
+   * as well. In addition, the @keyframes rules in the stylesheet are cached.
    *
    * @private
    * @param {CSSStyleSheet} aDomSheet the CSSStyleSheet object to cache.
    */
   _cacheSheet: function CssLogic_cacheSheet(aDomSheet)
   {
     if (aDomSheet.disabled) {
       return;
@@ -286,23 +299,25 @@ CssLogic.prototype = {
       return;
     }
 
     // Cache the sheet.
     let cssSheet = this.getSheet(aDomSheet, this._sheetIndex++);
     if (cssSheet._passId != this._passId) {
       cssSheet._passId = this._passId;
 
-      // Find import rules.
-      Array.prototype.forEach.call(aDomSheet.cssRules, function(aDomRule) {
+      // Find import and keyframes rules.
+      for (let aDomRule of aDomSheet.cssRules) {
         if (aDomRule.type == Ci.nsIDOMCSSRule.IMPORT_RULE && aDomRule.styleSheet &&
             this.mediaMatches(aDomRule)) {
           this._cacheSheet(aDomRule.styleSheet);
+        } else if (aDomRule.type == Ci.nsIDOMCSSRule.KEYFRAMES_RULE) {
+          this._keyframesRules.push(aDomRule);
         }
-      }, this);
+      }
     }
   },
 
   /**
    * Retrieve the list of stylesheets in the document.
    *
    * @return {array} the list of stylesheets in the document.
    */
@@ -318,16 +333,29 @@ CssLogic.prototype = {
         sheets.push(aSheet);
       }
     }, this);
 
     return sheets;
   },
 
   /**
+   * Retrieve the list of keyframes rules in the document.
+   *
+   * @ return {array} the list of keyframes rules in the document.
+   */
+  get keyframesRules()
+  {
+    if (!this._sheetsCached) {
+      this._cacheSheets();
+    }
+    return this._keyframesRules;
+  },
+
+  /**
    * Retrieve a CssSheet object for a given a CSSStyleSheet object. If the
    * stylesheet is already cached, you get the existing CssSheet object,
    * otherwise the new CSSStyleSheet object is cached.
    *
    * @param {CSSStyleSheet} aDomSheet the CSSStyleSheet object you want.
    * @param {number} aIndex the index, within the document, of the stylesheet.
    *
    * @return {CssSheet} the CssSheet object for the given CSSStyleSheet object.
@@ -615,17 +643,16 @@ CssLogic.prototype = {
           continue;
         }
 
         rule._matchId = this._matchId;
         rule._passId = this._passId;
         this._matchedRules.push([rule, status]);
       }
 
-
       // Add element.style information.
       if (element.style && element.style.length > 0) {
         let rule = new CssRule(null, { style: element.style }, element);
         rule._matchId = this._matchId;
         rule._passId = this._passId;
         this._matchedRules.push([rule, status]);
       }
     } while ((element = element.parentNode) &&
@@ -639,17 +666,17 @@ CssLogic.prototype = {
    * @return {boolean} True if the DOM CSS object matches the current view
    * media, or false otherwise.
    */
   mediaMatches: function CL_mediaMatches(aDomObject)
   {
     let mediaText = aDomObject.media.mediaText;
     return !mediaText || this.viewedDocument.defaultView.
                          matchMedia(mediaText).matches;
-   },
+  },
 };
 
 /**
  * If the element has an id, return '#id'. Otherwise return 'tagname[n]' where
  * n is the index of this element in its siblings.
  * <p>A technically more 'correct' output from the no-id case might be:
  * 'tagname:nth-of-type(n)' however this is unlikely to be more understood
  * and it is longer.
@@ -1530,19 +1557,19 @@ CssPropertyInfo.prototype = {
    * Retrieve the computed style value for the current property, for the
    * highlighted element.
    *
    * @return {string} the computed style value for the current property, for the
    * highlighted element.
    */
   get value()
   {
-    if (!this._value && this._cssLogic._computedStyle) {
+    if (!this._value && this._cssLogic.computedStyle) {
       try {
-        this._value = this._cssLogic._computedStyle.getPropertyValue(this.property);
+        this._value = this._cssLogic.computedStyle.getPropertyValue(this.property);
       } catch (ex) {
         Services.console.logStringMessage('Error reading computed style for ' +
           this.property);
         Services.console.logStringMessage(ex);
       }
     }
     return this._value;
   },
--- a/toolkit/locales/en-US/chrome/global/devtools/styleinspector.properties
+++ b/toolkit/locales/en-US/chrome/global/devtools/styleinspector.properties
@@ -30,16 +30,20 @@ rule.sourceInline=inline
 rule.sourceElement=element
 
 # LOCALIZATION NOTE (rule.inheritedFrom): Shown for CSS rules
 # that were inherited from a parent node. Will be passed a node
 # identifier of the parent node.
 # e.g "Inherited from body#bodyID"
 rule.inheritedFrom=Inherited from %S
 
+# LOCALIZATION NOTE (rule.keyframe): Shown for CSS Rules keyframe header.
+# Will be passed an identifier of the keyframe animation name.
+rule.keyframe=Keyframes %S
+
 # LOCALIZATION NOTE (rule.userAgentStyles): Shown next to the style sheet
 # link for CSS rules that were loaded from a user agent style sheet.
 # These styles will not be editable, and will only be visible if the
 # devtools.inspector.showUserAgentStyles pref is true.
 rule.userAgentStyles=(user agent)
 
 # LOCALIZATION NOTE (rule.pseudoElement): Shown for CSS rules
 # pseudo element header