Bug 694019 - we should be able to highlight and inspect a :pseudo element r=mratcliffe
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Wed, 04 Sep 2013 17:43:40 +0200
changeset 145486 4b65dc0a16cd164079203213b455d747f1da3273
parent 145485 d1d0a3fdbfe9bb9e2fcc28dd27e30f2937058b37
child 145487 e4ca2c1ba7c3adf9c21195b4f46fcb2aa504181c
push id25214
push userkwierso@gmail.com
push dateThu, 05 Sep 2013 00:02:20 +0000
treeherdermozilla-central@99bd249e5a20 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmratcliffe
bugs694019
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 694019 - we should be able to highlight and inspect a :pseudo element r=mratcliffe
browser/app/profile/firefox.js
browser/devtools/styleinspector/rule-view.js
browser/devtools/styleinspector/ruleview.css
browser/devtools/styleinspector/test/Makefile.in
browser/devtools/styleinspector/test/browser_ruleview_pseudoelement.html
browser/devtools/styleinspector/test/browser_ruleview_pseudoelement.js
browser/locales/en-US/chrome/browser/devtools/styleinspector.properties
browser/themes/linux/devtools/ruleview.css
browser/themes/osx/devtools/ruleview.css
browser/themes/windows/devtools/ruleview.css
toolkit/devtools/server/actors/styles.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1070,16 +1070,17 @@ pref("devtools.toolbox.selectedTool", "w
 pref("devtools.toolbox.toolbarSpec", '["paintflashing toggle","tilt toggle","scratchpad","resize toggle"]');
 pref("devtools.toolbox.sideEnabled", true);
 
 // Enable the Inspector
 pref("devtools.inspector.enabled", true);
 pref("devtools.inspector.activeSidebar", "ruleview");
 pref("devtools.inspector.markupPreview", false);
 pref("devtools.inspector.remote", false);
+pref("devtools.inspector.show_pseudo_elements", true);
 
 // Enable the Layout View
 pref("devtools.layoutview.enabled", true);
 pref("devtools.layoutview.open", false);
 
 // Enable the Responsive UI tool
 pref("devtools.responsiveUI.enabled", true);
 pref("devtools.responsiveUI.no-reload-notification", false);
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -6,17 +6,17 @@
 
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
 const promise = require("sdk/core/promise");
 
 let {CssLogic} = require("devtools/styleinspector/css-logic");
 let {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor");
-let {ELEMENT_STYLE} = require("devtools/server/actors/styles");
+let {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 /**
@@ -198,29 +198,41 @@ ElementStyle.prototype = {
 
         this.rules = [];
 
         for (let entry of entries) {
           this._maybeAddRule(entry);
         }
 
         // Mark overridden computed styles.
-        this.markOverridden();
+        this.markOverriddenAll();
+
+        this._sortRulesForPseudoElement();
 
         // We're done with the previous list of rules.
         delete this._refreshRules;
 
         return null;
       });
     }).then(null, promiseWarn);
     this.populated = populated;
     return this.populated;
   },
 
   /**
+   * Put pseudo elements in front of others.
+   */
+   _sortRulesForPseudoElement: function ElementStyle_sortRulesForPseudoElement()
+   {
+      this.rules = this.rules.sort((a, b) => {
+        return (a.pseudoElement || "z") > (b.pseudoElement || "z");
+      });
+   },
+
+  /**
    * Add a rule if it's one we care about.  Filters out duplicates and
    * inherited styles with no inherited properties.
    *
    * @param {object} aOptions
    *        Options for creating the Rule, see the Rule constructor.
    *
    * @return {bool} true if we added the rule.
    */
@@ -261,32 +273,48 @@ ElementStyle.prototype = {
       return false;
     }
 
     this.rules.push(rule);
     return true;
   },
 
   /**
-   * Mark the properties listed in this.rules with an overridden flag
-   * if an earlier property overrides it.
+   * Calls markOverridden with all supported pseudo elements
    */
-  markOverridden: function ElementStyle_markOverridden()
+  markOverriddenAll: function ElementStyle_markOverriddenAll()
+  {
+    this.markOverridden();
+    for (let pseudo of PSEUDO_ELEMENTS) {
+      this.markOverridden(pseudo);
+    }
+  },
+
+  /**
+   * 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 ElementStyle_markOverridden(pseudo="")
   {
     // Gather all the text properties applied by these rules, ordered
     // from more- to less-specific.
     let textProps = [];
-    for each (let rule in this.rules) {
-      textProps = textProps.concat(rule.textProps.slice(0).reverse());
+    for (let rule of this.rules) {
+      if (rule.pseudoElement == pseudo) {
+        textProps = textProps.concat(rule.textProps.slice(0).reverse());
+      }
     }
 
     // Gather all the computed properties applied by those text
     // properties.
     let computedProps = [];
-    for each (let textProp in textProps) {
+    for (let textProp of textProps) {
       computedProps = computedProps.concat(textProp.computed);
     }
 
     // Walk over the computed properties.  As we see a property name
     // for the first time, mark that property's name as taken by this
     // property.
     //
     // If we come across a property whose name is already taken, check
@@ -297,17 +325,17 @@ ElementStyle.prototype = {
     //   the new property.
     //
     //   If the new property is a lower or equal priority, mark it as
     //   overridden.
     //
     // _overriddenDirty will be set on each prop, indicating whether its
     // dirty status changed during this pass.
     let taken = {};
-    for each (let computedProp in computedProps) {
+    for (let computedProp of computedProps) {
       let earlier = taken[computedProp.name];
       let overridden;
       if (earlier
           && computedProp.priority === "important"
           && earlier.priority !== "important") {
         // New property is higher priority.  Mark the earlier property
         // overridden (which will reverse its dirty state).
         earlier._overriddenDirty = !earlier._overriddenDirty;
@@ -323,17 +351,17 @@ ElementStyle.prototype = {
         taken[computedProp.name] = computedProp;
       }
     }
 
     // For each TextProperty, mark it overridden if all of its
     // computed properties are marked overridden.  Update the text
     // property's associated editor, if any.  This will clear the
     // _overriddenDirty state on all computed properties.
-    for each (let textProp in textProps) {
+    for (let textProp of textProps) {
       // _updatePropertyOverridden will return true if the
       // overridden state has changed for the text property.
       if (this._updatePropertyOverridden(textProp)) {
         textProp.updateEditor();
       }
     }
   },
 
@@ -379,16 +407,17 @@ ElementStyle.prototype = {
  * @constructor
  */
 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.inherited = aOptions.inherited || null;
   this._modificationDepth = 0;
 
   if (this.domRule) {
     let parentRule = this.domRule.parentRule;
     if (parentRule && parentRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE) {
       this.mediaText = parentRule.mediaText;
@@ -553,17 +582,17 @@ Rule.prototype = {
             this.style, textProp.name,
             null,
             cssProp.value,
             textProp.value);
         }
         textProp.priority = cssProp.priority;
       }
 
-      this.elementStyle.markOverridden();
+      this.elementStyle.markOverriddenAll();
 
       if (promise === this._applyingModifications) {
         this._applyingModifications = null;
       }
 
       this.elementStyle._changed();
     }).then(null, promiseWarn);
     this._applyingModifications = promise;
@@ -1072,16 +1101,17 @@ CssRuleView.prototype = {
   _populate: function() {
     let elementStyle = this._elementStyle;
     return this._elementStyle.populate().then(() => {
       if (this._elementStyle != elementStyle) {
         return promise.reject("element changed");
       }
       this._createEditors();
 
+
       // Notify anyone that cares that we refreshed.
       var evt = this.doc.createEvent("Events");
       evt.initEvent("CssRuleViewRefreshed", true, false);
       this.element.dispatchEvent(evt);
       return undefined;
     }).then(null, promiseWarn);
   },
 
@@ -1127,43 +1157,128 @@ CssRuleView.prototype = {
   _changed: function CssRuleView_changed()
   {
     var evt = this.doc.createEvent("Events");
     evt.initEvent("CssRuleViewChanged", true, false);
     this.element.dispatchEvent(evt);
   },
 
   /**
+   * Text for header that shows above rules for this element
+   */
+  get selectedElementLabel ()
+  {
+    if (this._selectedElementLabel) {
+      return this._selectedElementLabel;
+    }
+    this._selectedElementLabel = CssLogic.l10n("rule.selectedElement");
+    return this._selectedElementLabel;
+  },
+
+  /**
+   * Text for header that shows above rules for pseudo elements
+   */
+  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 editor UI for each of the rules in _elementStyle.
    */
   _createEditors: function CssRuleView_createEditors()
   {
     // Run through the current list of rules, attaching
     // their editors in order.  Create editors if needed.
     let lastInheritedSource = "";
+    let seenPseudoElement = false;
+    let seenNormalElement = false;
+
     for (let rule of this._elementStyle.rules) {
       if (rule.domRule.system) {
         continue;
       }
 
+      // Only print header for this element if there are pseudo elements
+      if (seenPseudoElement && !seenNormalElement && !rule.pseudoElement) {
+        seenNormalElement = true;
+        let div = this.doc.createElementNS(HTML_NS, "div");
+        div.className = "theme-gutter ruleview-header";
+        div.textContent = this.selectedElementLabel;
+        this.element.appendChild(div);
+      }
+
       let inheritedSource = rule.inheritedSource;
       if (inheritedSource != lastInheritedSource) {
-        let h2 = this.doc.createElementNS(HTML_NS, "div");
-        h2.className = "ruleview-rule-inheritance theme-gutter";
-        h2.textContent = inheritedSource;
+        let div = this.doc.createElementNS(HTML_NS, "div");
+        div.className = "theme-gutter ruleview-header";
+        div.textContent = inheritedSource;
         lastInheritedSource = inheritedSource;
-        this.element.appendChild(h2);
+        this.element.appendChild(div);
+      }
+
+      if (!seenPseudoElement && rule.pseudoElement) {
+        seenPseudoElement = true;
+
+        let div = this.doc.createElementNS(HTML_NS, "div");
+        div.className = "theme-gutter ruleview-header";
+        div.textContent = this.pseudoElementLabel;
+
+        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);
       }
 
       if (!rule.editor) {
         new RuleEditor(this, rule);
       }
 
       this.element.appendChild(rule.editor.element);
     }
+
+    this.togglePseudoElementVisibility(this.showPseudoElements);
   },
 
   /**
    * Copy selected text from the rule view.
    *
    * @param {Event} aEvent
    *        The event object.
    */
@@ -1221,16 +1336,19 @@ function RuleEditor(aRuleView, aRule)
 }
 
 RuleEditor.prototype = {
   _create: function RuleEditor_create()
   {
     this.element = this.doc.createElementNS(HTML_NS, "div");
     this.element.className = "ruleview-rule theme-separator";
     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"
@@ -1352,22 +1470,25 @@ RuleEditor.prototype = {
    * Programatically add a new property to the rule.
    *
    * @param {string} aName
    *        Property name.
    * @param {string} aValue
    *        Property value.
    * @param {string} aPriority
    *        Property priority.
+   * @return {TextProperty}
+   *        The new property
    */
   addProperty: function RuleEditor_addProperty(aName, aValue, aPriority)
   {
     let prop = this.rule.createProperty(aName, aValue, aPriority);
     let editor = new TextPropertyEditor(this, prop);
     this.propertyList.appendChild(editor.element);
+    return prop;
   },
 
   /**
    * Create a text input for a property name.  If a non-empty property
    * name is given, we'll create a real TextProperty and add it to the
    * rule.
    */
   newProperty: function RuleEditor_newProperty()
--- a/browser/devtools/styleinspector/ruleview.css
+++ b/browser/devtools/styleinspector/ruleview.css
@@ -31,8 +31,26 @@
 .ruleview-propertycontainer a {
   cursor: pointer;
 }
 
 .ruleview-computedlist:not(.styleinspector-open),
 .ruleview-warning[hidden] {
   display: none;
 }
+
+.ruleview-rule-pseudo-element {
+  display: none;
+}
+
+.show-pseudo-elements .ruleview-rule-pseudo-element {
+  display: block;
+}
+
+.ruleview .ruleview-expander {
+  vertical-align: middle;
+}
+
+.ruleview-header {
+  vertical-align:middle;
+  height: 1.5em;
+  line-height: 1.5em;
+}
\ No newline at end of file
--- a/browser/devtools/styleinspector/test/Makefile.in
+++ b/browser/devtools/styleinspector/test/Makefile.in
@@ -38,16 +38,17 @@ MOCHITEST_BROWSER_FILES = \
   browser_computedview_734259_style_editor_link.js \
   browser_computedview_copy.js\
   browser_styleinspector_bug_677930_urls_clickable.js \
   browser_bug893965_css_property_completion_new_property.js \
   browser_bug893965_css_property_completion_existing_property.js \
   browser_bug894376_css_value_completion_new_property_value_pair.js \
   browser_bug894376_css_value_completion_existing_property_value_pair.js \
   browser_ruleview_bug_902966_revert_value_on_ESC.js \
+  browser_ruleview_pseudoelement.js \
   head.js \
   $(NULL)
 
 MOCHITEST_BROWSER_FILES += \
   browser_bug683672.html \
   browser_bug705707_is_content_stylesheet.html \
   browser_bug705707_is_content_stylesheet_imported.css \
   browser_bug705707_is_content_stylesheet_imported2.css \
@@ -55,11 +56,12 @@ MOCHITEST_BROWSER_FILES += \
   browser_bug705707_is_content_stylesheet_script.css \
   browser_bug705707_is_content_stylesheet.xul \
   browser_bug705707_is_content_stylesheet_xul.css \
   browser_bug722196_identify_media_queries.html \
   browser_styleinspector_bug_677930_urls_clickable.html \
   browser_styleinspector_bug_677930_urls_clickable \
   browser_styleinspector_bug_677930_urls_clickable/browser_styleinspector_bug_677930_urls_clickable.css \
   test-image.png \
+  browser_ruleview_pseudoelement.html \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_pseudoelement.html
@@ -0,0 +1,115 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+  <head>
+    <style>
+
+body {
+    color: #333;
+}
+
+.box {
+    float:left;
+    width: 128px;
+    height: 128px;
+    background: #ddd;
+    padding: 32px;
+    margin: 32px;
+    position:relative;
+}
+
+* {
+    cursor: default;
+}
+
+nothing {
+    cursor: pointer;
+}
+
+p::-moz-selection {
+    color: white;
+    background: black;
+}
+p::selection {
+    color: white;
+    background: black;
+}
+
+p:first-line {
+   background: blue;
+}
+p:first-letter {
+  color: red;
+  font-size: 130%;
+}
+
+.box:before {
+    background: green;
+    content: " ";
+    position: absolute;
+    height:32px;
+    width:32px;
+}
+
+.box:after {
+    background: red;
+    content: " ";
+    position: absolute;
+    border-radius: 50%;
+    height:32px;
+    width:32px;
+    top: 50%;
+    left: 50%;
+    margin-top: -16px;
+    margin-left: -16px;
+}
+
+.topleft:before {
+    top:0;
+    left:0;
+}
+
+.topright:before {
+    top:0;
+    right:0;
+}
+
+.bottomright:before {
+    bottom:10px;
+    right:10px;
+    color: red;
+}
+
+.bottomright:before {
+    bottom:0;
+    right:0;
+}
+
+.bottomleft:before {
+    bottom:0;
+    left:0;
+}
+
+    </style>
+  </head>
+  <body>
+    <h1>ruleview pseudoelement($("test"));</h1>
+
+    <div id="topleft" class="box topleft">
+        <p>Top Left<br />Position</p>
+    </div>
+
+    <div id="topright" class="box topright">
+        <p>Top Right<br />Position</p>
+    </div>
+
+    <div id="bottomright" class="box bottomright">
+        <p>Bottom Right<br />Position</p>
+    </div>
+
+    <div id="bottomleft" class="box bottomleft">
+        <p>Bottom Left<br />Position</p>
+    </div>
+
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_pseudoelement.js
@@ -0,0 +1,317 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let doc;
+let inspector;
+let view;
+
+const TEST_URI = "http://example.com/browser/browser/" +
+                 "devtools/styleinspector/test/" +
+                 "browser_ruleview_pseudoelement.html";
+
+function testPseudoElements(aInspector, aRuleView)
+{
+  inspector = aInspector;
+  view = aRuleView;
+
+  testTopLeft();
+}
+
+function testTopLeft()
+{
+  testNode(doc.querySelector("#topleft"), (element, elementStyle) => {
+    let elementRules = elementStyle.rules.filter((rule) => { return !rule.pseudoElement; });
+    let afterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":after"; });
+    let beforeRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":before"; });
+    let firstLineRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-line"; });
+    let firstLetterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-letter"; });
+    let selectionRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":-moz-selection"; });
+
+    is(elementRules.length, 4, "TopLeft has the correct number of non psuedo element rules");
+    is(afterRules.length, 1, "TopLeft has the correct number of :after rules");
+    is(beforeRules.length, 2, "TopLeft has the correct number of :before rules");
+    is(firstLineRules.length, 0, "TopLeft has the correct number of :first-line rules");
+    is(firstLetterRules.length, 0, "TopLeft has the correct number of :first-letter rules");
+    is(selectionRules.length, 0, "TopLeft has the correct number of :selection rules");
+
+    let gutters = view.element.querySelectorAll(".theme-gutter");
+    is (gutters.length, 3, "There are three gutter headings");
+    is (gutters[0].textContent, "Pseudo-elements", "Gutter heading is correct");
+    is (gutters[1].textContent, "This Element", "Gutter heading is correct");
+    is (gutters[2].textContent, "Inherited from body", "Gutter heading is correct");
+
+    // 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");
+    expander.click();
+    ok (!view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are collapsed by twisty");
+    expander.click();
+    ok (view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are expanded again");
+    expander.click();
+
+    let defaultView = element.ownerDocument.defaultView;
+    let elementRule = elementRules[0];
+    let elementRuleView = [].filter.call(view.element.children, (e) => {
+      return e._ruleEditor && e._ruleEditor.rule === elementRule;
+    })[0]._ruleEditor;
+
+    let elementAfterRule = afterRules[0];
+    let elementAfterRuleView = [].filter.call(view.element.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 = beforeRules[0];
+    let elementBeforeRuleView = [].filter.call(view.element.children, (e) => {
+      return e._ruleEditor && e._ruleEditor.rule === elementBeforeRule;
+    })[0]._ruleEditor;
+
+    is
+    (
+      convertTextPropsToString(elementBeforeRule.textProps),
+      "top: 0px; left: 0px",
+      "TopLeft before properties are correct"
+    );
+
+    let firstProp = elementAfterRuleView.addProperty("background-color", "rgb(0, 255, 0)", "");
+    let secondProp = elementAfterRuleView.addProperty("padding", "100px", "");
+
+    is (firstProp, elementAfterRule.textProps[elementAfterRule.textProps.length - 2],
+        "First added property is on back of array");
+    is (secondProp, elementAfterRule.textProps[elementAfterRule.textProps.length - 1],
+        "Second added property is on back of array");
+
+    promiseDone(elementAfterRule._applyingModifications.then(() => {
+      is(defaultView.getComputedStyle(element, ":after").getPropertyValue("background-color"),
+        "rgb(0, 255, 0)", "Added property should have been used.");
+      is(defaultView.getComputedStyle(element, ":after").getPropertyValue("padding-top"),
+        "100px", "Added property should have been used.");
+      is(defaultView.getComputedStyle(element).getPropertyValue("padding-top"),
+        "32px", "Added property should not apply to element");
+
+      secondProp.setEnabled(false);
+
+      return elementAfterRule._applyingModifications;
+    }).then(() => {
+      is(defaultView.getComputedStyle(element, ":after").getPropertyValue("padding-top"), "0px",
+        "Disabled property should have been used.");
+      is(defaultView.getComputedStyle(element).getPropertyValue("padding-top"), "32px",
+        "Added property should not apply to element");
+
+      secondProp.setEnabled(true);
+
+      return elementAfterRule._applyingModifications;
+    }).then(() => {
+      is(defaultView.getComputedStyle(element, ":after").getPropertyValue("padding-top"), "100px",
+        "Enabled property should have been used.");
+      is(defaultView.getComputedStyle(element).getPropertyValue("padding-top"), "32px",
+        "Added property should not apply to element");
+
+      let firstProp = elementRuleView.addProperty("background-color", "rgb(0, 0, 255)", "");
+
+      return elementRule._applyingModifications;
+    }).then(() => {
+      is(defaultView.getComputedStyle(element).getPropertyValue("background-color"), "rgb(0, 0, 255)",
+        "Added property should have been used.");
+      is(defaultView.getComputedStyle(element, ":after").getPropertyValue("background-color"), "rgb(0, 255, 0)",
+        "Added prop does not apply to pseudo");
+
+      testTopRight();
+    }));
+  });
+}
+
+function testTopRight()
+{
+  testNode(doc.querySelector("#topright"), (element, elementStyle) => {
+
+    let elementRules = elementStyle.rules.filter((rule) => { return !rule.pseudoElement; });
+    let afterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":after"; });
+    let beforeRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":before"; });
+    let firstLineRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-line"; });
+    let firstLetterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-letter"; });
+    let selectionRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":-moz-selection"; });
+
+    is(elementRules.length, 4, "TopRight has the correct number of non psuedo element rules");
+    is(afterRules.length, 1, "TopRight has the correct number of :after rules");
+    is(beforeRules.length, 2, "TopRight has the correct number of :before rules");
+    is(firstLineRules.length, 0, "TopRight has the correct number of :first-line rules");
+    is(firstLetterRules.length, 0, "TopRight has the correct number of :first-letter rules");
+    is(selectionRules.length, 0, "TopRight has the correct number of :selection rules");
+
+    let gutters = view.element.querySelectorAll(".theme-gutter");
+    is (gutters.length, 3, "There are three gutter headings");
+    is (gutters[0].textContent, "Pseudo-elements", "Gutter heading is correct");
+    is (gutters[1].textContent, "This Element", "Gutter heading is correct");
+    is (gutters[2].textContent, "Inherited from body", "Gutter heading is correct");
+
+    let expander = gutters[0].querySelector(".ruleview-expander");
+    ok (!view.element.classList.contains("show-pseudo-elements"), "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");
+
+    testBottomRight();
+  });
+}
+
+function testBottomRight()
+{
+  testNode(doc.querySelector("#bottomright"), (element, elementStyle) => {
+
+    let elementRules = elementStyle.rules.filter((rule) => { return !rule.pseudoElement; });
+    let afterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":after"; });
+    let beforeRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":before"; });
+    let firstLineRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-line"; });
+    let firstLetterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-letter"; });
+    let selectionRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":-moz-selection"; });
+
+    is(elementRules.length, 4, "BottomRight has the correct number of non psuedo element rules");
+    is(afterRules.length, 1, "BottomRight has the correct number of :after rules");
+    is(beforeRules.length, 3, "BottomRight has the correct number of :before rules");
+    is(firstLineRules.length, 0, "BottomRight has the correct number of :first-line rules");
+    is(firstLetterRules.length, 0, "BottomRight has the correct number of :first-letter rules");
+    is(selectionRules.length, 0, "BottomRight has the correct number of :selection rules");
+
+    testBottomLeft();
+  });
+}
+
+function testBottomLeft()
+{
+  testNode(doc.querySelector("#bottomleft"), (element, elementStyle) => {
+
+    let elementRules = elementStyle.rules.filter((rule) => { return !rule.pseudoElement; });
+    let afterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":after"; });
+    let beforeRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":before"; });
+    let firstLineRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-line"; });
+    let firstLetterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-letter"; });
+    let selectionRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":-moz-selection"; });
+
+    is(elementRules.length, 4, "BottomLeft has the correct number of non psuedo element rules");
+    is(afterRules.length, 1, "BottomLeft has the correct number of :after rules");
+    is(beforeRules.length, 2, "BottomLeft has the correct number of :before rules");
+    is(firstLineRules.length, 0, "BottomLeft has the correct number of :first-line rules");
+    is(firstLetterRules.length, 0, "BottomLeft has the correct number of :first-letter rules");
+    is(selectionRules.length, 0, "BottomLeft has the correct number of :selection rules");
+
+    testParagraph();
+  });
+}
+
+function testParagraph()
+{
+  testNode(doc.querySelector("#bottomleft p"), (element, elementStyle) => {
+
+    let elementRules = elementStyle.rules.filter((rule) => { return !rule.pseudoElement; });
+    let afterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":after"; });
+    let beforeRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":before"; });
+    let firstLineRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-line"; });
+    let firstLetterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-letter"; });
+    let selectionRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":-moz-selection"; });
+
+    is(elementRules.length, 3, "Paragraph has the correct number of non psuedo element rules");
+    is(afterRules.length, 0, "Paragraph has the correct number of :after rules");
+    is(beforeRules.length, 0, "Paragraph has the correct number of :before rules");
+    is(firstLineRules.length, 1, "Paragraph has the correct number of :first-line rules");
+    is(firstLetterRules.length, 1, "Paragraph has the correct number of :first-letter rules");
+    is(selectionRules.length, 1, "Paragraph has the correct number of :selection rules");
+
+    let gutters = view.element.querySelectorAll(".theme-gutter");
+    is (gutters.length, 3, "There are three gutter headings");
+    is (gutters[0].textContent, "Pseudo-elements", "Gutter heading is correct");
+    is (gutters[1].textContent, "This Element", "Gutter heading is correct");
+    is (gutters[2].textContent, "Inherited from body", "Gutter heading is correct");
+
+    let elementFirstLineRule = firstLineRules[0];
+    let elementFirstLineRuleView = [].filter.call(view.element.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 = firstLetterRules[0];
+    let elementFirstLetterRuleView = [].filter.call(view.element.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 = selectionRules[0];
+    let elementSelectionRuleView = [].filter.call(view.element.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"
+    );
+
+    testBody();
+  });
+}
+
+function testBody() {
+
+  testNode(doc.querySelector("body"), (element, elementStyle) => {
+
+    let gutters = view.element.querySelectorAll(".theme-gutter");
+    is (gutters.length, 0, "There are no gutter headings");
+
+    finishTest();
+  });
+
+}
+function convertTextPropsToString(textProps) {
+  return textProps.map((t) => {
+    return t.name + ": " + t.value;
+  }).join("; ");
+}
+
+function testNode(node, cb)
+{
+  inspector.once("inspector-updated", () => {
+    cb(node, view._elementStyle)
+  });
+  inspector.selection.setNode(node);
+}
+
+function finishTest()
+{
+  doc = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function test()
+{
+  waitForExplicitFinish();
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+    gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+    doc = content.document;
+    waitForFocus(() => openRuleView(testPseudoElements), content);
+  }, true);
+
+  content.location = TEST_URI;
+}
--- a/browser/locales/en-US/chrome/browser/devtools/styleinspector.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/styleinspector.properties
@@ -30,16 +30,24 @@ 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.pseudoElement): Shown for CSS rules
+# pseudo element header
+rule.pseudoElement=Pseudo-elements
+
+# LOCALIZATION NOTE (rule.pseudoElement): Shown for CSS rules
+# pseudo element header
+rule.selectedElement=This Element
+
 # LOCALIZATION NOTE (helpLinkTitle): For each style property
 # the user can hover it and get a help link button which allows one to
 # quickly jump to the documentation from the Mozilla Developer Network site.
 # This is the link title shown in the hover tooltip.
 helpLinkTitle=Read the documentation for this property
 
 # LOCALIZATION NOTE (rule.warning.title): When an invalid property value is
 # entered into the rule view a warning icon is displayed. This text is used for
--- a/browser/themes/linux/devtools/ruleview.css
+++ b/browser/themes/linux/devtools/ruleview.css
@@ -9,17 +9,17 @@
 .ruleview-rule-source {
   -moz-padding-start: 5px;
   cursor: pointer;
   text-align: right;
   float: right;
   -moz-user-select: none;
 }
 
-.ruleview-rule-inheritance {
+.ruleview-header {
   border-top-width: 1px;
   border-bottom-width: 1px;
   border-top-style: solid;
   border-bottom-style: solid;
   padding: 1px 4px;
   margin-top: 4px;
   -moz-user-select: none;
 }
--- a/browser/themes/osx/devtools/ruleview.css
+++ b/browser/themes/osx/devtools/ruleview.css
@@ -9,26 +9,30 @@
 .ruleview-rule-source {
   -moz-padding-start: 5px;
   cursor: pointer;
   text-align: right;
   float: right;
   -moz-user-select: none;
 }
 
-.ruleview-rule-inheritance {
+.ruleview-header {
   border-top-width: 1px;
   border-bottom-width: 1px;
   border-top-style: solid;
   border-bottom-style: solid;
   padding: 1px 4px;
-  margin-top: 4px;
   -moz-user-select: none;
 }
 
+.ruleview-rule-pseudo-element {
+  padding-left:20px;
+  border-left: solid 10px;
+}
+
 .ruleview-rule-source:hover {
   text-decoration: underline;
 }
 
 .ruleview-rule,
 #noResults {
   padding: 2px 4px;
 }
--- a/browser/themes/windows/devtools/ruleview.css
+++ b/browser/themes/windows/devtools/ruleview.css
@@ -9,17 +9,17 @@
 .ruleview-rule-source {
   -moz-padding-start: 5px;
   cursor: pointer;
   text-align: right;
   float: right;
   -moz-user-select: none;
 }
 
-.ruleview-rule-inheritance {
+.ruleview-header {
   border-top-width: 1px;
   border-bottom-width: 1px;
   border-top-style: solid;
   border-bottom-style: solid;
   padding: 1px 4px;
   margin-top: 4px;
   -moz-user-select: none;
 }
--- a/toolkit/devtools/server/actors/styles.js
+++ b/toolkit/devtools/server/actors/styles.js
@@ -15,16 +15,19 @@ loader.lazyGetter(this, "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;
 
+const PSEUDO_ELEMENTS = [":first-line", ":first-letter", ":before", ":after", ":-moz-selection"];
+exports.PSEUDO_ELEMENTS = PSEUDO_ELEMENTS;
+
 // 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");
@@ -355,46 +358,56 @@ var PageStyleActor = protocol.ActorClass
 
     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);
+    let pseudoElements = inherited ? [null] : [null, ...PSEUDO_ELEMENTS];
+    for (let pseudo of pseudoElements) {
 
-    // getCSSStyleRules returns ordered from least-specific to
-    // most-specific.
-    for (let i = domRules.Count() - 1; i >= 0; i--) {
-      let domRule = domRules.GetElementAt(i);
+      // Get the styles that apply to the element.
+      let domRules = DOMUtils.getCSSStyleRules(element, pseudo);
 
-      let isSystem = !CssLogic.isContentStylesheet(domRule.parentStyleSheet);
-
-      if (isSystem && options.filter != CssLogic.FILTER.UA) {
+      if (!domRules) {
         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) {
+      // 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,
+          pseudoElement: pseudo
+        });
       }
 
-      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