summaryrefslogtreecommitdiff
path: root/toolkit/devtools/styleinspector
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/devtools/styleinspector')
-rw-r--r--toolkit/devtools/styleinspector/computed-view.js1493
-rw-r--r--toolkit/devtools/styleinspector/computedview.xhtml115
-rw-r--r--toolkit/devtools/styleinspector/css-parsing-utils.js154
-rw-r--r--toolkit/devtools/styleinspector/cssruleview.xhtml38
-rw-r--r--toolkit/devtools/styleinspector/moz.build13
-rw-r--r--toolkit/devtools/styleinspector/rule-view.js3097
-rw-r--r--toolkit/devtools/styleinspector/ruleview.css61
-rw-r--r--toolkit/devtools/styleinspector/style-inspector-overlays.js405
-rw-r--r--toolkit/devtools/styleinspector/style-inspector.js251
-rw-r--r--toolkit/devtools/styleinspector/test/browser.ini126
-rw-r--r--toolkit/devtools/styleinspector/test/browser_computedview_browser-styles.js54
-rw-r--r--toolkit/devtools/styleinspector/test/browser_computedview_getNodeInfo.js177
-rw-r--r--toolkit/devtools/styleinspector/test/browser_computedview_keybindings_01.js79
-rw-r--r--toolkit/devtools/styleinspector/test/browser_computedview_keybindings_02.js62
-rw-r--r--toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors-toggle.js108
-rw-r--r--toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors_01.js36
-rw-r--r--toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors_02.js44
-rw-r--r--toolkit/devtools/styleinspector/test/browser_computedview_media-queries.js41
-rw-r--r--toolkit/devtools/styleinspector/test/browser_computedview_no-results-placeholder.js73
-rw-r--r--toolkit/devtools/styleinspector/test/browser_computedview_original-source-link.js74
-rw-r--r--toolkit/devtools/styleinspector/test/browser_computedview_pseudo-element_01.js41
-rw-r--r--toolkit/devtools/styleinspector/test/browser_computedview_refresh-on-style-change_01.js32
-rw-r--r--toolkit/devtools/styleinspector/test/browser_computedview_search-filter.js65
-rw-r--r--toolkit/devtools/styleinspector/test/browser_computedview_select-and-copy-styles.js118
-rw-r--r--toolkit/devtools/styleinspector/test/browser_computedview_style-editor-link.js146
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_add-property-and-reselect.js43
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_01.js55
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_02.js79
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_03.js68
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_add-property_01.js79
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_add-property_02.js70
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_01.js91
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_02.js78
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_03.js114
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_01.js60
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_02.js61
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-appears-on-swatch-click.js54
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-commit-on-ENTER.js59
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-edit-gradient.js75
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-hides-on-tooltip.js48
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-multiple-changes.js126
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-revert-on-ESC.js56
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-swatch-displayed.js55
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_completion-existing-property_01.js104
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_completion-existing-property_02.js101
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_completion-new-property_01.js91
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_completion-new-property_02.js104
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_content_01.js43
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_content_02.js83
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-appears-on-swatch-click.js70
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-commit-on-ENTER.js61
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-revert-on-ESC.js53
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-commit.js84
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-increments.js181
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-order.js68
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_edit-property_01.js84
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_edit-property_02.js142
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector-commit.js98
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector_01.js66
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js81
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_eyedropper.js195
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_inherit.js86
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_keybindings.js49
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_keyframes-rule_01.js127
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_keyframes-rule_02.js110
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_livepreview.js71
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_mathml-element.js54
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_media-queries.js31
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-duplicates.js54
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-priority.js42
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_01.js53
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_02.js52
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_multiple_properties_01.js45
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_multiple_properties_02.js49
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_original-source-link.js84
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_override.js146
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_pseudo-element_01.js247
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_pseudo-element_02.js32
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-attribute-change_01.js59
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-attribute-change_02.js123
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-style-change.js41
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_select-and-copy-styles.js129
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_selector-highlighter_01.js44
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_selector-highlighter_02.js85
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_style-editor-link.js163
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_urls-clickable.js61
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_user-agent-styles-uneditable.js50
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_user-agent-styles.js138
-rw-r--r--toolkit/devtools/styleinspector/test/browser_ruleview_user-property-reset.js90
-rw-r--r--toolkit/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_01.js139
-rw-r--r--toolkit/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_02.js99
-rw-r--r--toolkit/devtools/styleinspector/test/browser_styleinspector_csslogic-content-stylesheets.js67
-rw-r--r--toolkit/devtools/styleinspector/test/browser_styleinspector_output-parser.js312
-rw-r--r--toolkit/devtools/styleinspector/test/browser_styleinspector_refresh_when_active.js43
-rw-r--r--toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-background-image.js123
-rw-r--r--toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-closes-on-new-selection.js49
-rw-r--r--toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-longhand-fontfamily.js119
-rw-r--r--toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-multiple-background-images.js72
-rw-r--r--toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-shorthand-fontfamily.js60
-rw-r--r--toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-size.js86
-rw-r--r--toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-01.js44
-rw-r--r--toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-02.js64
-rw-r--r--toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-03.js91
-rw-r--r--toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-04.js62
-rw-r--r--toolkit/devtools/styleinspector/test/doc_content_stylesheet.html33
-rw-r--r--toolkit/devtools/styleinspector/test/doc_content_stylesheet.xul9
-rw-r--r--toolkit/devtools/styleinspector/test/doc_content_stylesheet_imported.css5
-rw-r--r--toolkit/devtools/styleinspector/test/doc_content_stylesheet_imported2.css3
-rw-r--r--toolkit/devtools/styleinspector/test/doc_content_stylesheet_linked.css3
-rw-r--r--toolkit/devtools/styleinspector/test/doc_content_stylesheet_script.css5
-rw-r--r--toolkit/devtools/styleinspector/test/doc_content_stylesheet_xul.css3
-rw-r--r--toolkit/devtools/styleinspector/test/doc_frame_script.js91
-rw-r--r--toolkit/devtools/styleinspector/test/doc_keyframeanimation.css84
-rw-r--r--toolkit/devtools/styleinspector/test/doc_keyframeanimation.html13
-rw-r--r--toolkit/devtools/styleinspector/test/doc_matched_selectors.html28
-rw-r--r--toolkit/devtools/styleinspector/test/doc_media_queries.html24
-rw-r--r--toolkit/devtools/styleinspector/test/doc_pseudoelement.html131
-rw-r--r--toolkit/devtools/styleinspector/test/doc_sourcemaps.css7
-rw-r--r--toolkit/devtools/styleinspector/test/doc_sourcemaps.css.map7
-rw-r--r--toolkit/devtools/styleinspector/test/doc_sourcemaps.html11
-rw-r--r--toolkit/devtools/styleinspector/test/doc_sourcemaps.scss10
-rw-r--r--toolkit/devtools/styleinspector/test/doc_style_editor_link.css3
-rw-r--r--toolkit/devtools/styleinspector/test/doc_test_image.pngbin0 -> 580 bytes
-rw-r--r--toolkit/devtools/styleinspector/test/doc_urls_clickable.css9
-rw-r--r--toolkit/devtools/styleinspector/test/doc_urls_clickable.html30
-rw-r--r--toolkit/devtools/styleinspector/test/head.js914
-rw-r--r--toolkit/devtools/styleinspector/test/unit/test_parseDeclarations.js208
-rw-r--r--toolkit/devtools/styleinspector/test/unit/test_parseSingleValue.js76
-rw-r--r--toolkit/devtools/styleinspector/test/unit/xpcshell.ini8
129 files changed, 15343 insertions, 0 deletions
diff --git a/toolkit/devtools/styleinspector/computed-view.js b/toolkit/devtools/styleinspector/computed-view.js
new file mode 100644
index 000000000..5e1cd3ffb
--- /dev/null
+++ b/toolkit/devtools/styleinspector/computed-view.js
@@ -0,0 +1,1493 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+const {Cc, Ci, Cu} = require("chrome");
+
+const ToolDefinitions = require("main").Tools;
+const {CssLogic} = require("devtools/styleinspector/css-logic");
+const {ELEMENT_STYLE} = require("devtools/server/actors/styles");
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+const {EventEmitter} = require("devtools/toolkit/event-emitter");
+const {OutputParser} = require("devtools/output-parser");
+const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils");
+const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+const overlays = require("devtools/styleinspector/style-inspector-overlays");
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/devtools/Templater.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+
+const FILTER_CHANGED_TIMEOUT = 300;
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+/**
+ * Helper for long-running processes that should yield occasionally to
+ * the mainloop.
+ *
+ * @param {Window} aWin
+ * Timeouts will be set on this window when appropriate.
+ * @param {Generator} aGenerator
+ * Will iterate this generator.
+ * @param {object} aOptions
+ * Options for the update process:
+ * onItem {function} Will be called with the value of each iteration.
+ * onBatch {function} Will be called after each batch of iterations,
+ * before yielding to the main loop.
+ * onDone {function} Will be called when iteration is complete.
+ * onCancel {function} Will be called if the process is canceled.
+ * threshold {int} How long to process before yielding, in ms.
+ *
+ * @constructor
+ */
+function UpdateProcess(aWin, aGenerator, aOptions)
+{
+ this.win = aWin;
+ this.iter = _Iterator(aGenerator);
+ this.onItem = aOptions.onItem || function() {};
+ this.onBatch = aOptions.onBatch || function () {};
+ this.onDone = aOptions.onDone || function() {};
+ this.onCancel = aOptions.onCancel || function() {};
+ this.threshold = aOptions.threshold || 45;
+
+ this.canceled = false;
+}
+
+UpdateProcess.prototype = {
+ /**
+ * Schedule a new batch on the main loop.
+ */
+ schedule: function UP_schedule()
+ {
+ if (this.canceled) {
+ return;
+ }
+ this._timeout = this.win.setTimeout(this._timeoutHandler.bind(this), 0);
+ },
+
+ /**
+ * Cancel the running process. onItem will not be called again,
+ * and onCancel will be called.
+ */
+ cancel: function UP_cancel()
+ {
+ if (this._timeout) {
+ this.win.clearTimeout(this._timeout);
+ this._timeout = 0;
+ }
+ this.canceled = true;
+ this.onCancel();
+ },
+
+ _timeoutHandler: function UP_timeoutHandler() {
+ this._timeout = null;
+ try {
+ this._runBatch();
+ this.schedule();
+ } catch(e) {
+ if (e instanceof StopIteration) {
+ this.onBatch();
+ this.onDone();
+ return;
+ }
+ console.error(e);
+ throw e;
+ }
+ },
+
+ _runBatch: function Y_runBatch()
+ {
+ let time = Date.now();
+ while(!this.canceled) {
+ // Continue until iter.next() throws...
+ let next = this.iter.next();
+ this.onItem(next[1]);
+ if ((Date.now() - time) > this.threshold) {
+ this.onBatch();
+ return;
+ }
+ }
+ }
+};
+
+/**
+ * CssHtmlTree is a panel that manages the display of a table sorted by style.
+ * There should be one instance of CssHtmlTree per style display (of which there
+ * will generally only be one).
+ *
+ * @params {StyleInspector} aStyleInspector The owner of this CssHtmlTree
+ * @param {PageStyleFront} aPageStyle
+ * Front for the page style actor that will be providing
+ * the style information.
+ *
+ * @constructor
+ */
+function CssHtmlTree(aStyleInspector, aPageStyle)
+{
+ this.styleWindow = aStyleInspector.doc.defaultView;
+ this.styleDocument = aStyleInspector.doc;
+ this.styleInspector = aStyleInspector;
+ this.inspector = this.styleInspector.inspector;
+ this.pageStyle = aPageStyle;
+ this.propertyViews = [];
+
+ this._outputParser = new OutputParser();
+
+ let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
+ getService(Ci.nsIXULChromeRegistry);
+ this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr";
+
+ // Create bound methods.
+ this.focusWindow = this.focusWindow.bind(this);
+ this._onContextMenu = this._onContextMenu.bind(this);
+ this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
+ this._onSelectAll = this._onSelectAll.bind(this);
+ this._onClick = this._onClick.bind(this);
+ this._onCopy = this._onCopy.bind(this);
+ this._onCopyColor = this._onCopyColor.bind(this);
+
+ this.styleDocument.addEventListener("copy", this._onCopy);
+ this.styleDocument.addEventListener("mousedown", this.focusWindow);
+ this.styleDocument.addEventListener("contextmenu", this._onContextMenu);
+
+ // Nodes used in templating
+ this.root = this.styleDocument.getElementById("root");
+ this.templateRoot = this.styleDocument.getElementById("templateRoot");
+ this.element = this.styleDocument.getElementById("propertyContainer");
+
+ // Listen for click events
+ this.element.addEventListener("click", this._onClick, false);
+
+ // No results text.
+ this.noResults = this.styleDocument.getElementById("noResults");
+
+ // Refresh panel when color unit changed.
+ this._handlePrefChange = this._handlePrefChange.bind(this);
+ gDevTools.on("pref-changed", this._handlePrefChange);
+
+ // Refresh panel when pref for showing original sources changes
+ this._updateSourceLinks = this._updateSourceLinks.bind(this);
+ this._prefObserver = new PrefObserver("devtools.");
+ this._prefObserver.on(PREF_ORIG_SOURCES, this._updateSourceLinks);
+
+ CssHtmlTree.processTemplate(this.templateRoot, this.root, this);
+
+ // The element that we're inspecting, and the document that it comes from.
+ this.viewedElement = null;
+
+ this._buildContextMenu();
+ this.createStyleViews();
+
+ // Add the tooltips and highlightersoverlay
+ this.tooltips = new overlays.TooltipsOverlay(this);
+ this.tooltips.addToView();
+ this.highlighters = new overlays.HighlightersOverlay(this);
+ this.highlighters.addToView();
+}
+
+/**
+ * Memoized lookup of a l10n string from a string bundle.
+ * @param {string} aName The key to lookup.
+ * @returns A localized version of the given key.
+ */
+CssHtmlTree.l10n = function CssHtmlTree_l10n(aName)
+{
+ try {
+ return CssHtmlTree._strings.GetStringFromName(aName);
+ } catch (ex) {
+ Services.console.logStringMessage("Error reading '" + aName + "'");
+ throw new Error("l10n error with " + aName);
+ }
+};
+
+/**
+ * Clone the given template node, and process it by resolving ${} references
+ * in the template.
+ *
+ * @param {nsIDOMElement} aTemplate the template note to use.
+ * @param {nsIDOMElement} aDestination the destination node where the
+ * processed nodes will be displayed.
+ * @param {object} aData the data to pass to the template.
+ * @param {Boolean} aPreserveDestination If true then the template will be
+ * appended to aDestination's content else aDestination.innerHTML will be
+ * cleared before the template is appended.
+ */
+CssHtmlTree.processTemplate = function CssHtmlTree_processTemplate(aTemplate,
+ aDestination, aData, aPreserveDestination)
+{
+ if (!aPreserveDestination) {
+ aDestination.innerHTML = "";
+ }
+
+ // All the templater does is to populate a given DOM tree with the given
+ // values, so we need to clone the template first.
+ let duplicated = aTemplate.cloneNode(true);
+
+ // See https://github.com/mozilla/domtemplate/blob/master/README.md
+ // for docs on the template() function
+ template(duplicated, aData, { allowEval: true });
+ while (duplicated.firstChild) {
+ aDestination.appendChild(duplicated.firstChild);
+ }
+};
+
+XPCOMUtils.defineLazyGetter(CssHtmlTree, "_strings", function() Services.strings
+ .createBundle("chrome://global/locale/devtools/styleinspector.properties"));
+
+XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
+ return Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+});
+
+CssHtmlTree.prototype = {
+ // Cache the list of properties that match the selected element.
+ _matchedProperties: null,
+
+ // Used for cancelling timeouts in the style filter.
+ _filterChangedTimeout: null,
+
+ // The search filter
+ searchField: null,
+
+ // Reference to the "Include browser styles" checkbox.
+ includeBrowserStylesCheckbox: null,
+
+ // Holds the ID of the panelRefresh timeout.
+ _panelRefreshTimeout: null,
+
+ // Toggle for zebra striping
+ _darkStripe: true,
+
+ // Number of visible properties
+ numVisibleProperties: 0,
+
+ setPageStyle: function(pageStyle) {
+ this.pageStyle = pageStyle;
+ },
+
+ get includeBrowserStyles()
+ {
+ return this.includeBrowserStylesCheckbox.checked;
+ },
+
+ _handlePrefChange: function(event, data) {
+ if (this._computed && (data.pref == "devtools.defaultColorUnit" ||
+ data.pref == PREF_ORIG_SOURCES)) {
+ this.refreshPanel();
+ }
+ },
+
+ /**
+ * Update the view with a new selected element.
+ * The CssHtmlTree panel will show the style information for the given element.
+ * @param {NodeFront} aElement The highlighted node to get styles for.
+ * @returns a promise that will be resolved when highlighting is complete.
+ */
+ selectElement: function(aElement) {
+ if (!aElement) {
+ this.viewedElement = null;
+ this.noResults.hidden = false;
+
+ if (this._refreshProcess) {
+ this._refreshProcess.cancel();
+ }
+ // Hiding all properties
+ for (let propView of this.propertyViews) {
+ propView.refresh();
+ }
+ return promise.resolve(undefined);
+ }
+
+ if (aElement === this.viewedElement) {
+ return promise.resolve(undefined);
+ }
+
+ this.viewedElement = aElement;
+ this.refreshSourceFilter();
+
+ return this.refreshPanel();
+ },
+
+ /**
+ * Get the type of a given node in the computed-view
+ * @param {DOMNode} node The node which we want information about
+ * @return {Object} The type information object contains the following props:
+ * - type {String} One of the VIEW_NODE_XXX_TYPE const in
+ * style-inspector-overlays
+ * - value {Object} Depends on the type of the node
+ * returns null of the node isn't anything we care about
+ */
+ getNodeInfo: function(node) {
+ if (!node) {
+ return null;
+ }
+
+ let classes = node.classList;
+
+ // Check if the node isn't a selector first since this doesn't require
+ // walking the DOM
+ if (classes.contains("matched") ||
+ classes.contains("bestmatch") ||
+ classes.contains("parentmatch")) {
+ let selectorText = "";
+ for (let child of node.childNodes) {
+ if (child.nodeType === node.TEXT_NODE) {
+ selectorText += child.textContent;
+ }
+ }
+ return {
+ type: overlays.VIEW_NODE_SELECTOR_TYPE,
+ value: selectorText.trim()
+ }
+ }
+
+ // Walk up the nodes to find out where node is
+ let propertyView;
+ let propertyContent;
+ let parent = node;
+ while (parent.parentNode) {
+ if (parent.classList.contains("property-view")) {
+ propertyView = parent;
+ break;
+ }
+ if (parent.classList.contains("property-content")) {
+ propertyContent = parent;
+ break;
+ }
+ parent = parent.parentNode;
+ }
+ if (!propertyView && !propertyContent) {
+ return null;
+ }
+
+ let value, type;
+
+ // Get the property and value for a node that's a property name or value
+ let isHref = classes.contains("theme-link") && !classes.contains("link");
+ if (propertyView && (classes.contains("property-name") ||
+ classes.contains("property-value") ||
+ isHref)) {
+ value = {
+ property: parent.querySelector(".property-name").textContent,
+ value: parent.querySelector(".property-value").textContent
+ };
+ }
+ if (propertyContent && (classes.contains("other-property-value") ||
+ isHref)) {
+ let view = propertyContent.previousSibling;
+ value = {
+ property: view.querySelector(".property-name").textContent,
+ value: node.textContent
+ };
+ }
+
+ // Get the type
+ if (classes.contains("property-name")) {
+ type = overlays.VIEW_NODE_PROPERTY_TYPE;
+ } else if (classes.contains("property-value") ||
+ classes.contains("other-property-value")) {
+ type = overlays.VIEW_NODE_VALUE_TYPE;
+ } else if (isHref) {
+ type = overlays.VIEW_NODE_IMAGE_URL_TYPE;
+ value.url = node.href;
+ } else {
+ return null;
+ }
+
+ return {type, value};
+ },
+
+ _createPropertyViews: function()
+ {
+ if (this._createViewsPromise) {
+ return this._createViewsPromise;
+ }
+
+ let deferred = promise.defer();
+ this._createViewsPromise = deferred.promise;
+
+ this.refreshSourceFilter();
+ this.numVisibleProperties = 0;
+ let fragment = this.styleDocument.createDocumentFragment();
+
+ this._createViewsProcess = new UpdateProcess(this.styleWindow, CssHtmlTree.propertyNames, {
+ onItem: (aPropertyName) => {
+ // Per-item callback.
+ let propView = new PropertyView(this, aPropertyName);
+ fragment.appendChild(propView.buildMain());
+ fragment.appendChild(propView.buildSelectorContainer());
+
+ if (propView.visible) {
+ this.numVisibleProperties++;
+ }
+ this.propertyViews.push(propView);
+ },
+ onCancel: () => {
+ deferred.reject("_createPropertyViews cancelled");
+ },
+ onDone: () => {
+ // Completed callback.
+ this.element.appendChild(fragment);
+ this.noResults.hidden = this.numVisibleProperties > 0;
+ deferred.resolve(undefined);
+ }
+ });
+
+ this._createViewsProcess.schedule();
+ return deferred.promise;
+ },
+
+ /**
+ * Refresh the panel content.
+ */
+ refreshPanel: function CssHtmlTree_refreshPanel()
+ {
+ if (!this.viewedElement) {
+ return promise.resolve();
+ }
+
+ // Capture the current viewed element to return from the promise handler
+ // early if it changed
+ let viewedElement = this.viewedElement;
+
+ return promise.all([
+ this._createPropertyViews(),
+ this.pageStyle.getComputed(this.viewedElement, {
+ filter: this._sourceFilter,
+ onlyMatched: !this.includeBrowserStyles,
+ markMatched: true
+ })
+ ]).then(([createViews, computed]) => {
+ if (viewedElement !== this.viewedElement) {
+ return;
+ }
+
+ this._matchedProperties = new Set;
+ for (let name in computed) {
+ if (computed[name].matched) {
+ this._matchedProperties.add(name);
+ }
+ }
+ this._computed = computed;
+
+ if (this._refreshProcess) {
+ this._refreshProcess.cancel();
+ }
+
+ this.noResults.hidden = true;
+
+ // Reset visible property count
+ this.numVisibleProperties = 0;
+
+ // Reset zebra striping.
+ this._darkStripe = true;
+
+ let deferred = promise.defer();
+ this._refreshProcess = new UpdateProcess(this.styleWindow, this.propertyViews, {
+ onItem: (aPropView) => {
+ aPropView.refresh();
+ },
+ onDone: () => {
+ this._refreshProcess = null;
+ this.noResults.hidden = this.numVisibleProperties > 0;
+ this.inspector.emit("computed-view-refreshed");
+ deferred.resolve(undefined);
+ }
+ });
+ this._refreshProcess.schedule();
+ return deferred.promise;
+ }).then(null, (err) => console.error(err));
+ },
+
+ /**
+ * Called when the user enters a search term.
+ *
+ * @param {Event} aEvent the DOM Event object.
+ */
+ filterChanged: function CssHtmlTree_filterChanged(aEvent)
+ {
+ let win = this.styleWindow;
+
+ if (this._filterChangedTimeout) {
+ win.clearTimeout(this._filterChangedTimeout);
+ }
+
+ this._filterChangedTimeout = win.setTimeout(() => {
+ this.refreshPanel();
+ this._filterChangeTimeout = null;
+ }, FILTER_CHANGED_TIMEOUT);
+ },
+
+ /**
+ * The change event handler for the includeBrowserStyles checkbox.
+ *
+ * @param {Event} aEvent the DOM Event object.
+ */
+ includeBrowserStylesChanged:
+ function CssHtmltree_includeBrowserStylesChanged(aEvent)
+ {
+ this.refreshSourceFilter();
+ this.refreshPanel();
+ },
+
+ /**
+ * When includeBrowserStyles.checked is false we only display properties that
+ * have matched selectors and have been included by the document or one of the
+ * document's stylesheets. If .checked is false we display all properties
+ * including those that come from UA stylesheets.
+ */
+ refreshSourceFilter: function CssHtmlTree_setSourceFilter()
+ {
+ this._matchedProperties = null;
+ this._sourceFilter = this.includeBrowserStyles ?
+ CssLogic.FILTER.UA :
+ CssLogic.FILTER.USER;
+ },
+
+ _updateSourceLinks: function CssHtmlTree__updateSourceLinks()
+ {
+ for (let propView of this.propertyViews) {
+ propView.updateSourceLinks();
+ }
+ this.inspector.emit("computed-view-sourcelinks-updated");
+ },
+
+ /**
+ * The CSS as displayed by the UI.
+ */
+ createStyleViews: function CssHtmlTree_createStyleViews()
+ {
+ if (CssHtmlTree.propertyNames) {
+ return;
+ }
+
+ CssHtmlTree.propertyNames = [];
+
+ // Here we build and cache a list of css properties supported by the browser
+ // We could use any element but let's use the main document's root element
+ let styles = this.styleWindow.getComputedStyle(this.styleDocument.documentElement);
+ let mozProps = [];
+ for (let i = 0, numStyles = styles.length; i < numStyles; i++) {
+ let prop = styles.item(i);
+ if (prop.startsWith("--")) {
+ // Skip any CSS variables used inside of browser CSS files
+ continue;
+ } else if (prop.startsWith("-")) {
+ mozProps.push(prop);
+ } else {
+ CssHtmlTree.propertyNames.push(prop);
+ }
+ }
+
+ CssHtmlTree.propertyNames.sort();
+ CssHtmlTree.propertyNames.push.apply(CssHtmlTree.propertyNames,
+ mozProps.sort());
+
+ this._createPropertyViews().then(null, e => {
+ if (!this.styleInspector) {
+ console.warn("The creation of property views was cancelled because the " +
+ "computed-view was destroyed before it was done creating views");
+ } else {
+ console.error(e);
+ }
+ });
+ },
+
+ /**
+ * Get a set of properties that have matched selectors.
+ *
+ * @return {Set} If a property name is in the set, it has matching selectors.
+ */
+ get matchedProperties()
+ {
+ return this._matchedProperties || new Set;
+ },
+
+ /**
+ * Focus the window on mousedown.
+ *
+ * @param aEvent The event object
+ */
+ focusWindow: function(aEvent)
+ {
+ let win = this.styleDocument.defaultView;
+ win.focus();
+ },
+
+ /**
+ * Create a context menu.
+ */
+ _buildContextMenu: function()
+ {
+ let doc = this.styleDocument.defaultView.parent.document;
+
+ this._contextmenu = this.styleDocument.createElementNS(XUL_NS, "menupopup");
+ this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate);
+ this._contextmenu.id = "computed-view-context-menu";
+
+ // Select All
+ this.menuitemSelectAll = createMenuItem(this._contextmenu, {
+ label: "computedView.contextmenu.selectAll",
+ accesskey: "computedView.contextmenu.selectAll.accessKey",
+ command: this._onSelectAll
+ });
+
+ // Copy
+ this.menuitemCopy = createMenuItem(this._contextmenu, {
+ label: "computedView.contextmenu.copy",
+ accesskey: "computedView.contextmenu.copy.accessKey",
+ command: this._onCopy
+ });
+
+ // Copy color
+ this.menuitemCopyColor = createMenuItem(this._contextmenu, {
+ label: "ruleView.contextmenu.copyColor",
+ accesskey: "ruleView.contextmenu.copyColor.accessKey",
+ command: this._onCopyColor
+ });
+
+ // Show Original Sources
+ this.menuitemSources= createMenuItem(this._contextmenu, {
+ label: "ruleView.contextmenu.showOrigSources",
+ accesskey: "ruleView.contextmenu.showOrigSources.accessKey",
+ command: this._onToggleOrigSources,
+ type: "checkbox"
+ });
+
+ let popupset = doc.documentElement.querySelector("popupset");
+ if (!popupset) {
+ popupset = doc.createElementNS(XUL_NS, "popupset");
+ doc.documentElement.appendChild(popupset);
+ }
+ popupset.appendChild(this._contextmenu);
+ },
+
+ /**
+ * Update the context menu. This means enabling or disabling menuitems as
+ * appropriate.
+ */
+ _contextMenuUpdate: function()
+ {
+ let win = this.styleDocument.defaultView;
+ let disable = win.getSelection().isCollapsed;
+ this.menuitemCopy.disabled = disable;
+
+ let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
+ this.menuitemSources.setAttribute("checked", showOrig);
+
+ this.menuitemCopyColor.hidden = !this._isColorPopup();
+ },
+
+ /**
+ * A helper that determines if the popup was opened with a click to a color
+ * value and saves the color to this._colorToCopy.
+ *
+ * @return {Boolean}
+ * true if click on color opened the popup, false otherwise.
+ */
+ _isColorPopup: function () {
+ this._colorToCopy = "";
+
+ let trigger = this.popupNode;
+ if (!trigger) {
+ return false;
+ }
+
+ let container = (trigger.nodeType == trigger.TEXT_NODE) ?
+ trigger.parentElement : trigger;
+
+ let isColorNode = el => el.dataset && "color" in el.dataset;
+
+ while (!isColorNode(container)) {
+ container = container.parentNode;
+ if (!container) {
+ return false;
+ }
+ }
+
+ this._colorToCopy = container.dataset["color"];
+ return true;
+ },
+
+ /**
+ * Context menu handler.
+ */
+ _onContextMenu: function(event) {
+ try {
+ this.popupNode = event.explicitOriginalTarget;
+ this.styleDocument.defaultView.focus();
+ this._contextmenu.openPopupAtScreen(event.screenX, event.screenY, true);
+ } catch(e) {
+ console.error(e);
+ }
+ },
+
+ /**
+ * Select all text.
+ */
+ _onSelectAll: function()
+ {
+ try {
+ let win = this.styleDocument.defaultView;
+ let selection = win.getSelection();
+
+ selection.selectAllChildren(this.styleDocument.documentElement);
+ } catch(e) {
+ console.error(e);
+ }
+ },
+
+ _onClick: function(event) {
+ let target = event.target;
+
+ if (target.nodeName === "a") {
+ event.stopPropagation();
+ event.preventDefault();
+ let browserWin = this.inspector.target.tab.ownerDocument.defaultView;
+ browserWin.openUILinkIn(target.href, "tab");
+ }
+ },
+
+ _onCopyColor: function() {
+ clipboardHelper.copyString(this._colorToCopy, this.styleDocument);
+ },
+
+ /**
+ * Copy selected text.
+ *
+ * @param event The event object
+ */
+ _onCopy: function(event)
+ {
+ try {
+ let win = this.styleDocument.defaultView;
+ let text = win.getSelection().toString().trim();
+
+ // Tidy up block headings by moving CSS property names and their values onto
+ // the same line and inserting a colon between them.
+ let textArray = text.split(/[\r\n]+/);
+ let result = "";
+
+ // Parse text array to output string.
+ if (textArray.length > 1) {
+ for (let prop of textArray) {
+ if (CssHtmlTree.propertyNames.indexOf(prop) !== -1) {
+ // Property name
+ result += prop;
+ } else {
+ // Property value
+ result += ": " + prop;
+ if (result.length > 0) {
+ result += ";\n";
+ }
+ }
+ }
+ } else {
+ // Short text fragment.
+ result = textArray[0];
+ }
+
+ clipboardHelper.copyString(result, this.styleDocument);
+
+ if (event) {
+ event.preventDefault();
+ }
+ } catch(e) {
+ console.error(e);
+ }
+ },
+
+ /**
+ * Toggle the original sources pref.
+ */
+ _onToggleOrigSources: function()
+ {
+ let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
+ Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
+ },
+
+ /**
+ * Destructor for CssHtmlTree.
+ */
+ destroy: function CssHtmlTree_destroy()
+ {
+ this.viewedElement = null;
+ this._outputParser = null;
+
+ // Remove event listeners
+ this.includeBrowserStylesCheckbox.removeEventListener("command",
+ this.includeBrowserStylesChanged);
+ this.searchField.removeEventListener("command", this.filterChanged);
+ gDevTools.off("pref-changed", this._handlePrefChange);
+
+ this._prefObserver.off(PREF_ORIG_SOURCES, this._updateSourceLinks);
+ this._prefObserver.destroy();
+
+ // Cancel tree construction
+ if (this._createViewsProcess) {
+ this._createViewsProcess.cancel();
+ }
+ if (this._refreshProcess) {
+ this._refreshProcess.cancel();
+ }
+
+ this.element.removeEventListener("click", this._onClick, false);
+
+ // Remove context menu
+ if (this._contextmenu) {
+ // Destroy the Select All menuitem.
+ this.menuitemCopy.removeEventListener("command", this._onCopy);
+ this.menuitemCopy = null;
+
+ // Destroy the Copy menuitem.
+ this.menuitemSelectAll.removeEventListener("command", this._onSelectAll);
+ this.menuitemSelectAll = null;
+
+ // Destroy Copy Color menuitem.
+ this.menuitemCopyColor.removeEventListener("command", this._onCopyColor);
+ this.menuitemCopyColor = null;
+
+ // Destroy the context menu.
+ this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate);
+ this._contextmenu.parentNode.removeChild(this._contextmenu);
+ this._contextmenu = null;
+ }
+
+ this.popupNode = null;
+
+ this.tooltips.destroy();
+ this.highlighters.destroy();
+
+ // Remove bound listeners
+ this.styleDocument.removeEventListener("contextmenu", this._onContextMenu);
+ this.styleDocument.removeEventListener("copy", this._onCopy);
+ this.styleDocument.removeEventListener("mousedown", this.focusWindow);
+
+ // Nodes used in templating
+ this.root = null;
+ this.element = null;
+ this.panel = null;
+
+ // The document in which we display the results (csshtmltree.xul).
+ this.styleDocument = null;
+
+ for (let propView of this.propertyViews) {
+ propView.destroy();
+ }
+
+ // The element that we're inspecting, and the document that it comes from.
+ this.propertyViews = null;
+ this.styleWindow = null;
+ this.styleDocument = null;
+ this.styleInspector = null;
+ }
+};
+
+function PropertyInfo(aTree, aName) {
+ this.tree = aTree;
+ this.name = aName;
+}
+PropertyInfo.prototype = {
+ get value() {
+ if (this.tree._computed) {
+ let value = this.tree._computed[this.name].value;
+ return value;
+ }
+ }
+};
+
+function createMenuItem(aMenu, aAttributes)
+{
+ let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem");
+
+ item.setAttribute("label", CssHtmlTree.l10n(aAttributes.label));
+ item.setAttribute("accesskey", CssHtmlTree.l10n(aAttributes.accesskey));
+ item.addEventListener("command", aAttributes.command);
+
+ aMenu.appendChild(item);
+
+ return item;
+}
+
+/**
+ * A container to give easy access to property data from the template engine.
+ *
+ * @constructor
+ * @param {CssHtmlTree} aTree the CssHtmlTree instance we are working with.
+ * @param {string} aName the CSS property name for which this PropertyView
+ * instance will render the rules.
+ */
+function PropertyView(aTree, aName)
+{
+ this.tree = aTree;
+ this.name = aName;
+ this.getRTLAttr = aTree.getRTLAttr;
+
+ this.link = "https://developer.mozilla.org/CSS/" + aName;
+
+ this.templateMatchedSelectors = aTree.styleDocument.getElementById("templateMatchedSelectors");
+ this._propertyInfo = new PropertyInfo(aTree, aName);
+}
+
+PropertyView.prototype = {
+ // The parent element which contains the open attribute
+ element: null,
+
+ // Property header node
+ propertyHeader: null,
+
+ // Destination for property names
+ nameNode: null,
+
+ // Destination for property values
+ valueNode: null,
+
+ // Are matched rules expanded?
+ matchedExpanded: false,
+
+ // Matched selector container
+ matchedSelectorsContainer: null,
+
+ // Matched selector expando
+ matchedExpander: null,
+
+ // Cache for matched selector views
+ _matchedSelectorViews: null,
+
+ // The previously selected element used for the selector view caches
+ prevViewedElement: null,
+
+ /**
+ * Get the computed style for the current property.
+ *
+ * @return {string} the computed style for the current property of the
+ * currently highlighted element.
+ */
+ get value()
+ {
+ return this.propertyInfo.value;
+ },
+
+ /**
+ * An easy way to access the CssPropertyInfo behind this PropertyView.
+ */
+ get propertyInfo()
+ {
+ return this._propertyInfo;
+ },
+
+ /**
+ * Does the property have any matched selectors?
+ */
+ get hasMatchedSelectors()
+ {
+ return this.tree.matchedProperties.has(this.name);
+ },
+
+ /**
+ * Should this property be visible?
+ */
+ get visible()
+ {
+ if (!this.tree.viewedElement) {
+ return false;
+ }
+
+ if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) {
+ return false;
+ }
+
+ let searchTerm = this.tree.searchField.value.toLowerCase();
+ if (searchTerm && this.name.toLowerCase().indexOf(searchTerm) == -1 &&
+ this.value.toLowerCase().indexOf(searchTerm) == -1) {
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Returns the className that should be assigned to the propertyView.
+ * @return string
+ */
+ get propertyHeaderClassName()
+ {
+ if (this.visible) {
+ let isDark = this.tree._darkStripe = !this.tree._darkStripe;
+ return isDark ? "property-view row-striped" : "property-view";
+ }
+ return "property-view-hidden";
+ },
+
+ /**
+ * Returns the className that should be assigned to the propertyView content
+ * container.
+ * @return string
+ */
+ get propertyContentClassName()
+ {
+ if (this.visible) {
+ let isDark = this.tree._darkStripe;
+ return isDark ? "property-content row-striped" : "property-content";
+ }
+ return "property-content-hidden";
+ },
+
+ /**
+ * Build the markup for on computed style
+ * @return Element
+ */
+ buildMain: function PropertyView_buildMain()
+ {
+ let doc = this.tree.styleDocument;
+
+ // Build the container element
+ this.onMatchedToggle = this.onMatchedToggle.bind(this);
+ this.element = doc.createElementNS(HTML_NS, "div");
+ this.element.setAttribute("class", this.propertyHeaderClassName);
+ this.element.addEventListener("dblclick", this.onMatchedToggle, false);
+
+ // Make it keyboard navigable
+ this.element.setAttribute("tabindex", "0");
+ this.onKeyDown = (aEvent) => {
+ let keyEvent = Ci.nsIDOMKeyEvent;
+ if (aEvent.keyCode == keyEvent.DOM_VK_F1) {
+ this.mdnLinkClick();
+ }
+ if (aEvent.keyCode == keyEvent.DOM_VK_RETURN ||
+ aEvent.keyCode == keyEvent.DOM_VK_SPACE) {
+ this.onMatchedToggle(aEvent);
+ }
+ };
+ this.element.addEventListener("keydown", this.onKeyDown, false);
+
+ // Build the twisty expand/collapse
+ this.matchedExpander = doc.createElementNS(HTML_NS, "div");
+ this.matchedExpander.className = "expander theme-twisty";
+ this.matchedExpander.addEventListener("click", this.onMatchedToggle, false);
+ this.element.appendChild(this.matchedExpander);
+
+ this.focusElement = () => this.element.focus();
+
+ // Build the style name element
+ this.nameNode = doc.createElementNS(HTML_NS, "div");
+ this.nameNode.setAttribute("class", "property-name theme-fg-color5");
+ // Reset its tabindex attribute otherwise, if an ellipsis is applied
+ // it will be reachable via TABing
+ this.nameNode.setAttribute("tabindex", "");
+ this.nameNode.textContent = this.nameNode.title = this.name;
+ // Make it hand over the focus to the container
+ this.onFocus = () => this.element.focus();
+ this.nameNode.addEventListener("click", this.onFocus, false);
+ this.element.appendChild(this.nameNode);
+
+ // Build the style value element
+ this.valueNode = doc.createElementNS(HTML_NS, "div");
+ this.valueNode.setAttribute("class", "property-value theme-fg-color1");
+ // Reset its tabindex attribute otherwise, if an ellipsis is applied
+ // it will be reachable via TABing
+ this.valueNode.setAttribute("tabindex", "");
+ this.valueNode.setAttribute("dir", "ltr");
+ // Make it hand over the focus to the container
+ this.valueNode.addEventListener("click", this.onFocus, false);
+ this.element.appendChild(this.valueNode);
+
+ return this.element;
+ },
+
+ buildSelectorContainer: function PropertyView_buildSelectorContainer()
+ {
+ let doc = this.tree.styleDocument;
+ let element = doc.createElementNS(HTML_NS, "div");
+ element.setAttribute("class", this.propertyContentClassName);
+ this.matchedSelectorsContainer = doc.createElementNS(HTML_NS, "div");
+ this.matchedSelectorsContainer.setAttribute("class", "matchedselectors");
+ element.appendChild(this.matchedSelectorsContainer);
+
+ return element;
+ },
+
+ /**
+ * Refresh the panel's CSS property value.
+ */
+ refresh: function PropertyView_refresh()
+ {
+ this.element.className = this.propertyHeaderClassName;
+ this.element.nextElementSibling.className = this.propertyContentClassName;
+
+ if (this.prevViewedElement != this.tree.viewedElement) {
+ this._matchedSelectorViews = null;
+ this.prevViewedElement = this.tree.viewedElement;
+ }
+
+ if (!this.tree.viewedElement || !this.visible) {
+ this.valueNode.textContent = this.valueNode.title = "";
+ this.matchedSelectorsContainer.parentNode.hidden = true;
+ this.matchedSelectorsContainer.textContent = "";
+ this.matchedExpander.removeAttribute("open");
+ return;
+ }
+
+ this.tree.numVisibleProperties++;
+
+ let outputParser = this.tree._outputParser;
+ let frag = outputParser.parseCssProperty(this.propertyInfo.name,
+ this.propertyInfo.value,
+ {
+ colorSwatchClass: "computedview-colorswatch",
+ urlClass: "theme-link"
+ // No need to use baseURI here as computed URIs are never relative.
+ });
+ this.valueNode.innerHTML = "";
+ this.valueNode.appendChild(frag);
+
+ this.refreshMatchedSelectors();
+ },
+
+ /**
+ * Refresh the panel matched rules.
+ */
+ refreshMatchedSelectors: function PropertyView_refreshMatchedSelectors()
+ {
+ let hasMatchedSelectors = this.hasMatchedSelectors;
+ this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors;
+
+ if (hasMatchedSelectors) {
+ this.matchedExpander.classList.add("expandable");
+ } else {
+ this.matchedExpander.classList.remove("expandable");
+ }
+
+ if (this.matchedExpanded && hasMatchedSelectors) {
+ return this.tree.pageStyle.getMatchedSelectors(this.tree.viewedElement, this.name).then(matched => {
+ if (!this.matchedExpanded) {
+ return;
+ }
+
+ this._matchedSelectorResponse = matched;
+ CssHtmlTree.processTemplate(this.templateMatchedSelectors,
+ this.matchedSelectorsContainer, this);
+ this.matchedExpander.setAttribute("open", "");
+ this.tree.inspector.emit("computed-view-property-expanded");
+ }).then(null, console.error);
+ } else {
+ this.matchedSelectorsContainer.innerHTML = "";
+ this.matchedExpander.removeAttribute("open");
+ this.tree.inspector.emit("computed-view-property-collapsed");
+ return promise.resolve(undefined);
+ }
+ },
+
+ get matchedSelectors()
+ {
+ return this._matchedSelectorResponse;
+ },
+
+ /**
+ * Provide access to the matched SelectorViews that we are currently
+ * displaying.
+ */
+ get matchedSelectorViews()
+ {
+ if (!this._matchedSelectorViews) {
+ this._matchedSelectorViews = [];
+ this._matchedSelectorResponse.forEach(
+ function matchedSelectorViews_convert(aSelectorInfo) {
+ this._matchedSelectorViews.push(new SelectorView(this.tree, aSelectorInfo));
+ }, this);
+ }
+
+ return this._matchedSelectorViews;
+ },
+
+ /**
+ * Update all the selector source links to reflect whether we're linking to
+ * original sources (e.g. Sass files).
+ */
+ updateSourceLinks: function PropertyView_updateSourceLinks()
+ {
+ if (!this._matchedSelectorViews) {
+ return;
+ }
+ for (let view of this._matchedSelectorViews) {
+ view.updateSourceLink();
+ }
+ },
+
+ /**
+ * The action when a user expands matched selectors.
+ *
+ * @param {Event} aEvent Used to determine the class name of the targets click
+ * event.
+ */
+ onMatchedToggle: function PropertyView_onMatchedToggle(aEvent)
+ {
+ this.matchedExpanded = !this.matchedExpanded;
+ this.refreshMatchedSelectors();
+ aEvent.preventDefault();
+ },
+
+ /**
+ * The action when a user clicks on the MDN help link for a property.
+ */
+ mdnLinkClick: function PropertyView_mdnLinkClick(aEvent)
+ {
+ let inspector = this.tree.inspector;
+
+ if (inspector.target.tab) {
+ let browserWin = inspector.target.tab.ownerDocument.defaultView;
+ browserWin.openUILinkIn(this.link, "tab");
+ }
+ aEvent.preventDefault();
+ },
+
+ /**
+ * Destroy this property view, removing event listeners
+ */
+ destroy: function PropertyView_destroy() {
+ this.element.removeEventListener("dblclick", this.onMatchedToggle, false);
+ this.element.removeEventListener("keydown", this.onKeyDown, false);
+ this.element = null;
+
+ this.matchedExpander.removeEventListener("click", this.onMatchedToggle, false);
+ this.matchedExpander = null;
+
+ this.nameNode.removeEventListener("click", this.onFocus, false);
+ this.nameNode = null;
+
+ this.valueNode.removeEventListener("click", this.onFocus, false);
+ this.valueNode = null;
+ }
+};
+
+/**
+ * A container to give us easy access to display data from a CssRule
+ * @param CssHtmlTree aTree, the owning CssHtmlTree
+ * @param aSelectorInfo
+ */
+function SelectorView(aTree, aSelectorInfo)
+{
+ this.tree = aTree;
+ this.selectorInfo = aSelectorInfo;
+ this._cacheStatusNames();
+
+ this.updateSourceLink();
+}
+
+/**
+ * Decode for cssInfo.rule.status
+ * @see SelectorView.prototype._cacheStatusNames
+ * @see CssLogic.STATUS
+ */
+SelectorView.STATUS_NAMES = [
+ // "Parent Match", "Matched", "Best Match"
+];
+
+SelectorView.CLASS_NAMES = [
+ "parentmatch", "matched", "bestmatch"
+];
+
+SelectorView.prototype = {
+ /**
+ * Cache localized status names.
+ *
+ * These statuses are localized inside the styleinspector.properties string
+ * bundle.
+ * @see css-logic.js - the CssLogic.STATUS array.
+ *
+ * @return {void}
+ */
+ _cacheStatusNames: function SelectorView_cacheStatusNames()
+ {
+ if (SelectorView.STATUS_NAMES.length) {
+ return;
+ }
+
+ for (let status in CssLogic.STATUS) {
+ let i = CssLogic.STATUS[status];
+ if (i > CssLogic.STATUS.UNMATCHED) {
+ let value = CssHtmlTree.l10n("rule.status." + status);
+ // Replace normal spaces with non-breaking spaces
+ SelectorView.STATUS_NAMES[i] = value.replace(/ /g, '\u00A0');
+ }
+ }
+ },
+
+ /**
+ * A localized version of cssRule.status
+ */
+ get statusText()
+ {
+ return SelectorView.STATUS_NAMES[this.selectorInfo.status];
+ },
+
+ /**
+ * Get class name for selector depending on status
+ */
+ get statusClass()
+ {
+ return SelectorView.CLASS_NAMES[this.selectorInfo.status - 1];
+ },
+
+ get href()
+ {
+ if (this._href) {
+ return this._href;
+ }
+ let sheet = this.selectorInfo.rule.parentStyleSheet;
+ this._href = sheet ? sheet.href : "#";
+ return this._href;
+ },
+
+ get sourceText()
+ {
+ return this.selectorInfo.sourceText;
+ },
+
+
+ get value()
+ {
+ return this.selectorInfo.value;
+ },
+
+ get outputFragment()
+ {
+ // Sadly, because this fragment is added to the template by DOM Templater
+ // we lose any events that are attached. This means that URLs will open in a
+ // new window. At some point we should fix this by stopping using the
+ // templater.
+ let outputParser = this.tree._outputParser;
+ let frag = outputParser.parseCssProperty(
+ this.selectorInfo.name,
+ this.selectorInfo.value, {
+ colorSwatchClass: "computedview-colorswatch",
+ urlClass: "theme-link",
+ baseURI: this.selectorInfo.rule.href
+ });
+ return frag;
+ },
+
+ /**
+ * Update the text of the source link to reflect whether we're showing
+ * original sources or not.
+ */
+ updateSourceLink: function()
+ {
+ return this.updateSource().then((oldSource) => {
+ if (oldSource != this.source && this.tree.element) {
+ let selector = '[sourcelocation="' + oldSource + '"]';
+ let link = this.tree.element.querySelector(selector);
+ if (link) {
+ link.textContent = this.source;
+ link.setAttribute("sourcelocation", this.source);
+ }
+ }
+ });
+ },
+
+ /**
+ * Update the 'source' store based on our original sources preference.
+ */
+ updateSource: function()
+ {
+ let rule = this.selectorInfo.rule;
+ this.sheet = rule.parentStyleSheet;
+
+ if (!rule || !this.sheet) {
+ let oldSource = this.source;
+ this.source = CssLogic.l10n("rule.sourceElement");
+ this.href = "#";
+ return promise.resolve(oldSource);
+ }
+
+ let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
+
+ if (showOrig && rule.type != ELEMENT_STYLE) {
+ let deferred = promise.defer();
+
+ // set as this first so we show something while we're fetching
+ this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line;
+
+ rule.getOriginalLocation().then(({href, line, column}) => {
+ let oldSource = this.source;
+ this.source = CssLogic.shortSource({href: href}) + ":" + line;
+ deferred.resolve(oldSource);
+ });
+
+ return deferred.promise;
+ }
+
+ let oldSource = this.source;
+ this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line;
+ return promise.resolve(oldSource);
+ },
+
+ /**
+ * Open the style editor if the RETURN key was pressed.
+ */
+ maybeOpenStyleEditor: function(aEvent)
+ {
+ let keyEvent = Ci.nsIDOMKeyEvent;
+ if (aEvent.keyCode == keyEvent.DOM_VK_RETURN) {
+ this.openStyleEditor();
+ }
+ },
+
+ /**
+ * When a css link is clicked this method is called in order to either:
+ * 1. Open the link in view source (for chrome stylesheets).
+ * 2. Open the link in the style editor.
+ *
+ * We can only view stylesheets contained in document.styleSheets inside the
+ * style editor.
+ *
+ * @param aEvent The click event
+ */
+ openStyleEditor: function(aEvent)
+ {
+ let inspector = this.tree.inspector;
+ let rule = this.selectorInfo.rule;
+
+ // The style editor can only display stylesheets coming from content because
+ // chrome stylesheets are not listed in the editor's stylesheet selector.
+ //
+ // If the stylesheet is a content stylesheet we send it to the style
+ // editor else we display it in the view source window.
+ let sheet = rule.parentStyleSheet;
+ if (!sheet || sheet.isSystem) {
+ let contentDoc = null;
+ if (this.tree.viewedElement.isLocal_toBeDeprecated()) {
+ let rawNode = this.tree.viewedElement.rawNode();
+ if (rawNode) {
+ contentDoc = rawNode.ownerDocument;
+ }
+ }
+ let viewSourceUtils = inspector.viewSourceUtils;
+ viewSourceUtils.viewSource(rule.href, null, contentDoc, rule.line);
+ return;
+ }
+
+ let location = promise.resolve(rule.location);
+ if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
+ location = rule.getOriginalLocation();
+ }
+ location.then(({source, href, line, column}) => {
+ let target = inspector.target;
+ if (ToolDefinitions.styleEditor.isTargetSupported(target)) {
+ gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
+ let sheet = source || href;
+ toolbox.getCurrentPanel().selectStyleSheet(sheet, line, column);
+ });
+ }
+ });
+ }
+};
+
+exports.CssHtmlTree = CssHtmlTree;
+exports.PropertyView = PropertyView;
diff --git a/toolkit/devtools/styleinspector/computedview.xhtml b/toolkit/devtools/styleinspector/computedview.xhtml
new file mode 100644
index 000000000..4ab8a9db2
--- /dev/null
+++ b/toolkit/devtools/styleinspector/computedview.xhtml
@@ -0,0 +1,115 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<!DOCTYPE window [
+ <!ENTITY % inspectorDTD SYSTEM "chrome://browser/locale/devtools/styleinspector.dtd">
+ %inspectorDTD;
+ <!ELEMENT loop ANY>
+ <!ATTLIST li foreach CDATA #IMPLIED>
+ <!ATTLIST div foreach CDATA #IMPLIED>
+ <!ATTLIST loop foreach CDATA #IMPLIED>
+ <!ATTLIST a target CDATA #IMPLIED>
+ <!ATTLIST a __pathElement CDATA #IMPLIED>
+ <!ATTLIST div _id CDATA #IMPLIED>
+ <!ATTLIST div save CDATA #IMPLIED>
+ <!ATTLIST table save CDATA #IMPLIED>
+ <!ATTLIST loop if CDATA #IMPLIED>
+ <!ATTLIST tr if CDATA #IMPLIED>
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ class="theme-sidebar">
+
+ <head>
+
+ <title>&computedViewTitle;</title>
+
+ <link rel="stylesheet" href="chrome://global/skin/global.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/computedview.css" type="text/css"/>
+
+ <script type="application/javascript;version=1.8" src="theme-switching.js"/>
+
+ <script type="application/javascript;version=1.8">
+ window.setPanel = function(panel, iframe) {
+ let {devtools} = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
+ let inspector = devtools.require("devtools/styleinspector/style-inspector");
+ this.computedview = new inspector.ComputedViewTool(panel, window, iframe);
+ }
+ window.onunload = function() {
+ if (this.computedview) {
+ this.computedview.destroy();
+ }
+ }
+ </script>
+ </head>
+
+ <body>
+
+ <!-- The output from #templateProperty (below) is appended here. -->
+ <div id="propertyContainer" class="devtools-monospace">
+ </div>
+
+ <!-- When no properties are found the following block is displayed. -->
+ <div id="noResults" hidden="">
+ &noPropertiesFound;
+ </div>
+
+ <!-- The output from #templateRoot (below) is inserted here. -->
+ <div id="root" class="devtools-monospace"></div>
+
+ <!--
+ To visually debug the templates without running firefox, alter the display:none
+ -->
+ <div style="display:none;">
+ <!--
+ templateRoot sits at the top of the window and contains the "include default
+ styles" checkbox. For data it needs an instance of CssHtmlTree.
+ -->
+ <div id="templateRoot">
+ <xul:hbox class="devtools-toolbar" flex="1" align="center">
+ <xul:checkbox class="includebrowserstyles"
+ save="${includeBrowserStylesCheckbox}"
+ oncommand="${includeBrowserStylesChanged}" checked="false"
+ label="&browserStylesLabel;"/>
+ <xul:textbox class="devtools-searchinput" type="search" save="${searchField}"
+ placeholder="&userStylesSearch;" flex="1"
+ oncommand="${filterChanged}"/>
+ </xul:hbox>
+ </div>
+
+
+ <!--
+ A templateMatchedSelectors sits inside each templateProperties showing the
+ list of selectors that affect that property. Each needs data like this:
+ {
+ matchedSelectorViews: ..., // from cssHtmlTree.propertyViews[name].matchedSelectorViews
+ }
+ This is a template so the parent does not need to be a table, except that
+ using a div as the parent causes the DOM to muck with the tr elements
+ -->
+ <div id="templateMatchedSelectors">
+ <loop foreach="selector in ${matchedSelectorViews}">
+ <p>
+ <span class="rule-link">
+ <a target="_blank" class="link theme-link"
+ onclick="${selector.openStyleEditor}"
+ onkeydown="${selector.maybeOpenStyleEditor}"
+ title="${selector.href}"
+ sourcelocation="${selector.source}"
+ tabindex="0">${selector.source}</a>
+ </span>
+ <span dir="ltr" class="rule-text ${selector.statusClass} theme-fg-color3" title="${selector.statusText}">
+ ${selector.sourceText}
+ <span class="other-property-value theme-fg-color1">${selector.outputFragment}</span>
+ </span>
+ </p>
+ </loop>
+ </div>
+ </div>
+
+ </body>
+</html>
diff --git a/toolkit/devtools/styleinspector/css-parsing-utils.js b/toolkit/devtools/styleinspector/css-parsing-utils.js
new file mode 100644
index 000000000..a1e670c45
--- /dev/null
+++ b/toolkit/devtools/styleinspector/css-parsing-utils.js
@@ -0,0 +1,154 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const cssTokenizer = require("devtools/sourceeditor/css-tokenizer");
+
+/**
+ * Returns the string enclosed in quotes
+ */
+function quoteString(string) {
+ let hasDoubleQuotes = string.contains('"');
+ let hasSingleQuotes = string.contains("'");
+
+ if (hasDoubleQuotes && !hasSingleQuotes) {
+ // In this case, no escaping required, just enclose in single-quotes
+ return "'" + string + "'";
+ }
+
+ // In all other cases, enclose in double-quotes, and escape any double-quote
+ // that may be in the string
+ return '"' + string.replace(/"/g, '\"') + '"';
+}
+
+/**
+ * Returns an array of CSS declarations given an string.
+ * For example, parseDeclarations("width: 1px; height: 1px") would return
+ * [{name:"width", value: "1px"}, {name: "height", "value": "1px"}]
+ *
+ * The input string is assumed to only contain declarations so { and } characters
+ * will be treated as part of either the property or value, depending where it's
+ * found.
+ *
+ * @param {string} inputString
+ * An input string of CSS
+ * @return {Array} an array of objects with the following signature:
+ * [{"name": string, "value": string, "priority": string}, ...]
+ */
+function parseDeclarations(inputString) {
+ let tokens = cssTokenizer(inputString);
+
+ let declarations = [{name: "", value: "", priority: ""}];
+
+ let current = "", hasBang = false, lastProp;
+ for (let token of tokens) {
+ lastProp = declarations[declarations.length - 1];
+
+ if (token.tokenType === ":") {
+ if (!lastProp.name) {
+ // Set the current declaration name if there's no name yet
+ lastProp.name = current.trim();
+ current = "";
+ hasBang = false;
+ } else {
+ // Otherwise, just append ':' to the current value (declaration value
+ // with colons)
+ current += ":";
+ }
+ } else if (token.tokenType === ";") {
+ lastProp.value = current.trim();
+ current = "";
+ hasBang = false;
+ declarations.push({name: "", value: "", priority: ""});
+ } else {
+ switch(token.tokenType) {
+ case "IDENT":
+ if (token.value === "important" && hasBang) {
+ lastProp.priority = "important";
+ hasBang = false;
+ } else {
+ if (hasBang) {
+ current += "!";
+ }
+ current += token.value;
+ }
+ break;
+ case "WHITESPACE":
+ current += " ";
+ break;
+ case "DIMENSION":
+ current += token.repr;
+ break;
+ case "HASH":
+ current += "#" + token.value;
+ break;
+ case "URL":
+ current += "url(" + quoteString(token.value) + ")";
+ break;
+ case "FUNCTION":
+ current += token.value + "(";
+ break;
+ case "(":
+ case ")":
+ current += token.tokenType;
+ break;
+ case "EOF":
+ break;
+ case "DELIM":
+ if (token.value === "!") {
+ hasBang = true;
+ } else {
+ current += token.value;
+ }
+ break;
+ case "STRING":
+ current += quoteString(token.value);
+ break;
+ case "{":
+ case "}":
+ current += token.tokenType;
+ break;
+ default:
+ current += token.value;
+ break;
+ }
+ }
+ }
+
+ // Handle whatever trailing properties or values might still be there
+ if (current) {
+ if (!lastProp.name) {
+ // Trailing property found, e.g. p1:v1;p2:v2;p3
+ lastProp.name = current.trim();
+ } else {
+ // Trailing value found, i.e. value without an ending ;
+ lastProp.value += current.trim();
+ }
+ }
+
+ // Remove declarations that have neither a name nor a value
+ declarations = declarations.filter(prop => prop.name || prop.value);
+
+ return declarations;
+};
+exports.parseDeclarations = parseDeclarations;
+
+/**
+ * Expects a single CSS value to be passed as the input and parses the value
+ * and priority.
+ *
+ * @param {string} value The value from the text editor.
+ * @return {object} an object with 'value' and 'priority' properties.
+ */
+function parseSingleValue(value) {
+ let declaration = parseDeclarations("a: " + value + ";")[0];
+ return {
+ value: declaration ? declaration.value : "",
+ priority: declaration ? declaration.priority : ""
+ };
+};
+exports.parseSingleValue = parseSingleValue;
diff --git a/toolkit/devtools/styleinspector/cssruleview.xhtml b/toolkit/devtools/styleinspector/cssruleview.xhtml
new file mode 100644
index 000000000..40e260d36
--- /dev/null
+++ b/toolkit/devtools/styleinspector/cssruleview.xhtml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<!DOCTYPE window [
+ <!ENTITY % inspectorDTD SYSTEM "chrome://browser/locale/devtools/styleinspector.dtd">
+ %inspectorDTD;
+]>
+
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ class="theme-sidebar">
+
+ <head>
+ <title>&ruleViewTitle;</title>
+ <link rel="stylesheet" href="chrome://global/skin/global.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/content/devtools/ruleview.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/ruleview.css" type="text/css"/>
+
+ <script type="application/javascript;version=1.8" src="theme-switching.js"/>
+
+ <script type="application/javascript;version=1.8">
+ window.setPanel = function(panel, iframe) {
+ let {devtools} = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
+ let inspector = devtools.require("devtools/styleinspector/style-inspector");
+ this.ruleview = new inspector.RuleViewTool(panel, window, iframe);
+ }
+ window.onunload = function() {
+ if (this.ruleview) {
+ this.ruleview.destroy();
+ }
+ }
+ </script>
+ </head>
+</html>
diff --git a/toolkit/devtools/styleinspector/moz.build b/toolkit/devtools/styleinspector/moz.build
index 9edb32500..93db4b302 100644
--- a/toolkit/devtools/styleinspector/moz.build
+++ b/toolkit/devtools/styleinspector/moz.build
@@ -4,6 +4,19 @@
# 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/.
+if CONFIG['MOZ_DEVTOOLS']:
+ BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+ XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+
EXTRA_JS_MODULES.devtools.styleinspector += [
'css-logic.js'
]
+
+if CONFIG['MOZ_DEVTOOLS']:
+ EXTRA_JS_MODULES.devtools.styleinspector += [
+ 'computed-view.js',
+ 'css-parsing-utils.js',
+ 'rule-view.js',
+ 'style-inspector-overlays.js',
+ 'style-inspector.js',
+ ] \ No newline at end of file
diff --git a/toolkit/devtools/styleinspector/rule-view.js b/toolkit/devtools/styleinspector/rule-view.js
new file mode 100644
index 000000000..87e747557
--- /dev/null
+++ b/toolkit/devtools/styleinspector/rule-view.js
@@ -0,0 +1,3097 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {Cc, Ci, Cu} = require("chrome");
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+const {CssLogic} = require("devtools/styleinspector/css-logic");
+const {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor");
+const {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles");
+const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+const {OutputParser} = require("devtools/output-parser");
+const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils");
+const {parseSingleValue, parseDeclarations} = require("devtools/styleinspector/css-parsing-utils");
+const overlays = require("devtools/styleinspector/style-inspector-overlays");
+
+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";
+const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
+const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit";
+
+/**
+ * These regular expressions are adapted from firebug's css.js, and are
+ * used to parse CSSStyleDeclaration's cssText attribute.
+ */
+
+// Used to split on css line separators
+const CSS_LINE_RE = /(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g;
+
+// Used to parse a single property line.
+const CSS_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*(?:! (important))?;?$/;
+
+// Used to parse an external resource from a property value
+const CSS_RESOURCE_RE = /url\([\'\"]?(.*?)[\'\"]?\)/;
+
+const IOService = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService);
+
+function promiseWarn(err) {
+ console.error(err);
+ return promise.reject(err);
+}
+
+/**
+ * To figure out how shorthand properties are interpreted by the
+ * engine, we will set properties on a dummy element and observe
+ * how their .style attribute reflects them as computed values.
+ * This function creates the document in which those dummy elements
+ * will be created.
+ */
+var gDummyPromise;
+function createDummyDocument() {
+ if (gDummyPromise) {
+ return gDummyPromise;
+ }
+ const { getDocShell, create: makeFrame } = require("sdk/frame/utils");
+
+ let frame = makeFrame(Services.appShell.hiddenDOMWindow.document, {
+ nodeName: "iframe",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ allowJavascript: false,
+ allowPlugins: false,
+ allowAuth: false
+ });
+ let docShell = getDocShell(frame);
+ let eventTarget = docShell.chromeEventHandler;
+ docShell.createAboutBlankContentViewer(Cc["@mozilla.org/nullprincipal;1"].createInstance(Ci.nsIPrincipal));
+ let window = docShell.contentViewer.DOMDocument.defaultView;
+ window.location = "data:text/html,<html></html>";
+ let deferred = promise.defer();
+ eventTarget.addEventListener("DOMContentLoaded", function handler(event) {
+ eventTarget.removeEventListener("DOMContentLoaded", handler, false);
+ deferred.resolve(window.document);
+ frame.remove();
+ }, false);
+ gDummyPromise = deferred.promise;
+ return gDummyPromise;
+}
+
+/**
+ * Our model looks like this:
+ *
+ * ElementStyle:
+ * Responsible for keeping track of which properties are overridden.
+ * Maintains a list of Rule objects that apply to the element.
+ * Rule:
+ * Manages a single style declaration or rule.
+ * Responsible for applying changes to the properties in a rule.
+ * Maintains a list of TextProperty objects.
+ * TextProperty:
+ * Manages a single property from the cssText attribute of the
+ * relevant declaration.
+ * Maintains a list of computed properties that come from this
+ * property declaration.
+ * Changes to the TextProperty are sent to its related Rule for
+ * application.
+ */
+
+/**
+ * ElementStyle maintains a list of Rule objects for a given element.
+ *
+ * @param {Element} aElement
+ * The element whose style we are viewing.
+ * @param {object} aStore
+ * The ElementStyle can use this object to store metadata
+ * that might outlast the rule view, particularly the current
+ * set of disabled properties.
+ * @param {PageStyleFront} aPageStyle
+ * Front for the page style actor that will be providing
+ * the style information.
+ * @param {bool} aShowUserAgentStyles
+ * Should user agent styles be inspected?
+ *
+ * @constructor
+ */
+function ElementStyle(aElement, aStore, aPageStyle, aShowUserAgentStyles) {
+ this.element = aElement;
+ this.store = aStore || {};
+ this.pageStyle = aPageStyle;
+ this.showUserAgentStyles = aShowUserAgentStyles;
+ this.rules = [];
+
+ // We don't want to overwrite this.store.userProperties so we only create it
+ // if it doesn't already exist.
+ if (!("userProperties" in this.store)) {
+ this.store.userProperties = new UserProperties();
+ }
+
+ if (!("disabled" in this.store)) {
+ this.store.disabled = new WeakMap();
+ }
+}
+
+// We're exporting _ElementStyle for unit tests.
+exports._ElementStyle = ElementStyle;
+
+ElementStyle.prototype = {
+ // The element we're looking at.
+ element: null,
+
+ // Empty, unconnected element of the same type as this node, used
+ // to figure out how shorthand properties will be parsed.
+ dummyElement: null,
+
+ init: function()
+ {
+ // To figure out how shorthand properties are interpreted by the
+ // engine, we will set properties on a dummy element and observe
+ // how their .style attribute reflects them as computed values.
+ return this.dummyElementPromise = createDummyDocument().then(document => {
+ // ::before and ::after do not have a namespaceURI
+ let namespaceURI = this.element.namespaceURI || document.documentElement.namespaceURI;
+ this.dummyElement = document.createElementNS(namespaceURI,
+ this.element.tagName);
+ document.documentElement.appendChild(this.dummyElement);
+ return this.dummyElement;
+ }).then(null, promiseWarn);
+ },
+
+ destroy: function() {
+ if (this.destroyed) {
+ return;
+ }
+ this.destroyed = true;
+
+ this.dummyElement = null;
+ this.dummyElementPromise.then(dummyElement => {
+ dummyElement.remove();
+ this.dummyElementPromise = null;
+ }, console.error);
+ },
+
+ /**
+ * Called by the Rule object when it has been changed through the
+ * setProperty* methods.
+ */
+ _changed: function() {
+ if (this.onChanged) {
+ this.onChanged();
+ }
+ },
+
+ /**
+ * Refresh the list of rules to be displayed for the active element.
+ * Upon completion, this.rules[] will hold a list of Rule objects.
+ *
+ * Returns a promise that will be resolved when the elementStyle is
+ * ready.
+ */
+ populate: function() {
+ let populated = this.pageStyle.getApplied(this.element, {
+ inherited: true,
+ matchedSelectors: true,
+ filter: this.showUserAgentStyles ? "ua" : undefined,
+ }).then(entries => {
+ if (this.destroyed) {
+ return;
+ }
+
+ // Make sure the dummy element has been created before continuing...
+ return this.dummyElementPromise.then(() => {
+ if (this.populated != populated) {
+ // Don't care anymore.
+ return;
+ }
+
+ // Store the current list of rules (if any) during the population
+ // process. They will be reused if possible.
+ this._refreshRules = this.rules;
+
+ this.rules = [];
+
+ for (let entry of entries) {
+ this._maybeAddRule(entry);
+ }
+
+ // Mark overridden computed styles.
+ 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() {
+ 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.
+ */
+ _maybeAddRule: function(aOptions) {
+ // If we've already included this domRule (for example, when a
+ // common selector is inherited), ignore it.
+ if (aOptions.rule &&
+ this.rules.some(function(rule) rule.domRule === aOptions.rule)) {
+ return false;
+ }
+
+ if (aOptions.system) {
+ return false;
+ }
+
+ let rule = null;
+
+ // If we're refreshing and the rule previously existed, reuse the
+ // Rule object.
+ if (this._refreshRules) {
+ for (let r of this._refreshRules) {
+ if (r.matches(aOptions)) {
+ rule = r;
+ rule.refresh(aOptions);
+ break;
+ }
+ }
+ }
+
+ // If this is a new rule, create its Rule object.
+ if (!rule) {
+ rule = new Rule(this, aOptions);
+ }
+
+ // Ignore inherited rules with no properties.
+ if (aOptions.inherited && rule.textProps.length == 0) {
+ return false;
+ }
+
+ this.rules.push(rule);
+ return true;
+ },
+
+ /**
+ * Calls markOverridden with all supported pseudo elements
+ */
+ markOverriddenAll: function() {
+ 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(pseudo="") {
+ // Gather all the text properties applied by these rules, ordered
+ // 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 && !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) {
+ 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
+ // its priority against the property that was found first:
+ //
+ // If the new property is a higher priority, mark the old
+ // property overridden and mark the property name as taken by
+ // 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 (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;
+ earlier.overridden = true;
+ overridden = false;
+ } else {
+ overridden = !!earlier;
+ }
+
+ computedProp._overriddenDirty = (!!computedProp.overridden != overridden);
+ computedProp.overridden = overridden;
+ if (!computedProp.overridden && computedProp.textProp.enabled) {
+ 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 (let textProp of textProps) {
+ // _updatePropertyOverridden will return true if the
+ // overridden state has changed for the text property.
+ if (this._updatePropertyOverridden(textProp)) {
+ textProp.updateEditor();
+ }
+ }
+ },
+
+ /**
+ * Mark a given TextProperty as overridden or not depending on the
+ * state of its computed properties. Clears the _overriddenDirty state
+ * on all computed properties.
+ *
+ * @param {TextProperty} aProp
+ * The text property to update.
+ *
+ * @return {bool} true if the TextProperty's overridden state (or any of its
+ * computed properties overridden state) changed.
+ */
+ _updatePropertyOverridden: function(aProp) {
+ let overridden = true;
+ let dirty = false;
+ for each (let computedProp in aProp.computed) {
+ if (!computedProp.overridden) {
+ overridden = false;
+ }
+ dirty = computedProp._overriddenDirty || dirty;
+ delete computedProp._overriddenDirty;
+ }
+
+ dirty = (!!aProp.overridden != overridden) || dirty;
+ aProp.overridden = overridden;
+ return dirty;
+ }
+};
+
+/**
+ * A single style rule or declaration.
+ *
+ * @param {ElementStyle} aElementStyle
+ * The ElementStyle to which this rule belongs.
+ * @param {object} aOptions
+ * The information used to construct this rule. Properties include:
+ * rule: A StyleRuleActor
+ * inherited: An element this rule was inherited from. If omitted,
+ * the rule applies directly to the current element.
+ * isSystem: Is this a user agent style?
+ * @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.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;
+ }
+ }
+
+ // Populate the text properties with the style's current cssText
+ // value, and add in any disabled properties from the store.
+ this.textProps = this._getTextProperties();
+ this.textProps = this.textProps.concat(this._getDisabledProperties());
+}
+
+Rule.prototype = {
+ mediaText: "",
+
+ get title() {
+ if (this._title) {
+ return this._title;
+ }
+ this._title = CssLogic.shortSource(this.sheet);
+ if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) {
+ this._title += ":" + this.ruleLine;
+ }
+
+ this._title = this._title + (this.mediaText ? " @media " + this.mediaText : "");
+ return this._title;
+ },
+
+ get inheritedSource() {
+ if (this._inheritedSource) {
+ return this._inheritedSource;
+ }
+ this._inheritedSource = "";
+ if (this.inherited) {
+ let eltText = this.inherited.tagName.toLowerCase();
+ if (this.inherited.id) {
+ 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() {
+ return this.domRule ? this.domRule.parentStyleSheet : null;
+ },
+
+ /**
+ * The rule's line within a stylesheet
+ */
+ get ruleLine() {
+ return this.domRule ? this.domRule.line : null;
+ },
+
+ /**
+ * The rule's column within a stylesheet
+ */
+ get ruleColumn() {
+ return this.domRule ? this.domRule.column : null;
+ },
+
+ /**
+ * Get display name for this rule based on the original source
+ * for this rule's style sheet.
+ *
+ * @return {Promise}
+ * Promise which resolves with location as an object containing
+ * both the full and short version of the source string.
+ */
+ getOriginalSourceStrings: function() {
+ if (this._originalSourceStrings) {
+ return promise.resolve(this._originalSourceStrings);
+ }
+ return this.domRule.getOriginalLocation().then(({href, line}) => {
+ let sourceStrings = {
+ full: href + ":" + line,
+ short: CssLogic.shortSource({href: href}) + ":" + line
+ };
+
+ this._originalSourceStrings = sourceStrings;
+ return sourceStrings;
+ }, console.error);
+ },
+
+ /**
+ * Returns true if the rule matches the creation options
+ * specified.
+ *
+ * @param {object} aOptions
+ * Creation options. See the Rule constructor for documentation.
+ */
+ matches: function(aOptions) {
+ return this.style === aOptions.rule;
+ },
+
+ /**
+ * Create a new TextProperty to include in the rule.
+ *
+ * @param {string} aName
+ * The text property name (such as "background" or "border-top").
+ * @param {string} aValue
+ * The property's value (not including priority).
+ * @param {string} aPriority
+ * The property's priority (either "important" or an empty string).
+ * @param {TextProperty} aSiblingProp
+ * Optional, property next to which the new property will be added.
+ */
+ createProperty: function(aName, aValue, aPriority, aSiblingProp) {
+ let prop = new TextProperty(this, aName, aValue, aPriority);
+
+ if (aSiblingProp) {
+ let ind = this.textProps.indexOf(aSiblingProp);
+ this.textProps.splice(ind + 1, 0, prop);
+ }
+ else {
+ this.textProps.push(prop);
+ }
+
+ this.applyProperties();
+ return prop;
+ },
+
+ /**
+ * Reapply all the properties in this rule, and update their
+ * computed styles. Store disabled properties in the element
+ * style's store. Will re-mark overridden properties.
+ *
+ * @param {string} [aName]
+ * A text property name (such as "background" or "border-top") used
+ * when calling from setPropertyValue & setPropertyName to signify
+ * that the property should be saved in store.userProperties.
+ */
+ applyProperties: function(aModifications, aName) {
+ this.elementStyle.markOverriddenAll();
+
+ if (!aModifications) {
+ aModifications = this.style.startModifyingProperties();
+ }
+ let disabledProps = [];
+ let store = this.elementStyle.store;
+
+ for (let prop of this.textProps) {
+ if (!prop.enabled) {
+ disabledProps.push({
+ name: prop.name,
+ value: prop.value,
+ priority: prop.priority
+ });
+ continue;
+ }
+ if (prop.value.trim() === "") {
+ continue;
+ }
+
+ aModifications.setProperty(prop.name, prop.value, prop.priority);
+
+ prop.updateComputed();
+ }
+
+ // Store disabled properties in the disabled store.
+ let disabled = this.elementStyle.store.disabled;
+ if (disabledProps.length > 0) {
+ disabled.set(this.style, disabledProps);
+ } else {
+ disabled.delete(this.style);
+ }
+
+ let promise = aModifications.apply().then(() => {
+ let cssProps = {};
+ for (let cssProp of parseDeclarations(this.style.cssText)) {
+ cssProps[cssProp.name] = cssProp;
+ }
+
+ for (let textProp of this.textProps) {
+ if (!textProp.enabled) {
+ continue;
+ }
+ let cssProp = cssProps[textProp.name];
+
+ if (!cssProp) {
+ cssProp = {
+ name: textProp.name,
+ value: "",
+ priority: ""
+ };
+ }
+
+ textProp.priority = cssProp.priority;
+ }
+
+ this.elementStyle.markOverriddenAll();
+
+ if (promise === this._applyingModifications) {
+ this._applyingModifications = null;
+ }
+
+ this.elementStyle._changed();
+ }).then(null, promiseWarn);
+
+ this._applyingModifications = promise;
+ return promise;
+ },
+
+ /**
+ * Renames a property.
+ *
+ * @param {TextProperty} aProperty
+ * The property to rename.
+ * @param {string} aName
+ * The new property name (such as "background" or "border-top").
+ */
+ setPropertyName: function(aProperty, aName) {
+ if (aName === aProperty.name) {
+ return;
+ }
+ let modifications = this.style.startModifyingProperties();
+ modifications.removeProperty(aProperty.name);
+ aProperty.name = aName;
+ this.applyProperties(modifications, aName);
+ },
+
+ /**
+ * Sets the value and priority of a property, then reapply all properties.
+ *
+ * @param {TextProperty} aProperty
+ * The property to manipulate.
+ * @param {string} aValue
+ * The property's value (not including priority).
+ * @param {string} aPriority
+ * The property's priority (either "important" or an empty string).
+ */
+ setPropertyValue: function(aProperty, aValue, aPriority) {
+ if (aValue === aProperty.value && aPriority === aProperty.priority) {
+ return;
+ }
+
+ aProperty.value = aValue;
+ aProperty.priority = aPriority;
+ this.applyProperties(null, aProperty.name);
+ },
+
+ /**
+ * Just sets the value and priority of a property, in order to preview its
+ * effect on the content document.
+ *
+ * @param {TextProperty} aProperty
+ * The property which value will be previewed
+ * @param {String} aValue
+ * The value to be used for the preview
+ * @param {String} aPriority
+ * The property's priority (either "important" or an empty string).
+ */
+ previewPropertyValue: function(aProperty, aValue, aPriority) {
+ aProperty.value = aValue;
+
+ let modifications = this.style.startModifyingProperties();
+ modifications.setProperty(aProperty.name, aValue, aPriority);
+ modifications.apply();
+ },
+
+ /**
+ * Disables or enables given TextProperty.
+ *
+ * @param {TextProperty} aProperty
+ * The property to enable/disable
+ * @param {Boolean} aValue
+ */
+ setPropertyEnabled: function(aProperty, aValue) {
+ aProperty.enabled = !!aValue;
+ let modifications = this.style.startModifyingProperties();
+ if (!aProperty.enabled) {
+ modifications.removeProperty(aProperty.name);
+ }
+ this.applyProperties(modifications);
+ },
+
+ /**
+ * Remove a given TextProperty from the rule and update the rule
+ * accordingly.
+ *
+ * @param {TextProperty} aProperty
+ * The property to be removed
+ */
+ removeProperty: function(aProperty) {
+ this.textProps = this.textProps.filter(function(prop) prop != aProperty);
+ let modifications = this.style.startModifyingProperties();
+ modifications.removeProperty(aProperty.name);
+ // Need to re-apply properties in case removing this TextProperty
+ // exposes another one.
+ this.applyProperties(modifications);
+ },
+
+ /**
+ * Get the list of TextProperties from the style. Needs
+ * to parse the style's cssText.
+ */
+ _getTextProperties: function() {
+ let textProps = [];
+ let store = this.elementStyle.store;
+ let props = parseDeclarations(this.style.cssText);
+ for (let prop of props) {
+ let name = prop.name;
+ if (this.inherited && !domUtils.isInheritedProperty(name)) {
+ continue;
+ }
+ let value = store.userProperties.getProperty(this.style, name, prop.value);
+ let textProp = new TextProperty(this, name, value, prop.priority);
+ textProps.push(textProp);
+ }
+
+ return textProps;
+ },
+
+ /**
+ * Return the list of disabled properties from the store for this rule.
+ */
+ _getDisabledProperties: function() {
+ let store = this.elementStyle.store;
+
+ // Include properties from the disabled property store, if any.
+ let disabledProps = store.disabled.get(this.style);
+ if (!disabledProps) {
+ return [];
+ }
+
+ let textProps = [];
+
+ for each (let prop in disabledProps) {
+ let value = store.userProperties.getProperty(this.style, prop.name, prop.value);
+ let textProp = new TextProperty(this, prop.name, value, prop.priority);
+ textProp.enabled = false;
+ textProps.push(textProp);
+ }
+
+ return textProps;
+ },
+
+ /**
+ * Reread the current state of the rules and rebuild text
+ * properties as needed.
+ */
+ refresh: function(aOptions) {
+ this.matchedSelectors = aOptions.matchedSelectors || [];
+ let newTextProps = this._getTextProperties();
+
+ // Update current properties for each property present on the style.
+ // This will mark any touched properties with _visited so we
+ // can detect properties that weren't touched (because they were
+ // removed from the style).
+ // Also keep track of properties that didn't exist in the current set
+ // of properties.
+ let brandNewProps = [];
+ for (let newProp of newTextProps) {
+ if (!this._updateTextProperty(newProp)) {
+ brandNewProps.push(newProp);
+ }
+ }
+
+ // Refresh editors and disabled state for all the properties that
+ // were updated.
+ for (let prop of this.textProps) {
+ // Properties that weren't touched during the update
+ // process must no longer exist on the node. Mark them disabled.
+ if (!prop._visited) {
+ prop.enabled = false;
+ prop.updateEditor();
+ } else {
+ delete prop._visited;
+ }
+ }
+
+ // Add brand new properties.
+ this.textProps = this.textProps.concat(brandNewProps);
+
+ // Refresh the editor if one already exists.
+ if (this.editor) {
+ this.editor.populate();
+ }
+ },
+
+ /**
+ * Update the current TextProperties that match a given property
+ * from the cssText. Will choose one existing TextProperty to update
+ * with the new property's value, and will disable all others.
+ *
+ * When choosing the best match to reuse, properties will be chosen
+ * by assigning a rank and choosing the highest-ranked property:
+ * Name, value, and priority match, enabled. (6)
+ * Name, value, and priority match, disabled. (5)
+ * Name and value match, enabled. (4)
+ * Name and value match, disabled. (3)
+ * Name matches, enabled. (2)
+ * Name matches, disabled. (1)
+ *
+ * If no existing properties match the property, nothing happens.
+ *
+ * @param {TextProperty} aNewProp
+ * The current version of the property, as parsed from the
+ * cssText in Rule._getTextProperties().
+ *
+ * @return {bool} true if a property was updated, false if no properties
+ * were updated.
+ */
+ _updateTextProperty: function(aNewProp) {
+ let match = { rank: 0, prop: null };
+
+ for each (let prop in this.textProps) {
+ if (prop.name != aNewProp.name)
+ continue;
+
+ // Mark this property visited.
+ prop._visited = true;
+
+ // Start at rank 1 for matching name.
+ let rank = 1;
+
+ // Value and Priority matches add 2 to the rank.
+ // Being enabled adds 1. This ranks better matches higher,
+ // with priority breaking ties.
+ if (prop.value === aNewProp.value) {
+ rank += 2;
+ if (prop.priority === aNewProp.priority) {
+ rank += 2;
+ }
+ }
+
+ if (prop.enabled) {
+ rank += 1;
+ }
+
+ if (rank > match.rank) {
+ if (match.prop) {
+ // We outrank a previous match, disable it.
+ match.prop.enabled = false;
+ match.prop.updateEditor();
+ }
+ match.rank = rank;
+ match.prop = prop;
+ } else if (rank) {
+ // A previous match outranks us, disable ourself.
+ prop.enabled = false;
+ prop.updateEditor();
+ }
+ }
+
+ // If we found a match, update its value with the new text property
+ // value.
+ if (match.prop) {
+ match.prop.set(aNewProp);
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Jump between editable properties in the UI. Will begin editing the next
+ * name, if possible. If this is the last element in the set, then begin
+ * editing the previous value. If this is the *only* element in the set,
+ * then settle for focusing the new property editor.
+ *
+ * @param {TextProperty} aTextProperty
+ * The text property that will be left to focus on a sibling.
+ *
+ */
+ editClosestTextProperty: function(aTextProperty) {
+ let index = this.textProps.indexOf(aTextProperty);
+ let previous = false;
+
+ // If this is the last element, move to the previous instead of next
+ if (index === this.textProps.length - 1) {
+ index = index - 1;
+ previous = true;
+ }
+ else {
+ index = index + 1;
+ }
+
+ let nextProp = this.textProps[index];
+
+ // If possible, begin editing the next name or previous value.
+ // Otherwise, settle for focusing the new property element.
+ if (nextProp) {
+ if (previous) {
+ nextProp.editor.valueSpan.click();
+ } else {
+ nextProp.editor.nameSpan.click();
+ }
+ } else {
+ aTextProperty.rule.editor.closeBrace.focus();
+ }
+ }
+};
+
+/**
+ * A single property in a rule's cssText.
+ *
+ * @param {Rule} aRule
+ * The rule this TextProperty came from.
+ * @param {string} aName
+ * The text property name (such as "background" or "border-top").
+ * @param {string} aValue
+ * The property's value (not including priority).
+ * @param {string} aPriority
+ * The property's priority (either "important" or an empty string).
+ *
+ */
+function TextProperty(aRule, aName, aValue, aPriority) {
+ this.rule = aRule;
+ this.name = aName;
+ this.value = aValue;
+ this.priority = aPriority;
+ this.enabled = true;
+ this.updateComputed();
+}
+
+TextProperty.prototype = {
+ /**
+ * Update the editor associated with this text property,
+ * if any.
+ */
+ updateEditor: function() {
+ if (this.editor) {
+ this.editor.update();
+ }
+ },
+
+ /**
+ * Update the list of computed properties for this text property.
+ */
+ updateComputed: function() {
+ if (!this.name) {
+ return;
+ }
+
+ // This is a bit funky. To get the list of computed properties
+ // for this text property, we'll set the property on a dummy element
+ // and see what the computed style looks like.
+ let dummyElement = this.rule.elementStyle.dummyElement;
+ let dummyStyle = dummyElement.style;
+ dummyStyle.cssText = "";
+ dummyStyle.setProperty(this.name, this.value, this.priority);
+
+ this.computed = [];
+
+ try {
+ // Manually get all the properties that are set when setting a value on
+ // this.name and check the computed style on dummyElement for each one.
+ // If we just read dummyStyle, it would skip properties when value == "".
+ let subProps = domUtils.getSubpropertiesForCSSProperty(this.name);
+
+ for (let prop of subProps) {
+ this.computed.push({
+ textProp: this,
+ name: prop,
+ value: dummyStyle.getPropertyValue(prop),
+ priority: dummyStyle.getPropertyPriority(prop),
+ });
+ }
+ } catch(e) {
+ // This is a partial property name, probably from cutting and pasting
+ // text. At this point don't check for computed properties.
+ }
+ },
+
+ /**
+ * Set all the values from another TextProperty instance into
+ * this TextProperty instance.
+ *
+ * @param {TextProperty} aOther
+ * The other TextProperty instance.
+ */
+ set: function(aOther) {
+ let changed = false;
+ for (let item of ["name", "value", "priority", "enabled"]) {
+ if (this[item] != aOther[item]) {
+ this[item] = aOther[item];
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ this.updateEditor();
+ }
+ },
+
+ setValue: function(aValue, aPriority, force=false) {
+ let store = this.rule.elementStyle.store;
+
+ if (this.editor && aValue !== this.editor.committed.value || force) {
+ store.userProperties.setProperty(this.rule.style, this.name, aValue);
+ }
+
+ this.rule.setPropertyValue(this, aValue, aPriority);
+ this.updateEditor();
+ },
+
+ setName: function(aName) {
+ let store = this.rule.elementStyle.store;
+
+ if (aName !== this.name) {
+ store.userProperties.setProperty(this.rule.style, aName,
+ this.editor.committed.value);
+ }
+
+ this.rule.setPropertyName(this, aName);
+ this.updateEditor();
+ },
+
+ setEnabled: function(aValue) {
+ this.rule.setPropertyEnabled(this, aValue);
+ this.updateEditor();
+ },
+
+ remove: function() {
+ this.rule.removeProperty(this);
+ }
+};
+
+/**
+ * View hierarchy mostly follows the model hierarchy.
+ *
+ * CssRuleView:
+ * Owns an ElementStyle and creates a list of RuleEditors for its
+ * Rules.
+ * RuleEditor:
+ * Owns a Rule object and creates a list of TextPropertyEditors
+ * for its TextProperties.
+ * Manages creation of new text properties.
+ * TextPropertyEditor:
+ * Owns a TextProperty object.
+ * Manages changes to the TextProperty.
+ * Can be expanded to display computed properties.
+ * Can mark a property disabled or enabled.
+ */
+
+/**
+ * CssRuleView is a view of the style rules and declarations that
+ * apply to a given element. After construction, the 'element'
+ * property will be available with the user interface.
+ *
+ * @param {Inspector} aInspector
+ * @param {Document} aDoc
+ * The document that will contain the rule view.
+ * @param {object} aStore
+ * The CSS rule view can use this object to store metadata
+ * that might outlast the rule view, particularly the current
+ * set of disabled properties.
+ * @param {PageStyleFront} aPageStyle
+ * The PageStyleFront for communicating with the remote server.
+ * @constructor
+ */
+function CssRuleView(aInspector, aDoc, aStore, aPageStyle) {
+ this.inspector = aInspector;
+ this.doc = aDoc;
+ this.store = aStore || {};
+ this.pageStyle = aPageStyle;
+ this.element = this.doc.createElementNS(HTML_NS, "div");
+ this.element.className = "ruleview devtools-monospace";
+ this.element.flex = 1;
+
+ this._outputParser = new OutputParser();
+
+ this._buildContextMenu = this._buildContextMenu.bind(this);
+ this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
+ this._onAddRule = this._onAddRule.bind(this);
+ this._onSelectAll = this._onSelectAll.bind(this);
+ this._onCopy = this._onCopy.bind(this);
+ this._onCopyColor = this._onCopyColor.bind(this);
+ this._onToggleOrigSources = this._onToggleOrigSources.bind(this);
+
+ this.element.addEventListener("copy", this._onCopy);
+
+ this._handlePrefChange = this._handlePrefChange.bind(this);
+ this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this);
+
+ this._prefObserver = new PrefObserver("devtools.");
+ this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
+ this._prefObserver.on(PREF_UA_STYLES, this._handlePrefChange);
+ this._prefObserver.on(PREF_DEFAULT_COLOR_UNIT, this._handlePrefChange);
+
+ this.showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES);
+
+ let options = {
+ autoSelect: true,
+ theme: "auto"
+ };
+ this.popup = new AutocompletePopup(aDoc.defaultView.parent.document, options);
+
+ this._buildContextMenu();
+ this._showEmpty();
+
+ // Add the tooltips and highlighters to the view
+ this.tooltips = new overlays.TooltipsOverlay(this);
+ this.tooltips.addToView();
+ this.highlighters = new overlays.HighlightersOverlay(this);
+ this.highlighters.addToView();
+}
+
+exports.CssRuleView = CssRuleView;
+
+CssRuleView.prototype = {
+ // The element that we're inspecting.
+ _viewedElement: null,
+
+ /**
+ * Build the context menu.
+ */
+ _buildContextMenu: function() {
+ let doc = this.doc.defaultView.parent.document;
+
+ this._contextmenu = doc.createElementNS(XUL_NS, "menupopup");
+ this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate);
+ this._contextmenu.id = "rule-view-context-menu";
+
+ this.menuitemAddRule = createMenuItem(this._contextmenu, {
+ label: "ruleView.contextmenu.addRule",
+ accesskey: "ruleView.contextmenu.addRule.accessKey",
+ command: this._onAddRule
+ });
+ this.menuitemSelectAll = createMenuItem(this._contextmenu, {
+ label: "ruleView.contextmenu.selectAll",
+ accesskey: "ruleView.contextmenu.selectAll.accessKey",
+ command: this._onSelectAll
+ });
+ this.menuitemCopy = createMenuItem(this._contextmenu, {
+ label: "ruleView.contextmenu.copy",
+ accesskey: "ruleView.contextmenu.copy.accessKey",
+ command: this._onCopy
+ });
+ this.menuitemCopyColor = createMenuItem(this._contextmenu, {
+ label: "ruleView.contextmenu.copyColor",
+ accesskey: "ruleView.contextmenu.copyColor.accessKey",
+ command: this._onCopyColor
+ });
+ this.menuitemSources = createMenuItem(this._contextmenu, {
+ label: "ruleView.contextmenu.showOrigSources",
+ accesskey: "ruleView.contextmenu.showOrigSources.accessKey",
+ command: this._onToggleOrigSources,
+ type: "checkbox"
+ });
+
+ let popupset = doc.documentElement.querySelector("popupset");
+ if (!popupset) {
+ popupset = doc.createElementNS(XUL_NS, "popupset");
+ doc.documentElement.appendChild(popupset);
+ }
+
+ popupset.appendChild(this._contextmenu);
+ },
+
+ /**
+ * Update the context menu. This means enabling or disabling menuitems as
+ * appropriate.
+ */
+ _contextMenuUpdate: function() {
+ let win = this.doc.defaultView;
+
+ // Copy selection.
+ let selection = win.getSelection();
+ let copy;
+
+ if (selection.toString()) {
+ // Panel text selected
+ copy = true;
+ } else if (selection.anchorNode) {
+ // input type="text"
+ let { selectionStart, selectionEnd } = this.doc.popupNode;
+
+ if (isFinite(selectionStart) && isFinite(selectionEnd) &&
+ selectionStart !== selectionEnd) {
+ copy = true;
+ }
+ } else {
+ // No text selected, disable copy.
+ copy = false;
+ }
+
+ this.menuitemCopyColor.hidden = !this._isColorPopup();
+ this.menuitemCopy.disabled = !copy;
+
+ var showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
+ this.menuitemSources.setAttribute("checked", showOrig);
+
+ this.menuitemAddRule.disabled = this.inspector.selection.isAnonymousNode();
+ },
+
+ /**
+ * Get the type of a given node in the rule-view
+ * @param {DOMNode} node The node which we want information about
+ * @return {Object} The type information object contains the following props:
+ * - type {String} One of the VIEW_NODE_XXX_TYPE const in
+ * style-inspector-overlays
+ * - value {Object} Depends on the type of the node
+ * returns null of the node isn't anything we care about
+ */
+ getNodeInfo: function(node) {
+ if (!node) {
+ return null;
+ }
+
+ let type, value;
+ let classes = node.classList;
+ let prop = getParentTextProperty(node);
+
+ if (classes.contains("ruleview-propertyname") && prop) {
+ type = overlays.VIEW_NODE_PROPERTY_TYPE;
+ value = {
+ property: node.textContent,
+ value: getPropertyNameAndValue(node).value,
+ enabled: prop.enabled,
+ overridden: prop.overridden,
+ pseudoElement: prop.rule.pseudoElement,
+ sheetHref: prop.rule.domRule.href
+ };
+ } else if (classes.contains("ruleview-propertyvalue") && prop) {
+ type = overlays.VIEW_NODE_VALUE_TYPE;
+ value = {
+ property: getPropertyNameAndValue(node).name,
+ value: node.textContent,
+ enabled: prop.enabled,
+ overridden: prop.overridden,
+ pseudoElement: prop.rule.pseudoElement,
+ sheetHref: prop.rule.domRule.href
+ };
+ } else if (classes.contains("theme-link") && prop) {
+ type = overlays.VIEW_NODE_IMAGE_URL_TYPE;
+ value = {
+ property: getPropertyNameAndValue(node).name,
+ value: node.parentNode.textContent,
+ url: node.href,
+ enabled: prop.enabled,
+ overridden: prop.overridden,
+ pseudoElement: prop.rule.pseudoElement,
+ sheetHref: prop.rule.domRule.href
+ };
+ } else if (classes.contains("ruleview-selector-unmatched") ||
+ classes.contains("ruleview-selector-matched")) {
+ type = overlays.VIEW_NODE_SELECTOR_TYPE;
+ value = node.textContent;
+ } else {
+ return null;
+ }
+
+ return {type, value};
+ },
+
+ /**
+ * A helper that determines if the popup was opened with a click to a color
+ * value and saves the color to this._colorToCopy.
+ *
+ * @return {Boolean}
+ * true if click on color opened the popup, false otherwise.
+ */
+ _isColorPopup: function () {
+ this._colorToCopy = "";
+
+ let trigger = this.doc.popupNode;
+ if (!trigger) {
+ return false;
+ }
+
+ let container = (trigger.nodeType == trigger.TEXT_NODE) ?
+ trigger.parentElement : trigger;
+
+ let isColorNode = el => el.dataset && "color" in el.dataset;
+
+ while (!isColorNode(container)) {
+ container = container.parentNode;
+ if (!container) {
+ return false;
+ }
+ }
+
+ this._colorToCopy = container.dataset["color"];
+ return true;
+ },
+
+ /**
+ * Select all text.
+ */
+ _onSelectAll: function() {
+ let win = this.doc.defaultView;
+ let selection = win.getSelection();
+
+ selection.selectAllChildren(this.doc.documentElement);
+ },
+
+ /**
+ * Copy selected text from the rule view.
+ *
+ * @param {Event} event
+ * The event object.
+ */
+ _onCopy: function(event) {
+ try {
+ let target = event.target;
+ let text;
+
+ if (event.target.nodeName === "menuitem") {
+ target = this.doc.popupNode;
+ }
+
+ if (target.nodeName == "input") {
+ let start = Math.min(target.selectionStart, target.selectionEnd);
+ let end = Math.max(target.selectionStart, target.selectionEnd);
+ let count = end - start;
+ text = target.value.substr(start, count);
+ } else {
+ let win = this.doc.defaultView;
+ let selection = win.getSelection();
+
+ text = selection.toString();
+
+ // Remove any double newlines.
+ text = text.replace(/(\r?\n)\r?\n/g, "$1");
+
+ // Remove "inline"
+ let inline = _strings.GetStringFromName("rule.sourceInline");
+ let rx = new RegExp("^" + inline + "\\r?\\n?", "g");
+ text = text.replace(rx, "");
+ }
+
+ clipboardHelper.copyString(text, this.doc);
+ event.preventDefault();
+ } catch(e) {
+ console.error(e);
+ }
+ },
+
+ /**
+ * Copy the most recently selected color value to clipboard.
+ */
+ _onCopyColor: function() {
+ clipboardHelper.copyString(this._colorToCopy, this.styleDocument);
+ },
+
+ /**
+ * Toggle the original sources pref.
+ */
+ _onToggleOrigSources: function() {
+ let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
+ Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
+ },
+
+ /**
+ * Add a new rule to the current element.
+ */
+ _onAddRule: function() {
+ let elementStyle = this._elementStyle;
+ let element = elementStyle.element;
+ let rules = elementStyle.rules;
+ let client = this.inspector.toolbox._target.client;
+
+ if (!client.traits.addNewRule) {
+ return;
+ }
+
+ this.pageStyle.addNewRule(element).then(options => {
+ let newRule = new Rule(elementStyle, options);
+ rules.push(newRule);
+ let editor = new RuleEditor(this, newRule);
+
+ // Insert the new rule editor after the inline element rule
+ if (rules.length <= 1) {
+ this.element.appendChild(editor.element);
+ } else {
+ for (let rule of rules) {
+ if (rule.domRule.type === ELEMENT_STYLE) {
+ let referenceElement = rule.editor.element.nextSibling;
+ this.element.insertBefore(editor.element, referenceElement);
+ break;
+ }
+ }
+ }
+
+ // Focus and make the new rule's selector editable
+ editor.selectorText.click();
+ elementStyle._changed();
+ });
+ },
+
+ setPageStyle: function(aPageStyle) {
+ this.pageStyle = aPageStyle;
+ },
+
+ /**
+ * Return {bool} true if the rule view currently has an input editor visible.
+ */
+ get isEditing() {
+ return this.element.querySelectorAll(".styleinspector-propertyeditor").length > 0
+ || this.tooltips.isEditing;
+ },
+
+ _handlePrefChange: function(pref) {
+ if (pref === PREF_UA_STYLES) {
+ this.showUserAgentStyles = Services.prefs.getBoolPref(pref);
+ }
+
+ // Reselect the currently selected element
+ let refreshOnPrefs = [PREF_UA_STYLES, PREF_DEFAULT_COLOR_UNIT];
+ if (refreshOnPrefs.indexOf(pref) > -1) {
+ let element = this._viewedElement;
+ this._viewedElement = null;
+ this.selectElement(element);
+ }
+ },
+
+ _onSourcePrefChanged: function() {
+ if (this.menuitemSources) {
+ let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
+ this.menuitemSources.setAttribute("checked", isEnabled);
+ }
+
+ // update text of source links if the rule-view is populated
+ if (this._elementStyle && this._elementStyle.rules) {
+ for (let rule of this._elementStyle.rules) {
+ if (rule.editor) {
+ rule.editor.updateSourceLink();
+ }
+ }
+ this.inspector.emit("rule-view-sourcelinks-updated");
+ }
+ },
+
+ destroy: function() {
+ this.isDestroyed = true;
+ this.clear();
+
+ gDummyPromise = null;
+
+ this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
+ this._prefObserver.off(PREF_UA_STYLES, this._handlePrefChange);
+ this._prefObserver.off(PREF_DEFAULT_COLOR_UNIT, this._handlePrefChange);
+ this._prefObserver.destroy();
+
+ this.element.removeEventListener("copy", this._onCopy);
+ this._onCopy = null;
+
+ this._outputParser = null;
+
+ // Remove context menu
+ if (this._contextmenu) {
+ // Destroy the Add Rule menuitem.
+ this.menuitemAddRule.removeEventListener("command", this._onAddRule);
+ this.menuitemAddRule = null;
+
+ // Destroy the Select All menuitem.
+ this.menuitemSelectAll.removeEventListener("command", this._onSelectAll);
+ this.menuitemSelectAll = null;
+
+ // Destroy the Copy menuitem.
+ this.menuitemCopy.removeEventListener("command", this._onCopy);
+ this.menuitemCopy = null;
+
+ // Destroy Copy Color menuitem.
+ this.menuitemCopyColor.removeEventListener("command", this._onCopyColor);
+ this.menuitemCopyColor = null;
+
+ this.menuitemSources.removeEventListener("command", this._onToggleOrigSources);
+ this.menuitemSources = null;
+
+ // Destroy the context menu.
+ this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate);
+ this._contextmenu.parentNode.removeChild(this._contextmenu);
+ this._contextmenu = null;
+ }
+
+ // We manage the popupNode ourselves so we also need to destroy it.
+ this.doc.popupNode = null;
+
+ this.tooltips.destroy();
+ this.highlighters.destroy();
+
+ if (this.element.parentNode) {
+ this.element.parentNode.removeChild(this.element);
+ }
+
+ if (this._elementStyle) {
+ this._elementStyle.destroy();
+ }
+
+ this.popup.destroy();
+ },
+
+ /**
+ * Update the view with a new selected element.
+ *
+ * @param {NodeActor} aElement
+ * The node whose style rules we'll inspect.
+ */
+ selectElement: function(aElement) {
+ if (this._viewedElement === aElement) {
+ return promise.resolve(undefined);
+ }
+
+ this.clear();
+
+ this._viewedElement = aElement;
+ if (!this._viewedElement) {
+ this._showEmpty();
+ return promise.resolve(undefined);
+ }
+
+ this._elementStyle = new ElementStyle(aElement, this.store,
+ this.pageStyle, this.showUserAgentStyles);
+
+ return this._elementStyle.init().then(() => {
+ if (this._viewedElement === aElement) {
+ return this._populate();
+ }
+ }).then(() => {
+ if (this._viewedElement === aElement) {
+ this._elementStyle.onChanged = () => {
+ this._changed();
+ };
+ }
+ }).then(null, console.error);
+ },
+
+ /**
+ * Update the rules for the currently highlighted element.
+ */
+ refreshPanel: function() {
+ // Ignore refreshes during editing or when no element is selected.
+ if (this.isEditing || !this._elementStyle) {
+ return;
+ }
+
+ // Repopulate the element style once the current modifications are done.
+ let promises = [];
+ for (let rule of this._elementStyle.rules) {
+ if (rule._applyingModifications) {
+ promises.push(rule._applyingModifications);
+ }
+ }
+
+ return promise.all(promises).then(() => {
+ return this._populate(true);
+ });
+ },
+
+ _populate: function(clearRules = false) {
+ let elementStyle = this._elementStyle;
+ return this._elementStyle.populate().then(() => {
+ if (this._elementStyle != elementStyle || this.isDestroyed) {
+ return;
+ }
+
+ if (clearRules) {
+ this._clearRules();
+ }
+ 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);
+ },
+
+ /**
+ * Show the user that the rule view has no node selected.
+ */
+ _showEmpty: function() {
+ if (this.doc.getElementById("noResults") > 0) {
+ return;
+ }
+
+ createChild(this.element, "div", {
+ id: "noResults",
+ textContent: CssLogic.l10n("rule.empty")
+ });
+ },
+
+ /**
+ * Clear the rules.
+ */
+ _clearRules: function() {
+ while (this.element.hasChildNodes()) {
+ this.element.removeChild(this.element.lastChild);
+ }
+ },
+
+ /**
+ * Clear the rule view.
+ */
+ clear: function() {
+ this._clearRules();
+ this._viewedElement = null;
+
+ if (this._elementStyle) {
+ this._elementStyle.destroy();
+ this._elementStyle = null;
+ }
+ },
+
+ /**
+ * Called when the user has made changes to the ElementStyle.
+ * Emits an event that clients can listen to.
+ */
+ _changed: function() {
+ 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;
+ },
+
+ 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;
+ }
+
+ // 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 = this._getRuleViewHeaderClassName();
+ div.textContent = this.selectedElementLabel;
+ this.element.appendChild(div);
+ }
+
+ let inheritedSource = rule.inheritedSource;
+ 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;
+ 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);
+ }
+
+ if (container && (rule.pseudoElement || keyframes)) {
+ container.appendChild(rule.editor.element);
+ } else {
+ this.element.appendChild(rule.editor.element);
+ }
+ }
+ }
+};
+
+/**
+ * Create a RuleEditor.
+ *
+ * @param {CssRuleView} aRuleView
+ * The CssRuleView containg the document holding this rule editor.
+ * @param {Rule} aRule
+ * The Rule object we're editing.
+ * @constructor
+ */
+function RuleEditor(aRuleView, aRule) {
+ this.ruleView = aRuleView;
+ this.doc = this.ruleView.doc;
+ this.rule = aRule;
+ this.isEditable = !aRule.isSystem;
+ // Flag that blocks updates of the selector and properties when it is
+ // being edited
+ this.isEditing = false;
+
+ this._onNewProperty = this._onNewProperty.bind(this);
+ this._newPropertyDestroy = this._newPropertyDestroy.bind(this);
+ this._onSelectorDone = this._onSelectorDone.bind(this);
+
+ this._create();
+}
+
+RuleEditor.prototype = {
+ get isSelectorEditable() {
+ let toolbox = this.ruleView.inspector.toolbox;
+ let trait = this.isEditable &&
+ toolbox.target.client.traits.selectorEditable &&
+ this.rule.domRule.type !== ELEMENT_STYLE &&
+ this.rule.domRule.type !== Ci.nsIDOMCSSRule.KEYFRAME_RULE;
+
+ // Do not allow editing anonymousselectors until we can
+ // detect mutations on pseudo elements in Bug 1034110.
+ return trait && !this.rule.elementStyle.element.isAnonymous;
+ },
+
+ _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;
+
+ // 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"
+ });
+ source.addEventListener("click", function() {
+ if (source.hasAttribute("unselectable")) {
+ return;
+ }
+ let rule = this.rule.domRule;
+ let evt = this.doc.createEvent("CustomEvent");
+ evt.initCustomEvent("CssRuleViewCSSLinkClicked", true, false, {
+ rule: rule,
+ });
+ this.element.dispatchEvent(evt);
+ }.bind(this));
+ let sourceLabel = this.doc.createElementNS(XUL_NS, "label");
+ sourceLabel.setAttribute("crop", "center");
+ sourceLabel.classList.add("source-link-label");
+ source.appendChild(sourceLabel);
+
+ this.updateSourceLink();
+
+ let code = createChild(this.element, "div", {
+ class: "ruleview-code"
+ });
+
+ let header = createChild(code, "div", {});
+
+ this.selectorContainer = createChild(header, "span", {
+ class: "ruleview-selectorcontainer"
+ });
+
+ this.selectorText = createChild(this.selectorContainer, "span", {
+ class: "ruleview-selector theme-fg-color3"
+ });
+
+ 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,
+ stopOnShiftTab: true,
+ stopOnTab: true,
+ stopOnReturn: true
+ });
+ }
+
+ this.openBrace = createChild(header, "span", {
+ class: "ruleview-ruleopen",
+ textContent: " {"
+ });
+
+ this.propertyList = createChild(code, "ul", {
+ class: "ruleview-propertylist"
+ });
+
+ this.populate();
+
+ this.closeBrace = createChild(code, "div", {
+ class: "ruleview-ruleclose",
+ tabindex: this.isEditable ? "0" : "-1",
+ textContent: "}"
+ });
+
+ this.element.addEventListener("contextmenu", event => {
+ try {
+ // In the sidebar we do not have this.doc.popupNode so we need to save
+ // the node ourselves.
+ this.doc.popupNode = event.explicitOriginalTarget;
+ let win = this.doc.defaultView;
+ win.focus();
+
+ this.ruleView._contextmenu.openPopupAtScreen(
+ event.screenX, event.screenY, true);
+
+ } catch(e) {
+ console.error(e);
+ }
+ }, false);
+
+ if (this.isEditable) {
+ code.addEventListener("click", () => {
+ let selection = this.doc.defaultView.getSelection();
+ if (selection.isCollapsed) {
+ this.newProperty();
+ }
+ }, false);
+
+ this.element.addEventListener("mousedown", () => {
+ this.doc.defaultView.focus();
+ }, false);
+
+ // Create a property editor when the close brace is clicked.
+ editableItem({ element: this.closeBrace }, (aElement) => {
+ this.newProperty();
+ });
+ }
+ },
+
+ updateSourceLink: function RuleEditor_updateSourceLink()
+ {
+ let sourceLabel = this.element.querySelector(".source-link-label");
+ let sourceHref = (this.rule.sheet && this.rule.sheet.href) ?
+ this.rule.sheet.href : this.rule.title;
+ let sourceLine = this.rule.ruleLine > 0 ? ":" + this.rule.ruleLine : "";
+
+ sourceLabel.setAttribute("tooltiptext", sourceHref + sourceLine);
+
+ if (this.rule.isSystem) {
+ let uaLabel = _strings.GetStringFromName("rule.userAgentStyles");
+ sourceLabel.setAttribute("value", uaLabel + " " + this.rule.title);
+
+ // 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
+ if (sourceHref === "about:PreferenceStyleSheet") {
+ sourceLabel.parentNode.setAttribute("unselectable", "true");
+ sourceLabel.setAttribute("value", uaLabel);
+ sourceLabel.removeAttribute("tooltiptext");
+ }
+ } else {
+ sourceLabel.setAttribute("value", this.rule.title);
+ if (this.rule.ruleLine == -1 && this.rule.domRule.parentStyleSheet) {
+ sourceLabel.parentNode.setAttribute("unselectable", "true");
+ }
+ }
+
+ let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
+ if (showOrig && !this.rule.isSystem && this.rule.domRule.type != ELEMENT_STYLE) {
+ this.rule.getOriginalSourceStrings().then((strings) => {
+ sourceLabel.setAttribute("value", strings.short);
+ sourceLabel.setAttribute("tooltiptext", strings.full);
+ }, console.error);
+ }
+ },
+
+ /**
+ * Update the rule editor with the contents of the rule.
+ */
+ populate: function() {
+ // Clear out existing viewers.
+ while (this.selectorText.hasChildNodes()) {
+ 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: ", "
+ });
+ }
+ let cls;
+ if (this.rule.matchedSelectors.indexOf(selector) > -1) {
+ cls = "ruleview-selector-matched";
+ } else {
+ cls = "ruleview-selector-unmatched";
+ }
+ createChild(this.selectorText, "span", {
+ class: cls,
+ textContent: selector
+ });
+ });
+ }
+
+ for (let prop of this.rule.textProps) {
+ if (!prop.editor) {
+ let editor = new TextPropertyEditor(this, prop);
+ this.propertyList.appendChild(editor.element);
+ }
+ }
+ },
+
+ /**
+ * Programatically add a new property to the rule.
+ *
+ * @param {string} aName
+ * Property name.
+ * @param {string} aValue
+ * Property value.
+ * @param {string} aPriority
+ * Property priority.
+ * @param {TextProperty} aSiblingProp
+ * Optional, property next to which the new property will be added.
+ * @return {TextProperty}
+ * The new property
+ */
+ addProperty: function(aName, aValue, aPriority, aSiblingProp) {
+ let prop = this.rule.createProperty(aName, aValue, aPriority, aSiblingProp);
+ let index = this.rule.textProps.indexOf(prop);
+ let editor = new TextPropertyEditor(this, prop);
+
+ // Insert this node before the DOM node that is currently at its new index
+ // in the property list. There is currently one less node in the DOM than
+ // in the property list, so this causes it to appear after aSiblingProp.
+ // If there is no node at its index, as is the case where this is the last
+ // node being inserted, then this behaves as appendChild.
+ this.propertyList.insertBefore(editor.element,
+ this.propertyList.children[index]);
+
+ return prop;
+ },
+
+ /**
+ * Programatically add a list of new properties to the rule. Focus the UI
+ * to the proper location after adding (either focus the value on the
+ * last property if it is empty, or create a new property and focus it).
+ *
+ * @param {Array} aProperties
+ * Array of properties, which are objects with this signature:
+ * {
+ * name: {string},
+ * value: {string},
+ * priority: {string}
+ * }
+ * @param {TextProperty} aSiblingProp
+ * Optional, the property next to which all new props should be added.
+ */
+ addProperties: function(aProperties, aSiblingProp) {
+ if (!aProperties || !aProperties.length) {
+ return;
+ }
+
+ let lastProp = aSiblingProp;
+ for (let p of aProperties) {
+ lastProp = this.addProperty(p.name, p.value, p.priority, lastProp);
+ }
+
+ // Either focus on the last value if incomplete, or start a new one.
+ if (lastProp && lastProp.value.trim() === "") {
+ lastProp.editor.valueSpan.click();
+ } else {
+ this.newProperty();
+ }
+ },
+
+ /**
+ * 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() {
+ // If we're already creating a new property, ignore this.
+ if (!this.closeBrace.hasAttribute("tabindex")) {
+ return;
+ }
+
+ // While we're editing a new property, it doesn't make sense to
+ // start a second new property editor, so disable focusing the
+ // close brace for now.
+ this.closeBrace.removeAttribute("tabindex");
+
+ this.newPropItem = createChild(this.propertyList, "li", {
+ class: "ruleview-property ruleview-newproperty",
+ });
+
+ this.newPropSpan = createChild(this.newPropItem, "span", {
+ class: "ruleview-propertyname",
+ tabindex: "0"
+ });
+
+ this.multipleAddedProperties = null;
+
+ this.editor = new InplaceEditor({
+ element: this.newPropSpan,
+ done: this._onNewProperty,
+ destroy: this._newPropertyDestroy,
+ advanceChars: ":",
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
+ popup: this.ruleView.popup
+ });
+
+ // Auto-close the input if multiple rules get pasted into new property.
+ this.editor.input.addEventListener("paste",
+ blurOnMultipleProperties, false);
+ },
+
+ /**
+ * Called when the new property input has been dismissed.
+ *
+ * @param {string} aValue
+ * The value in the editor.
+ * @param {bool} aCommit
+ * True if the value should be committed.
+ */
+ _onNewProperty: function(aValue, aCommit) {
+ if (!aValue || !aCommit) {
+ return;
+ }
+
+ // parseDeclarations allows for name-less declarations, but in the present
+ // case, we're creating a new declaration, it doesn't make sense to accept
+ // these entries
+ this.multipleAddedProperties = parseDeclarations(aValue).filter(d => d.name);
+
+ // Blur the editor field now and deal with adding declarations later when
+ // the field gets destroyed (see _newPropertyDestroy)
+ this.editor.input.blur();
+ },
+
+ /**
+ * Called when the new property editor is destroyed.
+ * This is where the properties (type TextProperty) are actually being
+ * added, since we want to wait until after the inplace editor `destroy`
+ * event has been fired to keep consistent UI state.
+ */
+ _newPropertyDestroy: function() {
+ // We're done, make the close brace focusable again.
+ this.closeBrace.setAttribute("tabindex", "0");
+
+ this.propertyList.removeChild(this.newPropItem);
+ delete this.newPropItem;
+ delete this.newPropSpan;
+
+ // If properties were added, we want to focus the proper element.
+ // If the last new property has no value, focus the value on it.
+ // Otherwise, start a new property and focus that field.
+ if (this.multipleAddedProperties && this.multipleAddedProperties.length) {
+ this.addProperties(this.multipleAddedProperties);
+ }
+ },
+
+ /**
+ * Called when the selector's inplace editor is closed.
+ * Ignores the change if the user pressed escape, otherwise
+ * commits it.
+ *
+ * @param {string} aValue
+ * The value contained in the editor.
+ * @param {boolean} aCommit
+ * True if the change should be applied.
+ */
+ _onSelectorDone: function(aValue, aCommit) {
+ if (!aCommit || this.isEditing || aValue === "" ||
+ aValue === this.rule.selectorText) {
+ return;
+ }
+
+ this.isEditing = true;
+
+ this.rule.domRule.modifySelector(aValue).then(isModified => {
+ this.isEditing = false;
+
+ if (isModified) {
+ this.ruleView.refreshPanel();
+ }
+ }).then(null, err => {
+ this.isEditing = false;
+ promiseWarn(err);
+ });
+ }
+};
+
+/**
+ * Create a TextPropertyEditor.
+ *
+ * @param {RuleEditor} aRuleEditor
+ * The rule editor that owns this TextPropertyEditor.
+ * @param {TextProperty} aProperty
+ * The text property to edit.
+ * @constructor
+ */
+function TextPropertyEditor(aRuleEditor, aProperty) {
+ this.ruleEditor = aRuleEditor;
+ this.doc = this.ruleEditor.doc;
+ this.popup = this.ruleEditor.ruleView.popup;
+ this.prop = aProperty;
+ this.prop.editor = this;
+ this.browserWindow = this.doc.defaultView.top;
+ this.removeOnRevert = this.prop.value === "";
+
+ this._onEnableClicked = this._onEnableClicked.bind(this);
+ this._onExpandClicked = this._onExpandClicked.bind(this);
+ this._onStartEditing = this._onStartEditing.bind(this);
+ this._onNameDone = this._onNameDone.bind(this);
+ this._onValueDone = this._onValueDone.bind(this);
+ this._onValidate = throttle(this._previewValue, 10, this);
+ this.update = this.update.bind(this);
+
+ this._create();
+ this.update();
+}
+
+TextPropertyEditor.prototype = {
+ /**
+ * Boolean indicating if the name or value is being currently edited.
+ */
+ get editing() {
+ return !!(this.nameSpan.inplaceEditor || this.valueSpan.inplaceEditor ||
+ this.ruleEditor.ruleView.tooltips.isEditing) || this.popup.isOpen;
+ },
+
+ /**
+ * Create the property editor's DOM.
+ */
+ _create: function() {
+ this.element = this.doc.createElementNS(HTML_NS, "li");
+ this.element.classList.add("ruleview-property");
+
+ // The enable checkbox will disable or enable the rule.
+ this.enable = createChild(this.element, "div", {
+ class: "ruleview-enableproperty theme-checkbox",
+ tabindex: "-1"
+ });
+
+ // Click to expand the computed properties of the text property.
+ this.expander = createChild(this.element, "span", {
+ class: "ruleview-expander theme-twisty"
+ });
+ this.expander.addEventListener("click", this._onExpandClicked, true);
+
+ this.nameContainer = createChild(this.element, "span", {
+ class: "ruleview-namecontainer"
+ });
+
+ // Property name, editable when focused. Property name
+ // is committed when the editor is unfocused.
+ this.nameSpan = createChild(this.nameContainer, "span", {
+ class: "ruleview-propertyname theme-fg-color5",
+ tabindex: this.ruleEditor.isEditable ? "0" : "-1",
+ });
+
+ appendText(this.nameContainer, ": ");
+
+ // Create a span that will hold the property and semicolon.
+ // Use this span to create a slightly larger click target
+ // for the value.
+ let propertyContainer = createChild(this.element, "span", {
+ class: "ruleview-propertycontainer"
+ });
+
+
+ // Property value, editable when focused. Changes to the
+ // property value are applied as they are typed, and reverted
+ // if the user presses escape.
+ this.valueSpan = createChild(propertyContainer, "span", {
+ class: "ruleview-propertyvalue theme-fg-color1",
+ tabindex: this.ruleEditor.isEditable ? "0" : "-1",
+ });
+
+ // Storing the TextProperty on the elements for easy access
+ // (for instance by the tooltip)
+ this.valueSpan.textProperty = this.prop;
+ this.nameSpan.textProperty = this.prop;
+
+ // If the value is a color property we need to put it through the parser
+ // so that colors can be coerced into the default color type. This prevents
+ // us from thinking that when colors are coerced they have been changed by
+ // the user.
+ let outputParser = this.ruleEditor.ruleView._outputParser;
+ let frag = outputParser.parseCssProperty(this.prop.name, this.prop.value);
+ let parsedValue = frag.textContent;
+
+ // Save the initial value as the last committed value,
+ // for restoring after pressing escape.
+ this.committed = { name: this.prop.name,
+ value: parsedValue,
+ priority: this.prop.priority };
+
+ appendText(propertyContainer, ";");
+
+ this.warning = createChild(this.element, "div", {
+ class: "ruleview-warning",
+ hidden: "",
+ title: CssLogic.l10n("rule.warning.title"),
+ });
+
+ // Holds the viewers for the computed properties.
+ // will be populated in |_updateComputed|.
+ this.computed = createChild(this.element, "ul", {
+ class: "ruleview-computedlist",
+ });
+
+ // Only bind event handlers if the rule is editable.
+ if (this.ruleEditor.isEditable) {
+ this.enable.addEventListener("click", this._onEnableClicked, true);
+
+ this.nameContainer.addEventListener("click", (aEvent) => {
+ // Clicks within the name shouldn't propagate any further.
+ aEvent.stopPropagation();
+ if (aEvent.target === propertyContainer) {
+ this.nameSpan.click();
+ }
+ }, false);
+
+ editableField({
+ start: this._onStartEditing,
+ element: this.nameSpan,
+ done: this._onNameDone,
+ destroy: this.update,
+ advanceChars: ':',
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
+ popup: this.popup
+ });
+
+ // Auto blur name field on multiple CSS rules get pasted in.
+ this.nameContainer.addEventListener("paste",
+ blurOnMultipleProperties, false);
+
+ propertyContainer.addEventListener("click", (aEvent) => {
+ // Clicks within the value shouldn't propagate any further.
+ aEvent.stopPropagation();
+
+ if (aEvent.target === propertyContainer) {
+ this.valueSpan.click();
+ }
+ }, false);
+
+ this.valueSpan.addEventListener("click", (event) => {
+ let target = event.target;
+
+ if (target.nodeName === "a") {
+ event.stopPropagation();
+ event.preventDefault();
+ this.browserWindow.openUILinkIn(target.href, "tab");
+ }
+ }, false);
+
+ editableField({
+ start: this._onStartEditing,
+ element: this.valueSpan,
+ done: this._onValueDone,
+ destroy: this.update,
+ validate: this._onValidate,
+ advanceChars: ';',
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
+ property: this.prop,
+ popup: this.popup
+ });
+ }
+ },
+
+ /**
+ * Get the path from which to resolve requests for this
+ * rule's stylesheet.
+ * @return {string} the stylesheet's href.
+ */
+ get sheetHref() {
+ let domRule = this.prop.rule.domRule;
+ if (domRule) {
+ return domRule.href || domRule.nodeHref;
+ }
+ },
+
+ /**
+ * Get the URI from which to resolve relative requests for
+ * this rule's stylesheet.
+ * @return {nsIURI} A URI based on the the stylesheet's href.
+ */
+ get sheetURI() {
+ if (this._sheetURI === undefined) {
+ if (this.sheetHref) {
+ this._sheetURI = IOService.newURI(this.sheetHref, null, null);
+ } else {
+ this._sheetURI = null;
+ }
+ }
+
+ return this._sheetURI;
+ },
+
+ /**
+ * Resolve a URI based on the rule stylesheet
+ * @param {string} relativePath the path to resolve
+ * @return {string} the resolved path.
+ */
+ resolveURI: function(relativePath) {
+ if (this.sheetURI) {
+ relativePath = this.sheetURI.resolve(relativePath);
+ }
+ return relativePath;
+ },
+
+ /**
+ * Check the property value to find an external resource (if any).
+ * @return {string} the URI in the property value, or null if there is no match.
+ */
+ getResourceURI: function() {
+ let val = this.prop.value;
+ let uriMatch = CSS_RESOURCE_RE.exec(val);
+ let uri = null;
+
+ if (uriMatch && uriMatch[1]) {
+ uri = uriMatch[1];
+ }
+
+ return uri;
+ },
+
+ /**
+ * Populate the span based on changes to the TextProperty.
+ */
+ update: function() {
+ if (this.ruleEditor.ruleView.isDestroyed) {
+ return;
+ }
+
+ if (this.prop.enabled) {
+ this.enable.style.removeProperty("visibility");
+ this.enable.setAttribute("checked", "");
+ } else {
+ this.enable.style.visibility = "visible";
+ this.enable.removeAttribute("checked");
+ }
+
+ this.warning.hidden = this.editing || this.isValid();
+
+ if ((this.prop.overridden || !this.prop.enabled) && !this.editing) {
+ this.element.classList.add("ruleview-overridden");
+ } else {
+ this.element.classList.remove("ruleview-overridden");
+ }
+
+ let name = this.prop.name;
+ this.nameSpan.textContent = name;
+
+ // Combine the property's value and priority into one string for
+ // the value.
+ let store = this.prop.rule.elementStyle.store;
+ let val = store.userProperties.getProperty(this.prop.rule.style, name,
+ this.prop.value);
+ if (this.prop.priority) {
+ val += " !" + this.prop.priority;
+ }
+
+ let propDirty = store.userProperties.contains(this.prop.rule.style, name);
+
+ if (propDirty) {
+ this.element.setAttribute("dirty", "");
+ } else {
+ this.element.removeAttribute("dirty");
+ }
+
+ let colorSwatchClass = "ruleview-colorswatch";
+ let bezierSwatchClass = "ruleview-bezierswatch";
+
+ let outputParser = this.ruleEditor.ruleView._outputParser;
+ let frag = outputParser.parseCssProperty(name, val, {
+ colorSwatchClass: colorSwatchClass,
+ colorClass: "ruleview-color",
+ bezierSwatchClass: bezierSwatchClass,
+ bezierClass: "ruleview-bezier",
+ defaultColorType: !propDirty,
+ urlClass: "theme-link",
+ baseURI: this.sheetURI
+ });
+ this.valueSpan.innerHTML = "";
+ this.valueSpan.appendChild(frag);
+
+ // Attach the color picker tooltip to the color swatches
+ this._colorSwatchSpans = this.valueSpan.querySelectorAll("." + colorSwatchClass);
+ if (this.ruleEditor.isEditable) {
+ for (let span of this._colorSwatchSpans) {
+ // Capture the original declaration value to be able to revert later
+ let originalValue = this.valueSpan.textContent;
+ // Adding this swatch to the list of swatches our colorpicker knows about
+ this.ruleEditor.ruleView.tooltips.colorPicker.addSwatch(span, {
+ onPreview: () => this._previewValue(this.valueSpan.textContent),
+ onCommit: () => this._applyNewValue(this.valueSpan.textContent),
+ onRevert: () => this._applyNewValue(originalValue, false)
+ });
+ }
+ }
+
+ // Attach the cubic-bezier tooltip to the bezier swatches
+ this._bezierSwatchSpans = this.valueSpan.querySelectorAll("." + bezierSwatchClass);
+ if (this.ruleEditor.isEditable) {
+ for (let span of this._bezierSwatchSpans) {
+ // Capture the original declaration value to be able to revert later
+ let originalValue = this.valueSpan.textContent;
+ // Adding this swatch to the list of swatches our colorpicker knows about
+ this.ruleEditor.ruleView.tooltips.cubicBezier.addSwatch(span, {
+ onPreview: () => this._previewValue(this.valueSpan.textContent),
+ onCommit: () => this._applyNewValue(this.valueSpan.textContent),
+ onRevert: () => this._applyNewValue(originalValue, false)
+ });
+ }
+ }
+
+ // Populate the computed styles.
+ this._updateComputed();
+ },
+
+ _onStartEditing: function() {
+ this.element.classList.remove("ruleview-overridden");
+ this._previewValue(this.prop.value);
+ },
+
+ /**
+ * Populate the list of computed styles.
+ */
+ _updateComputed: function () {
+ // Clear out existing viewers.
+ while (this.computed.hasChildNodes()) {
+ this.computed.removeChild(this.computed.lastChild);
+ }
+
+ let showExpander = false;
+ for each (let computed in this.prop.computed) {
+ // Don't bother to duplicate information already
+ // shown in the text property.
+ if (computed.name === this.prop.name) {
+ continue;
+ }
+
+ showExpander = true;
+
+ let li = createChild(this.computed, "li", {
+ class: "ruleview-computed"
+ });
+
+ if (computed.overridden) {
+ li.classList.add("ruleview-overridden");
+ }
+
+ createChild(li, "span", {
+ class: "ruleview-propertyname theme-fg-color5",
+ textContent: computed.name
+ });
+ appendText(li, ": ");
+
+ let outputParser = this.ruleEditor.ruleView._outputParser;
+ let frag = outputParser.parseCssProperty(
+ computed.name, computed.value, {
+ colorSwatchClass: "ruleview-colorswatch",
+ urlClass: "theme-link",
+ baseURI: this.sheetURI
+ }
+ );
+
+ createChild(li, "span", {
+ class: "ruleview-propertyvalue theme-fg-color1",
+ child: frag
+ });
+
+ appendText(li, ";");
+ }
+
+ // Show or hide the expander as needed.
+ if (showExpander) {
+ this.expander.style.visibility = "visible";
+ } else {
+ this.expander.style.visibility = "hidden";
+ }
+ },
+
+ /**
+ * Handles clicks on the disabled property.
+ */
+ _onEnableClicked: function(aEvent) {
+ let checked = this.enable.hasAttribute("checked");
+ if (checked) {
+ this.enable.removeAttribute("checked");
+ } else {
+ this.enable.setAttribute("checked", "");
+ }
+ this.prop.setEnabled(!checked);
+ aEvent.stopPropagation();
+ },
+
+ /**
+ * Handles clicks on the computed property expander.
+ */
+ _onExpandClicked: function(aEvent) {
+ this.computed.classList.toggle("styleinspector-open");
+ if (this.computed.classList.contains("styleinspector-open")) {
+ this.expander.setAttribute("open", "true");
+ } else {
+ this.expander.removeAttribute("open");
+ }
+ aEvent.stopPropagation();
+ },
+
+ /**
+ * Called when the property name's inplace editor is closed.
+ * Ignores the change if the user pressed escape, otherwise
+ * commits it.
+ *
+ * @param {string} aValue
+ * The value contained in the editor.
+ * @param {boolean} aCommit
+ * True if the change should be applied.
+ */
+ _onNameDone: function(aValue, aCommit) {
+ if (aCommit && !this.ruleEditor.isEditing) {
+ // Unlike the value editor, if a name is empty the entire property
+ // should always be removed.
+ if (aValue.trim() === "") {
+ this.remove();
+ } else {
+ // Adding multiple rules inside of name field overwrites the current
+ // property with the first, then adds any more onto the property list.
+ let properties = parseDeclarations(aValue);
+
+ if (properties.length) {
+ this.prop.setName(properties[0].name);
+ if (properties.length > 1) {
+ this.prop.setValue(properties[0].value, properties[0].priority);
+ this.ruleEditor.addProperties(properties.slice(1), this.prop);
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Remove property from style and the editors from DOM.
+ * Begin editing next available property.
+ */
+ remove: function() {
+ if (this._colorSwatchSpans && this._colorSwatchSpans.length) {
+ for (let span of this._colorSwatchSpans) {
+ this.ruleEditor.ruleView.tooltips.colorPicker.removeSwatch(span);
+ }
+ }
+
+ this.element.parentNode.removeChild(this.element);
+ this.ruleEditor.rule.editClosestTextProperty(this.prop);
+ this.nameSpan.textProperty = null;
+ this.valueSpan.textProperty = null;
+ this.prop.remove();
+ },
+
+ /**
+ * Called when a value editor closes. If the user pressed escape,
+ * revert to the value this property had before editing.
+ *
+ * @param {string} aValue
+ * The value contained in the editor.
+ * @param {bool} aCommit
+ * True if the change should be applied.
+ */
+ _onValueDone: function(aValue, aCommit) {
+ if (!aCommit && !this.ruleEditor.isEditing) {
+ // A new property should be removed when escape is pressed.
+ if (this.removeOnRevert) {
+ this.remove();
+ } else {
+ this.prop.setValue(this.committed.value, this.committed.priority);
+ }
+ return;
+ }
+
+ let {propertiesToAdd,firstValue} = this._getValueAndExtraProperties(aValue);
+
+ // First, set this property value (common case, only modified a property)
+ let val = parseSingleValue(firstValue);
+
+ this.prop.setValue(val.value, val.priority);
+ this.removeOnRevert = false;
+ this.committed.value = this.prop.value;
+ this.committed.priority = this.prop.priority;
+
+ // If needed, add any new properties after this.prop.
+ this.ruleEditor.addProperties(propertiesToAdd, this.prop);
+
+ // If the name or value is not actively being edited, and the value is
+ // empty, then remove the whole property.
+ // A timeout is used here to accurately check the state, since the inplace
+ // editor `done` and `destroy` events fire before the next editor
+ // is focused.
+ if (val.value.trim() === "") {
+ setTimeout(() => {
+ if (!this.editing) {
+ this.remove();
+ }
+ }, 0);
+ }
+ },
+
+ /**
+ * Parse a value string and break it into pieces, starting with the
+ * first value, and into an array of additional properties (if any).
+ *
+ * Example: Calling with "red; width: 100px" would return
+ * { firstValue: "red", propertiesToAdd: [{ name: "width", value: "100px" }] }
+ *
+ * @param {string} aValue
+ * The string to parse
+ * @return {object} An object with the following properties:
+ * firstValue: A string containing a simple value, like
+ * "red" or "100px!important"
+ * propertiesToAdd: An array with additional properties, following the
+ * parseDeclarations format of {name,value,priority}
+ */
+ _getValueAndExtraProperties: function(aValue) {
+ // The inplace editor will prevent manual typing of multiple properties,
+ // but we need to deal with the case during a paste event.
+ // Adding multiple properties inside of value editor sets value with the
+ // first, then adds any more onto the property list (below this property).
+ let firstValue = aValue;
+ let propertiesToAdd = [];
+
+ let properties = parseDeclarations(aValue);
+
+ // Check to see if the input string can be parsed as multiple properties
+ if (properties.length) {
+ // Get the first property value (if any), and any remaining properties (if any)
+ if (!properties[0].name && properties[0].value) {
+ firstValue = properties[0].value;
+ propertiesToAdd = properties.slice(1);
+ }
+ // In some cases, the value could be a property:value pair itself.
+ // Join them as one value string and append potentially following properties
+ else if (properties[0].name && properties[0].value) {
+ firstValue = properties[0].name + ": " + properties[0].value;
+ propertiesToAdd = properties.slice(1);
+ }
+ }
+
+ return {
+ propertiesToAdd: propertiesToAdd,
+ firstValue: firstValue
+ };
+ },
+
+ /**
+ * Apply a new value.
+ *
+ * @param {String} aValue
+ * The value to replace.
+ * @param {Boolean} markChanged=true
+ * Set this to false if you need to prevent the property from being
+ * marked as changed e.g. tooltips do this when <escape> is pressed
+ * in order to revert the value.
+ */
+ _applyNewValue: function(aValue, markChanged=true) {
+ let val = parseSingleValue(aValue);
+
+ if (!markChanged) {
+ let store = this.prop.rule.elementStyle.store;
+ this.prop.editor.committed.value = aValue;
+ store.userProperties.setProperty(this.prop.rule.style,
+ this.prop.rule.name, aValue);
+ }
+
+ this.prop.setValue(val.value, val.priority, markChanged);
+ this.removeOnRevert = false;
+ this.committed.value = this.prop.value;
+ this.committed.priority = this.prop.priority;
+ },
+
+ /**
+ * Live preview this property, without committing changes.
+ * @param {string} aValue The value to set the current property to.
+ */
+ _previewValue: function(aValue) {
+ // Since function call is throttled, we need to make sure we are still
+ // editing, and any selector modifications have been completed
+ if (!this.editing || this.ruleEditor.isEditing) {
+ return;
+ }
+
+ let val = parseSingleValue(aValue);
+ this.ruleEditor.rule.previewPropertyValue(this.prop, val.value, val.priority);
+ },
+
+ /**
+ * Validate this property. Does it make sense for this value to be assigned
+ * to this property name? This does not apply the property value
+ *
+ * @return {bool} true if the property value is valid, false otherwise.
+ */
+ isValid: function() {
+ return domUtils.cssPropertyIsValid(this.prop.name, this.prop.value);
+ }
+};
+
+/**
+ * Store of CSSStyleDeclarations mapped to properties that have been changed by
+ * the user.
+ */
+function UserProperties() {
+ this.map = new Map();
+}
+
+UserProperties.prototype = {
+ /**
+ * Get a named property for a given CSSStyleDeclaration.
+ *
+ * @param {CSSStyleDeclaration} aStyle
+ * The CSSStyleDeclaration against which the property is mapped.
+ * @param {string} aName
+ * The name of the property to get.
+ * @param {string} aDefault
+ * Default value.
+ * @return {string}
+ * The property value if it has previously been set by the user, null
+ * otherwise.
+ */
+ getProperty: function(aStyle, aName, aDefault) {
+ let key = this.getKey(aStyle);
+ let entry = this.map.get(key, null);
+
+ if (entry && aName in entry) {
+ return entry[aName];
+ }
+ return aDefault;
+ },
+
+ /**
+ * Set a named property for a given CSSStyleDeclaration.
+ *
+ * @param {CSSStyleDeclaration} aStyle
+ * The CSSStyleDeclaration against which the property is to be mapped.
+ * @param {String} aName
+ * The name of the property to set.
+ * @param {String} aUserValue
+ * The value of the property to set.
+ */
+ setProperty: function(aStyle, aName, aUserValue) {
+ let key = this.getKey(aStyle, aName);
+ let entry = this.map.get(key, null);
+
+ if (entry) {
+ entry[aName] = aUserValue;
+ } else {
+ let props = {};
+ props[aName] = aUserValue;
+ this.map.set(key, props);
+ }
+ },
+
+ /**
+ * Check whether a named property for a given CSSStyleDeclaration is stored.
+ *
+ * @param {CSSStyleDeclaration} aStyle
+ * The CSSStyleDeclaration against which the property would be mapped.
+ * @param {String} aName
+ * The name of the property to check.
+ */
+ contains: function(aStyle, aName) {
+ let key = this.getKey(aStyle, aName);
+ let entry = this.map.get(key, null);
+ return !!entry && aName in entry;
+ },
+
+ getKey: function(aStyle, aName) {
+ return aStyle.actorID + ":" + aName;
+ },
+
+ clear: function() {
+ this.map.clear();
+ }
+};
+
+/**
+ * Helper functions
+ */
+
+/**
+ * Create a child element with a set of attributes.
+ *
+ * @param {Element} aParent
+ * The parent node.
+ * @param {string} aTag
+ * The tag name.
+ * @param {object} aAttributes
+ * A set of attributes to set on the node.
+ */
+function createChild(aParent, aTag, aAttributes) {
+ let elt = aParent.ownerDocument.createElementNS(HTML_NS, aTag);
+ for (let attr in aAttributes) {
+ if (aAttributes.hasOwnProperty(attr)) {
+ if (attr === "textContent") {
+ elt.textContent = aAttributes[attr];
+ } else if(attr === "child") {
+ elt.appendChild(aAttributes[attr]);
+ } else {
+ elt.setAttribute(attr, aAttributes[attr]);
+ }
+ }
+ }
+ aParent.appendChild(elt);
+ return elt;
+}
+
+function createMenuItem(aMenu, aAttributes) {
+ let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem");
+
+ item.setAttribute("label", _strings.GetStringFromName(aAttributes.label));
+ item.setAttribute("accesskey", _strings.GetStringFromName(aAttributes.accesskey));
+ item.addEventListener("command", aAttributes.command);
+
+ aMenu.appendChild(item);
+
+ return item;
+}
+
+function setTimeout() {
+ let window = Services.appShell.hiddenDOMWindow;
+ return window.setTimeout.apply(window, arguments);
+}
+
+function clearTimeout() {
+ let window = Services.appShell.hiddenDOMWindow;
+ return window.clearTimeout.apply(window, arguments);
+}
+
+function throttle(func, wait, scope) {
+ var timer = null;
+ return function() {
+ if(timer) {
+ clearTimeout(timer);
+ }
+ var args = arguments;
+ timer = setTimeout(function() {
+ timer = null;
+ func.apply(scope, args);
+ }, wait);
+ };
+}
+
+/**
+ * Event handler that causes a blur on the target if the input has
+ * multiple CSS properties as the value.
+ */
+function blurOnMultipleProperties(e) {
+ setTimeout(() => {
+ let props = parseDeclarations(e.target.value);
+ if (props.length > 1) {
+ e.target.blur();
+ }
+ }, 0);
+}
+
+/**
+ * Append a text node to an element.
+ */
+function appendText(aParent, aText) {
+ aParent.appendChild(aParent.ownerDocument.createTextNode(aText));
+}
+
+/**
+ * Walk up the DOM from a given node until a parent property holder is found.
+ * For elements inside the computed property list, the non-computed parent
+ * property holder will be returned
+ * @param {DOMNode} node The node to start from
+ * @return {DOMNode} The parent property holder node, or null if not found
+ */
+function getParentTextPropertyHolder(node) {
+ while (true) {
+ if (!node || !node.classList) {
+ return null;
+ }
+ if (node.classList.contains("ruleview-property")) {
+ return node;
+ }
+ node = node.parentNode;
+ }
+}
+
+/**
+ * For any given node, find the TextProperty it is in if any
+ * @param {DOMNode} node The node to start from
+ * @return {TextProperty}
+ */
+function getParentTextProperty(node) {
+ let parent = getParentTextPropertyHolder(node);
+ if (!parent) {
+ return null;
+ }
+
+ let propValue = parent.querySelector(".ruleview-propertyvalue");
+ if (!propValue) {
+ return null;
+ }
+
+ return propValue.textProperty;
+}
+
+/**
+ * Walker up the DOM from a given node until a parent property holder is found,
+ * and return the textContent for the name and value nodes.
+ * Stops at the first property found, so if node is inside the computed property
+ * list, the computed property will be returned
+ * @param {DOMNode} node The node to start from
+ * @return {Object} {name, value}
+ */
+function getPropertyNameAndValue(node) {
+ while (true) {
+ if (!node || !node.classList) {
+ return null;
+ }
+ // Check first for ruleview-computed since it's the deepest
+ if (node.classList.contains("ruleview-computed") ||
+ node.classList.contains("ruleview-property")) {
+ return {
+ name: node.querySelector(".ruleview-propertyname").textContent,
+ value: node.querySelector(".ruleview-propertyvalue").textContent
+ };
+ }
+ node = node.parentNode;
+ }
+}
+
+XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
+ return Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+});
+
+XPCOMUtils.defineLazyGetter(this, "_strings", function() {
+ return Services.strings.createBundle(
+ "chrome://global/locale/devtools/styleinspector.properties");
+});
+
+XPCOMUtils.defineLazyGetter(this, "domUtils", function() {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+
+loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup);
diff --git a/toolkit/devtools/styleinspector/ruleview.css b/toolkit/devtools/styleinspector/ruleview.css
new file mode 100644
index 000000000..2f86a8839
--- /dev/null
+++ b/toolkit/devtools/styleinspector/ruleview.css
@@ -0,0 +1,61 @@
+/* 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/. */
+
+#root {
+ display: -moz-box;
+}
+
+.ruleview {
+ overflow: auto;
+ -moz-user-select: text;
+}
+
+.ruleview-code {
+ direction: ltr;
+}
+
+.ruleview-property:not(:hover) > .ruleview-enableproperty {
+ pointer-events: none;
+}
+
+.ruleview-namecontainer,
+.ruleview-selectorcontainer {
+ cursor: text;
+}
+
+.ruleview-propertycontainer {
+ cursor: text;
+ padding-right: 15px;
+}
+
+.ruleview-propertycontainer a {
+ cursor: pointer;
+}
+
+.ruleview-computedlist:not(.styleinspector-open),
+.ruleview-warning[hidden] {
+ display: none;
+}
+
+.ruleview-expandable-container {
+ display: none;
+}
+
+.show-expandable-container + .ruleview-expandable-container {
+ display: block;
+}
+
+.ruleview .ruleview-expander {
+ vertical-align: middle;
+}
+
+.ruleview-header {
+ vertical-align: middle;
+ min-height: 1.5em;
+ line-height: 1.5em;
+}
+
+.ruleview-header.ruleview-expandable-header {
+ cursor: pointer;
+}
diff --git a/toolkit/devtools/styleinspector/style-inspector-overlays.js b/toolkit/devtools/styleinspector/style-inspector-overlays.js
new file mode 100644
index 000000000..6b38ae8cf
--- /dev/null
+++ b/toolkit/devtools/styleinspector/style-inspector-overlays.js
@@ -0,0 +1,405 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// The style-inspector overlays are:
+// - tooltips that appear when hovering over property values
+// - editor tooltips that appear when clicking color swatches, etc.
+// - in-content highlighters that appear when hovering over property values
+// - etc.
+
+const {Cc, Ci, Cu} = require("chrome");
+const {
+ Tooltip,
+ SwatchColorPickerTooltip,
+ SwatchCubicBezierTooltip
+} = require("devtools/shared/widgets/Tooltip");
+const {CssLogic} = require("devtools/styleinspector/css-logic");
+const {Promise:promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const PREF_IMAGE_TOOLTIP_SIZE = "devtools.inspector.imagePreviewTooltipSize";
+
+// Types of existing tooltips
+const TOOLTIP_IMAGE_TYPE = "image";
+const TOOLTIP_FONTFAMILY_TYPE = "font-family";
+
+// Types of existing highlighters
+const HIGHLIGHTER_TRANSFORM_TYPE = "CssTransformHighlighter";
+const HIGHLIGHTER_SELECTOR_TYPE = "SelectorHighlighter";
+const HIGHLIGHTER_TYPES = [
+ HIGHLIGHTER_TRANSFORM_TYPE,
+ HIGHLIGHTER_SELECTOR_TYPE
+];
+
+// Types of nodes in the rule/computed-view
+const VIEW_NODE_SELECTOR_TYPE = exports.VIEW_NODE_SELECTOR_TYPE = 1;
+const VIEW_NODE_PROPERTY_TYPE = exports.VIEW_NODE_PROPERTY_TYPE = 2;
+const VIEW_NODE_VALUE_TYPE = exports.VIEW_NODE_VALUE_TYPE = 3;
+const VIEW_NODE_IMAGE_URL_TYPE = exports.VIEW_NODE_IMAGE_URL_TYPE = 4;
+
+/**
+ * Manages all highlighters in the style-inspector.
+ * @param {CssRuleView|CssHtmlTree} view Either the rule-view or computed-view
+ * panel
+ */
+function HighlightersOverlay(view) {
+ this.view = view;
+
+ let {CssRuleView} = require("devtools/styleinspector/rule-view");
+ this.isRuleView = view instanceof CssRuleView;
+
+ this.highlighterUtils = this.view.inspector.toolbox.highlighterUtils;
+
+ this._onMouseMove = this._onMouseMove.bind(this);
+ this._onMouseLeave = this._onMouseLeave.bind(this);
+
+ this.promises = {};
+ this.highlighters = {};
+
+ // Only initialize the overlay if at least one of the highlighter types is
+ // supported
+ this.supportsHighlighters = this.highlighterUtils.supportsCustomHighlighters();
+}
+
+exports.HighlightersOverlay = HighlightersOverlay;
+
+HighlightersOverlay.prototype = {
+ /**
+ * Add the highlighters overlay to the view. This will start tracking mouse
+ * movements and display highlighters when needed
+ */
+ addToView: function() {
+ if (!this.supportsHighlighters || this._isStarted || this._isDestroyed) {
+ return;
+ }
+
+ let el = this.view.element;
+ el.addEventListener("mousemove", this._onMouseMove, false);
+ el.addEventListener("mouseleave", this._onMouseLeave, false);
+
+ this._isStarted = true;
+ },
+
+ /**
+ * Remove the overlay from the current view. This will stop tracking mouse
+ * movement and showing highlighters
+ */
+ removeFromView: function() {
+ if (!this.supportsHighlighters || !this._isStarted || this._isDestroyed) {
+ return;
+ }
+
+ this._hideCurrent();
+
+ let el = this.view.element;
+ el.removeEventListener("mousemove", this._onMouseMove, false);
+ el.removeEventListener("mouseleave", this._onMouseLeave, false);
+
+ this._isStarted = false;
+ },
+
+ _onMouseMove: function(event) {
+ // Bail out if the target is the same as for the last mousemove
+ if (event.target === this._lastHovered) {
+ return;
+ }
+
+ // Only one highlighter can be displayed at a time, hide the currently shown
+ this._hideCurrent();
+
+ this._lastHovered = event.target;
+
+ let nodeInfo = this.view.getNodeInfo(event.target);
+ if (!nodeInfo) {
+ return;
+ }
+
+ // Choose the type of highlighter required for the hovered node
+ let type, options;
+ if (this._isRuleViewTransform(nodeInfo) ||
+ this._isComputedViewTransform(nodeInfo)) {
+ type = HIGHLIGHTER_TRANSFORM_TYPE;
+ } else if (nodeInfo.type === VIEW_NODE_SELECTOR_TYPE) {
+ type = HIGHLIGHTER_SELECTOR_TYPE;
+ options = {
+ selector: nodeInfo.value,
+ hideInfoBar: true,
+ showOnly: "border",
+ region: "border"
+ };
+ }
+
+ if (type) {
+ this.highlighterShown = type;
+ let node = this.view.inspector.selection.nodeFront;
+ this._getHighlighter(type).then(highlighter => {
+ highlighter.show(node, options);
+ });
+ }
+ },
+
+ _onMouseLeave: function(event) {
+ this._lastHovered = null;
+ this._hideCurrent();
+ },
+
+ /**
+ * Is the current hovered node a css transform property value in the rule-view
+ * @param {Object} nodeInfo
+ * @return {Boolean}
+ */
+ _isRuleViewTransform: function(nodeInfo) {
+ let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE &&
+ nodeInfo.value.property === "transform";
+ let isEnabled = nodeInfo.value.enabled &&
+ !nodeInfo.value.overridden &&
+ !nodeInfo.value.pseudoElement;
+ return this.isRuleView && isTransform && isEnabled;
+ },
+
+ /**
+ * Is the current hovered node a css transform property value in the
+ * computed-view
+ * @param {Object} nodeInfo
+ * @return {Boolean}
+ */
+ _isComputedViewTransform: function(nodeInfo) {
+ let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE &&
+ nodeInfo.value.property === "transform";
+ return !this.isRuleView && isTransform;
+ },
+
+ /**
+ * Hide the currently shown highlighter
+ */
+ _hideCurrent: function() {
+ if (this.highlighterShown) {
+ this._getHighlighter(this.highlighterShown).then(highlighter => {
+ // For some reason, the call to highlighter.hide doesn't always return a
+ // promise. This causes some tests to fail when trying to install a
+ // rejection handler on the result of the call. To avoid this, check
+ // whether the result is truthy before installing the handler.
+ let promise = highlighter.hide();
+ if (promise) {
+ promise.then(null, Cu.reportError);
+ }
+ this.highlighterShown = null;
+ });
+ }
+ },
+
+ /**
+ * Get a highlighter front given a type. It will only be initialized once
+ * @param {String} type The highlighter type. One of this.highlighters
+ * @return a promise that resolves to the highlighter
+ */
+ _getHighlighter: function(type) {
+ let utils = this.highlighterUtils;
+
+ if (this.promises[type]) {
+ return this.promises[type];
+ }
+
+ return this.promises[type] = utils.getHighlighterByType(type).then(highlighter => {
+ this.highlighters[type] = highlighter;
+ return highlighter;
+ });
+ },
+
+ /**
+ * Destroy this overlay instance, removing it from the view and destroying
+ * all initialized highlighters
+ */
+ destroy: function() {
+ this.removeFromView();
+
+ for (let type in this.highlighters) {
+ if (this.highlighters[type]) {
+ this.highlighters[type].finalize();
+ this.highlighters[type] = null;
+ }
+ }
+
+ this.promises = null;
+ this.view = null;
+ this.highlighterUtils = null;
+
+ this._isDestroyed = true;
+ }
+};
+
+/**
+ * Manages all tooltips in the style-inspector.
+ * @param {CssRuleView|CssHtmlTree} view Either the rule-view or computed-view
+ * panel
+ */
+function TooltipsOverlay(view) {
+ this.view = view;
+
+ let {CssRuleView} = require("devtools/styleinspector/rule-view");
+ this.isRuleView = view instanceof CssRuleView;
+
+ this._onNewSelection = this._onNewSelection.bind(this);
+ this.view.inspector.selection.on("new-node-front", this._onNewSelection);
+}
+
+exports.TooltipsOverlay = TooltipsOverlay;
+
+TooltipsOverlay.prototype = {
+ get isEditing() {
+ return this.colorPicker.tooltip.isShown() ||
+ this.colorPicker.eyedropperOpen ||
+ this.cubicBezier.tooltip.isShown();
+ },
+
+ /**
+ * Add the tooltips overlay to the view. This will start tracking mouse
+ * movements and display tooltips when needed
+ */
+ addToView: function() {
+ if (this._isStarted || this._isDestroyed) {
+ return;
+ }
+
+ // Image, fonts, ... preview tooltip
+ this.previewTooltip = new Tooltip(this.view.inspector.panelDoc);
+ this.previewTooltip.startTogglingOnHover(this.view.element,
+ this._onPreviewTooltipTargetHover.bind(this));
+
+ if (this.isRuleView) {
+ // Color picker tooltip
+ this.colorPicker = new SwatchColorPickerTooltip(this.view.inspector.panelDoc);
+ // Cubic bezier tooltip
+ this.cubicBezier = new SwatchCubicBezierTooltip(this.view.inspector.panelDoc);
+ }
+
+ this._isStarted = true;
+ },
+
+ /**
+ * Remove the tooltips overlay from the view. This will stop tracking mouse
+ * movements and displaying tooltips
+ */
+ removeFromView: function() {
+ if (!this._isStarted || this._isDestroyed) {
+ return;
+ }
+
+ this.previewTooltip.stopTogglingOnHover(this.view.element);
+ this.previewTooltip.destroy();
+
+ if (this.colorPicker) {
+ this.colorPicker.destroy();
+ }
+
+ if (this.cubicBezier) {
+ this.cubicBezier.destroy();
+ }
+
+ this._isStarted = false;
+ },
+
+ /**
+ * Given a hovered node info, find out which type of tooltip should be shown,
+ * if any
+ * @param {Object} nodeInfo
+ * @return {String} The tooltip type to be shown, or null
+ */
+ _getTooltipType: function({type, value:prop}) {
+ let tooltipType = null;
+ let inspector = this.view.inspector;
+
+ // Image preview tooltip
+ if (type === VIEW_NODE_IMAGE_URL_TYPE && inspector.hasUrlToImageDataResolver) {
+ tooltipType = TOOLTIP_IMAGE_TYPE;
+ }
+
+ // Font preview tooltip
+ if (type === VIEW_NODE_VALUE_TYPE && prop.property === "font-family") {
+ let value = prop.value.toLowerCase();
+ if (value !== "inherit" && value !== "unset" && value !== "initial") {
+ tooltipType = TOOLTIP_FONTFAMILY_TYPE;
+ }
+ }
+
+ return tooltipType;
+ },
+
+ /**
+ * Executed by the tooltip when the pointer hovers over an element of the view.
+ * Used to decide whether the tooltip should be shown or not and to actually
+ * put content in it.
+ * Checks if the hovered target is a css value we support tooltips for.
+ * @param {DOMNode} target The currently hovered node
+ */
+ _onPreviewTooltipTargetHover: function(target) {
+ let nodeInfo = this.view.getNodeInfo(target);
+ if (!nodeInfo) {
+ // The hovered node isn't something we care about
+ return promise.reject();
+ }
+
+ let type = this._getTooltipType(nodeInfo);
+ if (!type) {
+ // There is no tooltip type defined for the hovered node
+ return promise.reject();
+ }
+
+ if (this.isRuleView && this.colorPicker.tooltip.isShown()) {
+ this.colorPicker.revert();
+ this.colorPicker.hide();
+ }
+
+ if (this.isRuleView && this.cubicBezier.tooltip.isShown()) {
+ this.cubicBezier.revert();
+ this.cubicBezier.hide();
+ }
+
+ let inspector = this.view.inspector;
+
+ if (type === TOOLTIP_IMAGE_TYPE) {
+ let dim = Services.prefs.getIntPref(PREF_IMAGE_TOOLTIP_SIZE);
+ // nodeInfo contains an absolute uri
+ let uri = nodeInfo.value.url;
+ return this.previewTooltip.setRelativeImageContent(uri,
+ inspector.inspector, dim);
+ }
+
+ if (type === TOOLTIP_FONTFAMILY_TYPE) {
+ return this.previewTooltip.setFontFamilyContent(nodeInfo.value.value,
+ inspector.selection.nodeFront);
+ }
+ },
+
+ _onNewSelection: function() {
+ if (this.previewTooltip) {
+ this.previewTooltip.hide();
+ }
+
+ if (this.colorPicker) {
+ this.colorPicker.hide();
+ }
+
+ if (this.cubicBezier) {
+ this.cubicBezier.hide();
+ }
+ },
+
+ /**
+ * Destroy this overlay instance, removing it from the view
+ */
+ destroy: function() {
+ this.removeFromView();
+
+ this.view.inspector.selection.off("new-node-front", this._onNewSelection);
+ this.view = null;
+
+ this._isDestroyed = true;
+ }
+};
diff --git a/toolkit/devtools/styleinspector/style-inspector.js b/toolkit/devtools/styleinspector/style-inspector.js
new file mode 100644
index 000000000..1a63111e2
--- /dev/null
+++ b/toolkit/devtools/styleinspector/style-inspector.js
@@ -0,0 +1,251 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+const {Cc, Cu, Ci} = require("chrome");
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+const {Tools} = require("main");
+Cu.import("resource://gre/modules/Services.jsm");
+const {PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils");
+
+loader.lazyGetter(this, "gDevTools", () => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools);
+loader.lazyGetter(this, "RuleView", () => require("devtools/styleinspector/rule-view"));
+loader.lazyGetter(this, "ComputedView", () => require("devtools/styleinspector/computed-view"));
+loader.lazyGetter(this, "_strings", () => Services.strings
+ .createBundle("chrome://global/locale/devtools/styleinspector.properties"));
+
+// This module doesn't currently export any symbols directly, it only
+// registers inspector tools.
+
+function RuleViewTool(inspector, window, iframe) {
+ this.inspector = inspector;
+ this.doc = window.document;
+
+ this.view = new RuleView.CssRuleView(inspector, this.doc);
+ this.doc.documentElement.appendChild(this.view.element);
+
+ this.onLinkClicked = this.onLinkClicked.bind(this);
+ this.onSelected = this.onSelected.bind(this);
+ this.refresh = this.refresh.bind(this);
+ this.clearUserProperties = this.clearUserProperties.bind(this);
+ this.onPropertyChanged = this.onPropertyChanged.bind(this);
+ this.onViewRefreshed = this.onViewRefreshed.bind(this);
+ this.onPanelSelected = this.onPanelSelected.bind(this);
+
+ this.view.element.addEventListener("CssRuleViewChanged", this.onPropertyChanged);
+ this.view.element.addEventListener("CssRuleViewRefreshed", this.onViewRefreshed);
+ this.view.element.addEventListener("CssRuleViewCSSLinkClicked", this.onLinkClicked);
+
+ this.inspector.selection.on("detached", this.onSelected);
+ this.inspector.selection.on("new-node-front", this.onSelected);
+ this.inspector.on("layout-change", this.refresh);
+ this.inspector.selection.on("pseudoclass", this.refresh);
+ this.inspector.target.on("navigate", this.clearUserProperties);
+ this.inspector.sidebar.on("ruleview-selected", this.onPanelSelected);
+
+ this.onSelected();
+}
+
+
+RuleViewTool.prototype = {
+ isSidebarActive: function() {
+ if (!this.view) {
+ return false;
+ }
+ return this.inspector.sidebar.getCurrentTabID() == "ruleview";
+ },
+
+ onSelected: function(event) {
+ // Ignore the event if the view has been destroyed, or if it's inactive.
+ // But only if the current selection isn't null. If it's been set to null,
+ // let the update go through as this is needed to empty the view on navigation.
+ if (!this.view) {
+ return;
+ }
+
+ let isInactive = !this.isSidebarActive() && this.inspector.selection.nodeFront;
+ if (isInactive) {
+ return;
+ }
+
+ this.view.setPageStyle(this.inspector.pageStyle);
+
+ if (!this.inspector.selection.isConnected() ||
+ !this.inspector.selection.isElementNode()) {
+ this.view.selectElement(null);
+ return;
+ }
+
+ if (!event || event == "new-node-front") {
+ let done = this.inspector.updating("rule-view");
+ this.view.selectElement(this.inspector.selection.nodeFront).then(done, done);
+ }
+ },
+
+ refresh: function() {
+ if (this.isSidebarActive()) {
+ this.view.refreshPanel();
+ }
+ },
+
+ clearUserProperties: function() {
+ if (this.view && this.view.store && this.view.store.userProperties) {
+ this.view.store.userProperties.clear();
+ }
+ },
+
+ onPanelSelected: function() {
+ if (this.inspector.selection.nodeFront === this.view.viewedElement) {
+ this.refresh();
+ } else {
+ this.onSelected();
+ }
+ },
+
+ onLinkClicked: function(event) {
+ let rule = event.detail.rule;
+ let sheet = rule.parentStyleSheet;
+
+ // Chrome stylesheets are not listed in the style editor, so show
+ // these sheets in the view source window instead.
+ if (!sheet || sheet.isSystem) {
+ let contentDoc = this.inspector.selection.document;
+ let viewSourceUtils = this.inspector.viewSourceUtils;
+ let href = rule.nodeHref || rule.href;
+ viewSourceUtils.viewSource(href, null, contentDoc, rule.line || 0);
+ return;
+ }
+
+ let location = promise.resolve(rule.location);
+ if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
+ location = rule.getOriginalLocation();
+ }
+ location.then(({ source, href, line, column }) => {
+ let target = this.inspector.target;
+ if (Tools.styleEditor.isTargetSupported(target)) {
+ gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
+ let sheet = source || href;
+ toolbox.getCurrentPanel().selectStyleSheet(sheet, line, column);
+ });
+ }
+ return;
+ })
+ },
+
+ onPropertyChanged: function() {
+ this.inspector.markDirty();
+ },
+
+ onViewRefreshed: function() {
+ this.inspector.emit("rule-view-refreshed");
+ },
+
+ destroy: function() {
+ this.inspector.off("layout-change", this.refresh);
+ this.inspector.selection.off("pseudoclass", this.refresh);
+ this.inspector.selection.off("new-node-front", this.onSelected);
+ this.inspector.target.off("navigate", this.clearUserProperties);
+ this.inspector.sidebar.off("ruleview-selected", this.onPanelSelected);
+
+ this.view.element.removeEventListener("CssRuleViewCSSLinkClicked", this.onLinkClicked);
+ this.view.element.removeEventListener("CssRuleViewChanged", this.onPropertyChanged);
+ this.view.element.removeEventListener("CssRuleViewRefreshed", this.onViewRefreshed);
+
+ this.doc.documentElement.removeChild(this.view.element);
+
+ this.view.destroy();
+
+ this.view = this.doc = this.inspector = null;
+ }
+};
+
+function ComputedViewTool(inspector, window, iframe) {
+ this.inspector = inspector;
+ this.doc = window.document;
+
+ this.view = new ComputedView.CssHtmlTree(this, inspector.pageStyle);
+
+ this.onSelected = this.onSelected.bind(this);
+ this.refresh = this.refresh.bind(this);
+ this.onPanelSelected = this.onPanelSelected.bind(this);
+
+ this.inspector.selection.on("detached", this.onSelected);
+ this.inspector.selection.on("new-node-front", this.onSelected);
+ this.inspector.on("layout-change", this.refresh);
+ this.inspector.selection.on("pseudoclass", this.refresh);
+ this.inspector.sidebar.on("computedview-selected", this.onPanelSelected);
+
+ this.view.selectElement(null);
+
+ this.onSelected();
+}
+
+ComputedViewTool.prototype = {
+ isSidebarActive: function() {
+ if (!this.view) {
+ return;
+ }
+ return this.inspector.sidebar.getCurrentTabID() == "computedview";
+ },
+
+ onSelected: function(event) {
+ // Ignore the event if the view has been destroyed, or if it's inactive.
+ // But only if the current selection isn't null. If it's been set to null,
+ // let the update go through as this is needed to empty the view on navigation.
+ if (!this.view) {
+ return;
+ }
+
+ let isInactive = !this.isSidebarActive() && this.inspector.selection.nodeFront;
+ if (isInactive) {
+ return;
+ }
+
+ this.view.setPageStyle(this.inspector.pageStyle);
+
+ if (!this.inspector.selection.isConnected() ||
+ !this.inspector.selection.isElementNode()) {
+ this.view.selectElement(null);
+ return;
+ }
+
+ if (!event || event == "new-node-front") {
+ let done = this.inspector.updating("computed-view");
+ this.view.selectElement(this.inspector.selection.nodeFront).then(() => {
+ done();
+ });
+ }
+ },
+
+ refresh: function() {
+ if (this.isSidebarActive()) {
+ this.view.refreshPanel();
+ }
+ },
+
+ onPanelSelected: function() {
+ if (this.inspector.selection.nodeFront === this.view.viewedElement) {
+ this.refresh();
+ } else {
+ this.onSelected();
+ }
+ },
+
+ destroy: function() {
+ this.inspector.off("layout-change", this.refresh);
+ this.inspector.sidebar.off("computedview-selected", this.refresh);
+ this.inspector.selection.off("pseudoclass", this.refresh);
+ this.inspector.selection.off("new-node-front", this.onSelected);
+ this.inspector.sidebar.off("computedview-selected", this.onPanelSelected);
+
+ this.view.destroy();
+
+ this.view = this.cssLogic = this.cssHtmlTree = null;
+ this.doc = this.inspector = null;
+ }
+};
+
+exports.RuleViewTool = RuleViewTool;
+exports.ComputedViewTool = ComputedViewTool;
diff --git a/toolkit/devtools/styleinspector/test/browser.ini b/toolkit/devtools/styleinspector/test/browser.ini
new file mode 100644
index 000000000..d551f35c4
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser.ini
@@ -0,0 +1,126 @@
+[DEFAULT]
+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_frame_script.js
+ 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
+ doc_test_image.png
+ doc_urls_clickable.css
+ doc_urls_clickable.html
+ head.js
+
+[browser_computedview_browser-styles.js]
+[browser_computedview_getNodeInfo.js]
+[browser_computedview_keybindings_01.js]
+[browser_computedview_keybindings_02.js]
+[browser_computedview_matched-selectors-toggle.js]
+[browser_computedview_matched-selectors_01.js]
+[browser_computedview_matched-selectors_02.js]
+[browser_computedview_media-queries.js]
+[browser_computedview_no-results-placeholder.js]
+[browser_computedview_original-source-link.js]
+[browser_computedview_pseudo-element_01.js]
+[browser_computedview_refresh-on-style-change_01.js]
+[browser_computedview_search-filter.js]
+[browser_computedview_select-and-copy-styles.js]
+[browser_computedview_style-editor-link.js]
+[browser_ruleview_add-property-and-reselect.js]
+[browser_ruleview_add-property-cancel_01.js]
+[browser_ruleview_add-property-cancel_02.js]
+[browser_ruleview_add-property-cancel_03.js]
+[browser_ruleview_add-property_01.js]
+[browser_ruleview_add-property_02.js]
+[browser_ruleview_add-rule_01.js]
+[browser_ruleview_add-rule_02.js]
+[browser_ruleview_add-rule_03.js]
+[browser_ruleview_colorpicker-and-image-tooltip_01.js]
+[browser_ruleview_colorpicker-and-image-tooltip_02.js]
+[browser_ruleview_colorpicker-appears-on-swatch-click.js]
+[browser_ruleview_colorpicker-commit-on-ENTER.js]
+[browser_ruleview_colorpicker-edit-gradient.js]
+[browser_ruleview_colorpicker-hides-on-tooltip.js]
+[browser_ruleview_colorpicker-multiple-changes.js]
+[browser_ruleview_colorpicker-revert-on-ESC.js]
+[browser_ruleview_colorpicker-swatch-displayed.js]
+[browser_ruleview_completion-existing-property_01.js]
+[browser_ruleview_completion-existing-property_02.js]
+[browser_ruleview_completion-new-property_01.js]
+[browser_ruleview_completion-new-property_02.js]
+[browser_ruleview_content_01.js]
+[browser_ruleview_content_02.js]
+skip-if = e10s # Bug 1039528: "inspect element" contextual-menu doesn't work with e10s
+[browser_ruleview_cubicbezier-appears-on-swatch-click.js]
+[browser_ruleview_cubicbezier-commit-on-ENTER.js]
+[browser_ruleview_cubicbezier-revert-on-ESC.js]
+[browser_ruleview_edit-property-commit.js]
+[browser_ruleview_edit-property-increments.js]
+[browser_ruleview_edit-property-order.js]
+[browser_ruleview_edit-property_01.js]
+[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) || e10s # bug 963492: win. bug 1040653: e10s.
+[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]
+[browser_ruleview_multiple_properties_02.js]
+[browser_ruleview_original-source-link.js]
+[browser_ruleview_override.js]
+[browser_ruleview_pseudo-element_01.js]
+[browser_ruleview_pseudo-element_02.js]
+skip-if = e10s # Bug 1090340
+[browser_ruleview_refresh-on-attribute-change_01.js]
+[browser_ruleview_refresh-on-attribute-change_02.js]
+[browser_ruleview_refresh-on-style-change.js]
+[browser_ruleview_select-and-copy-styles.js]
+[browser_ruleview_selector-highlighter_01.js]
+[browser_ruleview_selector-highlighter_02.js]
+[browser_ruleview_style-editor-link.js]
+skip-if = e10s # bug 1040670 Cannot open inline styles in viewSourceUtils
+[browser_ruleview_urls-clickable.js]
+[browser_ruleview_user-agent-styles.js]
+[browser_ruleview_user-agent-styles-uneditable.js]
+[browser_ruleview_user-property-reset.js]
+[browser_styleinspector_context-menu-copy-color_01.js]
+[browser_styleinspector_context-menu-copy-color_02.js]
+[browser_styleinspector_csslogic-content-stylesheets.js]
+[browser_styleinspector_output-parser.js]
+[browser_styleinspector_refresh_when_active.js]
+[browser_styleinspector_tooltip-background-image.js]
+[browser_styleinspector_tooltip-closes-on-new-selection.js]
+skip-if = e10s # Bug 1111546
+[browser_styleinspector_tooltip-longhand-fontfamily.js]
+[browser_styleinspector_tooltip-multiple-background-images.js]
+[browser_styleinspector_tooltip-shorthand-fontfamily.js]
+[browser_styleinspector_tooltip-size.js]
+skip-if = e10s # Bug 1111546
+[browser_styleinspector_transform-highlighter-01.js]
+[browser_styleinspector_transform-highlighter-02.js]
+[browser_styleinspector_transform-highlighter-03.js]
+[browser_styleinspector_transform-highlighter-04.js]
diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_browser-styles.js b/toolkit/devtools/styleinspector/test/browser_computedview_browser-styles.js
new file mode 100644
index 000000000..7bb9f82ac
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_computedview_browser-styles.js
@@ -0,0 +1,54 @@
+/* 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";
+
+// Tests that the checkbox to include browser styles works properly.
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,default styles test");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = '<style type="text/css"> ' +
+ '.matches {color: #F00;}</style>' +
+ '<span id="matches" class="matches">Some styled text</span>' +
+ '</div>';
+ content.document.title = "Style Inspector Default Styles Test";
+
+ info("Opening the computed view");
+ let {toolbox, inspector, view} = yield openComputedView();
+
+ info("Selecting the test node");
+ yield selectNode("#matches", inspector);
+
+ info("Checking the default styles");
+ is(isPropertyVisible("color", view), true,
+ "span #matches color property is visible");
+ is(isPropertyVisible("background-color", view), false,
+ "span #matches background-color property is hidden");
+
+ info("Toggling the browser styles");
+ let doc = view.styleDocument;
+ let checkbox = doc.querySelector(".includebrowserstyles");
+ let onRefreshed = inspector.once("computed-view-refreshed");
+ checkbox.click();
+ yield onRefreshed;
+
+ info("Checking the browser styles");
+ is(isPropertyVisible("color", view), true,
+ "span color property is visible");
+ is(isPropertyVisible("background-color", view), true,
+ "span background-color property is visible");
+});
+
+function isPropertyVisible(name, view) {
+ info("Checking property visibility for " + name);
+ let propertyViews = view.propertyViews;
+ for each (let propView in propertyViews) {
+ if (propView.name == name) {
+ return propView.visible;
+ }
+ }
+ return false;
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_getNodeInfo.js b/toolkit/devtools/styleinspector/test/browser_computedview_getNodeInfo.js
new file mode 100644
index 000000000..4457ecd3a
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_computedview_getNodeInfo.js
@@ -0,0 +1,177 @@
+/* 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 various output of the computed-view's getNodeInfo method.
+// This method is used by the style-inspector-overlay on mouseover to decide
+// which tooltip or highlighter to show when hovering over a value/name/selector
+// if any.
+// For instance, browser_ruleview_selector-highlighter_01.js and
+// browser_ruleview_selector-highlighter_02.js test that the selector
+// highlighter appear when hovering over a selector in the rule-view.
+// Since the code to make this work for the computed-view is 90% the same, there
+// is no need for testing it again here.
+// This test however serves as a unit test for getNodeInfo.
+
+const {
+ VIEW_NODE_SELECTOR_TYPE,
+ VIEW_NODE_PROPERTY_TYPE,
+ VIEW_NODE_VALUE_TYPE,
+ VIEW_NODE_IMAGE_URL_TYPE
+} = devtools.require("devtools/styleinspector/style-inspector-overlays");
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body {',
+ ' background: red;',
+ ' color: white;',
+ ' }',
+ ' div {',
+ ' background: green;',
+ ' }',
+ ' div div {',
+ ' background-color: yellow;',
+ ' background-image: url(chrome://global/skin/icons/warning-64.png);',
+ ' color: red;',
+ ' }',
+ '</style>',
+ '<div><div id="testElement">Test element</div></div>'
+].join("\n");
+
+// Each item in this array must have the following properties:
+// - desc {String} will be logged for information
+// - getHoveredNode {Generator Function} received the computed-view instance as
+// argument and must return the node to be tested
+// - assertNodeInfo {Function} should check the validity of the nodeInfo
+// argument it receives
+const TEST_DATA = [
+ {
+ desc: "Testing a null node",
+ getHoveredNode: function*() {
+ return null;
+ },
+ assertNodeInfo: function(nodeInfo) {
+ is(nodeInfo, null);
+ }
+ },
+ {
+ desc: "Testing a useless node",
+ getHoveredNode: function*(view) {
+ return view.element;
+ },
+ assertNodeInfo: function(nodeInfo) {
+ is(nodeInfo, null);
+ }
+ },
+ {
+ desc: "Testing a property name",
+ getHoveredNode: function*(view) {
+ return getComputedViewProperty(view, "color").nameSpan;
+ },
+ assertNodeInfo: function(nodeInfo) {
+ is(nodeInfo.type, VIEW_NODE_PROPERTY_TYPE);
+ ok("property" in nodeInfo.value);
+ ok("value" in nodeInfo.value);
+ is(nodeInfo.value.property, "color");
+ is(nodeInfo.value.value, "#F00");
+ }
+ },
+ {
+ desc: "Testing a property value",
+ getHoveredNode: function*(view) {
+ return getComputedViewProperty(view, "color").valueSpan;
+ },
+ assertNodeInfo: function(nodeInfo) {
+ is(nodeInfo.type, VIEW_NODE_VALUE_TYPE);
+ ok("property" in nodeInfo.value);
+ ok("value" in nodeInfo.value);
+ is(nodeInfo.value.property, "color");
+ is(nodeInfo.value.value, "#F00");
+ }
+ },
+ {
+ desc: "Testing an image url",
+ getHoveredNode: function*(view) {
+ let {valueSpan} = getComputedViewProperty(view, "background-image");
+ return valueSpan.querySelector(".theme-link");
+ },
+ assertNodeInfo: function(nodeInfo) {
+ is(nodeInfo.type, VIEW_NODE_IMAGE_URL_TYPE);
+ ok("property" in nodeInfo.value);
+ ok("value" in nodeInfo.value);
+ is(nodeInfo.value.property, "background-image");
+ is(nodeInfo.value.value, "url(\"chrome://global/skin/icons/warning-64.png\")");
+ is(nodeInfo.value.url, "chrome://global/skin/icons/warning-64.png");
+ }
+ },
+ {
+ desc: "Testing a matched rule selector (bestmatch)",
+ getHoveredNode: function*(view) {
+ let content = yield getComputedViewMatchedRules(view, "background-color");
+ return content.querySelector(".bestmatch");
+ },
+ assertNodeInfo: function(nodeInfo) {
+ is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE);
+ is(nodeInfo.value, "div div");
+ }
+ },
+ {
+ desc: "Testing a matched rule selector (matched)",
+ getHoveredNode: function*(view) {
+ let content = yield getComputedViewMatchedRules(view, "background-color");
+ return content.querySelector(".matched");
+ },
+ assertNodeInfo: function(nodeInfo) {
+ is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE);
+ is(nodeInfo.value, "div");
+ }
+ },
+ {
+ desc: "Testing a matched rule selector (parentmatch)",
+ getHoveredNode: function*(view) {
+ let content = yield getComputedViewMatchedRules(view, "color");
+ return content.querySelector(".parentmatch");
+ },
+ assertNodeInfo: function(nodeInfo) {
+ is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE);
+ is(nodeInfo.value, "body");
+ }
+ },
+ {
+ desc: "Testing a matched rule value",
+ getHoveredNode: function*(view) {
+ let content = yield getComputedViewMatchedRules(view, "color");
+ return content.querySelector(".other-property-value");
+ },
+ assertNodeInfo: function(nodeInfo) {
+ is(nodeInfo.type, VIEW_NODE_VALUE_TYPE);
+ is(nodeInfo.value.property, "color");
+ is(nodeInfo.value.value, "#F00");
+ }
+ },
+ {
+ desc: "Testing a matched rule stylesheet link",
+ getHoveredNode: function*(view) {
+ let content = yield getComputedViewMatchedRules(view, "color");
+ return content.querySelector(".rule-link .theme-link");
+ },
+ assertNodeInfo: function(nodeInfo) {
+ is(nodeInfo, null);
+ }
+ }
+];
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8," + PAGE_CONTENT);
+
+ let {inspector, view} = yield openComputedView();
+ yield selectNode("#testElement", inspector);
+
+ for (let {desc, getHoveredNode, assertNodeInfo} of TEST_DATA) {
+ info(desc);
+ let nodeInfo = view.getNodeInfo(yield getHoveredNode(view));
+ assertNodeInfo(nodeInfo);
+ }
+});
diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_keybindings_01.js b/toolkit/devtools/styleinspector/test/browser_computedview_keybindings_01.js
new file mode 100644
index 000000000..0a805bd62
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_computedview_keybindings_01.js
@@ -0,0 +1,79 @@
+/* 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 computed view key bindings
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,default styles test");
+
+ info("Adding content to the test page");
+ content.document.body.innerHTML = '<style type="text/css"> ' +
+ '.matches {color: #F00;}</style>' +
+ '<span class="matches">Some styled text</span>' +
+ '</div>';
+
+ let {toolbox, inspector, view} = yield openComputedView();
+
+ info("Selecting the test node");
+ yield selectNode(".matches", inspector);
+
+ let propView = getFirstVisiblePropertyView(view);
+ let rulesTable = propView.matchedSelectorsContainer;
+ let matchedExpander = propView.element;
+
+ info("Focusing the property");
+ let onMatchedExpanderFocus = once(matchedExpander, "focus", true);
+ EventUtils.synthesizeMouseAtCenter(matchedExpander, {}, view.styleWindow);
+ yield onMatchedExpanderFocus;
+
+ yield checkToggleKeyBinding(view.styleWindow, "VK_SPACE", rulesTable, inspector);
+ yield checkToggleKeyBinding(view.styleWindow, "VK_RETURN", rulesTable, inspector);
+ yield checkHelpLinkKeybinding(view);
+});
+
+function getFirstVisiblePropertyView(view) {
+ let propView = null;
+ view.propertyViews.some(p => {
+ if (p.visible) {
+ propView = p;
+ return true;
+ }
+ return false;
+ });
+
+ return propView;
+}
+
+function* checkToggleKeyBinding(win, key, rulesTable, inspector) {
+ info("Pressing " + key + " key a couple of times to check that the property gets expanded/collapsed");
+
+ let onExpand = inspector.once("computed-view-property-expanded");
+ let onCollapse = inspector.once("computed-view-property-collapsed");
+
+ info("Expanding the property");
+ EventUtils.synthesizeKey(key, {}, win);
+ yield onExpand;
+ isnot(rulesTable.innerHTML, "", "The property has been expanded");
+
+ info("Collapsing the property");
+ EventUtils.synthesizeKey(key, {}, win);
+ yield onCollapse;
+ is(rulesTable.innerHTML, "", "The property has been collapsed");
+}
+
+function checkHelpLinkKeybinding(view) {
+ info("Check that MDN link is opened on \"F1\"");
+ let def = promise.defer();
+
+ let propView = getFirstVisiblePropertyView(view);
+ propView.mdnLinkClick = function(aEvent) {
+ ok(true, "Pressing F1 opened the MDN link");
+ def.resolve();
+ };
+
+ EventUtils.synthesizeKey("VK_F1", {}, view.styleWindow);
+ return def.promise;
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_keybindings_02.js b/toolkit/devtools/styleinspector/test/browser_computedview_keybindings_02.js
new file mode 100644
index 000000000..e7c3e9a6f
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_computedview_keybindings_02.js
@@ -0,0 +1,62 @@
+/* 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";
+
+// Tests the computed-view keyboard navigation
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,computed view keyboard nav test");
+
+ content.document.body.innerHTML = '<style type="text/css"> ' +
+ 'span { font-variant: small-caps; color: #000000; } ' +
+ '.nomatches {color: #ff0000;}</style> <div id="first" style="margin: 10em; ' +
+ 'font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA">\n' +
+ '<h1>Some header text</h1>\n' +
+ '<p id="salutation" style="font-size: 12pt">hi.</p>\n' +
+ '<p id="body" style="font-size: 12pt">I am a test-case. This text exists ' +
+ 'solely to provide some things to <span style="color: yellow">' +
+ 'highlight</span> and <span style="font-weight: bold">count</span> ' +
+ 'style list-items in the box at right. If you are reading this, ' +
+ 'you should go do something else instead. Maybe read a book. Or better ' +
+ 'yet, write some test-cases for another bit of code. ' +
+ '<span style="font-style: italic">some text</span></p>\n' +
+ '<p id="closing">more text</p>\n' +
+ '<p>even more text</p>' +
+ '</div>';
+ content.document.title = "Computed view keyboard navigation test";
+
+ info("Opening the computed-view");
+ let {toolbox, inspector, view} = yield openComputedView();
+
+ info("Selecting the test node");
+ yield selectNode("span", inspector);
+
+ info("Selecting the first computed style in the list");
+ let firstStyle = view.styleDocument.querySelector(".property-view");
+ ok(firstStyle, "First computed style found in panel");
+ firstStyle.focus();
+
+ info("Tab to select the 2nd style and press return");
+ let onExpanded = inspector.once("computed-view-property-expanded");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onExpanded;
+
+ info("Verify the 2nd style has been expanded");
+ let secondStyleSelectors = view.styleDocument.querySelectorAll(
+ ".property-content .matchedselectors")[1];
+ ok(secondStyleSelectors.childNodes.length > 0, "Matched selectors expanded");
+
+ info("Tab back up and test the same thing, with space");
+ onExpanded = inspector.once("computed-view-property-expanded");
+ EventUtils.synthesizeKey("VK_TAB", {shiftKey: true});
+ EventUtils.synthesizeKey("VK_SPACE", {});
+ yield onExpanded;
+
+ info("Verify the 1st style has been expanded too");
+ let firstStyleSelectors = view.styleDocument.querySelectorAll(
+ ".property-content .matchedselectors")[0];
+ ok(firstStyleSelectors.childNodes.length > 0, "Matched selectors expanded");
+});
diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors-toggle.js b/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors-toggle.js
new file mode 100644
index 000000000..7238b315b
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors-toggle.js
@@ -0,0 +1,108 @@
+/* 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 the computed view properties can be expanded and collapsed with
+// either the twisty or by dbl-clicking on the container
+
+const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent([
+ '<html>' +
+ '<head>' +
+ ' <title>Computed view toggling test</title>',
+ ' <style type="text/css"> ',
+ ' html { color: #000000; font-size: 15pt; } ',
+ ' h1 { color: red; } ',
+ ' </style>',
+ '</head>',
+ '<body>',
+ ' <h1>Some header text</h1>',
+ '</body>',
+ '</html>'
+].join("\n"));
+
+add_task(function*() {
+ yield addTab(TEST_URL);
+ let {toolbox, inspector, view} = yield openComputedView();
+
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ yield testExpandOnTwistyClick(view, inspector);
+ yield testCollapseOnTwistyClick(view, inspector);
+ yield testExpandOnDblClick(view, inspector);
+ yield testCollapseOnDblClick(view, inspector);
+});
+
+function* testExpandOnTwistyClick({styleDocument, styleWindow}, inspector) {
+ info("Testing that a property expands on twisty click");
+
+ info("Getting twisty element");
+ let twisty = styleDocument.querySelector(".expandable");
+ ok(twisty, "Twisty found");
+
+ let onExpand = inspector.once("computed-view-property-expanded");
+ info("Clicking on the twisty element");
+ twisty.click();
+
+ yield onExpand;
+
+ // Expanded means the matchedselectors div is not empty
+ let div = styleDocument.querySelector(".property-content .matchedselectors");
+ ok(div.childNodes.length > 0, "Matched selectors are expanded on twisty click");
+}
+
+function* testCollapseOnTwistyClick({styleDocument, styleWindow}, inspector) {
+ info("Testing that a property collapses on twisty click");
+
+ info("Getting twisty element");
+ let twisty = styleDocument.querySelector(".expandable");
+ ok(twisty, "Twisty found");
+
+ let onCollapse = inspector.once("computed-view-property-collapsed");
+ info("Clicking on the twisty element");
+ twisty.click();
+
+ yield onCollapse;
+
+ // Collapsed means the matchedselectors div is empty
+ let div = styleDocument.querySelector(".property-content .matchedselectors");
+ ok(div.childNodes.length === 0, "Matched selectors are collapsed on twisty click");
+}
+
+function* testExpandOnDblClick({styleDocument, styleWindow}, inspector) {
+ info("Testing that a property expands on container dbl-click");
+
+ info("Getting computed property container");
+ let container = styleDocument.querySelector(".property-view");
+ ok(container, "Container found");
+
+ let onExpand = inspector.once("computed-view-property-expanded");
+ info("Dbl-clicking on the container");
+ EventUtils.synthesizeMouseAtCenter(container, {clickCount: 2}, styleWindow);
+
+ yield onExpand;
+
+ // Expanded means the matchedselectors div is not empty
+ let div = styleDocument.querySelector(".property-content .matchedselectors");
+ ok(div.childNodes.length > 0, "Matched selectors are expanded on dblclick");
+}
+
+function* testCollapseOnDblClick({styleDocument, styleWindow}, inspector) {
+ info("Testing that a property collapses on container dbl-click");
+
+ info("Getting computed property container");
+ let container = styleDocument.querySelector(".property-view");
+ ok(container, "Container found");
+
+ let onCollapse = inspector.once("computed-view-property-collapsed");
+ info("Dbl-clicking on the container");
+ EventUtils.synthesizeMouseAtCenter(container, {clickCount: 2}, styleWindow);
+
+ yield onCollapse;
+
+ // Collapsed means the matchedselectors div is empty
+ let div = styleDocument.querySelector(".property-content .matchedselectors");
+ ok(div.childNodes.length === 0, "Matched selectors are collapsed on dblclick");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors_01.js b/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors_01.js
new file mode 100644
index 000000000..2c4347d6a
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors_01.js
@@ -0,0 +1,36 @@
+/* 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";
+
+// Checking selector counts, matched rules and titles in the computed-view
+
+const {PropertyView} = devtools.require("devtools/styleinspector/computed-view");
+const TEST_URI = TEST_URL_ROOT + "doc_matched_selectors.html";
+
+add_task(function*() {
+ yield addTab(TEST_URI);
+ let {toolbox, inspector, view} = yield openComputedView();
+
+ yield selectNode("#test", inspector);
+ yield testMatchedSelectors(view, inspector);
+});
+
+function* testMatchedSelectors(view, inspector) {
+ info("checking selector counts, matched rules and titles");
+
+ let nodeFront = yield getNodeFront("#test", inspector);
+ is(nodeFront, view.viewedElement, "style inspector node matches the selected node");
+
+ let propertyView = new PropertyView(view, "color");
+ propertyView.buildMain();
+ propertyView.buildSelectorContainer();
+ propertyView.matchedExpanded = true;
+
+ yield propertyView.refreshMatchedSelectors();
+
+ let numMatchedSelectors = propertyView.matchedSelectors.length;
+ is(numMatchedSelectors, 6, "CssLogic returns the correct number of matched selectors for div");
+ is(propertyView.hasMatchedSelectors, true, "hasMatchedSelectors returns true");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors_02.js b/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors_02.js
new file mode 100644
index 000000000..0d5193213
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors_02.js
@@ -0,0 +1,44 @@
+/* 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";
+
+// Tests for matched selector texts in the computed view
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,<div style='color:blue;'></div>");
+
+ info("Opening the computed view");
+ let {toolbox, inspector, view} = yield openComputedView();
+
+ info("Selecting the test node");
+ yield selectNode("div", inspector);
+
+ info("Checking the color property view");
+ let propertyView = getPropertyView(view, "color");
+ ok(propertyView, "found PropertyView for color");
+ is(propertyView.hasMatchedSelectors, true, "hasMatchedSelectors is true");
+
+ info("Expanding the matched selectors");
+ propertyView.matchedExpanded = true;
+ yield propertyView.refreshMatchedSelectors();
+
+ let span = propertyView.matchedSelectorsContainer.querySelector("span.rule-text");
+ ok(span, "Found the first table row");
+
+ let selector = propertyView.matchedSelectorViews[0];
+ ok(selector, "Found the first matched selector view");
+});
+
+function getPropertyView(computedView, name) {
+ let propertyView = null;
+ computedView.propertyViews.some(function(view) {
+ if (view.name == name) {
+ propertyView = view;
+ return true;
+ }
+ return false;
+ });
+ return propertyView;
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_media-queries.js b/toolkit/devtools/styleinspector/test/browser_computedview_media-queries.js
new file mode 100644
index 000000000..5159ad6e8
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_computedview_media-queries.js
@@ -0,0 +1,41 @@
+/* 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";
+
+// Tests that we correctly display appropriate media query titles in the
+// property view.
+
+const TEST_URI = TEST_URL_ROOT + "doc_media_queries.html";
+
+let {PropertyView} = devtools.require("devtools/styleinspector/computed-view");
+let {CssLogic} = devtools.require("devtools/styleinspector/css-logic");
+
+add_task(function*() {
+ yield addTab(TEST_URI);
+ let {toolbox, inspector, view} = yield openComputedView();
+
+ info("Selecting the test element");
+ yield selectNode("div", inspector);
+
+ info("Checking property view");
+ yield checkPropertyView(view);
+});
+
+function checkPropertyView(view) {
+ let propertyView = new PropertyView(view, "width");
+ propertyView.buildMain();
+ propertyView.buildSelectorContainer();
+ propertyView.matchedExpanded = true;
+
+ return propertyView.refreshMatchedSelectors().then(() => {
+ let numMatchedSelectors = propertyView.matchedSelectors.length;
+
+ is(numMatchedSelectors, 2,
+ "Property view has the correct number of matched selectors for div");
+
+ is(propertyView.hasMatchedSelectors, true,
+ "hasMatchedSelectors returns true");
+ });
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_no-results-placeholder.js b/toolkit/devtools/styleinspector/test/browser_computedview_no-results-placeholder.js
new file mode 100644
index 000000000..261eba91d
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_computedview_no-results-placeholder.js
@@ -0,0 +1,73 @@
+/* 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";
+
+// Tests that the no results placeholder works properly.
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,no results placeholder test");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = '<style type="text/css"> ' +
+ '.matches {color: #F00;}</style>' +
+ '<span id="matches" class="matches">Some styled text</span>';
+ content.document.title = "Tests that the no results placeholder works properly";
+
+ info("Opening the computed view");
+ let {toolbox, inspector, view} = yield openComputedView();
+
+ info("Selecting the test node");
+ yield selectNode("#matches", inspector);
+
+ yield enterInvalidFilter(inspector, view);
+ checkNoResultsPlaceholderShown(view);
+
+ yield clearFilterText(inspector, view);
+ checkNoResultsPlaceholderHidden(view);
+});
+
+function* enterInvalidFilter(inspector, computedView) {
+ let searchbar = computedView.searchField;
+ let searchTerm = "xxxxx";
+
+ info("setting filter text to \"" + searchTerm + "\"");
+
+ let onRefreshed = inspector.once("computed-view-refreshed");
+ searchbar.focus();
+ for each (let c in searchTerm) {
+ EventUtils.synthesizeKey(c, {}, computedView.styleWindow);
+ }
+ yield onRefreshed;
+}
+
+function checkNoResultsPlaceholderShown(computedView) {
+ info("Checking that the no results placeholder is shown");
+
+ let placeholder = computedView.noResults;
+ let win = computedView.styleWindow;
+ let display = win.getComputedStyle(placeholder).display;
+ is(display, "block", "placeholder is visible");
+}
+
+function* clearFilterText(inspector, computedView) {
+ info("Clearing the filter text");
+
+ let searchbar = computedView.searchField;
+
+ let onRefreshed = inspector.once("computed-view-refreshed");
+ searchbar.focus();
+ searchbar.value = "";
+ EventUtils.synthesizeKey("c", {}, computedView.styleWindow);
+ yield onRefreshed;
+}
+
+function checkNoResultsPlaceholderHidden(computedView) {
+ info("Checking that the no results placeholder is hidden");
+
+ let placeholder = computedView.noResults;
+ let win = computedView.styleWindow;
+ let display = win.getComputedStyle(placeholder).display;
+ is(display, "none", "placeholder is hidden");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_original-source-link.js b/toolkit/devtools/styleinspector/test/browser_computedview_original-source-link.js
new file mode 100644
index 000000000..6ff2d5315
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_computedview_original-source-link.js
@@ -0,0 +1,74 @@
+/* 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 the computed view shows the original source link when source maps
+// are enabled
+
+const TESTCASE_URI = TEST_URL_ROOT_SSL + "doc_sourcemaps.html";
+const PREF = "devtools.styleeditor.source-maps-enabled";
+const SCSS_LOC = "doc_sourcemaps.scss:4";
+const CSS_LOC = "doc_sourcemaps.css:1";
+
+add_task(function*() {
+ info("Turning the pref " + PREF + " on");
+ Services.prefs.setBoolPref(PREF, true);
+
+ yield addTab(TESTCASE_URI);
+ let {toolbox, inspector, view} = yield openComputedView();
+
+ info("Select the test node");
+ yield selectNode("div", inspector);
+
+ info("Expanding the first property");
+ yield expandComputedViewPropertyByIndex(view, 0);
+
+ info("Verifying the link text");
+ // Forcing a call to updateSourceLink on the SelectorView here. The
+ // computed-view already does it, but we have no way of waiting for it to be
+ // done here, so just call it again and wait for the returned promise to
+ // resolve.
+ let propertyView = getComputedViewPropertyView(view, "color");
+ yield propertyView.matchedSelectorViews[0].updateSourceLink();
+ verifyLinkText(view, SCSS_LOC);
+
+ info("Toggling the pref");
+ let onLinksUpdated = inspector.once("computed-view-sourcelinks-updated");
+ Services.prefs.setBoolPref(PREF, false);
+ yield onLinksUpdated;
+
+ info("Verifying that the link text has changed after the pref change");
+ yield verifyLinkText(view, CSS_LOC);
+
+ info("Toggling the pref again");
+ onLinksUpdated = inspector.once("computed-view-sourcelinks-updated");
+ Services.prefs.setBoolPref(PREF, true);
+ yield onLinksUpdated;
+
+ info("Testing that clicking on the link works");
+ yield testClickingLink(toolbox, view);
+
+ info("Turning the pref " + PREF + " off");
+ Services.prefs.clearUserPref(PREF);
+});
+
+function* testClickingLink(toolbox, view) {
+ let onEditor = waitForStyleEditor(toolbox, "doc_sourcemaps.scss");
+
+ info("Clicking the computedview stylesheet link");
+ let link = getComputedViewLinkByIndex(view, 0);
+ link.scrollIntoView();
+ link.click();
+
+ let editor = yield onEditor;
+
+ let {line, col} = editor.sourceEditor.getCursor();
+ is(line, 3, "cursor is at correct line number in original source");
+}
+
+function verifyLinkText(view, text) {
+ let link = getComputedViewLinkByIndex(view, 0);
+ is(link.textContent, text, "Linked text changed to display the correct location");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_pseudo-element_01.js b/toolkit/devtools/styleinspector/test/browser_computedview_pseudo-element_01.js
new file mode 100644
index 000000000..73051808f
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_computedview_pseudo-element_01.js
@@ -0,0 +1,41 @@
+/* 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 pseudoelements are displayed correctly in the rule view
+
+const TEST_URI = TEST_URL_ROOT + "doc_pseudoelement.html";
+
+add_task(function*() {
+ yield addTab(TEST_URI);
+ let {toolbox, inspector, view} = yield openComputedView();
+
+ yield testTopLeft(inspector, view);
+});
+
+function* testTopLeft(inspector, view) {
+ let node = yield getNodeFront("#topleft", inspector.markup);
+ yield selectNode(node, inspector);
+ let float = getComputedViewPropertyValue(view, "float");
+ is(float, "left", "The computed view shows the correct float");
+
+ let children = yield inspector.markup.walker.children(node);
+ is (children.nodes.length, 3, "Element has correct number of children");
+
+ let beforeElement = children.nodes[0];
+ yield selectNode(beforeElement, inspector);
+ let top = getComputedViewPropertyValue(view, "top");
+ is(top, "0px", "The computed view shows the correct top");
+ let left = getComputedViewPropertyValue(view, "left");
+ is(left, "0px", "The computed view shows the correct left");
+
+ let afterElement = children.nodes[children.nodes.length-1];
+ yield selectNode(afterElement, inspector);
+ top = getComputedViewPropertyValue(view, "top");
+ is(top, "50%", "The computed view shows the correct top");
+ left = getComputedViewPropertyValue(view, "left");
+ is(left, "50%", "The computed view shows the correct left");
+}
+
diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_refresh-on-style-change_01.js b/toolkit/devtools/styleinspector/test/browser_computedview_refresh-on-style-change_01.js
new file mode 100644
index 000000000..a550b6ba9
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_computedview_refresh-on-style-change_01.js
@@ -0,0 +1,32 @@
+/* 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 the computed view refreshes when the current node has its style
+// changed
+
+const TESTCASE_URI = 'data:text/html;charset=utf-8,' +
+ '<div id="testdiv" style="font-size:10px;">Test div!</div>';
+
+add_task(function*() {
+ yield addTab(TESTCASE_URI);
+
+ info("Opening the computed view and selecting the test node");
+ let {toolbox, inspector, view} = yield openComputedView();
+ yield selectNode("#testdiv", inspector);
+
+ let fontSize = getComputedViewPropertyValue(view, "font-size");
+ is(fontSize, "10px", "The computed view shows the right font-size");
+
+ info("Changing the node's style and waiting for the update");
+ let onUpdated = inspector.once("computed-view-refreshed");
+ getNode("#testdiv").style.cssText = "font-size: 15px; color: red;";
+ yield onUpdated;
+
+ fontSize = getComputedViewPropertyValue(view, "font-size");
+ is(fontSize, "15px", "The computed view shows the updated font-size");
+ let color = getComputedViewPropertyValue(view, "color");
+ is(color, "#F00", "The computed view also shows the color now");
+});
diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_search-filter.js b/toolkit/devtools/styleinspector/test/browser_computedview_search-filter.js
new file mode 100644
index 000000000..f1447f159
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_computedview_search-filter.js
@@ -0,0 +1,65 @@
+/* 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";
+
+// Tests that the search filter works properly.
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,default styles test");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = '<style type="text/css"> ' +
+ '.matches {color: #F00;}</style>' +
+ '<span id="matches" class="matches">Some styled text</span>' +
+ '</div>';
+ content.document.title = "Style Inspector Search Filter Test";
+
+ info("Opening the computed-view");
+ let {toolbox, inspector, view} = yield openComputedView();
+
+ info("Selecting the test node");
+ yield selectNode("#matches", inspector);
+
+ yield testToggleDefaultStyles(inspector, view);
+ yield testAddTextInFilter(inspector, view);
+});
+
+
+function* testToggleDefaultStyles(inspector, computedView) {
+ info("checking \"Browser styles\" checkbox");
+
+ let doc = computedView.styleDocument;
+ let checkbox = doc.querySelector(".includebrowserstyles");
+ let onRefreshed = inspector.once("computed-view-refreshed");
+ checkbox.click();
+ yield onRefreshed;
+}
+
+function* testAddTextInFilter(inspector, computedView) {
+ info("setting filter text to \"color\"");
+
+ let doc = computedView.styleDocument;
+ let searchbar = doc.querySelector(".devtools-searchinput");
+ let onRefreshed = inspector.once("computed-view-refreshed");
+ searchbar.focus();
+
+ let win = computedView.styleWindow;
+ EventUtils.synthesizeKey("c", {}, win);
+ EventUtils.synthesizeKey("o", {}, win);
+ EventUtils.synthesizeKey("l", {}, win);
+ EventUtils.synthesizeKey("o", {}, win);
+ EventUtils.synthesizeKey("r", {}, win);
+
+ yield onRefreshed;
+
+ info("check that the correct properties are visible");
+
+ let propertyViews = computedView.propertyViews;
+ propertyViews.forEach(function(propView) {
+ let name = propView.name;
+ is(propView.visible, name.indexOf("color") > -1,
+ "span " + name + " property visibility check");
+ });
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_select-and-copy-styles.js b/toolkit/devtools/styleinspector/test/browser_computedview_select-and-copy-styles.js
new file mode 100644
index 000000000..356ea9de3
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_computedview_select-and-copy-styles.js
@@ -0,0 +1,118 @@
+/* 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";
+
+// Tests that properties can be selected and copied from the computed view
+
+XPCOMUtils.defineLazyGetter(this, "osString", function() {
+ return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
+});
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,computed view copy test");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = '<style type="text/css"> ' +
+ 'span { font-variant-caps: small-caps; color: #000000; } ' +
+ '.nomatches {color: #ff0000;}</style> <div id="first" style="margin: 10em; ' +
+ 'font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA">\n' +
+ '<h1>Some header text</h1>\n' +
+ '<p id="salutation" style="font-size: 12pt">hi.</p>\n' +
+ '<p id="body" style="font-size: 12pt">I am a test-case. This text exists ' +
+ 'solely to provide some things to <span style="color: yellow">' +
+ 'highlight</span> and <span style="font-weight: bold">count</span> ' +
+ 'style list-items in the box at right. If you are reading this, ' +
+ 'you should go do something else instead. Maybe read a book. Or better ' +
+ 'yet, write some test-cases for another bit of code. ' +
+ '<span style="font-style: italic">some text</span></p>\n' +
+ '<p id="closing">more text</p>\n' +
+ '<p>even more text</p>' +
+ '</div>';
+ content.document.title = "Computed view context menu test";
+
+ info("Opening the computed view");
+ let {toolbox, inspector, view} = yield openComputedView();
+
+ info("Selecting the test node");
+ yield selectNode("span", inspector);
+
+ yield checkCopySelection(view);
+ yield checkSelectAll(view);
+});
+
+function checkCopySelection(view) {
+ info("Testing selection copy");
+
+ let contentDocument = view.styleDocument;
+ let props = contentDocument.querySelectorAll(".property-view");
+ ok(props, "captain, we have the property-view nodes");
+
+ let range = contentDocument.createRange();
+ range.setStart(props[1], 0);
+ range.setEnd(props[3], 3);
+ contentDocument.defaultView.getSelection().addRange(range);
+
+ info("Checking that cssHtmlTree.siBoundCopy() returns the correct clipboard value");
+
+ let expectedPattern = "font-family: helvetica,sans-serif;[\\r\\n]+" +
+ "font-size: 16px;[\\r\\n]+" +
+ "font-variant-caps: small-caps;[\\r\\n]*";
+
+ return waitForClipboard(() => {
+ fireCopyEvent(props[0]);
+ }, () => {
+ return checkClipboardData(expectedPattern);
+ }).then(() => {}, () => {
+ failedClipboard(expectedPattern);
+ });
+}
+
+function checkSelectAll(view) {
+ info("Testing select-all copy");
+
+ let contentDoc = view.styleDocument;
+ let prop = contentDoc.querySelector(".property-view");
+
+ info("Checking that _SelectAll() then copy returns the correct clipboard value");
+ view._onSelectAll();
+ let expectedPattern = "color: #FF0;[\\r\\n]+" +
+ "font-family: helvetica,sans-serif;[\\r\\n]+" +
+ "font-size: 16px;[\\r\\n]+" +
+ "font-variant-caps: small-caps;[\\r\\n]*";
+
+ return waitForClipboard(() => {
+ fireCopyEvent(prop);
+ }, () => {
+ return checkClipboardData(expectedPattern);
+ }).then(() => {}, () => {
+ failedClipboard(expectedPattern);
+ });
+}
+
+function checkClipboardData(expectedPattern) {
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+ let expectedRegExp = new RegExp(expectedPattern, "g");
+ return expectedRegExp.test(actual);
+}
+
+function failedClipboard(expectedPattern) {
+ // Format expected text for comparison
+ let terminator = osString == "WINNT" ? "\r\n" : "\n";
+ expectedPattern = expectedPattern.replace(/\[\\r\\n\][+*]/g, terminator);
+ expectedPattern = expectedPattern.replace(/\\\(/g, "(");
+ expectedPattern = expectedPattern.replace(/\\\)/g, ")");
+
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+
+ // Trim the right hand side of our strings. This is because expectedPattern
+ // accounts for windows sometimes adding a newline to our copied data.
+ expectedPattern = expectedPattern.trimRight();
+ actual = actual.trimRight();
+
+ dump("TEST-UNEXPECTED-FAIL | Clipboard text does not match expected ... " +
+ "results (escaped for accurate comparison):\n");
+ info("Actual: " + escape(actual));
+ info("Expected: " + escape(expectedPattern));
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_style-editor-link.js b/toolkit/devtools/styleinspector/test/browser_computedview_style-editor-link.js
new file mode 100644
index 000000000..7f56cf233
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_computedview_style-editor-link.js
@@ -0,0 +1,146 @@
+/* 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";
+
+///////////////////
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source");
+
+// Test the links from the computed view to the style editor
+
+const STYLESHEET_URL = "data:text/css,"+encodeURIComponent(
+ [".highlight {",
+ "color: blue",
+ "}"].join("\n"));
+
+const DOCUMENT_URL = "data:text/html;charset=utf-8,"+encodeURIComponent(
+ ['<html>' +
+ '<head>' +
+ '<title>Computed view style editor link test</title>',
+ '<style type="text/css"> ',
+ 'html { color: #000000; } ',
+ 'span { font-variant: small-caps; color: #000000; } ',
+ '.nomatches {color: #ff0000;}</style> <div id="first" style="margin: 10em; ',
+ 'font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA">',
+ '</style>',
+ '<style>',
+ 'div { color: #f06; }',
+ '</style>',
+ '<link rel="stylesheet" type="text/css" href="'+STYLESHEET_URL+'">',
+ '</head>',
+ '<body>',
+ '<h1>Some header text</h1>',
+ '<p id="salutation" style="font-size: 12pt">hi.</p>',
+ '<p id="body" style="font-size: 12pt">I am a test-case. This text exists ',
+ 'solely to provide some things to ',
+ '<span style="color: yellow" class="highlight">',
+ 'highlight</span> and <span style="font-weight: bold">count</span> ',
+ 'style list-items in the box at right. If you are reading this, ',
+ 'you should go do something else instead. Maybe read a book. Or better ',
+ 'yet, write some test-cases for another bit of code. ',
+ '<span style="font-style: italic">some text</span></p>',
+ '<p id="closing">more text</p>',
+ '<p>even more text</p>',
+ '</div>',
+ '</body>',
+ '</html>'].join("\n"));
+
+add_task(function*() {
+ yield addTab(DOCUMENT_URL);
+
+ info("Opening the computed-view");
+ let {toolbox, inspector, view} = yield openComputedView();
+
+ info("Selecting the test node");
+ yield selectNode("span", inspector);
+
+ yield testInlineStyle(view, inspector);
+ yield testFirstInlineStyleSheet(view, toolbox);
+ yield testSecondInlineStyleSheet(view, toolbox);
+ yield testExternalStyleSheet(view, toolbox);
+});
+
+function* testInlineStyle(view, inspector) {
+ info("Testing inline style");
+
+ yield expandComputedViewPropertyByIndex(view, 0);
+
+ let onWindow = waitForWindow();
+ info("Clicking on the first rule-link in the computed-view");
+ clickLinkByIndex(view, 0);
+
+ let win = yield onWindow;
+
+ let windowType = win.document.documentElement.getAttribute("windowtype");
+ is(windowType, "navigator:view-source", "View source window is open");
+ info("Closing window");
+ win.close();
+}
+
+function* testFirstInlineStyleSheet(view, toolbox) {
+ info("Testing inline stylesheet");
+
+ info("Listening for toolbox switch to the styleeditor");
+ let onSwitch = waitForStyleEditor(toolbox);
+
+ info("Clicking an inline stylesheet");
+ clickLinkByIndex(view, 2);
+ let editor = yield onSwitch;
+
+ ok(true, "Switched to the style-editor panel in the toolbox");
+
+ validateStyleEditorSheet(editor, 0);
+}
+
+function* testSecondInlineStyleSheet(view, toolbox) {
+ info("Testing second inline stylesheet");
+
+ info("Waiting for the stylesheet editor to be selected");
+ let panel = toolbox.getCurrentPanel();
+ let onSelected = panel.UI.once("editor-selected");
+
+ info("Switching back to the inspector panel in the toolbox");
+ yield toolbox.selectTool("inspector");
+
+ info("Clicking on second inline stylesheet link");
+ clickLinkByIndex(view, 4);
+ let editor = yield onSelected;
+
+ is(toolbox.currentToolId, "styleeditor", "The style editor is selected again");
+ validateStyleEditorSheet(editor, 1);
+}
+
+function* testExternalStyleSheet(view, toolbox) {
+ info("Testing external stylesheet");
+
+ info("Waiting for the stylesheet editor to be selected");
+ let panel = toolbox.getCurrentPanel();
+ let onSelected = panel.UI.once("editor-selected");
+
+ info("Switching back to the inspector panel in the toolbox");
+ yield toolbox.selectTool("inspector");
+
+ info("Clicking on an external stylesheet link");
+ clickLinkByIndex(view, 1);
+ let editor = yield onSelected;
+
+ is(toolbox.currentToolId, "styleeditor", "The style editor is selected again");
+ validateStyleEditorSheet(editor, 2);
+}
+
+function validateStyleEditorSheet(editor, expectedSheetIndex) {
+ info("Validating style editor stylesheet");
+ let sheet = content.document.styleSheets[expectedSheetIndex];
+ is(editor.styleSheet.href, sheet.href, "loaded stylesheet matches document stylesheet");
+}
+
+function clickLinkByIndex(view, index) {
+ let link = getComputedViewLinkByIndex(view, index);
+ link.scrollIntoView();
+ link.click();
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-and-reselect.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-and-reselect.js
new file mode 100644
index 000000000..03a45e4c6
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-and-reselect.js
@@ -0,0 +1,43 @@
+/* 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";
+
+// Tests that adding properties to rules work and reselecting the element still
+// show them
+
+const TEST_URI = TEST_URL_ROOT + "doc_content_stylesheet.html";
+
+add_task(function*() {
+ yield addTab(TEST_URI);
+
+ let target = getNode("#target");
+
+ let {toolbox, inspector, view} = yield openRuleView();
+ yield selectNode("#target", inspector);
+
+ info("Setting a font-weight property on all rules");
+ setPropertyOnAllRules(view);
+
+ info("Reselecting the element");
+ yield selectNode("body", inspector);
+ yield selectNode("#target", inspector);
+
+ checkPropertyOnAllRules(view);
+});
+
+function setPropertyOnAllRules(view) {
+ for (let rule of view._elementStyle.rules) {
+ rule.editor.addProperty("font-weight", "bold", "");
+ }
+}
+
+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");
+ }
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_01.js
new file mode 100644
index 000000000..215c1e9c8
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_01.js
@@ -0,0 +1,55 @@
+/* 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";
+
+// Testing various inplace-editor behaviors in the rule-view
+
+let TEST_URL = 'url("' + TEST_URL_ROOT + 'doc_test_image.png")';
+let PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' #testid {',
+ ' background-color: blue;',
+ ' }',
+ ' .testclass {',
+ ' background-color: green;',
+ ' }',
+ '</style>',
+ '<div id="testid" class="testclass">Styled Node</div>'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,test rule view user changes");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = PAGE_CONTENT;
+
+ info("Opening the rule-view");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test element");
+ yield selectNode("#testid", inspector);
+
+ yield testCancelNew(view);
+});
+
+function* testCancelNew(view) {
+ info("Test adding a new rule to the element's style declaration and leaving it empty.");
+
+ let elementRuleEditor = getRuleViewRuleEditor(view, 0);
+
+ info("Focusing a new property name in the rule-view");
+ let editor = yield focusEditableField(elementRuleEditor.closeBrace);
+ is(inplaceEditor(elementRuleEditor.newPropSpan), editor, "The new property editor got focused");
+
+ info("Bluring the editor input");
+ let onBlur = once(editor.input, "blur");
+ editor.input.blur();
+ yield onBlur;
+
+ info("Checking the state of canceling a new property name editor");
+ ok(!elementRuleEditor.rule._applyingModifications, "Shouldn't have an outstanding request after a cancel.");
+ is(elementRuleEditor.rule.textProps.length, 0, "Should have canceled creating a new text property.");
+ ok(!elementRuleEditor.propertyList.hasChildNodes(), "Should not have any properties.");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_02.js
new file mode 100644
index 000000000..6346284c7
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_02.js
@@ -0,0 +1,79 @@
+/* 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";
+
+// Testing various inplace-editor behaviors in the rule-view
+
+let TEST_URL = 'url("' + TEST_URL_ROOT + 'doc_test_image.png")';
+let PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' #testid {',
+ ' background-color: blue;',
+ ' }',
+ '</style>',
+ '<div id="testid">Styled Node</div>'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,test rule view user changes");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = PAGE_CONTENT;
+
+ info("Opening the rule-view");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test element");
+ yield selectNode("#testid", inspector);
+
+ info("Test creating a new property and escaping");
+
+ let elementRuleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing a new property name in the rule-view");
+ let editor = yield focusEditableField(elementRuleEditor.closeBrace);
+
+ is(inplaceEditor(elementRuleEditor.newPropSpan), editor, "The new property editor got focused.");
+ let input = editor.input;
+
+ info("Entering a value in the property name editor");
+ let onModifications = elementRuleEditor.rule._applyingModifications;
+ input.value = "color";
+ yield onModifications;
+
+ info("Pressing return to commit and focus the new value field");
+ let onValueFocus = once(elementRuleEditor.element, "focus", true);
+ onModifications = elementRuleEditor.rule._applyingModifications;
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+ yield onValueFocus;
+ yield onModifications;
+
+ // Getting the new value editor after focus
+ editor = inplaceEditor(view.doc.activeElement);
+ let textProp = elementRuleEditor.rule.textProps[1];
+
+ is(elementRuleEditor.rule.textProps.length, 2, "Created a new text property.");
+ is(elementRuleEditor.propertyList.children.length, 2, "Created a property editor.");
+ is(editor, inplaceEditor(textProp.editor.valueSpan), "Editing the value span now.");
+
+ info("Entering a property value");
+ editor.input.value = "red";
+
+ info("Escaping out of the field");
+ onModifications = elementRuleEditor.rule._applyingModifications;
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
+ yield onModifications;
+
+ info("Checking that the previous field is focused");
+ let focusedElement = inplaceEditor(elementRuleEditor.rule.textProps[0].editor.valueSpan).input;
+ is(focusedElement, focusedElement.ownerDocument.activeElement, "Correct element has focus");
+
+ onModifications = elementRuleEditor.rule._applyingModifications;
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
+ yield onModifications;
+
+ is(elementRuleEditor.rule.textProps.length, 1, "Removed the new text property.");
+ is(elementRuleEditor.propertyList.children.length, 1, "Removed the property editor.");
+});
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_03.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_03.js
new file mode 100644
index 000000000..aafd54b46
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_03.js
@@ -0,0 +1,68 @@
+/* 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 cancelling the addition of a new property in the rule-view
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,browser_ruleview_ui.js");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Creating the test document");
+ let style = "" +
+ "#testid {" +
+ " background-color: blue;" +
+ "}" +
+ ".testclass, .unmatched {" +
+ " background-color: green;" +
+ "}";
+ let styleNode = addStyle(content.document, style);
+ content.document.body.innerHTML = "<div id='testid' class='testclass'>Styled Node</div>" +
+ "<div id='testid2'>Styled Node</div>";
+
+ yield testCancelNew(inspector, view);
+ yield testCancelNewOnEscape(inspector, view);
+ yield inspector.once("inspector-updated");
+});
+
+function* testCancelNew(inspector, ruleView) {
+ // Start at the beginning: start to add a rule to the element's style
+ // declaration, but leave it empty.
+
+ let elementRuleEditor = getRuleViewRuleEditor(ruleView, 0);
+ let editor = yield focusEditableField(elementRuleEditor.closeBrace);
+
+ is(inplaceEditor(elementRuleEditor.newPropSpan), editor,
+ "Property editor is focused");
+
+ let onBlur = once(editor.input, "blur");
+ editor.input.blur();
+ yield onBlur;
+
+ ok(!elementRuleEditor.rule._applyingModifications, "Shouldn't have an outstanding modification request after a cancel.");
+ is(elementRuleEditor.rule.textProps.length, 0, "Should have canceled creating a new text property.");
+ ok(!elementRuleEditor.propertyList.hasChildNodes(), "Should not have any properties.");
+}
+
+function* testCancelNewOnEscape(inspector, ruleView) {
+ // Start at the beginning: start to add a rule to the element's style
+ // declaration, add some text, then press escape.
+
+ let elementRuleEditor = getRuleViewRuleEditor(ruleView, 0);
+ let editor = yield focusEditableField(elementRuleEditor.closeBrace);
+
+ is(inplaceEditor(elementRuleEditor.newPropSpan), editor, "Next focused editor should be the new property editor.");
+ for (let ch of "background") {
+ EventUtils.sendChar(ch, ruleView.doc.defaultView);
+ }
+
+ let onBlur = once(editor.input, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield onBlur;
+
+ ok(!elementRuleEditor.rule._applyingModifications, "Shouldn't have an outstanding modification request after a cancel.");
+ is(elementRuleEditor.rule.textProps.length, 0, "Should have canceled creating a new text property.");
+ ok(!elementRuleEditor.propertyList.hasChildNodes(), "Should not have any properties.");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_add-property_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property_01.js
new file mode 100644
index 000000000..a7f2a6d9a
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property_01.js
@@ -0,0 +1,79 @@
+/* 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";
+
+// Testing various inplace-editor behaviors in the rule-view
+// FIXME: To be split in several test files, and some of the inplace-editor
+// focus/blur/commit/revert stuff should be factored out in head.js
+
+let TEST_URL = 'url("' + TEST_URL_ROOT + 'doc_test_image.png")';
+let PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' #testid {',
+ ' background-color: blue;',
+ ' }',
+ ' .testclass {',
+ ' background-color: green;',
+ ' }',
+ '</style>',
+ '<div id="testid" class="testclass">Styled Node</div>'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,test rule view user changes");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = PAGE_CONTENT;
+
+ info("Opening the rule-view");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test element");
+ yield selectNode("#testid", inspector);
+
+ yield testCreateNew(view);
+});
+
+function* testCreateNew(view) {
+ info("Test creating a new property");
+
+ let elementRuleEditor = getRuleViewRuleEditor(view, 0);
+
+ info("Focusing a new property name in the rule-view");
+ let editor = yield focusEditableField(elementRuleEditor.closeBrace);
+
+ is(inplaceEditor(elementRuleEditor.newPropSpan), editor, "The new property editor got focused");
+ let input = editor.input;
+
+ info("Entering background-color in the property name editor");
+ input.value = "background-color";
+
+ info("Pressing return to commit and focus the new value field");
+ let onValueFocus = once(elementRuleEditor.element, "focus", true);
+ let onModifications = elementRuleEditor.rule._applyingModifications;
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+ yield onValueFocus;
+ yield onModifications;
+
+ // Getting the new value editor after focus
+ editor = inplaceEditor(view.doc.activeElement);
+ let textProp = elementRuleEditor.rule.textProps[0];
+
+ is(elementRuleEditor.rule.textProps.length, 1, "Created a new text property.");
+ is(elementRuleEditor.propertyList.children.length, 1, "Created a property editor.");
+ is(editor, inplaceEditor(textProp.editor.valueSpan), "Editing the value span now.");
+
+ info("Entering a value and bluring the field to expect a rule change");
+ editor.input.value = "#XYZ";
+ let onBlur = once(editor.input, "blur");
+ onModifications = elementRuleEditor.rule._applyingModifications;
+ editor.input.blur();
+ yield onBlur;
+ yield onModifications;
+
+ is(textProp.value, "#XYZ", "Text prop should have been changed.");
+ is(textProp.overridden, false, "Property should not be overridden");
+ is(textProp.editor.isValid(), false, "#XYZ should not be a valid entry");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_add-property_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property_02.js
new file mode 100644
index 000000000..a8beb0fff
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property_02.js
@@ -0,0 +1,70 @@
+/* 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 all sorts of additions and updates of properties in the rule-view
+// FIXME: TO BE SPLIT IN *MANY* SMALLER TESTS
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,browser_ruleview_ui.js");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Creating the test document");
+ let style = "" +
+ "#testid {" +
+ " background-color: blue;" +
+ "}" +
+ ".testclass, .unmatched {" +
+ " background-color: green;" +
+ "}";
+ let styleNode = addStyle(content.document, style);
+ content.document.body.innerHTML = "<div id='testid' class='testclass'>Styled Node</div>" +
+ "<div id='testid2'>Styled Node</div>";
+
+ yield testCreateNew(inspector, view);
+ yield inspector.once("inspector-updated");
+});
+
+function* testCreateNew(inspector, ruleView) {
+ // Create a new property.
+ let elementRuleEditor = getRuleViewRuleEditor(ruleView, 0);
+ let editor = yield focusEditableField(elementRuleEditor.closeBrace);
+
+ is(inplaceEditor(elementRuleEditor.newPropSpan), editor,
+ "Next focused editor should be the new property editor.");
+
+ let input = editor.input;
+
+ 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, {}, ruleView.doc.defaultView);
+ input.select();
+
+ info("Entering the property name");
+ input.value = "background-color";
+
+ info("Pressing RETURN and waiting for the value field focus");
+ let onFocus = once(elementRuleEditor.element, "focus", true);
+ EventUtils.sendKey("return", ruleView.doc.defaultView);
+ yield onFocus;
+ yield elementRuleEditor.rule._applyingModifications;
+
+ editor = inplaceEditor(ruleView.doc.activeElement);
+
+ is(elementRuleEditor.rule.textProps.length, 1, "Should have created a new text property.");
+ is(elementRuleEditor.propertyList.children.length, 1, "Should have created a property editor.");
+ let textProp = elementRuleEditor.rule.textProps[0];
+ is(editor, inplaceEditor(textProp.editor.valueSpan), "Should be editing the value span now.");
+
+ editor.input.value = "purple";
+ let onBlur = once(editor.input, "blur");
+ EventUtils.sendKey("return", ruleView.doc.defaultView);
+ yield onBlur;
+ yield elementRuleEditor.rule._applyingModifications;
+
+ is(textProp.value, "purple", "Text prop should have been changed.");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_01.js
new file mode 100644
index 000000000..51885ac37
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_01.js
@@ -0,0 +1,91 @@
+/* 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";
+
+// Tests the behaviour of adding a new rule to the rule view and the
+// various inplace-editor behaviours in the new rule editor
+
+let PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' .testclass {',
+ ' text-align: center;',
+ ' }',
+ '</style>',
+ '<div id="testid" class="testclass">Styled Node</div>',
+ '<span class="testclass2">This is a span</span>',
+ '<span class="class1 class2">Multiple classes</span>',
+ '<p>Empty<p>'
+].join("\n");
+
+const TEST_DATA = [
+ { node: "#testid", expected: "#testid" },
+ { node: ".testclass2", expected: ".testclass2" },
+ { node: ".class1.class2", expected: ".class1" },
+ { node: "p", expected: "p" }
+];
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,test rule view add rule");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = PAGE_CONTENT;
+
+ info("Opening the rule-view");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Iterating over the test data");
+ for (let data of TEST_DATA) {
+ yield runTestData(inspector, view, data);
+ }
+});
+
+function* runTestData(inspector, view, data) {
+ let {node, expected} = data;
+ info("Selecting the test element");
+ yield selectNode(node, inspector);
+
+ info("Waiting for context menu to be shown");
+ let onPopup = once(view._contextmenu, "popupshown");
+ let win = view.doc.defaultView;
+
+ EventUtils.synthesizeMouseAtCenter(view.element,
+ {button: 2, type: "contextmenu"}, win);
+ yield onPopup;
+
+ ok(!view.menuitemAddRule.hidden, "Add rule is visible");
+
+ info("Waiting for rule view to change");
+ let onRuleViewChanged = once(view.element, "CssRuleViewChanged");
+
+ info("Adding the new rule");
+ view.menuitemAddRule.click();
+ yield onRuleViewChanged;
+ view._contextmenu.hidePopup();
+
+ yield testNewRule(view, expected, 1);
+
+ info("Resetting page content");
+ content.document.body.innerHTML = PAGE_CONTENT;
+}
+
+function* testNewRule(view, expected, index) {
+ let idRuleEditor = getRuleViewRuleEditor(view, index);
+ let editor = idRuleEditor.selectorText.ownerDocument.activeElement;
+ is(editor.value, expected,
+ "Selector editor value is as expected: " + expected);
+
+ info("Entering the escape key");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+
+ is(idRuleEditor.selectorText.textContent, expected,
+ "Selector text value is as expected: " + expected);
+
+ info("Adding new properties to new rule: " + expected)
+ idRuleEditor.addProperty("font-weight", "bold", "");
+ let textProps = idRuleEditor.rule.textProps;
+ let lastRule = textProps[textProps.length - 1];
+ is(lastRule.name, "font-weight", "Last rule name is font-weight");
+ is(lastRule.value, "bold", "Last rule value is bold");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_02.js
new file mode 100644
index 000000000..11db0e265
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_02.js
@@ -0,0 +1,78 @@
+/* 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";
+
+// Tests the behaviour of adding a new rule to the rule view and editing
+// its selector
+
+let PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' #testid {',
+ ' text-align: center;',
+ ' }',
+ '</style>',
+ '<div id="testid">Styled Node</div>',
+ '<span>This is a span</span>'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,test rule view add rule");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = PAGE_CONTENT;
+
+ info("Opening the rule-view");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test element");
+ yield selectNode("#testid", inspector);
+
+ info("Waiting for context menu to be shown");
+ let onPopup = once(view._contextmenu, "popupshown");
+ let win = view.doc.defaultView;
+
+ EventUtils.synthesizeMouseAtCenter(view.element,
+ {button: 2, type: "contextmenu"}, win);
+ yield onPopup;
+
+ ok(!view.menuitemAddRule.hidden, "Add rule is visible");
+
+ info("Waiting for rule view to change");
+ let onRuleViewChanged = once(view.element, "CssRuleViewChanged");
+
+ info("Adding the new rule");
+ view.menuitemAddRule.click();
+ yield onRuleViewChanged;
+ view._contextmenu.hidePopup();
+
+ yield testEditSelector(view, "span");
+
+ info("Selecting the modified element");
+ yield selectNode("span", inspector);
+ yield checkModifiedElement(view, "span");
+});
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector field");
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = idRuleEditor.selectorText.ownerDocument.activeElement;
+
+ info("Entering a new selector name and committing");
+ editor.value = name;
+
+ info("Waiting for rule view to refresh");
+ let onRuleViewRefresh = once(view.element, "CssRuleViewRefreshed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewRefresh;
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+}
+
+function* checkModifiedElement(view, name) {
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_03.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_03.js
new file mode 100644
index 000000000..a67bd4ddb
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_03.js
@@ -0,0 +1,114 @@
+/* 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";
+
+// Tests the behaviour of adding a new rule to the rule view, adding a new
+// property and editing the selector
+
+let PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' #testid {',
+ ' text-align: center;',
+ ' }',
+ '</style>',
+ '<div id="testid">Styled Node</div>',
+ '<span>This is a span</span>'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,test rule view add rule");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = PAGE_CONTENT;
+
+ info("Opening the rule-view");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test element");
+ yield selectNode("#testid", inspector);
+
+ info("Waiting for context menu to be shown");
+ let onPopup = once(view._contextmenu, "popupshown");
+ let win = view.doc.defaultView;
+
+ EventUtils.synthesizeMouseAtCenter(view.element,
+ {button: 2, type: "contextmenu"}, win);
+ yield onPopup;
+
+ ok(!view.menuitemAddRule.hidden, "Add rule is visible");
+
+ info("Waiting for rule view to change");
+ let onRuleViewChanged = once(view.element, "CssRuleViewChanged");
+
+ info("Adding the new rule");
+ view.menuitemAddRule.click();
+ yield onRuleViewChanged;
+ view._contextmenu.hidePopup();
+
+ info("Adding new properties to the new rule");
+ yield testNewRule(view, "#testid", 1);
+
+ info("Editing existing selector field");
+ yield testEditSelector(view, "span");
+
+ info("Selecting the modified element");
+ yield selectNode("span", inspector);
+
+ info("Check new rule and property exist in the modified element");
+ yield checkModifiedElement(view, "span", 1);
+});
+
+function* testNewRule(view, expected, index) {
+ let idRuleEditor = getRuleViewRuleEditor(view, index);
+ let editor = idRuleEditor.selectorText.ownerDocument.activeElement;
+ is(editor.value, expected,
+ "Selector editor value is as expected: " + expected);
+
+ info("Entering the escape key");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+
+ is(idRuleEditor.selectorText.textContent, expected,
+ "Selector text value is as expected: " + expected);
+
+ info("Adding new properties to new rule: " + expected)
+ idRuleEditor.addProperty("font-weight", "bold", "");
+ let textProps = idRuleEditor.rule.textProps;
+ let lastRule = textProps[textProps.length - 1];
+ is(lastRule.name, "font-weight", "Last rule name is font-weight");
+ is(lastRule.value, "bold", "Last rule value is bold");
+}
+
+function* testEditSelector(view, name) {
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+
+ 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);
+ editor.input.value = name;
+
+ info("Waiting for rule view to refresh");
+ let onRuleViewRefresh = once(view.element, "CssRuleViewRefreshed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewRefresh;
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+}
+
+function* checkModifiedElement(view, name, index) {
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, index);
+ let textProps = idRuleEditor.rule.textProps;
+ let lastRule = textProps[textProps.length - 1];
+ is(lastRule.name, "font-weight", "Last rule name is font-weight");
+ is(lastRule.value, "bold", "Last rule value is bold");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_01.js
new file mode 100644
index 000000000..0d30f0246
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_01.js
@@ -0,0 +1,60 @@
+/* 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 after a color change, the image preview tooltip in the same
+// property is displayed and positioned correctly.
+// See bug 979292
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body {',
+ ' background: url("chrome://global/skin/icons/warning-64.png"), linear-gradient(white, #F06 400px);',
+ ' }',
+ '</style>',
+ 'Testing the color picker tooltip!'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test");
+ content.document.body.innerHTML = PAGE_CONTENT;
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ let value = getRuleViewProperty(view, "body", "background").valueSpan;
+ let swatch = value.querySelectorAll(".ruleview-colorswatch")[1];
+ let url = value.querySelector(".theme-link");
+ yield testImageTooltipAfterColorChange(swatch, url, view);
+});
+
+function* testImageTooltipAfterColorChange(swatch, url, ruleView) {
+ info("First, verify that the image preview tooltip works");
+ let anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip, url);
+ ok(anchor, "The image preview tooltip is shown on the url span");
+ is(anchor, url, "The anchor returned by the showOnHover callback is correct");
+
+ info("Open the color picker tooltip and change the color");
+ let picker = ruleView.tooltips.colorPicker;
+ let onShown = picker.tooltip.once("shown");
+ swatch.click();
+ yield onShown;
+ yield simulateColorPickerChange(picker, [0, 0, 0, 1], {
+ element: content.document.body,
+ name: "backgroundImage",
+ value: 'url("chrome://global/skin/icons/warning-64.png"), linear-gradient(rgb(0, 0, 0), rgb(255, 0, 102) 400px)'
+ });
+
+ let spectrum = yield picker.spectrum;
+ let onHidden = picker.tooltip.once("hidden");
+ EventUtils.sendKey("RETURN", spectrum.element.ownerDocument.defaultView);
+ yield onHidden;
+
+ info("Verify again that the image preview tooltip works");
+ // After a color change, the property is re-populated, we need to get the new
+ // dom node
+ url = getRuleViewProperty(ruleView, "body", "background").valueSpan.querySelector(".theme-link");
+ anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip, url);
+ ok(anchor, "The image preview tooltip is shown on the url span");
+ is(anchor, url, "The anchor returned by the showOnHover callback is correct");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_02.js
new file mode 100644
index 000000000..62b51e52e
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_02.js
@@ -0,0 +1,61 @@
+/* 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 after a color change, opening another tooltip, like the image
+// preview doesn't revert the color change in the ruleView.
+// This used to happen when the activeSwatch wasn't reset when the colorpicker
+// would hide.
+// See bug 979292
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body {',
+ ' background: red url("chrome://global/skin/icons/warning-64.png") no-repeat center center;',
+ ' }',
+ '</style>',
+ 'Testing the color picker tooltip!'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test");
+ content.document.body.innerHTML = PAGE_CONTENT;
+ let {toolbox, inspector, view} = yield openRuleView();
+ yield testColorChangeIsntRevertedWhenOtherTooltipIsShown(view);
+});
+
+function* testColorChangeIsntRevertedWhenOtherTooltipIsShown(ruleView) {
+ let swatch = getRuleViewProperty(ruleView, "body", "background").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ info("Open the color picker tooltip and change the color");
+ let picker = ruleView.tooltips.colorPicker;
+ let onShown = picker.tooltip.once("shown");
+ swatch.click();
+ yield onShown;
+
+ yield simulateColorPickerChange(picker, [0, 0, 0, 1], {
+ element: content.document.body,
+ name: "backgroundColor",
+ value: "rgb(0, 0, 0)"
+ });
+ let spectrum = yield picker.spectrum;
+ let onHidden = picker.tooltip.once("hidden");
+ EventUtils.sendKey("RETURN", spectrum.element.ownerDocument.defaultView);
+ yield onHidden;
+
+ info("Open the image preview tooltip");
+ let value = getRuleViewProperty(ruleView, "body", "background").valueSpan;
+ let url = value.querySelector(".theme-link");
+ onShown = ruleView.tooltips.previewTooltip.once("shown");
+ let anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip, url);
+ ruleView.tooltips.previewTooltip.show(anchor);
+ yield onShown;
+
+ info("Image tooltip is shown, verify that the swatch is still correct");
+ swatch = value.querySelector(".ruleview-colorswatch");
+ is(swatch.style.backgroundColor, "rgb(0, 0, 0)", "The swatch's color is correct");
+ is(swatch.nextSibling.textContent, "#000", "The color name is correct");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-appears-on-swatch-click.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-appears-on-swatch-click.js
new file mode 100644
index 000000000..806b35f09
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-appears-on-swatch-click.js
@@ -0,0 +1,54 @@
+/* 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 color pickers appear when clicking on color swatches
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body {',
+ ' color: red;',
+ ' background-color: #ededed;',
+ ' background-image: url(chrome://global/skin/icons/warning-64.png);',
+ ' border: 2em solid rgba(120, 120, 120, .5);',
+ ' }',
+ '</style>',
+ 'Testing the color picker tooltip!'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test");
+ content.document.body.innerHTML = PAGE_CONTENT;
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ let cSwatch = getRuleViewProperty(view, "body", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+ let bgSwatch = getRuleViewProperty(view, "body", "background-color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+ let bSwatch = getRuleViewProperty(view, "body", "border").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ for (let swatch of [cSwatch, bgSwatch, bSwatch]) {
+ info("Testing that the colorpicker appears colorswatch click");
+ yield testColorPickerAppearsOnColorSwatchClick(view, swatch);
+ }
+});
+
+function* testColorPickerAppearsOnColorSwatchClick(view, swatch) {
+ let cPicker = view.tooltips.colorPicker;
+ ok(cPicker, "The rule-view has the expected colorPicker property");
+
+ let cPickerPanel = cPicker.tooltip.panel;
+ ok(cPickerPanel, "The XUL panel for the color picker exists");
+
+ let onShown = cPicker.tooltip.once("shown");
+ swatch.click();
+ yield onShown;
+
+ ok(true, "The color picker was shown on click of the color swatch");
+ ok(!inplaceEditor(swatch.parentNode),
+ "The inplace editor wasn't shown as a result of the color swatch click");
+ cPicker.hide();
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-commit-on-ENTER.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-commit-on-ENTER.js
new file mode 100644
index 000000000..f866714e3
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-commit-on-ENTER.js
@@ -0,0 +1,59 @@
+/* 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 a color change in the color picker is committed when ENTER is pressed
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body {',
+ ' border: 2em solid rgba(120, 120, 120, .5);',
+ ' }',
+ '</style>',
+ 'Testing the color picker tooltip!'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test");
+ content.document.body.innerHTML = PAGE_CONTENT;
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ let swatch = getRuleViewProperty(view, "body" , "border").valueSpan
+ .querySelector(".ruleview-colorswatch");
+ yield testPressingEnterCommitsChanges(swatch, view);
+});
+
+function* testPressingEnterCommitsChanges(swatch, ruleView) {
+ let cPicker = ruleView.tooltips.colorPicker;
+
+ let onShown = cPicker.tooltip.once("shown");
+ swatch.click();
+ yield onShown;
+
+ yield simulateColorPickerChange(cPicker, [0, 255, 0, .5], {
+ element: content.document.body,
+ name: "borderLeftColor",
+ value: "rgba(0, 255, 0, 0.5)"
+ });
+
+ is(swatch.style.backgroundColor, "rgba(0, 255, 0, 0.5)",
+ "The color swatch's background was updated");
+ is(getRuleViewProperty(ruleView, "body", "border").valueSpan.textContent,
+ "2em solid rgba(0, 255, 0, 0.5)",
+ "The text of the border css property was updated");;
+
+ let spectrum = yield cPicker.spectrum;
+ let onHidden = cPicker.tooltip.once("hidden");
+ EventUtils.sendKey("RETURN", spectrum.element.ownerDocument.defaultView);
+ yield onHidden;
+
+ is(content.getComputedStyle(content.document.body).borderLeftColor,
+ "rgba(0, 255, 0, 0.5)", "The element's border was kept after RETURN");
+ is(swatch.style.backgroundColor, "rgba(0, 255, 0, 0.5)",
+ "The color swatch's background was kept after RETURN");
+ is(getRuleViewProperty(ruleView, "body", "border").valueSpan.textContent,
+ "2em solid rgba(0, 255, 0, 0.5)",
+ "The text of the border css property was kept after RETURN");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-edit-gradient.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-edit-gradient.js
new file mode 100644
index 000000000..7046369a6
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-edit-gradient.js
@@ -0,0 +1,75 @@
+/* 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 changing a color in a gradient css declaration using the tooltip
+// color picker works
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body {',
+ ' background-image: linear-gradient(to left, #f06 25%, #333 95%, #000 100%);',
+ ' }',
+ '</style>',
+ 'Updating a gradient declaration with the color picker tooltip'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = PAGE_CONTENT;
+
+ info("Opening the rule-view")
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Testing that the colors in gradient properties are parsed correctly");
+ testColorParsing(view);
+
+ info("Testing that changing one of the colors of a gradient property works");
+ yield testPickingNewColor(view);
+});
+
+function testColorParsing(view) {
+ let ruleEl = getRuleViewProperty(view, "body", "background-image");
+ ok(ruleEl, "The background-image gradient declaration was found");
+
+ let swatchEls = ruleEl.valueSpan.querySelectorAll(".ruleview-colorswatch");
+ ok(swatchEls, "The color swatch elements were found");
+ is(swatchEls.length, 3, "There are 3 color swatches");
+
+ let colorEls = ruleEl.valueSpan.querySelectorAll(".ruleview-color");
+ ok(colorEls, "The color elements were found");
+ is(colorEls.length, 3, "There are 3 color values");
+
+ let colors = ["#F06", "#333", "#000"];
+ for (let i = 0; i < colors.length; i ++) {
+ is(colorEls[i].textContent, colors[i], "The right color value was found");
+ }
+}
+
+function* testPickingNewColor(view) {
+ // Grab the first color swatch and color in the gradient
+ let ruleEl = getRuleViewProperty(view, "body", "background-image");
+ let swatchEl = ruleEl.valueSpan.querySelector(".ruleview-colorswatch");
+ let colorEl = ruleEl.valueSpan.querySelector(".ruleview-color");
+
+ info("Getting the color picker tooltip and clicking on the swatch to show it");
+ let cPicker = view.tooltips.colorPicker;
+ let onShown = cPicker.tooltip.once("shown");
+ swatchEl.click();
+ yield onShown;
+
+ yield simulateColorPickerChange(cPicker, [1, 1, 1, 1]);
+
+ is(swatchEl.style.backgroundColor, "rgb(1, 1, 1)",
+ "The color swatch's background was updated");
+ is(colorEl.textContent, "#010101", "The color text was updated");
+ is(content.getComputedStyle(content.document.body).backgroundImage,
+ "linear-gradient(to left, rgb(255, 0, 102) 25%, rgb(51, 51, 51) 95%, rgb(0, 0, 0) 100%)",
+ "The gradient has been updated correctly");
+
+ cPicker.hide();
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-hides-on-tooltip.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-hides-on-tooltip.js
new file mode 100644
index 000000000..369378082
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-hides-on-tooltip.js
@@ -0,0 +1,48 @@
+/* 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 the color picker tooltip hides when an image tooltip appears
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body {',
+ ' color: red;',
+ ' background-color: #ededed;',
+ ' background-image: url(chrome://global/skin/icons/warning-64.png);',
+ ' border: 2em solid rgba(120, 120, 120, .5);',
+ ' }',
+ '</style>',
+ 'Testing the color picker tooltip!'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test");
+ content.document.body.innerHTML = PAGE_CONTENT;
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ let swatch = getRuleViewProperty(view, "body", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ yield testColorPickerHidesWhenImageTooltipAppears(view, swatch);
+});
+
+function* testColorPickerHidesWhenImageTooltipAppears(view, swatch) {
+ let bgImageSpan = getRuleViewProperty(view, "body", "background-image").valueSpan;
+ let uriSpan = bgImageSpan.querySelector(".theme-link");
+ let tooltip = view.tooltips.colorPicker.tooltip;
+
+ info("Showing the color picker tooltip by clicking on the color swatch");
+ let onShown = tooltip.once("shown");
+ swatch.click();
+ yield onShown;
+
+ info("Now showing the image preview tooltip to hide the color picker");
+ let onHidden = tooltip.once("hidden");
+ yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
+ yield onHidden;
+
+ ok(true, "The color picker closed when the image preview tooltip appeared");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-multiple-changes.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-multiple-changes.js
new file mode 100644
index 000000000..24bef27ed
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-multiple-changes.js
@@ -0,0 +1,126 @@
+/* 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 the color in the colorpicker tooltip can be changed several times
+// without causing error in various cases:
+// - simple single-color property (color)
+// - color and image property (background-image)
+// - overridden property
+// See bug 979292 and bug 980225
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body {',
+ ' color: green;',
+ ' background: red url("chrome://global/skin/icons/warning-64.png") no-repeat center center;',
+ ' }',
+ ' p {',
+ ' color: blue;',
+ ' }',
+ '</style>',
+ '<p>Testing the color picker tooltip!</p>'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test");
+ content.document.body.innerHTML = PAGE_CONTENT;
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ yield testSimpleMultipleColorChanges(inspector, view);
+ yield testComplexMultipleColorChanges(inspector, view);
+ yield testOverriddenMultipleColorChanges(inspector, view);
+});
+
+function* testSimpleMultipleColorChanges(inspector, ruleView) {
+ yield selectNode("p", inspector);
+
+ info("Getting the <p> tag's color property");
+ let swatch = getRuleViewProperty(ruleView, "p", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ info("Opening the color picker");
+ let picker = ruleView.tooltips.colorPicker;
+ let onShown = picker.tooltip.once("shown");
+ swatch.click();
+ yield onShown;
+
+ info("Changing the color several times");
+ let colors = [
+ {rgba: [0, 0, 0, 1], computed: "rgb(0, 0, 0)"},
+ {rgba: [100, 100, 100, 1], computed: "rgb(100, 100, 100)"},
+ {rgba: [200, 200, 200, 1], computed: "rgb(200, 200, 200)"}
+ ];
+ for (let {rgba, computed} of colors) {
+ yield simulateColorPickerChange(picker, rgba, {
+ element: content.document.querySelector("p"),
+ name: "color",
+ value: computed
+ });
+ }
+}
+
+function* testComplexMultipleColorChanges(inspector, ruleView) {
+ yield selectNode("body", inspector);
+
+ info("Getting the <body> tag's color property");
+ let swatch = getRuleViewProperty(ruleView, "body", "background").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ info("Opening the color picker");
+ let picker = ruleView.tooltips.colorPicker;
+ let onShown = picker.tooltip.once("shown");
+ swatch.click();
+ yield onShown;
+
+ info("Changing the color several times");
+ let colors = [
+ {rgba: [0, 0, 0, 1], computed: "rgb(0, 0, 0)"},
+ {rgba: [100, 100, 100, 1], computed: "rgb(100, 100, 100)"},
+ {rgba: [200, 200, 200, 1], computed: "rgb(200, 200, 200)"}
+ ];
+ for (let {rgba, computed} of colors) {
+ yield simulateColorPickerChange(picker, rgba, {
+ element: content.document.body,
+ name: "backgroundColor",
+ value: computed
+ });
+ }
+
+ info("Closing the color picker");
+ let onHidden = picker.tooltip.once("hidden");
+ picker.tooltip.hide();
+ yield onHidden;
+}
+
+function* testOverriddenMultipleColorChanges(inspector, ruleView) {
+ yield selectNode("p", inspector);
+
+ info("Getting the <body> tag's color property");
+ let swatch = getRuleViewProperty(ruleView, "body", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ info("Opening the color picker");
+ let picker = ruleView.tooltips.colorPicker;
+ let onShown = picker.tooltip.once("shown");
+ swatch.click();
+ yield onShown;
+
+ info("Changing the color several times");
+ let colors = [
+ {rgba: [0, 0, 0, 1], computed: "rgb(0, 0, 0)"},
+ {rgba: [100, 100, 100, 1], computed: "rgb(100, 100, 100)"},
+ {rgba: [200, 200, 200, 1], computed: "rgb(200, 200, 200)"}
+ ];
+ for (let {rgba, computed} of colors) {
+ yield simulateColorPickerChange(picker, rgba, {
+ element: content.document.body,
+ name: "color",
+ value: computed
+ });
+ is(content.getComputedStyle(content.document.querySelector("p")).color,
+ "rgb(200, 200, 200)", "The color of the P tag is still correct");
+ }
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-revert-on-ESC.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-revert-on-ESC.js
new file mode 100644
index 000000000..f2b4e6c2a
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-revert-on-ESC.js
@@ -0,0 +1,56 @@
+/* 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 a color change in the color picker is reverted when ESC is pressed
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body {',
+ ' background-color: #ededed;',
+ ' }',
+ '</style>',
+ 'Testing the color picker tooltip!'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test");
+ content.document.body.innerHTML = PAGE_CONTENT;
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ let swatch = getRuleViewProperty(view, "body", "background-color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+ yield testPressingEscapeRevertsChanges(swatch, view);
+});
+
+function* testPressingEscapeRevertsChanges(swatch, ruleView) {
+ let cPicker = ruleView.tooltips.colorPicker;
+
+ let onShown = cPicker.tooltip.once("shown");
+ swatch.click();
+ yield onShown;
+
+ yield simulateColorPickerChange(cPicker, [0, 0, 0, 1], {
+ element: content.document.body,
+ name: "backgroundColor",
+ value: "rgb(0, 0, 0)"
+ });
+
+ is(swatch.style.backgroundColor, "rgb(0, 0, 0)",
+ "The color swatch's background was updated");
+ is(getRuleViewProperty(ruleView, "body", "background-color").valueSpan.textContent,
+ "#000", "The text of the background-color css property was updated");
+
+ let spectrum = yield cPicker.spectrum;
+
+ // ESC out of the color picker
+ let onHidden = cPicker.tooltip.once("hidden");
+ EventUtils.sendKey("ESCAPE", spectrum.element.ownerDocument.defaultView);
+ yield onHidden;
+
+ yield waitForSuccess(() => {
+ return content.getComputedStyle(content.document.body).backgroundColor === "rgb(237, 237, 237)";
+ }, "The element's background-color was reverted");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-swatch-displayed.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-swatch-displayed.js
new file mode 100644
index 000000000..56b94a463
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-swatch-displayed.js
@@ -0,0 +1,55 @@
+/* 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 color swatches are displayed next to colors in the rule-view
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body {',
+ ' color: red;',
+ ' background-color: #ededed;',
+ ' background-image: url(chrome://global/skin/icons/warning-64.png);',
+ ' border: 2em solid rgba(120, 120, 120, .5);',
+ ' }',
+ ' * {',
+ ' color: blue;',
+ ' box-shadow: inset 0 0 2px 20px red, inset 0 0 2px 40px blue;',
+ ' }',
+ '</style>',
+ 'Testing the color picker tooltip!'
+].join("\n");
+
+// Tests that properties in the rule-view contain color swatches
+// Each entry in the test array should contain:
+// {
+// selector: the rule-view selector to look for the property in
+// propertyName: the property to test
+// nb: the number of color swatches this property should have
+// }
+const TESTS = [
+ {selector: "body", propertyName: "color", nb: 1},
+ {selector: "body", propertyName: "background-color", nb: 1},
+ {selector: "body", propertyName: "border", nb: 1},
+ {selector: "*", propertyName: "color", nb: 1},
+ {selector: "*", propertyName: "box-shadow", nb: 2},
+];
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test");
+ content.document.body.innerHTML = PAGE_CONTENT;
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ for (let {selector, propertyName, nb} of TESTS) {
+ info("Looking for color swatches in property " + propertyName +
+ " in selector " + selector);
+
+ let prop = getRuleViewProperty(view, selector, propertyName).valueSpan;
+ let swatches = prop.querySelectorAll(".ruleview-colorswatch");
+
+ ok(swatches.length, "Swatches found in the property");
+ is(swatches.length, nb, "Correct number of swatches found in the property");
+ }
+});
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_completion-existing-property_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_completion-existing-property_01.js
new file mode 100644
index 000000000..f8c7aa715
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_completion-existing-property_01.js
@@ -0,0 +1,104 @@
+/* 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 CSS property names are autocompleted and cycled correctly when
+// editing an existing property in the rule view
+
+const MAX_ENTRIES = 10;
+
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// selectedIndex of the popup,
+// total items in the popup
+// ]
+let testData = [
+ ["VK_RIGHT", "font", -1, 0],
+ ["-","font-family", 0, MAX_ENTRIES],
+ ["f","font-family", 0, 2],
+ ["VK_BACK_SPACE", "font-f", -1, 0],
+ ["VK_BACK_SPACE", "font-", -1, 0],
+ ["VK_BACK_SPACE", "font", -1, 0],
+ ["VK_BACK_SPACE", "fon", -1, 0],
+ ["VK_BACK_SPACE", "fo", -1, 0],
+ ["VK_BACK_SPACE", "f", -1, 0],
+ ["VK_BACK_SPACE", "", -1, 0],
+ ["d", "direction", 0, 3],
+ ["VK_DOWN", "display", 1, 3],
+ ["VK_DOWN", "dominant-baseline", 2, 3],
+ ["VK_DOWN", "direction", 0, 3],
+ ["VK_DOWN", "display", 1, 3],
+ ["VK_UP", "direction", 0, 3],
+ ["VK_UP", "dominant-baseline", 2, 3],
+ ["VK_UP", "display", 1, 3],
+ ["VK_BACK_SPACE", "d", -1, 0],
+ ["i", "direction", 0, 2],
+ ["s", "display", -1, 0],
+ ["VK_BACK_SPACE", "dis", -1, 0],
+ ["VK_BACK_SPACE", "di", -1, 0],
+ ["VK_BACK_SPACE", "d", -1, 0],
+ ["VK_BACK_SPACE", "", -1, 0],
+ ["f", "fill", 0, MAX_ENTRIES],
+ ["i", "fill", 0, 4],
+ ["VK_LEFT", "fill", -1, 0],
+ ["VK_LEFT", "fill", -1, 0],
+ ["i", "fiill", -1, 0],
+ ["VK_ESCAPE", null, -1, 0],
+];
+
+let TEST_URL = "data:text/html;charset=utf-8,<h1 style='font: 24px serif'>Filename" +
+ ": browser_bug893965_css_property_completion_existing_property.js</h1>";
+
+add_task(function*() {
+ yield addTab(TEST_URL);
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing the css property editable field");
+ let propertyName = view.doc.querySelectorAll(".ruleview-propertyname")[0];
+ let editor = yield focusEditableField(propertyName);
+
+ info("Starting to test for css property completion");
+ for (let i = 0; i < testData.length; i ++) {
+ yield testCompletion(testData[i], editor, view);
+ }
+});
+
+function* testCompletion([key, completion, index, total], editor, view) {
+ info("Pressing key " + key);
+ info("Expecting " + completion + ", " + index + ", " + total);
+
+ let onSuggest;
+
+ if (/(left|right|back_space|escape)/ig.test(key)) {
+ info("Adding event listener for left|right|back_space|escape keys");
+ onSuggest = once(editor.input, "keypress");
+ } else {
+ info("Waiting for after-suggest event on the editor");
+ onSuggest = editor.once("after-suggest");
+ }
+
+ info("Synthesizing key " + key);
+ EventUtils.synthesizeKey(key, {}, view.doc.defaultView);
+
+ yield onSuggest;
+ yield wait(1); // Equivalent of executeSoon
+
+ info("Checking the state");
+ if (completion != null) {
+ is(editor.input.value, completion, "Correct value is autocompleted");
+ }
+ if (total == 0) {
+ ok(!(editor.popup && editor.popup.isOpen), "Popup is closed");
+ } else {
+ ok(editor.popup._panel.state == "open" || editor.popup._panel.state == "showing", "Popup is open");
+ is(editor.popup.getItems().length, total, "Number of suggestions match");
+ is(editor.popup.selectedIndex, index, "Correct item is selected");
+ }
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_completion-existing-property_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_completion-existing-property_02.js
new file mode 100644
index 000000000..439d51c69
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_completion-existing-property_02.js
@@ -0,0 +1,101 @@
+/* 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 CSS property names and values are autocompleted and cycled correctly
+// when editing existing properties in the rule view
+
+// format :
+// [
+// what key to press,
+// modifers,
+// expected input box value after keypress,
+// selectedIndex of the popup,
+// total items in the popup
+// ]
+let testData = [
+ ["b", {}, "beige", 0, 8],
+ ["l", {}, "black", 0, 4],
+ ["VK_DOWN", {}, "blanchedalmond", 1, 4],
+ ["VK_DOWN", {}, "blue", 2, 4],
+ ["VK_RIGHT", {}, "blue", -1, 0],
+ [" ", {}, "blue !important", 0, 10],
+ ["!", {}, "blue !important", 0, 0],
+ ["VK_BACK_SPACE", {}, "blue !", -1, 0],
+ ["VK_BACK_SPACE", {}, "blue ", -1, 0],
+ ["VK_BACK_SPACE", {}, "blue", -1, 0],
+ ["VK_TAB", {shiftKey: true}, "color", -1, 0],
+ ["VK_BACK_SPACE", {}, "", -1, 0],
+ ["d", {}, "direction", 0, 3],
+ ["i", {}, "direction", 0, 2],
+ ["s", {}, "display", -1, 0],
+ ["VK_TAB", {}, "blue", -1, 0],
+ ["n", {}, "none", -1, 0],
+ ["VK_RETURN", {}, null, -1, 0]
+];
+
+let TEST_URL = "data:text/html;charset=utf-8,<h1 style='color: red'>Filename: " +
+ "browser_bug894376_css_value_completion_existing_property_value_pair.js</h1>";
+
+add_task(function*() {
+ yield addTab(TEST_URL);
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing the css property editable value");
+ let value = view.doc.querySelectorAll(".ruleview-propertyvalue")[0];
+ let editor = yield focusEditableField(value);
+
+ info("Starting to test for css property completion");
+ for (let i = 0; i < testData.length; i ++) {
+ // Re-define the editor at each iteration, because the focus may have moved
+ // from property to value and back
+ editor = inplaceEditor(view.doc.activeElement);
+ yield testCompletion(testData[i], editor, view);
+ }
+});
+
+function* testCompletion([key, modifiers, completion, index, total], editor, view) {
+ info("Pressing key " + key);
+ info("Expecting " + completion + ", " + index + ", " + total);
+
+ let onKeyPress;
+
+ if (/tab/ig.test(key)) {
+ info("Waiting for the new property or value editor to get focused");
+ let brace = view.doc.querySelector(".ruleview-ruleclose");
+ onKeyPress = once(brace.parentNode, "focus", true);
+ } else if (/(right|return|back_space)/ig.test(key)) {
+ info("Adding event listener for right|return|back_space keys");
+ onKeyPress = once(editor.input, "keypress");
+ } else {
+ info("Waiting for after-suggest event on the editor");
+ onKeyPress = editor.once("after-suggest");
+ }
+
+ info("Synthesizing key " + key + ", modifiers: " + Object.keys(modifiers));
+ EventUtils.synthesizeKey(key, modifiers, view.doc.defaultView);
+
+ yield onKeyPress;
+ yield wait(1); // Equivalent of executeSoon
+
+ // The key might have been a TAB or shift-TAB, in which case the editor will
+ // be a new one
+ editor = inplaceEditor(view.doc.activeElement);
+
+ info("Checking the state");
+ if (completion != null) {
+ is(editor.input.value, completion, "Correct value is autocompleted");
+ }
+ if (total == 0) {
+ ok(!(editor.popup && editor.popup.isOpen), "Popup is closed");
+ } else {
+ ok(editor.popup._panel.state == "open" || editor.popup._panel.state == "showing", "Popup is open");
+ is(editor.popup.getItems().length, total, "Number of suggestions match");
+ is(editor.popup.selectedIndex, index, "Correct item is selected");
+ }
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_completion-new-property_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_completion-new-property_01.js
new file mode 100644
index 000000000..13b7becb9
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_completion-new-property_01.js
@@ -0,0 +1,91 @@
+/* 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 CSS property names are autocompleted and cycled correctly when
+// creating a new property in the ruleview
+
+const MAX_ENTRIES = 10;
+
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// selectedIndex of the popup,
+// total items in the popup
+// ]
+let testData = [
+ ["d", "direction", 0, 3],
+ ["VK_DOWN", "display", 1, 3],
+ ["VK_DOWN", "dominant-baseline", 2, 3],
+ ["VK_DOWN", "direction", 0, 3],
+ ["VK_DOWN", "display", 1, 3],
+ ["VK_UP", "direction", 0, 3],
+ ["VK_UP", "dominant-baseline", 2, 3],
+ ["VK_UP", "display", 1, 3],
+ ["VK_BACK_SPACE", "d", -1, 0],
+ ["i", "direction", 0, 2],
+ ["s", "display", -1, 0],
+ ["VK_BACK_SPACE", "dis", -1, 0],
+ ["VK_BACK_SPACE", "di", -1, 0],
+ ["VK_BACK_SPACE", "d", -1, 0],
+ ["VK_BACK_SPACE", "", -1, 0],
+ ["f", "fill", 0, MAX_ENTRIES],
+ ["i", "fill", 0, 4],
+ ["VK_ESCAPE", null, -1, 0],
+];
+
+let TEST_URL = "data:text/html;charset=utf-8,<h1 style='border: 1px solid red'>Filename:" +
+ "browser_bug893965_css_property_completion_new_property.js</h1>";
+
+add_task(function*() {
+ yield addTab(TEST_URL);
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing the css property editable field");
+ let brace = view.doc.querySelector(".ruleview-ruleclose");
+ let editor = yield focusEditableField(brace);
+
+ info("Starting to test for css property completion");
+ for (let i = 0; i < testData.length; i ++) {
+ yield testCompletion(testData[i], editor, view);
+ }
+});
+
+function* testCompletion([key, completion, index, total], editor, view) {
+ info("Pressing key " + key);
+ info("Expecting " + completion + ", " + index + ", " + total);
+
+ let onSuggest;
+
+ if (/(right|back_space|escape)/ig.test(key)) {
+ info("Adding event listener for right|back_space|escape keys");
+ onSuggest = once(editor.input, "keypress");
+ } else {
+ info("Waiting for after-suggest event on the editor");
+ onSuggest = editor.once("after-suggest");
+ }
+
+ info("Synthesizing key " + key);
+ EventUtils.synthesizeKey(key, {}, view.doc.defaultView);
+
+ yield onSuggest;
+ yield wait(1); // Equivalent of executeSoon
+
+ info("Checking the state");
+ if (completion != null) {
+ is(editor.input.value, completion, "Correct value is autocompleted");
+ }
+ if (total == 0) {
+ ok(!(editor.popup && editor.popup.isOpen), "Popup is closed");
+ } else {
+ ok(editor.popup._panel.state == "open" || editor.popup._panel.state == "showing", "Popup is open");
+ is(editor.popup.getItems().length, total, "Number of suggestions match");
+ is(editor.popup.selectedIndex, index, "Correct item is selected");
+ }
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_completion-new-property_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_completion-new-property_02.js
new file mode 100644
index 000000000..f26eaec16
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_completion-new-property_02.js
@@ -0,0 +1,104 @@
+/* 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 CSS property names and values are autocompleted and cycled correctly
+// when editing new properties in the rule view
+
+// format :
+// [
+// what key to press,
+// modifers,
+// expected input box value after keypress,
+// selectedIndex of the popup,
+// total items in the popup
+// ]
+let testData = [
+ ["a", {accelKey: true, ctrlKey: true}, "", -1, 0],
+ ["d", {}, "direction", 0, 3],
+ ["VK_DOWN", {}, "display", 1, 3],
+ ["VK_TAB", {}, "", -1, 10],
+ ["VK_DOWN", {}, "-moz-box", 0, 10],
+ ["n", {}, "none", -1, 0],
+ ["VK_TAB", {shiftKey: true}, "display", -1, 0],
+ ["VK_BACK_SPACE", {}, "", -1, 0],
+ ["c", {}, "caption-side", 0, 10],
+ ["o", {}, "color", 0, 6],
+ ["VK_TAB", {}, "none", -1, 0],
+ ["r", {}, "rebeccapurple", 0, 6],
+ ["VK_DOWN", {}, "red", 1, 6],
+ ["VK_DOWN", {}, "rgb", 2, 6],
+ ["VK_DOWN", {}, "rgba", 3, 6],
+ ["VK_DOWN", {}, "rosybrown", 4, 6],
+ ["VK_DOWN", {}, "royalblue", 5, 6],
+ ["VK_RIGHT", {}, "royalblue", -1, 0],
+ [" ", {}, "royalblue !important", 0, 10],
+ ["!", {}, "royalblue !important", 0, 0],
+ ["VK_ESCAPE", {}, null, -1, 0]
+];
+
+let TEST_URL = "data:text/html;charset=utf-8,<style>h1{border: 1px solid red}</style>" +
+ "<h1>Test element</h1>";
+
+add_task(function*() {
+ yield addTab(TEST_URL);
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing a new css property editable property");
+ let brace = view.doc.querySelectorAll(".ruleview-ruleclose")[1];
+ let editor = yield focusEditableField(brace);
+
+ info("Starting to test for css property completion");
+ for (let i = 0; i < testData.length; i ++) {
+ // Re-define the editor at each iteration, because the focus may have moved
+ // from property to value and back
+ editor = inplaceEditor(view.doc.activeElement);
+ yield testCompletion(testData[i], editor, view);
+ }
+});
+
+function* testCompletion([key, modifiers, completion, index, total], editor, view) {
+ info("Pressing key " + key);
+ info("Expecting " + completion + ", " + index + ", " + total);
+
+ let onKeyPress;
+
+ if (/tab/ig.test(key)) {
+ info("Waiting for the new property or value editor to get focused");
+ let brace = view.doc.querySelectorAll(".ruleview-ruleclose")[1];
+ onKeyPress = once(brace.parentNode, "focus", true);
+ } else if (/(right|back_space|escape|return)/ig.test(key) ||
+ (modifiers.accelKey || modifiers.ctrlKey)) {
+ info("Adding event listener for right|escape|back_space|return keys");
+ onKeyPress = once(editor.input, "keypress");
+ } else {
+ info("Waiting for after-suggest event on the editor");
+ onKeyPress = editor.once("after-suggest");
+ }
+
+ info("Synthesizing key " + key + ", modifiers: " + Object.keys(modifiers));
+ EventUtils.synthesizeKey(key, modifiers, view.doc.defaultView);
+
+ yield onKeyPress;
+ yield wait(1); // Equivalent of executeSoon
+
+ info("Checking the state");
+ if (completion != null) {
+ // The key might have been a TAB or shift-TAB, in which case the editor will
+ // be a new one
+ editor = inplaceEditor(view.doc.activeElement);
+ is(editor.input.value, completion, "Correct value is autocompleted");
+ }
+ if (total == 0) {
+ ok(!(editor.popup && editor.popup.isOpen), "Popup is closed");
+ } else {
+ ok(editor.popup._panel.state == "open" || editor.popup._panel.state == "showing", "Popup is open");
+ is(editor.popup.getItems().length, total, "Number of suggestions match");
+ is(editor.popup.selectedIndex, index, "Correct item is selected");
+ }
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_content_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_content_01.js
new file mode 100644
index 000000000..ff51123cf
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_content_01.js
@@ -0,0 +1,43 @@
+/* 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 the rule-view content is correct
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,browser_ruleview_content.js");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Creating the test document");
+ let style = "" +
+ "#testid {" +
+ " background-color: blue;" +
+ "}" +
+ ".testclass, .unmatched {" +
+ " background-color: green;" +
+ "}";
+ let styleNode = addStyle(content.document, style);
+ content.document.body.innerHTML = "<div id='testid' class='testclass'>Styled Node</div>" +
+ "<div id='testid2'>Styled Node</div>";
+
+ yield testContentAfterNodeSelection(inspector, view);
+});
+
+function* testContentAfterNodeSelection(inspector, ruleView) {
+ yield selectNode("#testid", inspector);
+ is(ruleView.element.querySelectorAll("#noResults").length, 0,
+ "After a highlight, no longer has a no-results element.");
+
+ yield clearCurrentNodeSelection(inspector)
+ is(ruleView.element.querySelectorAll("#noResults").length, 1,
+ "After highlighting null, has a no-results element again.");
+
+ yield selectNode("#testid", inspector);
+ let classEditor = getRuleViewRuleEditor(ruleView, 2);
+ is(classEditor.selectorText.querySelector(".ruleview-selector-matched").textContent,
+ ".testclass", ".textclass should be matched.");
+ is(classEditor.selectorText.querySelector(".ruleview-selector-unmatched").textContent,
+ ".unmatched", ".unmatched should not be matched.");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_content_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_content_02.js
new file mode 100644
index 000000000..c5764b8e3
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_content_02.js
@@ -0,0 +1,83 @@
+/* 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 the rule-view content when the inspector gets opened via the page
+// ctx-menu "inspect element"
+
+const CONTENT = '<body style="color:red;">\
+ <div style="color:blue;">\
+ <p style="color:green;">\
+ <span style="color:yellow;">test element</span>\
+ </p>\
+ </div>\
+ </body>';
+
+const STRINGS = Services.strings
+ .createBundle("chrome://global/locale/devtools/styleinspector.properties");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8," + CONTENT);
+
+ info("Getting the test element");
+ let element = getNode("span");
+
+ info("Opening the inspector using the content context-menu");
+ let onInspectorReady = gDevTools.once("inspector-ready");
+
+ document.popupNode = element;
+ let contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
+ let contextMenu = new nsContextMenu(contentAreaContextMenu);
+ yield contextMenu.inspectNode();
+
+ // Clean up context menu:
+ contextMenu.hiding();
+
+ yield onInspectorReady;
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+
+ info("Getting the inspector and making sure it is fully updated");
+ let inspector = toolbox.getPanel("inspector");
+ yield inspector.once("inspector-updated");
+
+ let view = inspector.sidebar.getWindowForTab("ruleview")["ruleview"].view;
+
+ checkRuleViewContent(view);
+});
+
+function checkRuleViewContent({doc}) {
+ info("Making sure the rule-view contains the expected content");
+
+ let headers = [...doc.querySelectorAll(".ruleview-header")];
+ is(headers.length, 3, "There are 3 headers for inherited rules");
+
+ is(headers[0].textContent,
+ STRINGS.formatStringFromName("rule.inheritedFrom", ["p"], 1),
+ "The first header is correct");
+ is(headers[1].textContent,
+ STRINGS.formatStringFromName("rule.inheritedFrom", ["div"], 1),
+ "The second header is correct");
+ is(headers[2].textContent,
+ STRINGS.formatStringFromName("rule.inheritedFrom", ["body"], 1),
+ "The third header is correct");
+
+ let rules = doc.querySelectorAll(".ruleview-rule");
+ is(rules.length, 4, "There are 4 rules in the view");
+
+ for (let rule of rules) {
+ let selector = rule.querySelector(".ruleview-selector");
+ is(selector.textContent,
+ STRINGS.GetStringFromName("rule.sourceElement"),
+ "The rule's selector is correct");
+
+ let propertyNames = [...rule.querySelectorAll(".ruleview-propertyname")];
+ is(propertyNames.length, 1, "There's only one property name, as expected");
+
+ let propertyValues = [...rule.querySelectorAll(".ruleview-propertyvalue")];
+ is(propertyValues.length, 1, "There's only one property value, as expected");
+ }
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-appears-on-swatch-click.js b/toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-appears-on-swatch-click.js
new file mode 100644
index 000000000..1e9132b4a
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-appears-on-swatch-click.js
@@ -0,0 +1,70 @@
+/* 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 cubic-bezier pickers appear when clicking on cubic-bezier swatches
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' div {',
+ ' animation: move 3s linear;',
+ ' transition: top 4s cubic-bezier(.1, 1.45, 1, -1.2);',
+ ' }',
+ ' .test {',
+ ' animation-timing-function: ease-in-out;',
+ ' transition-timing-function: ease-out;',
+ ' }',
+ '</style>',
+ '<div class="test">Testing the cubic-bezier tooltip!</div>'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,rule view cubic-bezier tooltip test");
+ content.document.body.innerHTML = PAGE_CONTENT;
+ let {toolbox, inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ let swatches = [];
+ swatches.push(
+ getRuleViewProperty(view, "div", "animation").valueSpan
+ .querySelector(".ruleview-bezierswatch")
+ );
+ swatches.push(
+ getRuleViewProperty(view, "div", "transition").valueSpan
+ .querySelector(".ruleview-bezierswatch")
+ );
+ swatches.push(
+ getRuleViewProperty(view, ".test", "animation-timing-function").valueSpan
+ .querySelector(".ruleview-bezierswatch")
+ );
+ swatches.push(
+ getRuleViewProperty(view, ".test", "transition-timing-function").valueSpan
+ .querySelector(".ruleview-bezierswatch")
+ );
+
+ for (let swatch of swatches) {
+ info("Testing that the cubic-bezier appears on cubicswatch click");
+ yield testAppears(view, swatch);
+ }
+});
+
+function* testAppears(view, swatch) {
+ ok(swatch, "The cubic-swatch exists");
+
+ let bezier = view.tooltips.cubicBezier;
+ ok(bezier, "The rule-view has the expected cubicBezier property");
+
+ let bezierPanel = bezier.tooltip.panel;
+ ok(bezierPanel, "The XUL panel for the cubic-bezier tooltip exists");
+
+ let onShown = bezier.tooltip.once("shown");
+ swatch.click();
+ yield onShown;
+
+ ok(true, "The cubic-bezier tooltip was shown on click of the cibuc swatch");
+ ok(!inplaceEditor(swatch.parentNode),
+ "The inplace editor wasn't shown as a result of the cibuc swatch click");
+ bezier.hide();
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-commit-on-ENTER.js b/toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-commit-on-ENTER.js
new file mode 100644
index 000000000..3095cbfa2
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-commit-on-ENTER.js
@@ -0,0 +1,61 @@
+/* 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 a curve change in the cubic-bezier tooltip is committed when ENTER
+// is pressed
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body {',
+ ' transition: top 2s linear;',
+ ' }',
+ '</style>'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,rule view cubic-bezier tooltip test");
+ content.document.body.innerHTML = PAGE_CONTENT;
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Getting the bezier swatch element");
+ let swatch = getRuleViewProperty(view, "body" , "transition").valueSpan
+ .querySelector(".ruleview-bezierswatch");
+
+ yield testPressingEnterCommitsChanges(swatch, view);
+});
+
+function* testPressingEnterCommitsChanges(swatch, ruleView) {
+ let bezierTooltip = ruleView.tooltips.cubicBezier;
+
+ info("Showing the tooltip");
+ let onShown = bezierTooltip.tooltip.once("shown");
+ swatch.click();
+ yield onShown;
+
+ let widget = yield bezierTooltip.widget;
+ info("Simulating a change of curve in the widget");
+ widget.coordinates = [0.1, 2, 0.9, -1];
+ let expected = "cubic-bezier(0.1, 2, 0.9, -1)";
+
+ yield waitForSuccess(() => {
+ return content.getComputedStyle(content.document.body).transitionTimingFunction === expected;
+ }, "Waiting for the change to be previewed on the element");
+
+ ok(getRuleViewProperty(ruleView, "body", "transition").valueSpan.textContent
+ .indexOf("cubic-bezier(") !== -1,
+ "The text of the timing-function was updated");
+
+ info("Sending RETURN key within the tooltip document");
+ let onHidden = bezierTooltip.tooltip.once("hidden");
+ EventUtils.sendKey("RETURN", widget.parent.ownerDocument.defaultView);
+ yield onHidden;
+
+ is(content.getComputedStyle(content.document.body).transitionTimingFunction,
+ expected, "The element's timing-function was kept after RETURN");
+ ok(getRuleViewProperty(ruleView, "body", "transition").valueSpan.textContent
+ .indexOf("cubic-bezier(") !== -1,
+ "The text of the timing-function was kept after RETURN");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-revert-on-ESC.js b/toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-revert-on-ESC.js
new file mode 100644
index 000000000..998ab2021
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-revert-on-ESC.js
@@ -0,0 +1,53 @@
+/* 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 changes made to the cubic-bezier timing-function in the cubic-bezier
+// tooltip are reverted when ESC is pressed
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body {',
+ ' animation-timing-function: linear;',
+ ' }',
+ '</style>',
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,rule view cubic-bezier tooltip test");
+ content.document.body.innerHTML = PAGE_CONTENT;
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Getting the bezier swatch element");
+ let swatch = getRuleViewProperty(view, "body", "animation-timing-function").valueSpan
+ .querySelector(".ruleview-bezierswatch");
+ yield testPressingEscapeRevertsChanges(swatch, view);
+});
+
+function* testPressingEscapeRevertsChanges(swatch, ruleView) {
+ let bezierTooltip = ruleView.tooltips.cubicBezier;
+
+ let onShown = bezierTooltip.tooltip.once("shown");
+ swatch.click();
+ yield onShown;
+
+ let widget = yield bezierTooltip.widget;
+ info("Simulating a change of curve in the widget");
+ widget.coordinates = [0.1, 2, 0.9, -1];
+ let expected = "cubic-bezier(0.1, 2, 0.9, -1)";
+
+ yield waitForSuccess(() => {
+ return content.getComputedStyle(content.document.body).animationTimingFunction === expected;
+ }, "Waiting for the change to be previewed on the element");
+
+ info("Pressing ESCAPE to close the tooltip");
+ let onHidden = bezierTooltip.tooltip.once("hidden");
+ EventUtils.sendKey("ESCAPE", widget.parent.ownerDocument.defaultView);
+ yield onHidden;
+
+ yield waitForSuccess(() => {
+ return content.getComputedStyle(content.document.body).animationTimingFunction === "cubic-bezier(0, 0, 1, 1)";
+ }, "Waiting for the change to be reverted on the element");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-commit.js b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-commit.js
new file mode 100644
index 000000000..6fbcec2a0
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-commit.js
@@ -0,0 +1,84 @@
+/* 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 original value is correctly displayed when ESCaping out of the
+// inplace editor in the style inspector.
+
+const originalValue = "#00F";
+
+// Test data format
+// {
+// value: what char sequence to type,
+// commitKey: what key to type to "commit" the change,
+// modifiers: commitKey modifiers,
+// expected: what value is expected as a result
+// }
+const testData = [
+ {value: "red", commitKey: "VK_ESCAPE", modifiers: {}, expected: originalValue},
+ {value: "red", commitKey: "VK_RETURN", modifiers: {}, expected: "red"},
+ {value: "invalid", commitKey: "VK_RETURN", modifiers: {}, expected: "invalid"},
+ {value: "blue", commitKey: "VK_TAB", modifiers: {shiftKey: true}, expected: "blue"}
+];
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,test escaping property change reverts back to original value");
+
+ info("Creating the test document");
+ createDocument();
+
+ info("Opening the rule view");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test node");
+ yield selectNode("#testid", inspector);
+
+ info("Iterating over the test data");
+ for (let data of testData) {
+ yield runTestData(view, data);
+ }
+});
+
+function createDocument() {
+ let style = '' +
+ '#testid {' +
+ ' color: ' + originalValue + ';' +
+ '}';
+
+ let node = content.document.createElement('style');
+ node.setAttribute("type", "text/css");
+ node.textContent = style;
+ content.document.getElementsByTagName("head")[0].appendChild(node);
+
+ content.document.body.innerHTML = '<div id="testid">Styled Node</div>';
+}
+
+function* runTestData(view, {value, commitKey, modifiers, expected}) {
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = idRuleEditor.rule.textProps[0].editor;
+
+ info("Focusing the inplace editor field");
+ let editor = yield focusEditableField(propEditor.valueSpan);
+ is(inplaceEditor(propEditor.valueSpan), editor, "Focused editor should be the value span.");
+
+ info("Entering test data " + value);
+ for (let ch of value) {
+ EventUtils.sendChar(ch, view.doc.defaultView);
+ }
+
+ info("Waiting for focus on the field");
+ let onBlur = once(editor.input, "blur");
+
+ info("Entering the commit key " + commitKey + " " + modifiers);
+ EventUtils.synthesizeKey(commitKey, modifiers);
+ yield onBlur;
+
+ if (commitKey === "VK_ESCAPE") {
+ is(propEditor.valueSpan.textContent, expected, "Value is as expected: " + expected);
+ } else {
+ yield once(view.element, "CssRuleViewChanged");
+ is(propEditor.valueSpan.textContent, expected, "Value is as expected: " + expected);
+ }
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-increments.js b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-increments.js
new file mode 100644
index 000000000..78dc7a165
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-increments.js
@@ -0,0 +1,181 @@
+/* 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 increasing/decreasing values in rule view using
+// arrow keys works correctly.
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,sample document for bug 722691");
+ createDocument();
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ yield selectNode("#test", inspector);
+
+ yield testMarginIncrements(view);
+ yield testVariousUnitIncrements(view);
+ yield testHexIncrements(view);
+ yield testRgbIncrements(view);
+ yield testShorthandIncrements(view);
+ yield testOddCases(view);
+});
+
+function createDocument() {
+ content.document.body.innerHTML = '' +
+ '<style>' +
+ ' #test {' +
+ ' margin-top:0px;' +
+ ' padding-top: 0px;' +
+ ' color:#000000;' +
+ ' background-color: #000000;' +
+ ' }' +
+ '</style>' +
+ '<div id="test"></div>';
+}
+
+function* testMarginIncrements(view) {
+ info("Testing keyboard increments on the margin property");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let marginPropEditor = idRuleEditor.rule.textProps[0].editor;
+
+ yield runIncrementTest(marginPropEditor, view, {
+ 1: {alt: true, start: "0px", end: "0.1px", selectAll: true},
+ 2: {start: "0px", end: "1px", selectAll: true},
+ 3: {shift: true, start: "0px", end: "10px", selectAll: true},
+ 4: {down: true, alt: true, start: "0.1px", end: "0px", selectAll: true},
+ 5: {down: true, start: "0px", end: "-1px", selectAll: true},
+ 6: {down: true, shift: true, start: "0px", end: "-10px", selectAll: true},
+ 7: {pageUp: true, shift: true, start: "0px", end: "100px", selectAll: true},
+ 8: {pageDown: true, shift: true, start: "0px", end: "-100px", selectAll: true}
+ });
+}
+
+function* testVariousUnitIncrements(view) {
+ info("Testing keyboard increments on values with various units");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let paddingPropEditor = idRuleEditor.rule.textProps[1].editor;
+
+ yield runIncrementTest(paddingPropEditor, view, {
+ 1: {start: "0px", end: "1px", selectAll: true},
+ 2: {start: "0pt", end: "1pt", selectAll: true},
+ 3: {start: "0pc", end: "1pc", selectAll: true},
+ 4: {start: "0em", end: "1em", selectAll: true},
+ 5: {start: "0%", end: "1%", selectAll: true},
+ 6: {start: "0in", end: "1in", selectAll: true},
+ 7: {start: "0cm", end: "1cm", selectAll: true},
+ 8: {start: "0mm", end: "1mm", selectAll: true},
+ 9: {start: "0ex", end: "1ex", selectAll: true}
+ });
+};
+
+function* testHexIncrements(view) {
+ info("Testing keyboard increments with hex colors");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let hexColorPropEditor = idRuleEditor.rule.textProps[2].editor;
+
+ yield runIncrementTest(hexColorPropEditor, view, {
+ 1: {start: "#CCCCCC", end: "#CDCDCD", selectAll: true},
+ 2: {shift: true, start: "#CCCCCC", end: "#DCDCDC", selectAll: true},
+ 3: {start: "#CCCCCC", end: "#CDCCCC", selection: [1,3]},
+ 4: {shift: true, start: "#CCCCCC", end: "#DCCCCC", selection: [1,3]},
+ 5: {start: "#FFFFFF", end: "#FFFFFF", selectAll: true},
+ 6: {down: true, shift: true, start: "#000000", end: "#000000", selectAll: true}
+ });
+};
+
+function* testRgbIncrements(view) {
+ info("Testing keyboard increments with rgb colors");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let rgbColorPropEditor = idRuleEditor.rule.textProps[3].editor;
+
+ yield runIncrementTest(rgbColorPropEditor, view, {
+ 1: {start: "rgb(0,0,0)", end: "rgb(0,1,0)", selection: [6,7]},
+ 2: {shift: true, start: "rgb(0,0,0)", end: "rgb(0,10,0)", selection: [6,7]},
+ 3: {start: "rgb(0,255,0)", end: "rgb(0,255,0)", selection: [6,9]},
+ 4: {shift: true, start: "rgb(0,250,0)", end: "rgb(0,255,0)", selection: [6,9]},
+ 5: {down: true, start: "rgb(0,0,0)", end: "rgb(0,0,0)", selection: [6,7]},
+ 6: {down: true, shift: true, start: "rgb(0,5,0)", end: "rgb(0,0,0)", selection: [6,7]}
+ });
+};
+
+function* testShorthandIncrements(view) {
+ info("Testing keyboard increments within shorthand values");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let paddingPropEditor = idRuleEditor.rule.textProps[1].editor;
+
+ yield runIncrementTest(paddingPropEditor, view, {
+ 1: {start: "0px 0px 0px 0px", end: "0px 1px 0px 0px", selection: [4,7]},
+ 2: {shift: true, start: "0px 0px 0px 0px", end: "0px 10px 0px 0px", selection: [4,7]},
+ 3: {start: "0px 0px 0px 0px", end: "1px 0px 0px 0px", selectAll: true},
+ 4: {shift: true, start: "0px 0px 0px 0px", end: "10px 0px 0px 0px", selectAll: true},
+ 5: {down: true, start: "0px 0px 0px 0px", end: "0px 0px -1px 0px", selection: [8,11]},
+ 6: {down: true, shift: true, start: "0px 0px 0px 0px", end: "-10px 0px 0px 0px", selectAll: true},
+ 7: {up: true, start: "0.1em .1em 0em 0em", end: "0.1em 1.1em 0em 0em", selection: [6, 9]},
+ 8: {up: true, alt: true, start: "0.1em .9em 0em 0em", end: "0.1em 1em 0em 0em", selection: [6, 9]},
+ 9: {up: true, shift: true, start: "0.2em .2em 0em 0em", end: "0.2em 10.2em 0em 0em", selection: [6, 9]}
+ });
+};
+
+function* testOddCases(view) {
+ info("Testing some more odd cases");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let marginPropEditor = idRuleEditor.rule.textProps[0].editor;
+
+ yield runIncrementTest(marginPropEditor, view, {
+ 1: {start: "98.7%", end: "99.7%", selection: [3,3]},
+ 2: {alt: true, start: "98.7%", end: "98.8%", selection: [3,3]},
+ 3: {start: "0", end: "1"},
+ 4: {down: true, start: "0", end: "-1"},
+ 5: {start: "'a=-1'", end: "'a=0'", selection: [4,4]},
+ 6: {start: "0 -1px", end: "0 0px", selection: [2,2]},
+ 7: {start: "url(-1)", end: "url(-1)", selection: [4,4]},
+ 8: {start: "url('test1.1.png')", end: "url('test1.2.png')", selection: [11,11]},
+ 9: {start: "url('test1.png')", end: "url('test2.png')", selection: [9,9]},
+ 10: {shift: true, start: "url('test1.1.png')", end: "url('test11.1.png')", selection: [9,9]},
+ 11: {down: true, start: "url('test-1.png')", end: "url('test-2.png')", selection: [9,11]},
+ 12: {start: "url('test1.1.png')", end: "url('test1.2.png')", selection: [11,12]},
+ 13: {down: true, alt: true, start: "url('test-0.png')", end: "url('test--0.1.png')", selection: [10,11]},
+ 14: {alt: true, start: "url('test--0.1.png')", end: "url('test-0.png')", selection: [10,14]}
+ });
+};
+
+function* runIncrementTest(propertyEditor, view, tests) {
+ let editor = yield focusEditableField(propertyEditor.valueSpan);
+
+ for(let test in tests) {
+ yield testIncrement(editor, tests[test], view, propertyEditor);
+ }
+}
+
+function* testIncrement(editor, options, view, {ruleEditor}) {
+ editor.input.value = options.start;
+ let input = editor.input;
+
+ if (options.selectAll) {
+ input.select();
+ } else if (options.selection) {
+ input.setSelectionRange(options.selection[0], options.selection[1]);
+ }
+
+ is(input.value, options.start, "Value initialized at " + options.start);
+
+ let onModifications = ruleEditor.rule._applyingModifications;
+ let onKeyUp = once(input, "keyup");
+ let key;
+ key = options.down ? "VK_DOWN" : "VK_UP";
+ key = options.pageDown ? "VK_PAGE_DOWN" : options.pageUp ? "VK_PAGE_UP" : key;
+ EventUtils.synthesizeKey(key, {altKey: options.alt, shiftKey: options.shift},
+ view.doc.defaultView);
+ yield onKeyUp;
+ yield onModifications;
+
+ is(editor.input.value, options.end, "Value changed to " + options.end);
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-order.js b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-order.js
new file mode 100644
index 000000000..7cfd6bcd7
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-order.js
@@ -0,0 +1,68 @@
+/* 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";
+
+// Checking properties orders and overrides in the rule-view
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,browser_ruleview_manipulation.js");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Creating the test document and getting the test node");
+ content.document.body.innerHTML = '<div id="testid">Styled Node</div>';
+ let element = getNode("#testid");
+
+ yield selectNode("#testid", inspector);
+
+ let elementStyle = view._elementStyle;
+ let elementRule = elementStyle.rules[0];
+
+ info("Checking rules insertion order and checking the applied style");
+ let firstProp = elementRule.createProperty("background-color", "green", "");
+ let secondProp = elementRule.createProperty("background-color", "blue", "");
+ is(elementRule.textProps[0], firstProp, "Rules should be in addition order.");
+ is(elementRule.textProps[1], secondProp, "Rules should be in addition order.");
+ yield elementRule._applyingModifications;
+ is(element.style.getPropertyValue("background-color"), "blue", "Second property should have been used.");
+
+ info("Removing the second property and checking the applied style again");
+ secondProp.remove();
+ yield elementRule._applyingModifications;
+ is(element.style.getPropertyValue("background-color"), "green", "After deleting second property, first should be used.");
+
+ info("Creating a new second property and checking that the insertion order is still the same");
+ secondProp = elementRule.createProperty("background-color", "blue", "");
+ yield elementRule._applyingModifications;
+ is(element.style.getPropertyValue("background-color"), "blue", "New property should be used.");
+ is(elementRule.textProps[0], firstProp, "Rules shouldn't have switched places.");
+ is(elementRule.textProps[1], secondProp, "Rules shouldn't have switched places.");
+
+ info("Disabling the second property and checking the applied style");
+ secondProp.setEnabled(false);
+ yield elementRule._applyingModifications;
+ is(element.style.getPropertyValue("background-color"), "green", "After disabling second property, first value should be used");
+
+ info("Disabling the first property too and checking the applied style");
+ firstProp.setEnabled(false);
+ yield elementRule._applyingModifications;
+ is(element.style.getPropertyValue("background-color"), "", "After disabling both properties, value should be empty.");
+
+ info("Re-enabling the second propertyt and checking the applied style");
+ secondProp.setEnabled(true);
+ yield elementRule._applyingModifications;
+ is(element.style.getPropertyValue("background-color"), "blue", "Value should be set correctly after re-enabling");
+
+ info("Re-enabling the first property and checking the insertion order is still respected");
+ firstProp.setEnabled(true);
+ yield elementRule._applyingModifications;
+ is(element.style.getPropertyValue("background-color"), "blue", "Re-enabling an earlier property shouldn't make it override a later property.");
+ is(elementRule.textProps[0], firstProp, "Rules shouldn't have switched places.");
+ is(elementRule.textProps[1], secondProp, "Rules shouldn't have switched places.");
+
+ info("Modifying the first property and checking the applied style");
+ firstProp.setValue("purple", "");
+ yield elementRule._applyingModifications;
+ is(element.style.getPropertyValue("background-color"), "blue", "Modifying an earlier property shouldn't override a later property.");
+});
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property_01.js
new file mode 100644
index 000000000..044d5f834
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property_01.js
@@ -0,0 +1,84 @@
+/* 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";
+
+// Testing various inplace-editor behaviors in the rule-view
+// FIXME: To be split in several test files, and some of the inplace-editor
+// focus/blur/commit/revert stuff should be factored out in head.js
+
+let TEST_URL = 'url("' + TEST_URL_ROOT + 'doc_test_image.png")';
+let PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' #testid {',
+ ' background-color: blue;',
+ ' }',
+ ' .testclass {',
+ ' background-color: green;',
+ ' }',
+ '</style>',
+ '<div id="testid" class="testclass">Styled Node</div>'
+].join("\n");
+
+add_task(function*() {
+ let tab = yield addTab("data:text/html;charset=utf-8,test rule view user changes");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = PAGE_CONTENT;
+
+ info("Opening the rule-view");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test element");
+ yield selectNode("#testid", inspector);
+
+ yield testEditProperty(view, "border-color", "red", tab.linkedBrowser);
+ yield testEditProperty(view, "background-image", TEST_URL, tab.linkedBrowser);
+});
+
+function* testEditProperty(view, name, value, browser) {
+ info("Test editing existing property name/value fields");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = idRuleEditor.rule.textProps[0].editor;
+
+ info("Focusing an existing property name in the rule-view");
+ let editor = yield focusEditableField(propEditor.nameSpan, 32, 1);
+
+ is(inplaceEditor(propEditor.nameSpan), editor, "The property name editor got focused");
+ let input = editor.input;
+
+ info("Entering a new property name, including : to commit and focus the value");
+ let onValueFocus = once(idRuleEditor.element, "focus", true);
+ let onModifications = idRuleEditor.rule._applyingModifications;
+ for (let ch of name + ":") {
+ EventUtils.sendChar(ch, view.doc.defaultView);
+ }
+ yield onValueFocus;
+ yield onModifications;
+
+ // Getting the value editor after focus
+ editor = inplaceEditor(view.doc.activeElement);
+ input = editor.input;
+ is(inplaceEditor(propEditor.valueSpan), editor, "Focus moved to the value.");
+
+ info("Entering a new value, including ; to commit and blur the value");
+ let onBlur = once(input, "blur");
+ onModifications = idRuleEditor.rule._applyingModifications;
+ for (let ch of value + ";") {
+ EventUtils.sendChar(ch, view.doc.defaultView);
+ }
+ yield onBlur;
+ yield onModifications;
+
+ is(propEditor.isValid(), true, value + " is a valid entry");
+
+ info("Checking that the style property was changed on the content page");
+ let propValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name
+ });
+ is(propValue, value, name + " should have been set.");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property_02.js
new file mode 100644
index 000000000..5086873f6
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property_02.js
@@ -0,0 +1,142 @@
+/* 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 several types of rule-view property edition
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,browser_ruleview_ui.js");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Creating the test document");
+ let style = "" +
+ "#testid {" +
+ " background-color: blue;" +
+ "}" +
+ ".testclass, .unmatched {" +
+ " background-color: green;" +
+ "}";
+ let styleNode = addStyle(content.document, style);
+ content.document.body.innerHTML = "<div id='testid' class='testclass'>Styled Node</div>" +
+ "<div id='testid2'>Styled Node</div>";
+
+ yield selectNode("#testid", inspector);
+ yield testEditProperty(inspector, view);
+ yield testDisableProperty(inspector, view);
+ yield testPropertyStillMarkedDirty(inspector, view);
+
+ gBrowser.removeCurrentTab();
+});
+
+function* testEditProperty(inspector, ruleView) {
+ let idRuleEditor = getRuleViewRuleEditor(ruleView, 1);
+ let propEditor = idRuleEditor.rule.textProps[0].editor;
+
+ let editor = yield focusEditableField(propEditor.nameSpan);
+ let input = editor.input;
+ is(inplaceEditor(propEditor.nameSpan), editor, "Next focused editor should be the name 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, {}, ruleView.doc.defaultView);
+ input.select();
+
+ info("Entering property name \"border-color\" followed by a colon to focus the value");
+ let onFocus = once(idRuleEditor.element, "focus", true);
+ for (let ch of "border-color:") {
+ EventUtils.sendChar(ch, ruleView.doc.defaultView);
+ }
+ yield onFocus;
+ yield idRuleEditor.rule._applyingModifications;
+
+ info("Verifying that the focused field is the valueSpan");
+ editor = inplaceEditor(ruleView.doc.activeElement);
+ input = editor.input;
+ is(inplaceEditor(propEditor.valueSpan), editor, "Focus should have moved to the value.");
+ ok(input.selectionStart === 0 && input.selectionEnd === input.value.length, "Editor contents are selected.");
+
+ info("Entering a value following by a semi-colon to commit it");
+ let onBlur = once(editor.input, "blur");
+ for (let ch of "red;") {
+ EventUtils.sendChar(ch, ruleView.doc.defaultView);
+ is(propEditor.warning.hidden, true,
+ "warning triangle is hidden or shown as appropriate");
+ }
+ yield onBlur;
+ yield idRuleEditor.rule._applyingModifications;
+
+ let newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "border-color"
+ });
+ is(newValue, "red", "border-color should have been set.");
+
+ info("Entering property name \"color\" followed by a colon to focus the value");
+ onFocus = once(idRuleEditor.element, "focus", true);
+ for (let ch of "color:") {
+ EventUtils.sendChar(ch, ruleView.doc.defaultView);
+ }
+ yield onFocus;
+
+ info("Verifying that the focused field is the valueSpan");
+ editor = inplaceEditor(ruleView.doc.activeElement);
+
+ info("Entering a value following by a semi-colon to commit it");
+ onBlur = once(editor.input, "blur");
+ for (let ch of "red;") {
+ EventUtils.sendChar(ch, ruleView.doc.defaultView);
+ }
+ yield onBlur;
+ yield idRuleEditor.rule._applyingModifications;
+
+ let props = ruleView.element.querySelectorAll(".ruleview-property");
+ for (let i = 0; i < props.length; i++) {
+ is(props[i].hasAttribute("dirty"), i <= 1,
+ "props[" + i + "] marked dirty as appropriate");
+ }
+}
+
+function* testDisableProperty(inspector, ruleView) {
+ let idRuleEditor = getRuleViewRuleEditor(ruleView, 1);
+ let propEditor = idRuleEditor.rule.textProps[0].editor;
+
+ info("Disabling a property");
+ propEditor.enable.click();
+ yield idRuleEditor.rule._applyingModifications;
+
+ let newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "border-color"
+ });
+ is(newValue, "", "Border-color should have been unset.");
+
+ info("Enabling the property again");
+ propEditor.enable.click();
+ yield idRuleEditor.rule._applyingModifications;
+
+ newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "border-color"
+ });
+ is(newValue, "red", "Border-color should have been reset.");
+}
+
+function* testPropertyStillMarkedDirty(inspector, ruleView) {
+ // Select an unstyled node.
+ yield selectNode("#testid2", inspector);
+
+ // Select the original node again.
+ yield selectNode("#testid", inspector);
+
+ let props = ruleView.element.querySelectorAll(".ruleview-property");
+ for (let i = 0; i < props.length; i++) {
+ is(props[i].hasAttribute("dirty"), i <= 1,
+ "props[" + i + "] marked dirty as appropriate");
+ }
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector-commit.js b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector-commit.js
new file mode 100644
index 000000000..5d5c7516a
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector-commit.js
@@ -0,0 +1,98 @@
+/* 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 selector value is correctly displayed when committing the inplace editor
+// with ENTER, ESC, SHIFT+TAB and TAB
+
+let PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' #testid {',
+ ' text-align: center;',
+ ' }',
+ '</style>',
+ '<div id="testid" class="testclass">Styled Node</div>',
+].join("\n");
+
+const TEST_DATA = [
+ {
+ node: "#testid",
+ value: ".testclass",
+ commitKey: "VK_ESCAPE",
+ modifiers: {},
+ expected: "#testid"
+ },
+ {
+ node: "#testid",
+ value: ".testclass",
+ commitKey: "VK_RETURN",
+ modifiers: {},
+ expected: ".testclass"
+ },
+ {
+ node: "#testid",
+ value: ".testclass",
+ commitKey: "VK_TAB",
+ modifiers: {},
+ expected: ".testclass"
+ },
+ {
+ node: "#testid",
+ value: ".testclass",
+ commitKey: "VK_TAB",
+ modifiers: {shiftKey: true},
+ expected: ".testclass"
+ }
+];
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,test escaping selector change reverts back to original value");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = PAGE_CONTENT;
+
+ info("Opening the rule-view");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Iterating over the test data");
+ for (let data of TEST_DATA) {
+ yield runTestData(inspector, view, data);
+ }
+});
+
+function* runTestData(inspector, view, data) {
+ let {node, value, commitKey, modifiers, expected} = data;
+
+ info("Updating " + node + " to " + value + " and committing with " + commitKey + ". Expecting: " + expected);
+
+ info("Selecting the test element");
+ yield selectNode(node, inspector);
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+
+ 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("Enter the new selector value: " + value);
+ editor.input.value = value;
+
+ info("Entering the commit key " + commitKey + " " + modifiers);
+ EventUtils.synthesizeKey(commitKey, modifiers);
+
+ if (commitKey === "VK_ESCAPE") {
+ is(idRuleEditor.rule.selectorText, expected,
+ "Value is as expected: " + expected);
+ is(idRuleEditor.isEditing, false, "Selector is not being edited.")
+ } else {
+ yield once(view.element, "CssRuleViewRefreshed");
+ ok(getRuleViewRule(view, expected),
+ "Rule with " + name + " selector exists.");
+ }
+
+ info("Resetting page content");
+ content.document.body.innerHTML = PAGE_CONTENT;
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector_01.js
new file mode 100644
index 000000000..6a8dedc0a
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector_01.js
@@ -0,0 +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";
+
+// Testing selector inplace-editor behaviors in the rule-view
+
+let PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' .testclass {',
+ ' text-align: center;',
+ ' }',
+ '</style>',
+ '<div id="testid" class="testclass">Styled Node</div>',
+ '<span id="testid2">This is a span</span>'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,test rule view selector changes");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = PAGE_CONTENT;
+
+ info("Opening the rule-view");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test element");
+ yield selectNode("#testid", inspector);
+ yield testEditSelector(view, "span");
+
+ info("Selecting the modified element");
+ yield selectNode("#testid2", inspector);
+ yield checkModifiedElement(view, "span");
+});
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+
+ 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 and committing");
+ editor.input.value = name;
+
+ info("Waiting for rule view to refresh");
+ let onRuleViewRefresh = once(view.element, "CssRuleViewRefreshed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewRefresh;
+
+ is(view._elementStyle.rules.length, 1, "Should have 1 rule.");
+ is(getRuleViewRule(view, name), undefined,
+ name + " selector has been removed.");
+}
+
+function* checkModifiedElement(view, name) {
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js
new file mode 100644
index 000000000..52b96529f
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js
@@ -0,0 +1,81 @@
+/* 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";
+
+// Testing selector inplace-editor behaviors in the rule-view with pseudo
+// classes.
+
+let PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' .testclass {',
+ ' text-align: center;',
+ ' }',
+ ' #testid3:first-letter {',
+ ' text-decoration: "italic"',
+ ' }',
+ '</style>',
+ '<div id="testid">Styled Node</div>',
+ '<span class="testclass">This is a span</span>',
+ '<div class="testclass2">A</div>',
+ '<div id="testid3">B</div>'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,test rule view selector changes");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = PAGE_CONTENT;
+
+ info("Opening the rule-view");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test element");
+ yield selectNode(".testclass", inspector);
+ yield testEditSelector(view, "div:nth-child(2)");
+
+ info("Selecting the modified element");
+ yield selectNode("#testid", inspector);
+ yield checkModifiedElement(view, "div:nth-child(2)");
+
+ info("Selecting the test element");
+ yield selectNode("#testid3", inspector);
+ yield testEditSelector(view, ".testclass2::first-letter");
+
+ info("Selecting the modified element");
+ yield selectNode(".testclass2", inspector);
+ yield checkModifiedElement(view, ".testclass2::first-letter");
+});
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ 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);
+ editor.input.value = name;
+
+ info("Waiting for rule view to refresh");
+ let onRuleViewRefresh = once(view.element, "CssRuleViewRefreshed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewRefresh;
+
+ is(view._elementStyle.rules.length, 1, "Should have 1 rule.");
+ is(getRuleViewRule(view, name), undefined,
+ name + " selector has been removed.");
+}
+
+function* checkModifiedElement(view, name) {
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_eyedropper.js b/toolkit/devtools/styleinspector/test/browser_ruleview_eyedropper.js
new file mode 100644
index 000000000..afaa40e25
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_eyedropper.js
@@ -0,0 +1,195 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// So we can test collecting telemetry on the eyedropper
+let oldCanRecord = Services.telemetry.canRecord;
+Services.telemetry.canRecord = true;
+registerCleanupFunction(function () {
+ Services.telemetry.canRecord = oldCanRecord;
+});
+const HISTOGRAM_ID = "DEVTOOLS_PICKER_EYEDROPPER_OPENED_BOOLEAN";
+const FLAG_HISTOGRAM_ID = "DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG";
+const EXPECTED_TELEMETRY = {
+ "DEVTOOLS_PICKER_EYEDROPPER_OPENED_BOOLEAN": 2,
+ "DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG": 1
+}
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body {',
+ ' background-color: white;',
+ ' padding: 0px',
+ ' }',
+ '',
+ ' #div1 {',
+ ' background-color: #ff5;',
+ ' width: 20px;',
+ ' height: 20px;',
+ ' }',
+ '',
+ ' #div2 {',
+ ' margin-left: 20px;',
+ ' width: 20px;',
+ ' height: 20px;',
+ ' background-color: #f09;',
+ ' }',
+ '</style>',
+ '<body><div id="div1"></div><div id="div2"></div></body>'
+].join("\n");
+
+const ORIGINAL_COLOR = "rgb(255, 0, 153)"; // #f09
+const EXPECTED_COLOR = "rgb(255, 255, 85)"; // #ff5
+
+// Test opening the eyedropper from the color picker. Pressing escape
+// to close it, and clicking the page to select a color.
+
+add_task(function*() {
+ // clear telemetry so we can get accurate counts
+ clearTelemetry();
+
+ yield addTab("data:text/html;charset=utf-8,rule view eyedropper test");
+ content.document.body.innerHTML = PAGE_CONTENT;
+
+ let {toolbox, inspector, view} = yield openRuleView();
+ yield selectNode("#div2", inspector);
+
+ let property = getRuleViewProperty(view, "#div2", "background-color");
+ let swatch = property.valueSpan.querySelector(".ruleview-colorswatch");
+ ok(swatch, "Color swatch is displayed for the bg-color property");
+
+ let dropper = yield openEyedropper(view, swatch);
+
+ let tooltip = view.tooltips.colorPicker.tooltip;
+ ok(tooltip.isHidden(),
+ "color picker tooltip is closed after opening eyedropper");
+
+ yield testESC(swatch, dropper);
+
+ dropper = yield openEyedropper(view, swatch);
+
+ ok(dropper, "dropper opened");
+
+ yield testSelect(swatch, dropper);
+
+ checkTelemetry();
+});
+
+function testESC(swatch, dropper) {
+ let deferred = promise.defer();
+
+ dropper.once("destroy", () => {
+ let color = swatch.style.backgroundColor;
+ is(color, ORIGINAL_COLOR, "swatch didn't change after pressing ESC");
+
+ deferred.resolve();
+ });
+
+ inspectPage(dropper, false).then(pressESC);
+
+ return deferred.promise;
+}
+
+function testSelect(swatch, dropper) {
+ let deferred = promise.defer();
+
+ dropper.once("destroy", () => {
+ let color = swatch.style.backgroundColor;
+ is(color, EXPECTED_COLOR, "swatch changed colors");
+
+ // the change to the content is done async after rule view change
+ executeSoon(() => {
+ let element = content.document.querySelector("div");
+ is(content.window.getComputedStyle(element).backgroundColor,
+ EXPECTED_COLOR,
+ "div's color set to body color after dropper");
+
+ deferred.resolve();
+ });
+ });
+
+ inspectPage(dropper);
+
+ return deferred.promise;
+}
+
+function clearTelemetry() {
+ for (let histogramId in EXPECTED_TELEMETRY) {
+ let histogram = Services.telemetry.getHistogramById(histogramId);
+ histogram.clear();
+ }
+}
+
+function checkTelemetry() {
+ for (let histogramId in EXPECTED_TELEMETRY) {
+ let expected = EXPECTED_TELEMETRY[histogramId];
+ let histogram = Services.telemetry.getHistogramById(histogramId);
+ let snapshot = histogram.snapshot();
+
+ is (snapshot.counts[1], expected,
+ "eyedropper telemetry value correct for " + histogramId);
+ }
+}
+
+/* Helpers */
+
+function openEyedropper(view, swatch) {
+ let deferred = promise.defer();
+
+ let tooltip = view.tooltips.colorPicker.tooltip;
+
+ tooltip.once("shown", () => {
+ let tooltipDoc = tooltip.content.contentDocument;
+ let dropperButton = tooltipDoc.querySelector("#eyedropper-button");
+
+ tooltip.once("eyedropper-opened", (event, dropper) => {
+ deferred.resolve(dropper)
+ });
+ dropperButton.click();
+ });
+
+ swatch.click();
+ return deferred.promise;
+}
+
+function inspectPage(dropper, click=true) {
+ let target = document.documentElement;
+ let win = window;
+
+ // get location of the content, offset from browser window
+ let box = gBrowser.selectedBrowser.getBoundingClientRect();
+ let x = box.left + 1;
+ let y = box.top + 1;
+
+ return dropperStarted(dropper).then(() => {
+ EventUtils.synthesizeMouse(target, x, y, { type: "mousemove" }, win);
+
+ return dropperLoaded(dropper).then(() => {
+ EventUtils.synthesizeMouse(target, x + 10, y + 10, { type: "mousemove" }, win);
+
+ if (click) {
+ EventUtils.synthesizeMouse(target, x + 10, y + 10, {}, win);
+ }
+ });
+ });
+}
+
+function dropperStarted(dropper) {
+ if (dropper.isStarted) {
+ return promise.resolve();
+ }
+ return dropper.once("started");
+}
+
+function dropperLoaded(dropper) {
+ if (dropper.loaded) {
+ return promise.resolve();
+ }
+ return dropper.once("load");
+}
+
+function pressESC() {
+ EventUtils.synthesizeKey("VK_ESCAPE", { });
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_inherit.js b/toolkit/devtools/styleinspector/test/browser_ruleview_inherit.js
new file mode 100644
index 000000000..f8914e3b1
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_inherit.js
@@ -0,0 +1,86 @@
+/* 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";
+
+// Check that inherited properties appear as such in the rule-view
+
+let {ELEMENT_STYLE} = devtools.require("devtools/server/actors/styles");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,browser_inspector_changes.js");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ yield simpleInherit(inspector, view);
+ yield emptyInherit(inspector, view);
+ yield elementStyleInherit(inspector, view);
+});
+
+function* simpleInherit(inspector, view) {
+ let style = '' +
+ '#test2 {' +
+ ' background-color: green;' +
+ ' color: purple;' +
+ '}';
+
+ let styleNode = addStyle(content.document, style);
+ content.document.body.innerHTML = '<div id="test2"><div id="test1">Styled Node</div></div>';
+
+ yield selectNode("#test1", inspector);
+
+ let elementStyle = view._elementStyle;
+ is(elementStyle.rules.length, 2, "Should have 2 rules.");
+
+ let elementRule = elementStyle.rules[0];
+ ok(!elementRule.inherited, "Element style attribute should not consider itself inherited.");
+
+ let inheritRule = elementStyle.rules[1];
+ is(inheritRule.selectorText, "#test2", "Inherited rule should be the one that includes inheritable properties.");
+ ok(!!inheritRule.inherited, "Rule should consider itself inherited.");
+ is(inheritRule.textProps.length, 1, "Should only display one inherited style");
+ let inheritProp = inheritRule.textProps[0];
+ is(inheritProp.name, "color", "color should have been inherited.");
+
+ styleNode.remove();
+}
+
+function* emptyInherit(inspector, view) {
+ // No inheritable styles, this rule shouldn't show up.
+ let style = '' +
+ '#test2 {' +
+ ' background-color: green;' +
+ '}';
+
+ let styleNode = addStyle(content.document, style);
+ content.document.body.innerHTML = '<div id="test2"><div id="test1">Styled Node</div></div>';
+
+ yield selectNode("#test1", inspector);
+
+ let elementStyle = view._elementStyle;
+ is(elementStyle.rules.length, 1, "Should have 1 rule.");
+
+ let elementRule = elementStyle.rules[0];
+ ok(!elementRule.inherited, "Element style attribute should not consider itself inherited.");
+
+ styleNode.parentNode.removeChild(styleNode);
+}
+
+function* elementStyleInherit(inspector, view) {
+ content.document.body.innerHTML = '<div id="test2" style="color: red"><div id="test1">Styled Node</div></div>';
+
+ yield selectNode("#test1", inspector);
+
+ let elementStyle = view._elementStyle;
+ is(elementStyle.rules.length, 2, "Should have 2 rules.");
+
+ let elementRule = elementStyle.rules[0];
+ ok(!elementRule.inherited, "Element style attribute should not consider itself inherited.");
+
+ let inheritRule = elementStyle.rules[1];
+ is(inheritRule.domRule.type, ELEMENT_STYLE, "Inherited rule should be an element style, not a rule.");
+ ok(!!inheritRule.inherited, "Rule should consider itself inherited.");
+ is(inheritRule.textProps.length, 1, "Should only display one inherited style");
+ let inheritProp = inheritRule.textProps[0];
+ is(inheritProp.name, "color", "color should have been inherited.");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_keybindings.js b/toolkit/devtools/styleinspector/test/browser_ruleview_keybindings.js
new file mode 100644
index 000000000..9ffb85b3c
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_keybindings.js
@@ -0,0 +1,49 @@
+/* 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 focus doesn't leave the style editor when adding a property
+// (bug 719916)
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,<h1>Some header text</h1>");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Getting the ruleclose brace element");
+ let brace = view.doc.querySelector(".ruleview-ruleclose");
+
+ info("Clicking on the brace element to focus the new property field");
+ let onFocus = once(brace.parentNode, "focus", true);
+ brace.click();
+ yield onFocus;
+
+ info("Entering a property name");
+ let editor = getCurrentInplaceEditor(view);
+ editor.input.value = "color";
+
+ info("Typing ENTER to focus the next field: property value");
+ onFocus = once(brace.parentNode, "focus", true);
+ EventUtils.sendKey("return");
+ yield onFocus;
+ ok(true, "The value field was focused");
+
+ info("Entering a property value");
+ editor = getCurrentInplaceEditor(view);
+ editor.input.value = "green";
+
+ info("Typing ENTER again should focus a new property name");
+ onFocus = once(brace.parentNode, "focus", true);
+ EventUtils.sendKey("return");
+ yield onFocus;
+ ok(true, "The new property name field was focused");
+ getCurrentInplaceEditor(view).input.blur();
+});
+
+function getCurrentInplaceEditor(view) {
+ return inplaceEditor(view.doc.activeElement);
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_keyframes-rule_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_keyframes-rule_01.js
new file mode 100644
index 000000000..23a65ddf9
--- /dev/null
+++ b/toolkit/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";
+
+add_task(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(selector, 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;
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_keyframes-rule_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_keyframes-rule_02.js
new file mode 100644
index 000000000..93f70e815
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_keyframes-rule_02.js
@@ -0,0 +1,110 @@
+/* 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";
+
+add_task(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"
+ );
+
+ // Dynamic changes test disabled because of Bug 1050940
+
+ // 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");
+
+ // yield ruleEditor._applyingModifications;
+ // yield once(element, "animationend");
+
+ // is
+ // (
+ // convertTextPropsToString(rules.keyframeRules[1].textProps),
+ // "left: 750px; opacity: 0",
+ // "Keyframe pacman (100%) property is correct"
+ // );
+
+ // is(defaultView.getComputedStyle(element).getPropertyValue("opacity"), "0",
+ // "Added opacity 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(selector, 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};
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_livepreview.js b/toolkit/devtools/styleinspector/test/browser_ruleview_livepreview.js
new file mode 100644
index 000000000..c51e70e6f
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_livepreview.js
@@ -0,0 +1,71 @@
+/* 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 changes are previewed when editing a property value
+
+// Format
+// {
+// value : what to type in the field
+// expected : expected computed style on the targeted element
+// }
+const TEST_DATA = [
+ {value: "inline", expected: "inline"},
+ {value: "inline-block", expected: "inline-block"},
+
+ // Invalid property values should not apply, and should fall back to default
+ {value: "red", expected: "block"},
+ {value: "something", expected: "block"},
+
+ {escape: true, value: "inline", expected: "block"}
+];
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,test rule view live preview on user changes");
+
+ let style = '#testid {display:block;}';
+ let styleNode = addStyle(content.document, style);
+ content.document.body.innerHTML = '<div id="testid">Styled Node</div><span>inline element</span>';
+
+ let {toolbox, inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ for (let data of TEST_DATA) {
+ yield testLivePreviewData(data, view, "#testid");
+ }
+});
+
+function* testLivePreviewData(data, ruleView, selector) {
+ let testElement = getNode(selector);
+ let idRuleEditor = getRuleViewRuleEditor(ruleView, 1);
+ let propEditor = idRuleEditor.rule.textProps[0].editor;
+
+ info("Focusing the property value inplace-editor");
+ let editor = yield focusEditableField(propEditor.valueSpan);
+ is(inplaceEditor(propEditor.valueSpan), editor, "The focused editor is the value");
+
+ info("Enter a value in the editor")
+ for (let ch of data.value) {
+ EventUtils.sendChar(ch, ruleView.doc.defaultView);
+ }
+ if (data.escape) {
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ } else {
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }
+
+ // Wait for the modifyproperties request to complete before
+ // checking the computed style.
+ for (let rule of ruleView._elementStyle.rules) {
+ if (rule._applyingModifications) {
+ yield rule._applyingModifications;
+ }
+ }
+
+ // While the editor is still focused in, the display should have changed already
+ is((yield getComputedStyleProperty(selector, null, "display")),
+ data.expected,
+ "Element should be previewed as " + data.expected);
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_mathml-element.js b/toolkit/devtools/styleinspector/test/browser_ruleview_mathml-element.js
new file mode 100644
index 000000000..288226378
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_mathml-element.js
@@ -0,0 +1,54 @@
+/* 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";
+
+// Tests that the rule-view displays correctly on MathML elements
+
+const TEST_URL = [
+ "data:text/html;charset=utf-8,",
+ "<div>",
+ " <math xmlns=\"http://www.w3.org/1998/Math/MathML\">",
+ " <mfrac>",
+ " <msubsup>",
+ " <mi>a</mi>",
+ " <mi>i</mi>",
+ " <mi>j</mi>",
+ " </msubsup>",
+ " <msub>",
+ " <mi>x</mi>",
+ " <mn>0</mn>",
+ " </msub>",
+ " </mfrac>",
+ " </math>",
+ "</div>"
+].join("");
+
+add_task(function*() {
+ yield addTab(TEST_URL);
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Select the DIV node and verify the rule-view shows rules");
+ yield selectNode("div", inspector);
+ ok(view.element.querySelectorAll(".ruleview-rule").length,
+ "The rule-view shows rules for the div element");
+
+ info("Select various MathML nodes and verify the rule-view is empty");
+ yield selectNode("math", inspector);
+ ok(!view.element.querySelectorAll(".ruleview-rule").length,
+ "The rule-view is empty for the math element");
+
+ yield selectNode("msubsup", inspector);
+ ok(!view.element.querySelectorAll(".ruleview-rule").length,
+ "The rule-view is empty for the msubsup element");
+
+ yield selectNode("mn", inspector);
+ ok(!view.element.querySelectorAll(".ruleview-rule").length,
+ "The rule-view is empty for the mn element");
+
+ info("Select again the DIV node and verify the rule-view shows rules");
+ yield selectNode("div", inspector);
+ ok(view.element.querySelectorAll(".ruleview-rule").length,
+ "The rule-view shows rules for the div element");
+});
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_media-queries.js b/toolkit/devtools/styleinspector/test/browser_ruleview_media-queries.js
new file mode 100644
index 000000000..0eaebcbdd
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_media-queries.js
@@ -0,0 +1,31 @@
+/* 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";
+
+// Tests that we correctly display appropriate media query titles in the
+// rule view.
+
+const TEST_URI = TEST_URL_ROOT + "doc_media_queries.html";
+
+add_task(function*() {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("div", inspector);
+
+ let elementStyle = view._elementStyle;
+
+ let _strings = Services.strings
+ .createBundle("chrome://global/locale/devtools/styleinspector.properties");
+
+ let inline = _strings.GetStringFromName("rule.sourceInline");
+
+ is(elementStyle.rules.length, 3, "Should have 3 rules.");
+ is(elementStyle.rules[0].title, inline, "check rule 0 title");
+ is(elementStyle.rules[1].title, inline +
+ ":15 @media screen and (min-width: 1px)", "check rule 1 title");
+ is(elementStyle.rules[2].title, inline + ":8", "check rule 2 title");
+});
+
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-duplicates.js b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-duplicates.js
new file mode 100644
index 000000000..0dd816c1a
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-duplicates.js
@@ -0,0 +1,54 @@
+/* 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 the rule-view behaves correctly when entering mutliple and/or
+// unfinished properties/values in inplace-editors
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,test rule view user changes");
+ content.document.body.innerHTML = "<h1>Testing Multiple Properties</h1>";
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Creating the test element");
+ let newElement = content.document.createElement("div");
+ newElement.textContent = "Test Element";
+ content.document.body.appendChild(newElement);
+ yield selectNode("div", inspector);
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+
+ yield testCreateNewMultiDuplicates(inspector, ruleEditor);
+});
+
+function* testCreateNewMultiDuplicates(inspector, ruleEditor) {
+ yield createNewRuleViewProperty(ruleEditor,
+ "color:red;color:orange;color:yellow;color:green;color:blue;color:indigo;color:violet;");
+
+ is(ruleEditor.rule.textProps.length, 7, "Should have created new text properties.");
+ is(ruleEditor.propertyList.children.length, 8, "Should have created new property editors.");
+
+ is(ruleEditor.rule.textProps[0].name, "color", "Should have correct property name");
+ is(ruleEditor.rule.textProps[0].value, "red", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[1].name, "color", "Should have correct property name");
+ is(ruleEditor.rule.textProps[1].value, "orange", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[2].name, "color", "Should have correct property name");
+ is(ruleEditor.rule.textProps[2].value, "yellow", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[3].name, "color", "Should have correct property name");
+ is(ruleEditor.rule.textProps[3].value, "green", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[4].name, "color", "Should have correct property name");
+ is(ruleEditor.rule.textProps[4].value, "blue", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[5].name, "color", "Should have correct property name");
+ is(ruleEditor.rule.textProps[5].value, "indigo", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[6].name, "color", "Should have correct property name");
+ is(ruleEditor.rule.textProps[6].value, "violet", "Should have correct property value");
+
+ yield inspector.once("inspector-updated");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-priority.js b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-priority.js
new file mode 100644
index 000000000..eb82938c3
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-priority.js
@@ -0,0 +1,42 @@
+/* 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 the rule-view behaves correctly when entering mutliple and/or
+// unfinished properties/values in inplace-editors
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,test rule view user changes");
+ content.document.body.innerHTML = "<h1>Testing Multiple Properties</h1>";
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Creating the test element");
+ let newElement = content.document.createElement("div");
+ newElement.textContent = "Test Element";
+ content.document.body.appendChild(newElement);
+ yield selectNode("div", inspector);
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+
+ yield testCreateNewMultiPriority(inspector, ruleEditor);
+});
+
+function* testCreateNewMultiPriority(inspector, ruleEditor) {
+ yield createNewRuleViewProperty(ruleEditor,
+ "color:red;width:100px;height: 100px;");
+
+ is(ruleEditor.rule.textProps.length, 3, "Should have created new text properties.");
+ is(ruleEditor.propertyList.children.length, 4, "Should have created new property editors.");
+
+ is(ruleEditor.rule.textProps[0].name, "color", "Should have correct property name");
+ is(ruleEditor.rule.textProps[0].value, "red", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[1].name, "width", "Should have correct property name");
+ is(ruleEditor.rule.textProps[1].value, "100px", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[2].name, "height", "Should have correct property name");
+ is(ruleEditor.rule.textProps[2].value, "100px", "Should have correct property value");
+
+ yield inspector.once("inspector-updated");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_01.js
new file mode 100644
index 000000000..1262fd194
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_01.js
@@ -0,0 +1,53 @@
+/* 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 the rule-view behaves correctly when entering mutliple and/or
+// unfinished properties/values in inplace-editors
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,test rule view user changes");
+ content.document.body.innerHTML = "<h1>Testing Multiple Properties</h1>";
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Creating the test element");
+ let newElement = content.document.createElement("div");
+ newElement.textContent = "Test Element";
+ content.document.body.appendChild(newElement);
+ yield selectNode("div", inspector);
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+
+ yield testCreateNewMultiUnfinished(inspector, ruleEditor, view);
+});
+
+function* testCreateNewMultiUnfinished(inspector, ruleEditor, view) {
+ yield createNewRuleViewProperty(ruleEditor,
+ "color:blue;background : orange ; text-align:center; border-color: ");
+
+ is(ruleEditor.rule.textProps.length, 4, "Should have created new text properties.");
+ is(ruleEditor.propertyList.children.length, 4, "Should have created property editors.");
+
+ for (let ch of "red") {
+ EventUtils.sendChar(ch, view.doc.defaultView);
+ }
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is(ruleEditor.rule.textProps.length, 4, "Should have the same number of text properties.");
+ is(ruleEditor.propertyList.children.length, 5, "Should have added the changed value editor.");
+
+ is(ruleEditor.rule.textProps[0].name, "color", "Should have correct property name");
+ is(ruleEditor.rule.textProps[0].value, "blue", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[1].name, "background", "Should have correct property name");
+ is(ruleEditor.rule.textProps[1].value, "orange", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[2].name, "text-align", "Should have correct property name");
+ is(ruleEditor.rule.textProps[2].value, "center", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[3].name, "border-color", "Should have correct property name");
+ is(ruleEditor.rule.textProps[3].value, "red", "Should have correct property value");
+
+ yield inspector.once("inspector-updated");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_02.js
new file mode 100644
index 000000000..18ac71d69
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_02.js
@@ -0,0 +1,52 @@
+/* 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 the rule-view behaves correctly when entering mutliple and/or
+// unfinished properties/values in inplace-editors
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,test rule view user changes");
+ content.document.body.innerHTML = "<h1>Testing Multiple Properties</h1>";
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Creating the test element");
+ let newElement = content.document.createElement("div");
+ newElement.textContent = "Test Element";
+ content.document.body.appendChild(newElement);
+ yield selectNode("div", inspector);
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+
+ yield testCreateNewMultiPartialUnfinished(inspector, ruleEditor, view);
+});
+
+function* testCreateNewMultiPartialUnfinished(inspector, ruleEditor, view) {
+ yield createNewRuleViewProperty(ruleEditor, "width: 100px; heig");
+
+ is(ruleEditor.rule.textProps.length, 2, "Should have created a new text property.");
+ is(ruleEditor.propertyList.children.length, 2, "Should have created a property editor.");
+
+ // Value is focused, lets add multiple rules here and make sure they get added
+ let valueEditor = ruleEditor.propertyList.children[1].querySelector("input");
+ valueEditor.value = "10px;background:orangered;color: black;";
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is(ruleEditor.rule.textProps.length, 4, "Should have added the changed value.");
+ is(ruleEditor.propertyList.children.length, 5, "Should have added the changed value editor.");
+
+ is(ruleEditor.rule.textProps[0].name, "width", "Should have correct property name");
+ is(ruleEditor.rule.textProps[0].value, "100px", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[1].name, "heig", "Should have correct property name");
+ is(ruleEditor.rule.textProps[1].value, "10px", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[2].name, "background", "Should have correct property name");
+ is(ruleEditor.rule.textProps[2].value, "orangered", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[3].name, "color", "Should have correct property name");
+ is(ruleEditor.rule.textProps[3].value, "black", "Should have correct property value");
+
+ yield inspector.once("inspector-updated");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_multiple_properties_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple_properties_01.js
new file mode 100644
index 000000000..8c37c7cb1
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple_properties_01.js
@@ -0,0 +1,45 @@
+/* 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 the rule-view behaves correctly when entering mutliple and/or
+// unfinished properties/values in inplace-editors
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,test rule view user changes");
+ content.document.body.innerHTML = "<h1>Testing Multiple Properties</h1>";
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Creating the test element");
+ let newElement = content.document.createElement("div");
+ newElement.textContent = "Test Element";
+ content.document.body.appendChild(newElement);
+ yield selectNode("div", inspector);
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+
+ yield testCreateNewMulti(inspector, ruleEditor);
+});
+
+function* testCreateNewMulti(inspector, ruleEditor) {
+ yield createNewRuleViewProperty(ruleEditor,
+ "color:blue;background : orange ; text-align:center; border-color: green;");
+
+ is(ruleEditor.rule.textProps.length, 4, "Should have created a new text property.");
+ is(ruleEditor.propertyList.children.length, 5, "Should have created a new property editor.");
+
+ is(ruleEditor.rule.textProps[0].name, "color", "Should have correct property name");
+ is(ruleEditor.rule.textProps[0].value, "blue", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[1].name, "background", "Should have correct property name");
+ is(ruleEditor.rule.textProps[1].value, "orange", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[2].name, "text-align", "Should have correct property name");
+ is(ruleEditor.rule.textProps[2].value, "center", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[3].name, "border-color", "Should have correct property name");
+ is(ruleEditor.rule.textProps[3].value, "green", "Should have correct property value");
+
+ yield inspector.once("inspector-updated");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_multiple_properties_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple_properties_02.js
new file mode 100644
index 000000000..3179a7b51
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple_properties_02.js
@@ -0,0 +1,49 @@
+/* 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 the rule-view behaves correctly when entering mutliple and/or
+// unfinished properties/values in inplace-editors
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,test rule view user changes");
+ content.document.body.innerHTML = "<h1>Testing Multiple Properties</h1>";
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Creating the test element");
+ let newElement = content.document.createElement("div");
+ newElement.textContent = "Test Element";
+ content.document.body.appendChild(newElement);
+ yield selectNode("div", inspector);
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+
+ yield testMultiValues(inspector, ruleEditor, view);
+});
+
+function* testMultiValues(inspector, ruleEditor, view) {
+ yield createNewRuleViewProperty(ruleEditor, "width:");
+
+ is(ruleEditor.rule.textProps.length, 1, "Should have created a new text property.");
+ is(ruleEditor.propertyList.children.length, 1, "Should have created a property editor.");
+
+ // Value is focused, lets add multiple rules here and make sure they get added
+ let valueEditor = ruleEditor.propertyList.children[0].querySelector("input");
+ valueEditor.value = "height: 10px;color:blue"
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is(ruleEditor.rule.textProps.length, 2, "Should have added the changed value.");
+ is(ruleEditor.propertyList.children.length, 3, "Should have added the changed value editor.");
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
+ is(ruleEditor.propertyList.children.length, 2, "Should have removed the value editor.");
+
+ is(ruleEditor.rule.textProps[0].name, "width", "Should have correct property name");
+ is(ruleEditor.rule.textProps[0].value, "height: 10px", "Should have correct property value");
+
+ is(ruleEditor.rule.textProps[1].name, "color", "Should have correct property name");
+ is(ruleEditor.rule.textProps[1].value, "blue", "Should have correct property value");
+
+ yield inspector.once("inspector-updated");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_original-source-link.js b/toolkit/devtools/styleinspector/test/browser_ruleview_original-source-link.js
new file mode 100644
index 000000000..4baa84df3
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_original-source-link.js
@@ -0,0 +1,84 @@
+/* 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 the stylesheet links in the rule view are correct when source maps
+// are involved
+
+const TESTCASE_URI = TEST_URL_ROOT + "doc_sourcemaps.html";
+const PREF = "devtools.styleeditor.source-maps-enabled";
+const SCSS_LOC = "doc_sourcemaps.scss:4";
+const CSS_LOC = "doc_sourcemaps.css:1";
+
+add_task(function*() {
+ info("Setting the " + PREF + " pref to true");
+ Services.prefs.setBoolPref(PREF, true);
+
+ info("Opening the test page and opening the inspector");
+ yield addTab(TESTCASE_URI);
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test node");
+ yield selectNode("div", inspector);
+
+ yield verifyLinkText(SCSS_LOC, view);
+
+ info("Setting the " + PREF + " pref to false");
+ Services.prefs.setBoolPref(PREF, false);
+ yield verifyLinkText(CSS_LOC, view);
+
+ info("Setting the " + PREF + " pref to true again");
+ Services.prefs.setBoolPref(PREF, true);
+
+ yield testClickingLink(toolbox, view);
+ yield checkDisplayedStylesheet(toolbox);
+
+ info("Clearing the " + PREF + " pref");
+ Services.prefs.clearUserPref(PREF);
+});
+
+function* testClickingLink(toolbox, view) {
+ info("Listening for switch to the style editor");
+ let onStyleEditorReady = toolbox.once("styleeditor-ready");
+
+ info("Finding the stylesheet link and clicking it");
+ let link = getRuleViewLinkByIndex(view, 1);
+ link.scrollIntoView();
+ link.click();
+ yield onStyleEditorReady;
+}
+
+function checkDisplayedStylesheet(toolbox) {
+ let def = promise.defer();
+
+ let panel = toolbox.getCurrentPanel();
+ panel.UI.on("editor-selected", (event, editor) => {
+ // The style editor selects the first sheet at first load before
+ // selecting the desired sheet.
+ if (editor.styleSheet.href.endsWith("scss")) {
+ info("Original source editor selected");
+ editor.getSourceEditor().then(editorSelected).then(def.resolve, def.reject);
+ }
+ });
+
+ return def.promise;
+}
+
+function editorSelected(editor) {
+ let href = editor.styleSheet.href;
+ ok(href.endsWith("doc_sourcemaps.scss"), "selected stylesheet is correct one");
+
+ let {line, col} = editor.sourceEditor.getCursor();
+ is(line, 3, "cursor is at correct line number in original source");
+}
+
+function verifyLinkText(text, view) {
+ info("Verifying that the rule-view stylesheet link is " + text);
+ let label = getRuleViewLinkByIndex(view, 1).querySelector("label");
+ return waitForSuccess(
+ () => label.getAttribute("value") == text,
+ "Link text changed to display correct location: " + text
+ );
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_override.js b/toolkit/devtools/styleinspector/test/browser_ruleview_override.js
new file mode 100644
index 000000000..289f65da4
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_override.js
@@ -0,0 +1,146 @@
+/* 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 the display of overridden declarations in the rule-view
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,browser_ruleview_override.js");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ yield simpleOverride(inspector, view);
+ yield partialOverride(inspector, view);
+ yield importantOverride(inspector, view);
+ yield disableOverride(inspector, view);
+});
+
+function* createTestContent(inspector, style) {
+ let onMutated = inspector.once("markupmutation");
+ let styleNode = addStyle(content.document, style);
+ content.document.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>';
+ yield onMutated;
+ yield selectNode("#testid", inspector);
+ return styleNode;
+}
+
+function* simpleOverride(inspector, view) {
+ let styleNode = yield createTestContent(inspector, '' +
+ '#testid {' +
+ ' background-color: blue;' +
+ '} ' +
+ '.testclass {' +
+ ' background-color: green;' +
+ '}');
+
+ let elementStyle = view._elementStyle;
+
+ let idRule = elementStyle.rules[1];
+ let idProp = idRule.textProps[0];
+ is(idProp.name, "background-color", "First ID prop should be background-color");
+ ok(!idProp.overridden, "ID prop should not be overridden.");
+
+ let classRule = elementStyle.rules[2];
+ let classProp = classRule.textProps[0];
+ is(classProp.name, "background-color", "First class prop should be background-color");
+ ok(classProp.overridden, "Class property should be overridden.");
+
+ // Override background-color by changing the element style.
+ let elementRule = elementStyle.rules[0];
+ elementRule.createProperty("background-color", "purple", "");
+ yield elementRule._applyingModifications;
+
+ let elementProp = elementRule.textProps[0];
+ is(classProp.name, "background-color", "First element prop should now be background-color");
+ ok(!elementProp.overridden, "Element style property should not be overridden");
+ ok(idProp.overridden, "ID property should be overridden");
+ ok(classProp.overridden, "Class property should be overridden");
+
+ styleNode.remove();
+}
+
+function* partialOverride(inspector, view) {
+ let styleNode = yield createTestContent(inspector, '' +
+ // Margin shorthand property...
+ '.testclass {' +
+ ' margin: 2px;' +
+ '}' +
+ // ... will be partially overridden.
+ '#testid {' +
+ ' margin-left: 1px;' +
+ '}');
+
+ let elementStyle = view._elementStyle;
+
+ let classRule = elementStyle.rules[2];
+ let classProp = classRule.textProps[0];
+ ok(!classProp.overridden,
+ "Class prop shouldn't be overridden, some props are still being used.");
+
+ for (let computed of classProp.computed) {
+ if (computed.name.indexOf("margin-left") == 0) {
+ ok(computed.overridden, "margin-left props should be overridden.");
+ } else {
+ ok(!computed.overridden, "Non-margin-left props should not be overridden.");
+ }
+ }
+
+ styleNode.remove();
+}
+
+function* importantOverride(inspector, view) {
+ let styleNode = yield createTestContent(inspector, '' +
+ // Margin shorthand property...
+ '.testclass {' +
+ ' background-color: green !important;' +
+ '}' +
+ // ... will be partially overridden.
+ '#testid {' +
+ ' background-color: blue;' +
+ '}');
+
+ let elementStyle = view._elementStyle;
+
+ let idRule = elementStyle.rules[1];
+ let idProp = idRule.textProps[0];
+ ok(idProp.overridden, "Not-important rule should be overridden.");
+
+ let classRule = elementStyle.rules[2];
+ let classProp = classRule.textProps[0];
+ ok(!classProp.overridden, "Important rule should not be overridden.");
+
+ styleNode.remove();
+
+ let elementRule = elementStyle.rules[0];
+ let elementProp = elementRule.createProperty("background-color", "purple", "important");
+ yield elementRule._applyingModifications;
+
+ ok(classProp.overridden, "New important prop should override class property.");
+ ok(!elementProp.overridden, "New important prop should not be overriden.");
+}
+
+function* disableOverride(inspector, view) {
+ let styleNode = yield createTestContent(inspector, '' +
+ '#testid {' +
+ ' background-color: blue;' +
+ '}' +
+ '.testclass {' +
+ ' background-color: green;' +
+ '}');
+
+ let elementStyle = view._elementStyle;
+
+ let idRule = elementStyle.rules[1];
+ let idProp = idRule.textProps[0];
+
+ idProp.setEnabled(false);
+ yield idRule._applyingModifications;
+
+ let classRule = elementStyle.rules[2];
+ let classProp = classRule.textProps[0];
+ ok(!classProp.overridden, "Class prop should not be overridden after id prop was disabled.");
+
+ styleNode.remove();
+ yield inspector.once("inspector-updated");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_pseudo-element_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_pseudo-element_01.js
new file mode 100644
index 000000000..6050f76f1
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_pseudo-element_01.js
@@ -0,0 +1,247 @@
+/* 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 pseudoelements are displayed correctly in the rule view
+
+const TEST_URI = TEST_URL_ROOT + "doc_pseudoelement.html";
+
+add_task(function*() {
+ yield addTab(TEST_URI);
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ yield testTopLeft(inspector, view);
+ yield testTopRight(inspector, view);
+ yield testBottomRight(inspector, view);
+ yield testBottomLeft(inspector, view);
+ yield testParagraph(inspector, view);
+ yield testBody(inspector, view);
+});
+
+function* testTopLeft(inspector, view) {
+ let selector = "#topleft";
+ let {
+ rules,
+ element,
+ elementStyle
+ } = yield assertPseudoElementRulesNumbers(selector, inspector, view, {
+ elementRulesNb: 4,
+ firstLineRulesNb: 2,
+ firstLetterRulesNb: 1,
+ 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.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are expanded");
+ expander.click();
+ ok (!view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are collapsed by twisty");
+ expander.click();
+ 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.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are collapsed by dblclicking");
+
+ let defaultView = element.ownerDocument.defaultView;
+
+ let elementRule = rules.elementRules[0];
+ let elementRuleView = getRuleViewRuleEditor(view, 3);
+
+ let elementFirstLineRule = rules.firstLineRules[0];
+ let elementFirstLineRuleView = [].filter.call(view.element.children[1].children, (e) => {
+ return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule;
+ })[0]._ruleEditor;
+
+ is
+ (
+ convertTextPropsToString(elementFirstLineRule.textProps),
+ "color: orange",
+ "TopLeft firstLine properties are correct"
+ );
+
+ let firstProp = elementFirstLineRuleView.addProperty("background-color", "rgb(0, 255, 0)", "");
+ let secondProp = elementFirstLineRuleView.addProperty("font-style", "italic", "");
+
+ is (firstProp, elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 2],
+ "First added property is on back of array");
+ is (secondProp, elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 1],
+ "Second added property is on back of array");
+
+ yield elementFirstLineRule._applyingModifications;
+
+ is((yield getComputedStyleProperty(selector, ":first-line", "background-color")),
+ "rgb(0, 255, 0)", "Added property should have been used.");
+ is((yield getComputedStyleProperty(selector, ":first-line", "font-style")),
+ "italic", "Added property should have been used.");
+ is((yield getComputedStyleProperty(selector, null, "text-decoration")),
+ "none", "Added property should not apply to element");
+
+ firstProp.setEnabled(false);
+ yield elementFirstLineRule._applyingModifications;
+
+ is((yield getComputedStyleProperty(selector, ":first-line", "background-color")),
+ "rgb(255, 0, 0)", "Disabled property should now have been used.");
+ is((yield getComputedStyleProperty(selector, null, "background-color")),
+ "rgb(221, 221, 221)", "Added property should not apply to element");
+
+ firstProp.setEnabled(true);
+ yield elementFirstLineRule._applyingModifications;
+
+ is((yield getComputedStyleProperty(selector, ":first-line", "background-color")),
+ "rgb(0, 255, 0)", "Added property should have been used.");
+ is((yield getComputedStyleProperty(selector, null, "text-decoration")),
+ "none", "Added property should not apply to element");
+
+ firstProp = elementRuleView.addProperty("background-color", "rgb(0, 0, 255)", "");
+ yield elementRule._applyingModifications;
+
+ is((yield getComputedStyleProperty(selector, null, "background-color")),
+ "rgb(0, 0, 255)", "Added property should have been used.");
+ is((yield getComputedStyleProperty(selector, ":first-line", "background-color")),
+ "rgb(0, 255, 0)", "Added prop does not apply to pseudo");
+}
+
+function* testTopRight(inspector, view) {
+ let {
+ rules,
+ element,
+ elementStyle
+ } = yield assertPseudoElementRulesNumbers("#topright", inspector, view, {
+ elementRulesNb: 4,
+ firstLineRulesNb: 1,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 0
+ });
+
+ let gutters = assertGutters(view);
+
+ let expander = gutters[0].querySelector(".ruleview-expander");
+ ok (!view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements remain collapsed after switching element");
+ expander.scrollIntoView();
+ expander.click();
+ 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,
+ firstLineRulesNb: 1,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 0
+ });
+}
+
+function* testBottomLeft(inspector, view) {
+ yield assertPseudoElementRulesNumbers("#bottomleft", inspector, view, {
+ elementRulesNb: 4,
+ firstLineRulesNb: 1,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 0
+ });
+}
+
+function* testParagraph(inspector, view) {
+ let {
+ rules,
+ element,
+ elementStyle
+ } = yield assertPseudoElementRulesNumbers("#bottomleft p", inspector, view, {
+ elementRulesNb: 3,
+ firstLineRulesNb: 1,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 1
+ });
+
+ let gutters = assertGutters(view);
+
+ let elementFirstLineRule = rules.firstLineRules[0];
+ let elementFirstLineRuleView = [].filter.call(view.element.children[1].children, (e) => {
+ return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule;
+ })[0]._ruleEditor;
+
+ is
+ (
+ convertTextPropsToString(elementFirstLineRule.textProps),
+ "background: blue none repeat scroll 0% 0%",
+ "Paragraph first-line properties are correct"
+ );
+
+ let elementFirstLetterRule = rules.firstLetterRules[0];
+ 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[1].children, (e) => {
+ return e._ruleEditor && e._ruleEditor.rule === elementSelectionRule;
+ })[0]._ruleEditor;
+
+ is
+ (
+ convertTextPropsToString(elementSelectionRule.textProps),
+ "color: white; background: black none repeat scroll 0% 0%",
+ "Paragraph first-letter properties are correct"
+ );
+}
+
+function* testBody(inspector, view) {
+ let {element, elementStyle} = yield testNode("body", inspector, view);
+
+ let gutters = view.element.querySelectorAll(".theme-gutter");
+ is (gutters.length, 0, "There are no gutter headings");
+}
+
+function convertTextPropsToString(textProps) {
+ return textProps.map(t => t.name + ": " + t.value).join("; ");
+}
+
+function* testNode(selector, inspector, view) {
+ let element = getNode(selector);
+ yield selectNode(selector, inspector);
+ let elementStyle = view._elementStyle;
+ return {element: element, elementStyle: elementStyle};
+}
+
+function* assertPseudoElementRulesNumbers(selector, inspector, view, ruleNbs) {
+ let {element, elementStyle} = yield testNode(selector, inspector, view);
+
+ let rules = {
+ elementRules: elementStyle.rules.filter(rule => !rule.pseudoElement),
+ firstLineRules: elementStyle.rules.filter(rule => rule.pseudoElement === ":first-line"),
+ firstLetterRules: elementStyle.rules.filter(rule => rule.pseudoElement === ":first-letter"),
+ selectionRules: elementStyle.rules.filter(rule => rule.pseudoElement === ":-moz-selection")
+ };
+
+ is(rules.elementRules.length, ruleNbs.elementRulesNb, selector +
+ " has the correct number of non pseudo element rules");
+ is(rules.firstLineRules.length, ruleNbs.firstLineRulesNb, selector +
+ " has the correct number of :first-line rules");
+ is(rules.firstLetterRules.length, ruleNbs.firstLetterRulesNb, selector +
+ " has the correct number of :first-letter rules");
+ is(rules.selectionRules.length, ruleNbs.selectionRulesNb, selector +
+ " has the correct number of :selection rules");
+
+ return {rules: rules, element: element, elementStyle: elementStyle};
+}
+
+function assertGutters(view) {
+ let gutters = view.element.querySelectorAll(".theme-gutter");
+ is (gutters.length, 3, "There are 3 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");
+
+ return gutters;
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_pseudo-element_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_pseudo-element_02.js
new file mode 100644
index 000000000..918889816
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_pseudo-element_02.js
@@ -0,0 +1,32 @@
+/* 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 pseudoelements are displayed correctly in the rule view
+
+const TEST_URI = TEST_URL_ROOT + "doc_pseudoelement.html";
+
+add_task(function*() {
+ yield addTab(TEST_URI);
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ yield testTopLeft(inspector, view);
+});
+
+function* testTopLeft(inspector, view) {
+ let node = inspector.markup.walker.frontForRawNode(getNode("#topleft"));
+ let children = yield inspector.markup.walker.children(node);
+
+ is (children.nodes.length, 3, "Element has correct number of children");
+
+ let beforeElement = children.nodes[0];
+ is (beforeElement.tagName, "_moz_generated_content_before", "tag name is correct");
+ yield selectNode(beforeElement, inspector);
+
+ let afterElement = children.nodes[children.nodes.length-1];
+ is (afterElement.tagName, "_moz_generated_content_after", "tag name is correct");
+ yield selectNode(afterElement, inspector);
+}
+
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-attribute-change_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-attribute-change_01.js
new file mode 100644
index 000000000..063fc329d
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-attribute-change_01.js
@@ -0,0 +1,59 @@
+/* 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 changing the current element's attributes refreshes the rule-view
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,browser_ruleview_refresh-on-attribute-change.js");
+
+ info("Preparing the test document and node");
+ let style = '' +
+ '#testid {' +
+ ' background-color: blue;' +
+ '} ' +
+ '.testclass {' +
+ ' background-color: green;' +
+ '}';
+ let styleNode = addStyle(content.document, style);
+ content.document.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>';
+ let testElement = getNode("#testid");
+ let elementStyle = 'margin-top: 1px; padding-top: 5px;'
+ testElement.setAttribute("style", elementStyle);
+
+ let {toolbox, inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Checking that the rule-view has the element, #testid and .testclass selectors");
+ checkRuleViewContent(view, ["element", "#testid", ".testclass"]);
+
+ info("Changing the node's ID attribute and waiting for the rule-view refresh");
+ let ruleViewRefreshed = inspector.once("rule-view-refreshed");
+ testElement.setAttribute("id", "differentid");
+ yield ruleViewRefreshed;
+
+ info("Checking that the rule-view doesn't have the #testid selector anymore");
+ checkRuleViewContent(view, ["element", ".testclass"]);
+
+ info("Reverting the ID attribute change");
+ ruleViewRefreshed = inspector.once("rule-view-refreshed");
+ testElement.setAttribute("id", "testid");
+ yield ruleViewRefreshed;
+
+ info("Checking that the rule-view has all the selectors again");
+ checkRuleViewContent(view, ["element", "#testid", ".testclass"]);
+});
+
+function checkRuleViewContent(view, expectedSelectors) {
+ let selectors = view.doc.querySelectorAll(".ruleview-selector");
+
+ is(selectors.length, expectedSelectors.length,
+ expectedSelectors.length + " selectors are displayed");
+
+ for (let i = 0; i < expectedSelectors.length; i ++) {
+ is(selectors[i].textContent.indexOf(expectedSelectors[i]), 0,
+ "Selector " + (i + 1) + " is " + expectedSelectors[i]);
+ }
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-attribute-change_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-attribute-change_02.js
new file mode 100644
index 000000000..26017aac4
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-attribute-change_02.js
@@ -0,0 +1,123 @@
+/* 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 changing the current element's style attribute refreshes the rule-view
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,browser_ruleview_update.js");
+
+ content.document.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>';
+ let testElement = getNode("#testid");
+ testElement.setAttribute("style", "margin-top: 1px; padding-top: 5px;");
+
+ let {toolbox, inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ yield testPropertyChanges(inspector, view, testElement);
+ yield testPropertyChange0(inspector, view, testElement);
+ yield testPropertyChange1(inspector, view, testElement);
+ yield testPropertyChange2(inspector, view, testElement);
+ yield testPropertyChange3(inspector, view, testElement);
+ yield testPropertyChange4(inspector, view, testElement);
+ yield testPropertyChange5(inspector, view, testElement);
+ yield testPropertyChange6(inspector, view, testElement);
+});
+
+function* testPropertyChanges(inspector, ruleView, testElement) {
+ info("Adding a second margin-top value in the element selector");
+ let ruleEditor = ruleView._elementStyle.rules[0].editor;
+ let onRefreshed = inspector.once("rule-view-refreshed");
+ ruleEditor.addProperty("margin-top", "5px", "");
+ yield onRefreshed;
+
+ let rule = ruleView._elementStyle.rules[0];
+ validateTextProp(rule.textProps[0], false, "margin-top", "1px", "Original margin property active");
+}
+
+function* testPropertyChange0(inspector, ruleView, testElement) {
+ yield changeElementStyle(testElement, "margin-top: 1px; padding-top: 5px", inspector);
+
+ let rule = ruleView._elementStyle.rules[0];
+ is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, "Correct number of properties");
+ validateTextProp(rule.textProps[0], true, "margin-top", "1px", "First margin property re-enabled");
+ validateTextProp(rule.textProps[2], false, "margin-top", "5px", "Second margin property disabled");
+}
+
+function* testPropertyChange1(inspector, ruleView, testElement) {
+ info("Now set it back to 5px, the 5px value should be re-enabled.");
+ yield changeElementStyle(testElement, "margin-top: 5px; padding-top: 5px;", inspector);
+
+ let rule = ruleView._elementStyle.rules[0];
+ is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, "Correct number of properties");
+ validateTextProp(rule.textProps[0], false, "margin-top", "1px", "First margin property re-enabled");
+ validateTextProp(rule.textProps[2], true, "margin-top", "5px", "Second margin property disabled");
+}
+
+function* testPropertyChange2(inspector, ruleView, testElement) {
+ info("Set the margin property to a value that doesn't exist in the editor.");
+ info("Should reuse the currently-enabled element (the second one.)");
+ yield changeElementStyle(testElement, "margin-top: 15px; padding-top: 5px;", inspector);
+
+ let rule = ruleView._elementStyle.rules[0];
+ is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, "Correct number of properties");
+ validateTextProp(rule.textProps[0], false, "margin-top", "1px", "First margin property re-enabled");
+ validateTextProp(rule.textProps[2], true, "margin-top", "15px", "Second margin property disabled");
+}
+
+function* testPropertyChange3(inspector, ruleView, testElement) {
+ info("Remove the padding-top attribute. Should disable the padding property but not remove it.");
+ yield changeElementStyle(testElement, "margin-top: 5px;", inspector);
+
+ let rule = ruleView._elementStyle.rules[0];
+ is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, "Correct number of properties");
+ validateTextProp(rule.textProps[1], false, "padding-top", "5px", "Padding property disabled");
+}
+
+function* testPropertyChange4(inspector, ruleView, testElement) {
+ info("Put the padding-top attribute back in, should re-enable the padding property.");
+ yield changeElementStyle(testElement, "margin-top: 5px; padding-top: 25px", inspector);
+
+ let rule = ruleView._elementStyle.rules[0];
+ is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, "Correct number of properties");
+ validateTextProp(rule.textProps[1], true, "padding-top", "25px", "Padding property enabled");
+}
+
+function* testPropertyChange5(inspector, ruleView, testElement) {
+ info("Add an entirely new property");
+ yield changeElementStyle(testElement, "margin-top: 5px; padding-top: 25px; padding-left: 20px;", inspector);
+
+ let rule = ruleView._elementStyle.rules[0];
+ is(rule.editor.element.querySelectorAll(".ruleview-property").length, 4, "Added a property");
+ validateTextProp(rule.textProps[3], true, "padding-left", "20px", "Padding property enabled");
+}
+
+function* testPropertyChange6(inspector, ruleView, testElement) {
+ info("Add an entirely new property again");
+ yield changeElementStyle(testElement, "background: red url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%", inspector);
+
+ let rule = ruleView._elementStyle.rules[0];
+ is(rule.editor.element.querySelectorAll(".ruleview-property").length, 5, "Added a property");
+ validateTextProp(rule.textProps[4], true, "background",
+ "red url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%",
+ "shortcut property correctly set",
+ "#F00 url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%");
+}
+
+function* changeElementStyle(testElement, style, inspector) {
+ let onRefreshed = inspector.once("rule-view-refreshed");
+ testElement.setAttribute("style", style);
+ yield onRefreshed;
+}
+
+function validateTextProp(aProp, aEnabled, aName, aValue, aDesc, valueSpanText) {
+ is(aProp.enabled, aEnabled, aDesc + ": enabled.");
+ is(aProp.name, aName, aDesc + ": name.");
+ is(aProp.value, aValue, aDesc + ": value.");
+
+ is(aProp.editor.enable.hasAttribute("checked"), aEnabled, aDesc + ": enabled checkbox.");
+ is(aProp.editor.nameSpan.textContent, aName, aDesc + ": name span.");
+ is(aProp.editor.valueSpan.textContent, valueSpanText || aValue, aDesc + ": value span.");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-style-change.js b/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-style-change.js
new file mode 100644
index 000000000..7813ac927
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-style-change.js
@@ -0,0 +1,41 @@
+/* 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 the rule view refreshes when the current node has its style
+// changed
+
+const TESTCASE_URI = 'data:text/html;charset=utf-8,' +
+ '<div id="testdiv" style="font-size:10px;">Test div!</div>';
+
+add_task(function*() {
+ yield addTab(TESTCASE_URI);
+
+ Services.prefs.setCharPref("devtools.defaultColorUnit", "name");
+
+ info("Getting the test node");
+ let div = getNode("#testdiv");
+
+ info("Opening the rule view and selecting the test node");
+ let {toolbox, inspector, view} = yield openRuleView();
+ yield selectNode("#testdiv", inspector);
+
+ let fontSize = getRuleViewPropertyValue(view, "element", "font-size");
+ is(fontSize, "10px", "The rule view shows the right font-size");
+
+ info("Changing the node's style and waiting for the update");
+ let onUpdated = inspector.once("rule-view-refreshed");
+ div.style.cssText = "font-size: 3em; color: lightgoldenrodyellow; text-align: right; text-transform: uppercase";
+ yield onUpdated;
+
+ let textAlign = getRuleViewPropertyValue(view, "element", "text-align");
+ is(textAlign, "right", "The rule view shows the new text align.");
+ let color = getRuleViewPropertyValue(view, "element", "color");
+ is(color, "lightgoldenrodyellow", "The rule view shows the new color.")
+ fontSize = getRuleViewPropertyValue(view, "element", "font-size");
+ is(fontSize, "3em", "The rule view shows the new font size.");
+ let textTransform = getRuleViewPropertyValue(view, "element", "text-transform");
+ is(textTransform, "uppercase", "The rule view shows the new text transform.");
+});
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_select-and-copy-styles.js b/toolkit/devtools/styleinspector/test/browser_ruleview_select-and-copy-styles.js
new file mode 100644
index 000000000..cb6d3f912
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_select-and-copy-styles.js
@@ -0,0 +1,129 @@
+/* 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";
+
+// Tests that properties can be selected and copied from the rule view
+
+XPCOMUtils.defineLazyGetter(this, "osString", function() {
+ return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
+});
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,<p>rule view context menu test</p>");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = '<style type="text/css"> ' +
+ 'html { color: #000000; } ' +
+ 'span { font-variant: small-caps; color: #000000; } ' +
+ '.nomatches {color: #ff0000;}</style> <div id="first" style="margin: 10em; ' +
+ 'font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA">\n' +
+ '<h1>Some header text</h1>\n' +
+ '<p id="salutation" style="font-size: 12pt">hi.</p>\n' +
+ '<p id="body" style="font-size: 12pt">I am a test-case. This text exists ' +
+ 'solely to provide some things to <span style="color: yellow">' +
+ 'highlight</span> and <span style="font-weight: bold">count</span> ' +
+ 'style list-items in the box at right. If you are reading this, ' +
+ 'you should go do something else instead. Maybe read a book. Or better ' +
+ 'yet, write some test-cases for another bit of code. ' +
+ '<span style="font-style: italic">some text</span></p>\n' +
+ '<p id="closing">more text</p>\n' +
+ '<p>even more text</p>' +
+ '</div>';
+ content.document.title = "Rule view context menu test";
+
+ info("Opening the computed view");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test node");
+ yield selectNode("div", inspector);
+
+ yield checkCopySelection(view);
+ yield checkSelectAll(view);
+});
+
+function checkCopySelection(view) {
+ info("Testing selection copy");
+
+ let contentDoc = view.doc;
+ let prop = contentDoc.querySelector(".ruleview-property");
+ let values = contentDoc.querySelectorAll(".ruleview-propertycontainer");
+
+ let range = contentDoc.createRange();
+ range.setStart(prop, 0);
+ range.setEnd(values[4], 2);
+ let selection = view.doc.defaultView.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: #AAA;[\\r\\n]+" +
+ "}[\\r\\n]+" +
+ "html {[\\r\\n]+" +
+ " color: #000;[\\r\\n]*";
+
+ return waitForClipboard(() => {
+ fireCopyEvent(prop);
+ }, () => {
+ return checkClipboardData(expectedPattern);
+ }).then(() => {}, () => {
+ failedClipboard(expectedPattern);
+ });
+}
+
+function checkSelectAll(view) {
+ info("Testing select-all copy");
+
+ let contentDoc = view.doc;
+ let prop = contentDoc.querySelector(".ruleview-property");
+
+ info("Checking that _SelectAll() then copy returns the correct clipboard value");
+ view._onSelectAll();
+ let expectedPattern = "[\\r\\n]+" +
+ "element {[\\r\\n]+" +
+ " margin: 10em;[\\r\\n]+" +
+ " font-size: 14pt;[\\r\\n]+" +
+ " font-family: helvetica,sans-serif;[\\r\\n]+" +
+ " color: #AAA;[\\r\\n]+" +
+ "}[\\r\\n]+" +
+ "html {[\\r\\n]+" +
+ " color: #000;[\\r\\n]+" +
+ "}[\\r\\n]*";
+
+ return waitForClipboard(() => {
+ fireCopyEvent(prop);
+ }, () => {
+ return checkClipboardData(expectedPattern);
+ }).then(() => {}, () => {
+ failedClipboard(expectedPattern);
+ });
+}
+
+function checkClipboardData(expectedPattern) {
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+ let expectedRegExp = new RegExp(expectedPattern, "g");
+ return expectedRegExp.test(actual);
+}
+
+function failedClipboard(expectedPattern) {
+ // Format expected text for comparison
+ let terminator = osString == "WINNT" ? "\r\n" : "\n";
+ expectedPattern = expectedPattern.replace(/\[\\r\\n\][+*]/g, terminator);
+ expectedPattern = expectedPattern.replace(/\\\(/g, "(");
+ expectedPattern = expectedPattern.replace(/\\\)/g, ")");
+
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+
+ // Trim the right hand side of our strings. This is because expectedPattern
+ // accounts for windows sometimes adding a newline to our copied data.
+ expectedPattern = expectedPattern.trimRight();
+ actual = actual.trimRight();
+
+ dump("TEST-UNEXPECTED-FAIL | Clipboard text does not match expected ... " +
+ "results (escaped for accurate comparison):\n");
+ info("Actual: " + escape(actual));
+ info("Expected: " + escape(expectedPattern));
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_selector-highlighter_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_selector-highlighter_01.js
new file mode 100644
index 000000000..03d3f1469
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_selector-highlighter_01.js
@@ -0,0 +1,44 @@
+/* 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 the selector highlighter is created when hovering over a selector
+// in the rule view
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body, p, td {',
+ ' background: red;',
+ ' }',
+ '</style>',
+ 'Test the selector highlighter'
+].join("\n");
+
+let TYPE = "SelectorHighlighter";
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8," + PAGE_CONTENT);
+
+ let {view: rView} = yield openRuleView();
+ let hs = rView.highlighters;
+
+ ok(!hs.highlighters[TYPE], "No highlighter exists in the rule-view (1)");
+ ok(!hs.promises[TYPE], "No highlighter is being created in the rule-view (1)");
+
+ info("Faking a mousemove NOT on a selector");
+ let {valueSpan} = getRuleViewProperty(rView, "body, p, td", "background");
+ hs._onMouseMove({target: valueSpan});
+ ok(!hs.highlighters[TYPE], "No highlighter exists in the rule-view (2)");
+ ok(!hs.promises[TYPE], "No highlighter is being created in the rule-view (2)");
+
+ info("Faking a mousemove on the body selector");
+ let selectorContainer = getRuleViewSelector(rView, "body, p, td");
+ // The highlighter appears for individual selectors only
+ let bodySelector = selectorContainer.firstElementChild;
+ hs._onMouseMove({target: bodySelector});
+ ok(hs.promises[TYPE], "The highlighter is being initialized");
+ let h = yield hs.promises[TYPE];
+ is(h, hs.highlighters[TYPE], "The initialized highlighter is the right one");
+});
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_selector-highlighter_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_selector-highlighter_02.js
new file mode 100644
index 000000000..3f07cbe34
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_selector-highlighter_02.js
@@ -0,0 +1,85 @@
+/* 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 the selector highlighter is shown when hovering over a selector
+// in the rule-view
+
+// Note that in this test, we mock the highlighter front, merely testing the
+// behavior of the style-inspector UI for now
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body {',
+ ' background: red;',
+ ' }',
+ ' p {',
+ ' color: white;',
+ ' }',
+ '</style>',
+ '<p>Testing the selector highlighter</p>'
+].join("\n");
+
+const TYPE = "SelectorHighlighter";
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8," + PAGE_CONTENT);
+
+ let {inspector, view: rView} = yield openRuleView();
+
+ // Mock the highlighter front to get the reference of the NodeFront
+ let HighlighterFront = {
+ isShown: false,
+ nodeFront: null,
+ options: null,
+ show: function(nodeFront, options) {
+ this.nodeFront = nodeFront;
+ this.options = options;
+ this.isShown = true;
+ },
+ hide: function() {
+ this.nodeFront = null;
+ this.options = null;
+ this.isShown = false;
+ }
+ };
+
+ // Inject the mock highlighter in the rule-view
+ rView.highlighters.promises[TYPE] = {
+ then: function(cb) {
+ cb(HighlighterFront);
+ }
+ };
+
+ let selectorSpan = getRuleViewSelector(rView, "body").firstElementChild;
+
+ info("Checking that the HighlighterFront's show/hide methods are called");
+ rView.highlighters._onMouseMove({target: selectorSpan});
+ ok(HighlighterFront.isShown, "The highlighter is shown");
+ rView.highlighters._onMouseLeave();
+ ok(!HighlighterFront.isShown, "The highlighter is hidden");
+
+ info("Checking that the right NodeFront reference and options are passed");
+ yield selectNode("p", inspector);
+ selectorSpan = getRuleViewSelector(rView, "p").firstElementChild;
+ rView.highlighters._onMouseMove({target: selectorSpan});
+ is(HighlighterFront.nodeFront.tagName, "P",
+ "The right NodeFront is passed to the highlighter (1)");
+ is(HighlighterFront.options.selector, "p",
+ "The right selector option is passed to the highlighter (1)");
+
+ yield selectNode("body", inspector);
+ selectorSpan = getRuleViewSelector(rView, "body").firstElementChild;
+ rView.highlighters._onMouseMove({target: selectorSpan});
+ is(HighlighterFront.nodeFront.tagName, "BODY",
+ "The right NodeFront is passed to the highlighter (2)");
+ is(HighlighterFront.options.selector, "body",
+ "The right selector option is passed to the highlighter (2)");
+
+ info("Checking that the highlighter gets hidden when hovering somewhere else");
+ let {valueSpan} = getRuleViewProperty(rView, "body", "background");
+ rView.highlighters._onMouseMove({target: valueSpan});
+ ok(!HighlighterFront.isShown, "The highlighter is hidden");
+});
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_style-editor-link.js b/toolkit/devtools/styleinspector/test/browser_ruleview_style-editor-link.js
new file mode 100644
index 000000000..559ba0570
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_style-editor-link.js
@@ -0,0 +1,163 @@
+/* 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";
+
+///////////////////
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source");
+
+// Test the links from the rule-view to the styleeditor
+
+const STYLESHEET_URL = "data:text/css,"+encodeURIComponent(
+ ["#first {",
+ "color: blue",
+ "}"].join("\n"));
+
+const EXTERNAL_STYLESHEET_FILE_NAME = "doc_style_editor_link.css";
+const EXTERNAL_STYLESHEET_URL = TEST_URL_ROOT + EXTERNAL_STYLESHEET_FILE_NAME;
+
+const DOCUMENT_URL = "data:text/html;charset=utf-8,"+encodeURIComponent(
+ ['<html>' +
+ '<head>' +
+ '<title>Rule view style editor link test</title>',
+ '<style type="text/css"> ',
+ 'html { color: #000000; } ',
+ 'div { font-variant: small-caps; color: #000000; } ',
+ '.nomatches {color: #ff0000;}</style> <div id="first" style="margin: 10em; ',
+ 'font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA">',
+ '</style>',
+ '<style>',
+ 'div { font-weight: bold; }',
+ '</style>',
+ '<link rel="stylesheet" type="text/css" href="'+STYLESHEET_URL+'">',
+ '<link rel="stylesheet" type="text/css" href="'+EXTERNAL_STYLESHEET_URL+'">',
+ '</head>',
+ '<body>',
+ '<h1>Some header text</h1>',
+ '<p id="salutation" style="font-size: 12pt">hi.</p>',
+ '<p id="body" style="font-size: 12pt">I am a test-case. This text exists ',
+ 'solely to provide some things to ',
+ '<span style="color: yellow" class="highlight">',
+ 'highlight</span> and <span style="font-weight: bold">count</span> ',
+ 'style list-items in the box at right. If you are reading this, ',
+ 'you should go do something else instead. Maybe read a book. Or better ',
+ 'yet, write some test-cases for another bit of code. ',
+ '<span style="font-style: italic">some text</span></p>',
+ '<p id="closing">more text</p>',
+ '<p>even more text</p>',
+ '</div>',
+ '</body>',
+ '</html>'].join("\n"));
+
+add_task(function*() {
+ yield addTab(DOCUMENT_URL);
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Select the test node");
+ yield selectNode("div", inspector);
+
+ yield testInlineStyle(view, inspector);
+ yield testFirstInlineStyleSheet(view, toolbox);
+ yield testSecondInlineStyleSheet(view, toolbox);
+ yield testExternalStyleSheet(view, toolbox);
+});
+
+function* testInlineStyle(view, inspector) {
+ info("Testing inline style");
+
+ let onWindow = waitForWindow();
+ info("Clicking on the first link in the rule-view");
+ clickLinkByIndex(view, 0);
+
+ let win = yield onWindow;
+
+ let windowType = win.document.documentElement.getAttribute("windowtype");
+ is(windowType, "navigator:view-source", "View source window is open");
+ info("Closing window");
+ win.close();
+}
+
+function* testFirstInlineStyleSheet(view, toolbox) {
+ info("Testing inline stylesheet");
+
+ info("Listening for toolbox switch to the styleeditor");
+ let onSwitch = waitForStyleEditor(toolbox);
+
+ info("Clicking an inline stylesheet");
+ clickLinkByIndex(view, 4);
+ let editor = yield onSwitch;
+
+ ok(true, "Switched to the style-editor panel in the toolbox");
+
+ validateStyleEditorSheet(editor, 0);
+}
+
+function* testSecondInlineStyleSheet(view, toolbox) {
+ info("Testing second inline stylesheet");
+
+ info("Waiting for the stylesheet editor to be selected");
+ let panel = toolbox.getCurrentPanel();
+ let onSelected = panel.UI.once("editor-selected");
+
+ info("Switching back to the inspector panel in the toolbox");
+ yield toolbox.selectTool("inspector");
+
+ info("Clicking on second inline stylesheet link");
+ testRuleViewLinkLabel(view);
+ clickLinkByIndex(view, 3);
+ let editor = yield onSelected;
+
+ is(toolbox.currentToolId, "styleeditor", "The style editor is selected again");
+ validateStyleEditorSheet(editor, 1);
+}
+
+function* testExternalStyleSheet(view, toolbox) {
+ info("Testing external stylesheet");
+
+ info("Waiting for the stylesheet editor to be selected");
+ let panel = toolbox.getCurrentPanel();
+ let onSelected = panel.UI.once("editor-selected");
+
+ info("Switching back to the inspector panel in the toolbox");
+ yield toolbox.selectTool("inspector");
+
+ info("Clicking on an external stylesheet link");
+ testRuleViewLinkLabel(view);
+ clickLinkByIndex(view, 1);
+ let editor = yield onSelected;
+
+ is(toolbox.currentToolId, "styleeditor", "The style editor is selected again");
+ validateStyleEditorSheet(editor, 2);
+}
+
+function validateStyleEditorSheet(editor, expectedSheetIndex) {
+ info("validating style editor stylesheet");
+ is(editor.styleSheet.styleSheetIndex, expectedSheetIndex,
+ "loaded stylesheet index matches document stylesheet");
+
+ let sheet = content.document.styleSheets[expectedSheetIndex];
+ is(editor.styleSheet.href, sheet.href, "loaded stylesheet href matches document stylesheet");
+}
+
+function testRuleViewLinkLabel(view) {
+ let link = getRuleViewLinkByIndex(view, 2);
+ let labelElem = link.querySelector(".source-link-label");
+ let value = labelElem.getAttribute("value");
+ let tooltipText = labelElem.getAttribute("tooltiptext");
+
+ is(value, EXTERNAL_STYLESHEET_FILE_NAME + ":1",
+ "rule view stylesheet display value matches filename and line number");
+ is(tooltipText, EXTERNAL_STYLESHEET_URL + ":1",
+ "rule view stylesheet tooltip text matches the full URI path");
+}
+
+function clickLinkByIndex(view, index) {
+ let link = getRuleViewLinkByIndex(view, index);
+ link.scrollIntoView();
+ link.click();
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_urls-clickable.js b/toolkit/devtools/styleinspector/test/browser_ruleview_urls-clickable.js
new file mode 100644
index 000000000..e84df40fa
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_urls-clickable.js
@@ -0,0 +1,61 @@
+/* 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";
+
+// Tests to make sure that URLs are clickable in the rule view
+
+const TEST_URI = TEST_URL_ROOT + "doc_urls_clickable.html";
+const TEST_IMAGE = TEST_URL_ROOT + "doc_test_image.png";
+const BASE_64_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+
+add_task(function*() {
+ yield addTab(TEST_URI);
+ let {toolbox, inspector, view} = yield openRuleView();
+ yield selectNodes(inspector, view);
+});
+
+function* selectNodes(inspector, ruleView) {
+ let relative1 = ".relative1";
+ let relative2 = ".relative2";
+ let absolute = ".absolute";
+ let inline = ".inline";
+ let base64 = ".base64";
+ let noimage = ".noimage";
+ let inlineresolved = ".inline-resolved";
+
+ yield selectNode(relative1, inspector);
+ let relativeLink = ruleView.doc.querySelector(".ruleview-propertycontainer a");
+ ok(relativeLink, "Link exists for relative1 node");
+ is(relativeLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ yield selectNode(relative2, inspector);
+ relativeLink = ruleView.doc.querySelector(".ruleview-propertycontainer a");
+ ok(relativeLink, "Link exists for relative2 node");
+ is(relativeLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ yield selectNode(absolute, inspector);
+ let absoluteLink = ruleView.doc.querySelector(".ruleview-propertycontainer a");
+ ok(absoluteLink, "Link exists for absolute node");
+ is(absoluteLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ yield selectNode(inline, inspector);
+ let inlineLink = ruleView.doc.querySelector(".ruleview-propertycontainer a");
+ ok(inlineLink, "Link exists for inline node");
+ is(inlineLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ yield selectNode(base64, inspector);
+ let base64Link = ruleView.doc.querySelector(".ruleview-propertycontainer a");
+ ok(base64Link, "Link exists for base64 node");
+ is(base64Link.getAttribute("href"), BASE_64_URL, "href matches");
+
+ yield selectNode(inlineresolved, inspector);
+ let inlineResolvedLink = ruleView.doc.querySelector(".ruleview-propertycontainer a");
+ ok(inlineResolvedLink, "Link exists for style tag node");
+ is(inlineResolvedLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ yield selectNode(noimage, inspector);
+ let noimageLink = ruleView.doc.querySelector(".ruleview-propertycontainer a");
+ ok(!noimageLink, "There is no link for the node with no background image");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_user-agent-styles-uneditable.js b/toolkit/devtools/styleinspector/test/browser_ruleview_user-agent-styles-uneditable.js
new file mode 100644
index 000000000..f898bf607
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_user-agent-styles-uneditable.js
@@ -0,0 +1,50 @@
+/* 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";
+
+// Check that user agent styles are never editable via
+// the UI
+
+let PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<blockquote type=cite>" +
+ " <pre _moz_quote=true>" +
+ " inspect <a href='foo' style='color:orange'>user agent</a> styles" +
+ " </pre>" +
+ "</blockquote>";
+
+add_task(function*() {
+ info ("Starting the test with the pref set to true before toolbox is opened");
+ Services.prefs.setBoolPref(PREF_UA_STYLES, true);
+
+ info ("Opening the testcase and toolbox")
+ yield addTab(TEST_URI);
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ yield userAgentStylesUneditable(inspector, view);
+
+ info("Resetting " + PREF_UA_STYLES);
+ Services.prefs.clearUserPref(PREF_UA_STYLES);
+});
+
+function* userAgentStylesUneditable(inspector, view) {
+ info ("Making sure that UI is not editable for user agent styles");
+
+ yield selectNode("a", inspector);
+ let uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable);
+
+ for (let rule of uaRules) {
+ ok (rule.editor.element.hasAttribute("uneditable"), "UA rules have uneditable attribute");
+
+ ok (!rule.textProps[0].editor.nameSpan._editable, "nameSpan is not editable");
+ ok (!rule.textProps[0].editor.valueSpan._editable, "valueSpan is not editable");
+ ok (!rule.editor.closeBrace._editable, "closeBrace is not editable");
+
+ let colorswatch = rule.editor.element.querySelector(".ruleview-colorswatch");
+ if (colorswatch) {
+ ok (!view.tooltips.colorPicker.swatches.has(colorswatch), "The swatch is not editable");
+ }
+ }
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_user-agent-styles.js b/toolkit/devtools/styleinspector/test/browser_ruleview_user-agent-styles.js
new file mode 100644
index 000000000..e4142569c
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_user-agent-styles.js
@@ -0,0 +1,138 @@
+/* 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";
+
+// Check that user agent styles are inspectable via rule view if
+// it is preffed on.
+
+let PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
+const { PrefObserver } = devtools.require("devtools/styleeditor/utils");
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<blockquote type=cite>" +
+ " <pre _moz_quote=true>" +
+ " inspect <a href='foo' style='color:orange'>user agent</a> styles" +
+ " </pre>" +
+ "</blockquote>";
+
+add_task(function*() {
+ info ("Starting the test with the pref set to true before toolbox is opened");
+ yield setUserAgentStylesPref(true);
+
+ info ("Opening the testcase and toolbox")
+ yield addTab(TEST_URI);
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info ("Making sure that UA styles are visible on initial load")
+ yield userAgentStylesVisible(inspector, view);
+
+ info ("Making sure that setting the pref to false hides UA styles");
+ yield setUserAgentStylesPref(false);
+ yield userAgentStylesNotVisible(inspector, view);
+
+ info ("Making sure that resetting the pref to true shows UA styles again");
+ yield setUserAgentStylesPref(true);
+ yield userAgentStylesVisible(inspector, view);
+
+ info("Resetting " + PREF_UA_STYLES);
+ Services.prefs.clearUserPref(PREF_UA_STYLES);
+});
+
+function* setUserAgentStylesPref(val) {
+ info("Setting the pref " + PREF_UA_STYLES + " to: " + val);
+
+ // Reset the pref and wait for PrefObserver to callback so UI
+ // has a chance to get updated.
+ let oncePrefChanged = promise.defer();
+ let prefObserver = new PrefObserver("devtools.");
+ prefObserver.on(PREF_UA_STYLES, oncePrefChanged.resolve);
+ Services.prefs.setBoolPref(PREF_UA_STYLES, val);
+ yield oncePrefChanged.promise;
+ prefObserver.off(PREF_UA_STYLES, oncePrefChanged.resolve);
+}
+
+function* userAgentStylesVisible(inspector, view) {
+ info ("Making sure that user agent styles are currently visible");
+
+ yield selectNode("blockquote", inspector);
+ yield compareAppliedStylesWithUI(inspector, view, "ua");
+
+ let userRules = view._elementStyle.rules.filter(rule=>rule.editor.isEditable);
+ let uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable);
+ is (userRules.length, 1, "Correct number of user rules");
+ ok (uaRules.length > 0, "Has UA rules");
+
+ yield selectNode("pre", inspector);
+ yield compareAppliedStylesWithUI(inspector, view, "ua");
+
+ userRules = view._elementStyle.rules.filter(rule=>rule.editor.isEditable);
+ uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable);
+ is (userRules.length, 1, "Correct number of user rules");
+ ok (uaRules.length > 0, "Has UA rules");
+
+ yield selectNode("a", inspector);
+ yield compareAppliedStylesWithUI(inspector, view, "ua");
+
+ userRules = view._elementStyle.rules.filter(rule=>rule.editor.isEditable);
+ uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable);
+ is (userRules.length, 1, "Correct number of user rules");
+
+ ok (userRules.some(rule=> rule.matchedSelectors.length === 0),
+ "There is an inline style for element in user styles");
+
+ ok (uaRules.some(rule=> rule.matchedSelectors.indexOf(":-moz-any-link")),
+ "There is a rule for :-moz-any-link");
+ ok (uaRules.some(rule=> rule.matchedSelectors.indexOf("*|*:link")),
+ "There is a rule for *|*:link");
+ ok (!uaRules.some(rule=> rule.matchedSelectors.length === 0),
+ "No inline styles for ua styles");
+}
+
+function* userAgentStylesNotVisible(inspector, view) {
+ info ("Making sure that user agent styles are not currently visible");
+
+ yield selectNode("blockquote", inspector);
+ yield compareAppliedStylesWithUI(inspector, view);
+
+ let userRules = view._elementStyle.rules.filter(rule=>rule.editor.isEditable);
+ let uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable);
+ is (userRules.length, 1, "Correct number of user rules");
+ is (uaRules.length, 0, "No UA rules");
+
+ yield selectNode("pre", inspector);
+ yield compareAppliedStylesWithUI(inspector, view);
+
+ userRules = view._elementStyle.rules.filter(rule=>rule.editor.isEditable);
+ uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable);
+ is (userRules.length, 1, "Correct number of user rules");
+ is (uaRules.length, 0, "No UA rules");
+
+ yield selectNode("a", inspector);
+ yield compareAppliedStylesWithUI(inspector, view);
+
+ userRules = view._elementStyle.rules.filter(rule=>rule.editor.isEditable);
+ uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable);
+ is (userRules.length, 1, "Correct number of user rules");
+ is (uaRules.length, 0, "No UA rules");
+}
+
+function* compareAppliedStylesWithUI(inspector, view, filter) {
+ info ("Making sure that UI is consistent with pageStyle.getApplied");
+
+ let entries = yield inspector.pageStyle.getApplied(inspector.selection.nodeFront, {
+ inherited: true,
+ matchedSelectors: true,
+ filter: filter
+ });
+
+ let elementStyle = view._elementStyle;
+ is(elementStyle.rules.length, entries.length, "Should have correct number of rules (" + entries.length + ")");
+
+ entries.forEach((entry, i) => {
+ let elementStyleRule = elementStyle.rules[i];
+ is (elementStyleRule.inherited, entry.inherited, "Same inherited (" +entry.inherited+ ")" );
+ is (elementStyleRule.isSystem, entry.isSystem, "Same isSystem (" +entry.isSystem+ ")");
+ is (elementStyleRule.editor.isEditable, !entry.isSystem, "Editor isEditable opposite of UA (" +entry.isSystem+ ")");
+ });
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_user-property-reset.js b/toolkit/devtools/styleinspector/test/browser_ruleview_user-property-reset.js
new file mode 100644
index 000000000..cbc8ca862
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_ruleview_user-property-reset.js
@@ -0,0 +1,90 @@
+/* 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 user set style properties can be changed from the markup-view and
+// don't survive page reload
+
+let TEST_PAGE = [
+ "data:text/html;charset=utf-8,",
+ "<p id='id1' style='width:200px;'>element 1</p>",
+ "<p id='id2' style='width:100px;'>element 2</p>"
+].join("");
+
+add_task(function*() {
+ yield addTab(TEST_PAGE);
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ yield selectNode("#id1", inspector);
+ yield modifyRuleViewWidth("300px", view, inspector);
+ yield assertRuleAndMarkupViewWidth("id1", "300px", view, inspector);
+
+ yield selectNode("#id2", inspector);
+ yield assertRuleAndMarkupViewWidth("id2", "100px", view, inspector);
+ yield modifyRuleViewWidth("50px", view, inspector);
+ yield assertRuleAndMarkupViewWidth("id2", "50px", view, inspector);
+
+ yield reloadPage(inspector);
+
+ yield selectNode("#id1", inspector);
+ yield assertRuleAndMarkupViewWidth("id1", "200px", view, inspector);
+ yield selectNode("#id2", inspector);
+ yield assertRuleAndMarkupViewWidth("id2", "100px", view, inspector);
+});
+
+function getStyleRule(ruleView) {
+ return ruleView.doc.querySelector(".ruleview-rule");
+}
+
+function* modifyRuleViewWidth(value, ruleView, inspector) {
+ info("Getting the property value element");
+ let valueSpan = getStyleRule(ruleView).querySelector(".ruleview-propertyvalue");
+
+ info("Focusing the property value to set it to edit mode");
+ let editor = yield focusEditableField(valueSpan.parentNode);
+
+ ok(editor.input, "The inplace-editor field is ready");
+ info("Setting the new value");
+ editor.input.value = value;
+
+ info("Pressing return and waiting for the field to blur and for the markup-view to show the mutation");
+ let onBlur = once(editor.input, "blur", true);
+ let onMutation = inspector.once("markupmutation");
+ EventUtils.sendKey("return");
+ yield onBlur;
+ yield onMutation;
+
+ info("Escaping out of the new property field that has been created after the value was edited");
+ let onNewFieldBlur = once(ruleView.doc.activeElement, "blur", true);
+ EventUtils.sendKey("escape");
+ yield onNewFieldBlur;
+}
+
+function* getContainerStyleAttrValue(id, {walker, markup}) {
+ let front = yield walker.querySelector(walker.rootNode, "#" + id);
+ let container = markup.getContainer(front);
+
+ let attrIndex = 0;
+ for (let attrName of container.elt.querySelectorAll(".attr-name")) {
+ if (attrName.textContent === "style") {
+ return container.elt.querySelectorAll(".attr-value")[attrIndex];
+ }
+ attrIndex ++;
+ }
+}
+
+function* assertRuleAndMarkupViewWidth(id, value, ruleView, inspector) {
+ let valueSpan = getStyleRule(ruleView).querySelector(".ruleview-propertyvalue");
+ is(valueSpan.textContent, value, "Rule-view style width is " + value + " as expected");
+
+ let attr = yield getContainerStyleAttrValue(id, inspector);
+ is(attr.textContent.replace(/\s/g, ""), "width:" + value + ";", "Markup-view style attribute width is " + value);
+}
+
+function reloadPage(inspector) {
+ let onNewRoot = inspector.once("new-root");
+ content.location.reload();
+ return onNewRoot.then(inspector.markup._waitForChildren);
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_01.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_01.js
new file mode 100644
index 000000000..998d018a4
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_01.js
@@ -0,0 +1,139 @@
+/* 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_COLOR = "#123ABC";
+const COLOR_SELECTOR = "span[data-color]";
+
+add_task(function* () {
+ const TEST_DOC = '<html> \
+ <body> \
+ <div style="color: ' + TEST_COLOR + '; \
+ margin: 0px; \
+ background: ' + TEST_COLOR + ';"> \
+ Test "Copy color" context menu option \
+ </div> \
+ </body> \
+ </html>';
+
+ const TEST_CASES = [
+ {
+ viewName: "RuleView",
+ initializer: openRuleView
+ },
+ {
+ viewName: "ComputedView",
+ initializer: openComputedView
+ }
+ ];
+
+ yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_DOC));
+
+ for (let test of TEST_CASES) {
+ yield testView(test);
+ }
+});
+
+function* testView({viewName, initializer}) {
+ info("Testing " + viewName);
+
+ let {inspector, view} = yield initializer();
+ yield selectNode("div", inspector);
+
+ testIsColorValueNode(view);
+ testIsColorPopupOnAllNodes(view);
+ yield clearCurrentNodeSelection(inspector);
+}
+
+/**
+ * A function testing that isColorValueNode correctly detects nodes part of
+ * color values.
+ */
+function testIsColorValueNode(view) {
+ info("Testing that child nodes of color nodes are detected.");
+ let root = rootElement(view);
+ let colorNode = root.querySelector(COLOR_SELECTOR);
+
+ ok(colorNode, "Color node found");
+ for (let node of iterateNodes(colorNode)) {
+ ok(isColorValueNode(node), "Node is part of color value.");
+ }
+}
+
+/**
+ * A function testing that _isColorPopup returns a correct value for all nodes
+ * in the view.
+ */
+function testIsColorPopupOnAllNodes(view) {
+ let root = rootElement(view);
+ for (let node of iterateNodes(root)) {
+ testIsColorPopupOnNode(view, node);
+ }
+}
+
+/**
+ * Test result of _isColorPopup with given node.
+ * @param object view
+ * A CSSRuleView or CssHtmlTree instance.
+ * @param Node node
+ * A node to check.
+ */
+function testIsColorPopupOnNode(view, node) {
+ info("Testing node " + node);
+ if (view.doc) {
+ view.doc.popupNode = node;
+ }
+ else {
+ view.popupNode = node;
+ }
+ view._colorToCopy = "";
+
+ let result = view._isColorPopup();
+ let correct = isColorValueNode(node);
+
+ is(result, correct, "_isColorPopup returned the expected value " + correct);
+ is(view._colorToCopy, (correct) ? TEST_COLOR : "",
+ "_colorToCopy was set to the expected value");
+}
+
+/**
+ * Check if a node is part of color value i.e. it has parent with a 'data-color'
+ * attribute.
+ */
+function isColorValueNode(node) {
+ let container = (node.nodeType == node.TEXT_NODE) ?
+ node.parentElement : node;
+
+ let isColorNode = el => el.dataset && "color" in el.dataset;
+
+ while (!isColorNode(container)) {
+ container = container.parentNode;
+ if (!container) {
+ info("No color. Node is not part of color value.");
+ return false;
+ }
+ }
+
+ info("Found a color. Node is part of color value.");
+
+ return true;
+}
+
+/**
+ * A generator that iterates recursively trough all child nodes of baseNode.
+ */
+function* iterateNodes(baseNode) {
+ yield baseNode;
+
+ for (let child of baseNode.childNodes) {
+ yield* iterateNodes(child);
+ }
+}
+
+/**
+ * Returns the root element for the given view, rule or computed.
+ */
+let rootElement = view => (view.element) ? view.element : view.styleDocument;
diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_02.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_02.js
new file mode 100644
index 000000000..2ef90a648
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_02.js
@@ -0,0 +1,99 @@
+/* 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 #2: Test that correct color is
+// copied if the color changes.
+
+const TEST_COLOR = "#123ABC";
+
+add_task(function* () {
+ const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' div {',
+ ' color: ' + TEST_COLOR + ';',
+ ' }',
+ '</style>',
+ '<div>Testing the color picker tooltip!</div>'
+ ].join("\n");
+
+ yield addTab("data:text/html;charset=utf8,Test context menu Copy color");
+ content.document.body.innerHTML = PAGE_CONTENT;
+
+ let {inspector, view} = yield openRuleView();
+ yield testCopyToClipboard(inspector, view);
+ yield testManualEdit(inspector, view);
+ yield testColorPickerEdit(inspector, view);
+});
+
+function* testCopyToClipboard(inspector, view) {
+ info("Testing that color is copied to clipboard");
+
+ yield selectNode("div", inspector);
+
+ let win = view.doc.defaultView;
+ let element = getRuleViewProperty(view, "div", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ let popup = once(view._contextmenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(element, {button: 2, type: "contextmenu"}, win);
+ yield popup;
+
+ ok(!view.menuitemCopyColor.hidden, "Copy color is visible");
+
+ yield waitForClipboard(() => view.menuitemCopyColor.click(), TEST_COLOR);
+ view._contextmenu.hidePopup();
+}
+
+function* testManualEdit(inspector, view) {
+ info("Testing manually edited colors");
+ yield selectNode("div", inspector);
+
+ let {valueSpan} = getRuleViewProperty(view, "div", "color");
+
+ let newColor = "#C9184E"
+ let editor = yield focusEditableField(valueSpan);
+
+ info("Typing new value");
+ let input = editor.input;
+ let onBlur = once(input, "blur");
+ for (let ch of newColor + ";"){
+ EventUtils.sendChar(ch, view.doc.defaultView);
+ }
+
+ yield onBlur;
+ yield wait(1);
+
+ let colorValue = getRuleViewProperty(view, "div", "color").valueSpan.firstChild;
+ is(colorValue.dataset.color, newColor, "data-color was updated");
+
+ view.doc.popupNode = colorValue;
+ view._isColorPopup();
+
+ is(view._colorToCopy, newColor, "_colorToCopy has the new value");
+}
+
+function* testColorPickerEdit(inspector, view) {
+ info("Testing colors edited via color picker");
+ yield selectNode("div", inspector);
+
+ let swatch = getRuleViewProperty(view, "div", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ info("Opening the color picker");
+ let picker = view.tooltips.colorPicker;
+ let onShown = picker.tooltip.once("shown");
+ swatch.click();
+ yield onShown;
+
+ let rgbaColor = [83, 183, 89, 1];
+ let rgbaColorText = "rgba(83, 183, 89, 1)";
+ yield simulateColorPickerChange(picker, rgbaColor);
+
+ is(swatch.parentNode.dataset.color, rgbaColorText, "data-color was updated");
+ view.doc.popupNode = swatch;
+ view._isColorPopup();
+
+ is(view._colorToCopy, rgbaColorText, "_colorToCopy has the new value");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_csslogic-content-stylesheets.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_csslogic-content-stylesheets.js
new file mode 100644
index 000000000..4b82c166b
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_csslogic-content-stylesheets.js
@@ -0,0 +1,67 @@
+/* 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";
+
+// Check stylesheets on HMTL and XUL document
+
+// FIXME: this test opens the devtools for nothing, it should be changed into a
+// toolkit/devtools/server/tests/mochitest/test_css-logic-...something...html test
+
+const TEST_URI_HTML = TEST_URL_ROOT + "doc_content_stylesheet.html";
+const TEST_URI_XUL = TEST_URL_ROOT + "doc_content_stylesheet.xul";
+const XUL_URI = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService)
+ .newURI(TEST_URI_XUL, null, null);
+const XUL_PRINCIPAL = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
+ .getService(Ci.nsIScriptSecurityManager)
+ .getNoAppCodebasePrincipal(XUL_URI);
+
+add_task(function*() {
+ info("Checking stylesheets on HTML document");
+ yield addTab(TEST_URI_HTML);
+ let target = getNode("#target");
+
+ let {toolbox, inspector, view} = yield openRuleView();
+ yield selectNode("#target", inspector);
+
+ info("Checking stylesheets");
+ yield checkSheets(target);
+
+ info("Checking stylesheets on XUL document");
+ info("Allowing XUL content");
+ allowXUL();
+ yield addTab(TEST_URI_XUL);
+
+ ({toolbox, inspector, view} = yield openRuleView());
+ target = getNode("#target");
+ yield selectNode("#target", inspector);
+
+ yield checkSheets(target);
+ info("Disallowing XUL content");
+ disallowXUL();
+});
+
+function allowXUL() {
+ Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager)
+ .addFromPrincipal(XUL_PRINCIPAL, 'allowXULXBL', Ci.nsIPermissionManager.ALLOW_ACTION);
+}
+
+function disallowXUL() {
+ Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager)
+ .addFromPrincipal(XUL_PRINCIPAL, 'allowXULXBL', Ci.nsIPermissionManager.DENY_ACTION);
+}
+
+function* checkSheets(target) {
+ let sheets = yield executeInContent("Test:GetStyleSheetsInfoForNode", {}, {target});
+
+ for (let sheet of sheets) {
+ if (!sheet.href ||
+ /doc_content_stylesheet_/.test(sheet.href)) {
+ ok(sheet.isContentSheet, sheet.href + " identified as content stylesheet");
+ } else {
+ ok(!sheet.isContentSheet, sheet.href + " identified as non-content stylesheet");
+ }
+ }
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_output-parser.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_output-parser.js
new file mode 100644
index 000000000..72f25d8e5
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_output-parser.js
@@ -0,0 +1,312 @@
+/* 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 expected outputs of the output-parser's parseCssProperty function.
+
+// This is more of a unit test than a mochitest-browser test, but can't be
+// tested with an xpcshell test as the output-parser requires the DOM to work.
+
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let {OutputParser} = devtools.require("devtools/output-parser");
+
+const COLOR_CLASS = "color-class";
+const URL_CLASS = "url-class";
+const CUBIC_BEZIER_CLASS = "bezier-class";
+
+function test() {
+ function countAll(fragment) {
+ return fragment.querySelectorAll("*").length;
+ }
+ function countColors(fragment) {
+ return fragment.querySelectorAll("." + COLOR_CLASS).length;
+ }
+ function countUrls(fragment) {
+ return fragment.querySelectorAll("." + URL_CLASS).length;
+ }
+ function countCubicBeziers(fragment) {
+ return fragment.querySelectorAll("." + CUBIC_BEZIER_CLASS).length;
+ }
+ function getColor(fragment, index) {
+ return fragment.querySelectorAll("." + COLOR_CLASS)[index||0].textContent;
+ }
+ function getUrl(fragment, index) {
+ return fragment.querySelectorAll("." + URL_CLASS)[index||0].textContent;
+ }
+ function getCubicBezier(fragment, index) {
+ return fragment.querySelectorAll("." + CUBIC_BEZIER_CLASS)[index||0].textContent;
+ }
+
+ let testData = [
+ {
+ name: "width",
+ value: "100%",
+ test: fragment => {
+ is(countAll(fragment), 0);
+ is(fragment.textContent, "100%");
+ }
+ },
+ {
+ name: "width",
+ value: "blue",
+ test: fragment => {
+ is(countAll(fragment), 0);
+ }
+ },
+ {
+ name: "content",
+ value: "'red url(test.png) repeat top left'",
+ test: fragment => {
+ is(countAll(fragment), 0);
+ }
+ },
+ {
+ name: "content",
+ value: "\"blue\"",
+ test: fragment => {
+ is(countAll(fragment), 0);
+ }
+ },
+ {
+ name: "margin-left",
+ value: "url(something.jpg)",
+ test: fragment => {
+ is(countAll(fragment), 0);
+ }
+ },
+ {
+ name: "background-color",
+ value: "transparent",
+ test: fragment => {
+ is(countAll(fragment), 2);
+ is(countColors(fragment), 1);
+ is(fragment.textContent, "transparent");
+ }
+ },
+ {
+ name: "color",
+ value: "red",
+ test: fragment => {
+ is(countColors(fragment), 1);
+ is(fragment.textContent, "red");
+ }
+ },
+ {
+ name: "color",
+ value: "#F06",
+ test: fragment => {
+ is(countColors(fragment), 1);
+ is(fragment.textContent, "#F06");
+ }
+ },
+ {
+ name: "border",
+ value: "80em dotted pink",
+ test: fragment => {
+ is(countAll(fragment), 2);
+ is(countColors(fragment), 1);
+ is(getColor(fragment), "pink");
+ }
+ },
+ {
+ name: "color",
+ value: "red !important",
+ test: fragment => {
+ is(countColors(fragment), 1);
+ is(fragment.textContent, "red !important");
+ }
+ },
+ {
+ name: "background",
+ value: "red url(test.png) repeat top left",
+ test: fragment => {
+ is(countColors(fragment), 1);
+ is(countUrls(fragment), 1);
+ is(getColor(fragment), "red");
+ is(getUrl(fragment), "test.png");
+ is(countAll(fragment), 3);
+ }
+ },
+ {
+ name: "background",
+ value: "blue url(test.png) repeat top left !important",
+ test: fragment => {
+ is(countColors(fragment), 1);
+ is(countUrls(fragment), 1);
+ is(getColor(fragment), "blue");
+ is(getUrl(fragment), "test.png");
+ is(countAll(fragment), 3);
+ }
+ },
+ {
+ name: "list-style-image",
+ value: "url(\"images/arrow.gif\")",
+ test: fragment => {
+ is(countAll(fragment), 1);
+ is(getUrl(fragment), "images/arrow.gif");
+ }
+ },
+ {
+ name: "list-style-image",
+ value: "url(\"images/arrow.gif\")!important",
+ test: fragment => {
+ is(countAll(fragment), 1);
+ is(getUrl(fragment), "images/arrow.gif");
+ is(fragment.textContent, "url(\"images/arrow.gif\")!important");
+ }
+ },
+ {
+ name: "-moz-binding",
+ value: "url(http://somesite.com/path/to/binding.xml#someid)",
+ test: fragment => {
+ is(countAll(fragment), 1);
+ is(countUrls(fragment), 1);
+ is(getUrl(fragment), "http://somesite.com/path/to/binding.xml#someid");
+ }
+ },
+ {
+ name: "background",
+ value: "linear-gradient(to right, rgba(183,222,237,1) 0%, rgba(33,180,226,1) 30%, rgba(31,170,217,.5) 44%, #F06 75%, red 100%)",
+ test: fragment => {
+ is(countAll(fragment), 10);
+ let allSwatches = fragment.querySelectorAll("." + COLOR_CLASS);
+ is(allSwatches.length, 5);
+ is(allSwatches[0].textContent, "rgba(183,222,237,1)");
+ is(allSwatches[1].textContent, "rgba(33,180,226,1)");
+ is(allSwatches[2].textContent, "rgba(31,170,217,.5)");
+ is(allSwatches[3].textContent, "#F06");
+ is(allSwatches[4].textContent, "red");
+ }
+ },
+ {
+ name: "background",
+ value: "-moz-radial-gradient(center 45deg, circle closest-side, orange 0%, red 100%)",
+ test: fragment => {
+ is(countAll(fragment), 4);
+ let allSwatches = fragment.querySelectorAll("." + COLOR_CLASS);
+ is(allSwatches.length, 2);
+ is(allSwatches[0].textContent, "orange");
+ is(allSwatches[1].textContent, "red");
+ }
+ },
+ {
+ name: "background",
+ value: "white url(http://test.com/wow_such_image.png) no-repeat top left",
+ test: fragment => {
+ is(countAll(fragment), 3);
+ is(countUrls(fragment), 1);
+ is(countColors(fragment), 1);
+ }
+ },
+ {
+ name: "background",
+ value: "url(\"http://test.com/wow_such_(oh-noes)image.png?testid=1&color=red#w00t\")",
+ test: fragment => {
+ is(countAll(fragment), 1);
+ is(getUrl(fragment), "http://test.com/wow_such_(oh-noes)image.png?testid=1&color=red#w00t");
+ }
+ },
+ {
+ name: "background-image",
+ value: "url(this-is-an-incredible-image.jpeg)",
+ test: fragment => {
+ is(countAll(fragment), 1);
+ is(getUrl(fragment), "this-is-an-incredible-image.jpeg");
+ }
+ },
+ {
+ name: "background",
+ value: "red url( \"http://wow.com/cool/../../../you're(doingit)wrong\" ) repeat center",
+ test: fragment => {
+ is(countAll(fragment), 3);
+ is(countColors(fragment), 1);
+ is(getUrl(fragment), "http://wow.com/cool/../../../you're(doingit)wrong");
+ }
+ },
+ {
+ name: "background-image",
+ value: "url(../../../look/at/this/folder/structure/../../red.blue.green.svg )",
+ test: fragment => {
+ is(countAll(fragment), 1);
+ is(getUrl(fragment), "../../../look/at/this/folder/structure/../../red.blue.green.svg");
+ }
+ },
+ {
+ name: "transition-timing-function",
+ value: "linear",
+ test: fragment => {
+ is(countCubicBeziers(fragment), 1);
+ is(getCubicBezier(fragment), "linear");
+ }
+ },
+ {
+ name: "animation-timing-function",
+ value: "ease-in-out",
+ test: fragment => {
+ is(countCubicBeziers(fragment), 1);
+ is(getCubicBezier(fragment), "ease-in-out");
+ }
+ },
+ {
+ name: "animation-timing-function",
+ value: "cubic-bezier(.1, 0.55, .9, -3.45)",
+ test: fragment => {
+ is(countCubicBeziers(fragment), 1);
+ is(getCubicBezier(fragment), "cubic-bezier(.1, 0.55, .9, -3.45)");
+ }
+ },
+ {
+ name: "animation",
+ value: "move 3s cubic-bezier(.1, 0.55, .9, -3.45)",
+ test: fragment => {
+ is(countCubicBeziers(fragment), 1);
+ is(getCubicBezier(fragment), "cubic-bezier(.1, 0.55, .9, -3.45)");
+ }
+ },
+ {
+ name: "transition",
+ value: "top 1s ease-in",
+ test: fragment => {
+ is(countCubicBeziers(fragment), 1);
+ is(getCubicBezier(fragment), "ease-in");
+ }
+ },
+ {
+ name: "transition",
+ value: "top 3s steps(4, end)",
+ test: fragment => {
+ is(countAll(fragment), 0);
+ }
+ },
+ {
+ name: "transition",
+ value: "top 3s step-start",
+ test: fragment => {
+ is(countAll(fragment), 0);
+ }
+ },
+ {
+ name: "transition",
+ value: "top 3s step-end",
+ test: fragment => {
+ is(countAll(fragment), 0);
+ }
+ }
+ ];
+
+ let parser = new OutputParser();
+ for (let i = 0; i < testData.length; i ++) {
+ let data = testData[i];
+ info("Output-parser test data " + i + ". {" + data.name + " : " + data.value + ";}");
+ data.test(parser.parseCssProperty(data.name, data.value, {
+ colorClass: COLOR_CLASS,
+ urlClass: URL_CLASS,
+ bezierClass: CUBIC_BEZIER_CLASS,
+ defaultColorType: false
+ }));
+ }
+
+ finish();
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_refresh_when_active.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_refresh_when_active.js
new file mode 100644
index 000000000..5a2205ef5
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_refresh_when_active.js
@@ -0,0 +1,43 @@
+/* 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 the style-inspector views only refresh when they are active.
+
+const TEST_URL = 'data:text/html;charset=utf-8,' +
+ '<div id="one" style="color:red;">one</div>' +
+ '<div id="two" style="color:blue;">two</div>';
+
+add_task(function*() {
+ yield addTab(TEST_URL);
+
+ info("Opening the rule-view and selecting test node one");
+ let {inspector, view: rView} = yield openRuleView();
+ yield selectNode("#one", inspector);
+
+ is(getRuleViewPropertyValue(rView, "element", "color"), "#F00",
+ "The rule-view shows the properties for test node one");
+
+ let cView = inspector.sidebar.getWindowForTab("computedview")["computedview"].view;
+ let prop = getComputedViewProperty(cView, "color");
+ ok(!prop, "The computed-view doesn't show the properties for test node one");
+
+ info("Switching to the computed-view");
+ let onComputedViewReady = inspector.once("computed-view-refreshed");
+ yield openComputedView();
+ yield onComputedViewReady;
+
+ ok(getComputedViewPropertyValue(cView, "color"), "#F00",
+ "The computed-view shows the properties for test node one");
+
+ info("Selecting test node two");
+ yield selectNode("#two", inspector);
+
+ ok(getComputedViewPropertyValue(cView, "color"), "#00F",
+ "The computed-view shows the properties for test node two");
+
+ is(getRuleViewPropertyValue(rView, "element", "color"), "#F00",
+ "The rule-view doesn't the properties for test node two");
+}); \ No newline at end of file
diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-background-image.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-background-image.js
new file mode 100644
index 000000000..7a46bea58
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-background-image.js
@@ -0,0 +1,123 @@
+/* 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 background-image URLs have image preview tooltips in the rule-view
+// and computed-view
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body {',
+ ' padding: 1em;',
+ ' background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAADI5JREFUeNrsWwuQFNUVPf1m5z87szv7HWSWj8CigBFMEFZKiQsB1PgJwUAZg1HBpIQsKmokEhNjWUnFVPnDWBT+KolJYbRMoqUVq0yCClpqiX8sCchPWFwVlt2db7+X93pez7zu6Vn2NxsVWh8987p7pu+9555z7+tZjTGGY3kjOMa34w447oBjfKsY7i/UNM3Y8eFSAkD50Plgw03K5P9gvGv7U5ieeR3PszeREiPNX3/0DL4hjslzhm8THh+OITfXk3dhiv4GDtGPVzCaeJmPLYzuu5qJuWfuw2QTlcN1X9pwQU7LhdZ/ZAseD45cOh9hHvDkc/yAF/DNhdb5Mrr3PvBMaAYW8fMSIi2G497IMEK/YutGtAYr6+ej+nxu/NN8Ks3N7AR6HgcLz0Eg1Ljg1UcxZzi5qewIkMYLRweTr2Kzp+nmyXAd5pS3XQDd+N/4h4zgu9FI7brlXf90nMEnuwQxlvv+hosE3TuexmWeysmT4W+WxkMaLzf9Y8ATgjcUn7T9H1gqrpFq8eV1gMn6t16NhngjfoX6q4DUP032Rd4LJgpSLwJ1yzFqBG69eRkah0MVyo0Acfe+yy9AG4nMiYCkeM53KKFXncBLAXqEm+wCqZwaueq7WCmuLTcKSJmj737ol2hurA9eq9VdyiO8yWa3NNyog+SB5CZodSsQq/dfu34tJpYbBaTMzvVddDZu16q5smXf4G8zEvqm4cyaAmJPuTJk3oJWdS4WzcVtfMZbThSQckb/pYfRGgo3zNOqZnEHbJPGK4abaDCQIIsT8V/qTaBqHkLh6LzXH8XZQhbLhYKyyCC/WeHYcNdmvOgfe8skzbWL270/T3wf7tSx/lGCbTu8xlzzmCSWLc5iwmgikcCHi3Mga0Ry913vBFvQwg90l6M4ImWKfsWOp7DSWxmfpPlCFuPFfsNfKrCnPYpQKIRgqBK7D0SxYaNHwkEiJMtl0ReDp3Lc5D3PGoTo/sKngCl7a5chFqvBatKwjBd7WwqIlzB/78NcoUcp5VSgGxm+7b8eqQRGnHMO634epO4S1EZww09/iFg5UmGoESDuznP1xVhTUX1WWHPzjpd25wyH0hRxI3LGM75nxmuNEEUVpAN0XgxmPoKralakbQnWlIMQyVBD/w+3orkq4lvualjKyWwzt4MaxqspQHVhPOWG64bxYuhZXSFGWhipbSDVragOu5Y9eAsmDDUKyBA703vemVhHoueD6e9wAzJK1WfmN0Umk5GGM4kEMZcuIECqgjm0nldAqmbjwtm4VxZH5AvlADP6mx9Eqy9Q0+KqW8Ch+47FaMMYmnNGfY1iPMshoC6qFxme4wQ+0p+ARE6H3+9veWEDWgUhDhUKyFARn4jM5BNxT0XsMg7bfymGK1ov3wtjDfhL4w0HVGUVBEjDaaE+QNdrcNWch1PG4W6xrjBUXECGivg++Cva3JUT4iQUz3V2RsSVaKLwOuDT89A3HdBQoxhNC+fnVm74ual2EG893P6G+PuP4SfiO4cCBWQooL9qCWKNXPbcI37Aa/lnlZxXRt4RFONGwSDCPAHqOuqjWct1QiEMw5mChM5X4K47FyNqcd3aK9AwFH0CGYLoe1ctxk2eWi57rg5JfGp9rzC6ggCdFlAgHBDw5Yxlcg6G8SyHCjMlsgmDD9zhSeHlF+JnAgWDTQUy2NxfdwOao1UVV3pi3+bE97YSbWpLAbn6zefHNQkp1PMpIBwwvslKgIYTKM2nEpNzrGcH3FXTEal0L38kJ4uDQgEZbO4vnI173LXf5NHZaiUxtaCxyZuo/rK6LpUg54yg3zTWRAArvDcRIPZ6BqzrQ1REpmL+DNw32OKIDCb3X1qPVn8wNNMT4w2bvs+q4bAZrqBh2skaL3yyhhIIZ4i6oHkUK0RckcB8GigEyRIH4A6Mgc8fatl0/+BkkQxC9gIT4ljna1rIZW9rEdNbjJcNjsnoYj7LHWCUwpITzEgzRQKZ3XAFHbTzA3hrz8TEUUZxFBhoKpABQt/97p+w0hMZG68I8R6FtlsJT3FELndZntjM+VMnylKYq8GJI3UZaRMpquGSGFVOEfv0YZBMNzz+uvjbfzS6xQERIhlI9FcvQWNdFVb7x1zCb+QNK8vb9NsiifmI5hBgVoOCBC1sb0ab5RomqENxLO3eA1/0NDRU47q2RQNbRCUDIb7lF2CNL3ZGxEV4n08TVvZWYG4pZyV0zUdS45tyCBByOHWiyvZmxFXDCyRo1ge5+Sy0TA+8lWMiP/6O0S32exGV9Jf4fr8azdUR3zL/CZz4MtvzdX5uOYs6NDOmpkuj5Huh+7qUQSYl0ThHzw0YQzcGo6bhzEqoYq5rN3yRiYiG3Vfe2Ybm/qKA9NNZ3nNm4F7/yDkg9AN+U1mHiBcXP8zuDN76jj8hg1QyiWQigalj02BJPhK8I0zxijAjhp5zhlpLUDvS+BCy2HMAvvB4XDgL9/SXC0g/ou/5+6/xLX8w0uJrOIkXfPvyhY0F6gr7M8H0KWFYikcqAXakB+xwD9CdREBLoau7Gz3cAdSIdLFxFtJTCqRChSjnutvhDcREtzjz2Tswtz+yeNRFUeXZXtWux7C1fuoVcbd3J//ipDX3uZZDLGrwweS+UBLL5TDliVBnF8P7H+XI8aRRGsIBJg/Zlslt1+W+D1JWoSyi+kD9jfhs78t7mhZhSl+fLfY1Bdyv3I8V/qpY3B1McgN7ZFT5/vNO0I5DPLLdPBIJA8qc4h2I0QplYfDpJwHT+aj0246r5S8rToG8OjCle8wk4OLvvYGa+Ovr84uo2qBSwJS9G5egoZFLTfiEqWDtbwGfHgKOdPHcS+ai7XDzMPW/FJRLGGcxnBbK4YJC2K+h+T6Bdu5CqHqCWERd3bawb7JI+iJ735+LNaHaprBLLHBm08U3XxShEsdt+f3eTh3v7aC95Dct4RCWL5OZWh/oXBZThxAIxyOXLzBk8aiEWJID8rK3CpPOmeHaGpvCS+7EHv5FujVHUSJPLXvIFeHcNc+9xrB2gws9KZdxuLFax/WLM5gzzSm/lTXF/OdAcapyvjxPqxqHjr2v4ckX2bS2dRBrc5lSdpKjEJ9/9tdwX2WMd53ZQ2IVo3RES+UwVSpCPvYepNx4gmTGDUKIMQ4eduPnD7mx9xOn/KZKOlFbStjONxHTtR+BYAPmnoZ1Zp8wkBRwP/EL3u0F/C2hGl7vpz7vW37T3vP7if8wroKuoh8ribknX9BK5rcF+mo1qKaKyRPJTgTDjbzY8szcuLb3bpH00u35T47j7prRpwDJTxzyG0dHgxPp5bPG8VdkpfPbUg3SgoOo2mwVukb98D5EqpswZTTulCggTk4gpYhv0++wIhCJxr0+Hq1sondis0SE2oxQe3qWXwWyO4DSQg9gJ8Iiw1VFcGqXxet0N9xE4ygIxv/9W6wo9WyROEX/R+eiobYSq2vHTOR631Eiv2lRfh9dvxkumkXh92Qsx8XrAJ+7YGbWuhxOi/U+31NQmzyqNYG8N/3wfo6CRtRHcN01FzkvojohwLu0VVvDa56IS/xcj2b7nN+O+m0jqpE1wMPXZxAN9iCVThtDvH7gmiRGRpU8Lspv1Uhq4wIVdQoyuGSLNYPKUCS8+CzNURbzMmjK3i8u0U793lmuV0ef9nWQ5MGC/DiUqEUSaCtXna9RJEspZS1lrXINK/pcq+SpT50t98QKMq1FRmDfx3vxty102k0PM4ssEnvuz5+G26Ij4yDpz6z9fV8bkyIkqBFkhej0Ib+ZQ34XJK9AfozaiimqIoX3Jp3tiISrcfYpuN2+iFph/02P36PNC9fVcCnp6H9jYouKyfaWufz5Tp9tVxcUniw7IohZv4dZz81/ns67z3AYPrc2n0+Ix2q8k0PWjgBy88XaibnfK9A+5LdDY2Ivhy36fbT8Zv3Lb1U1qLqUxorXEEXIs0mjjrtxoTZWtdvigNs2sgPiujTv6DIZLld6b/V5742JZV3fUsUVFy5gdsNtKWFzUCEVbNepD1MkSMVbsb6SZm7jI3/zODtQKgUMsOw8wDZ63t5xcV1TnaEAxoc6wrqY+Fj+N4DsqOnhOIdicrQSm1MPYCPlIqHn5bbHg8/bj2D3QfZnCX3mpAICDZV8jH5kpbZqTD0W+DxaA74CWzLN2nd14OlL72J38Lf7+TjC7dadZFDoZJQPrtaIKL/G0L6ktptPZVJ8fMqHYPZOKYPMyQGadIJfDvdXwAFiZOTvDBPydf5vk4rWA+RfdhBlaF/yDDBRoMu9pfnSjv/p7DG+HXfAcQcc49v/BBgAcFAO4DmB2GQAAAAASUVORK5CYII=);',
+ ' background-repeat: repeat-y;',
+ ' background-position: right top;',
+ ' }',
+ ' .test-element {',
+ ' font-family: verdana;',
+ ' color: #333;',
+ ' background: url(chrome://global/skin/icons/warning-64.png) no-repeat left center;',
+ ' padding-left: 70px;',
+ ' }',
+ '</style>',
+ '<div class="test-element">test element</div>'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,rule view tooltip test");
+ content.document.body.innerHTML = PAGE_CONTENT;
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Testing the background-image property on the body rule");
+ yield testBodyRuleView(view);
+
+ info("Selecting the test div node");
+ yield selectNode(".test-element", inspector);
+ info("Testing the the background property on the .test-element rule");
+ yield testDivRuleView(view);
+
+ info("Testing that image preview tooltips show even when there are fields being edited");
+ yield testTooltipAppearsEvenInEditMode(view);
+
+ info("Switching over to the computed-view");
+ let onComputedViewReady = inspector.once("computed-view-refreshed");
+ ({view} = yield openComputedView());
+ yield onComputedViewReady;
+
+ info("Testing that the background-image computed style has a tooltip too");
+ yield testComputedView(view);
+});
+
+function* testBodyRuleView(view) {
+ info("Testing tooltips in the rule view");
+ let panel = view.tooltips.previewTooltip.panel;
+
+ // Check that the rule view has a tooltip and that a XUL panel has been created
+ ok(view.tooltips.previewTooltip, "Tooltip instance exists");
+ ok(panel, "XUL panel exists");
+
+ // Get the background-image property inside the rule view
+ let {valueSpan} = getRuleViewProperty(view, "body", "background-image");
+ let uriSpan = valueSpan.querySelector(".theme-link");
+
+ yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
+
+ let images = panel.getElementsByTagName("image");
+ is(images.length, 1, "Tooltip contains an image");
+ ok(images[0].getAttribute("src").indexOf("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHe") !== -1,
+ "The image URL seems fine");
+}
+
+function* testDivRuleView(view) {
+ let panel = view.tooltips.previewTooltip.panel;
+
+ // Get the background property inside the rule view
+ let {valueSpan} = getRuleViewProperty(view, ".test-element", "background");
+ let uriSpan = valueSpan.querySelector(".theme-link");
+
+ yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
+
+ let images = panel.getElementsByTagName("image");
+ is(images.length, 1, "Tooltip contains an image");
+ ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected");
+}
+
+function* testTooltipAppearsEvenInEditMode(view) {
+ let panel = view.tooltips.previewTooltip.panel;
+
+ info("Switching to edit mode in the rule view");
+ let editor = yield turnToEditMode(view);
+
+ info("Now trying to show the preview tooltip");
+ let {valueSpan} = getRuleViewProperty(view, ".test-element", "background");
+ let uriSpan = valueSpan.querySelector(".theme-link");
+ yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
+
+ is(view.doc.activeElement, editor.input,
+ "Tooltip was shown in edit mode, and inplace-editor still focused");
+}
+
+function turnToEditMode(ruleView) {
+ let brace = ruleView.doc.querySelector(".ruleview-ruleclose");
+ return focusEditableField(brace);
+}
+
+function* testComputedView(view) {
+ let tooltip = view.tooltips.previewTooltip;
+ ok(tooltip, "The computed-view has a tooltip defined");
+
+ let panel = tooltip.panel;
+ ok(panel, "The computed-view tooltip has a XUL panel");
+
+ let {valueSpan} = getComputedViewProperty(view, "background-image");
+ let uriSpan = valueSpan.querySelector(".theme-link");
+
+ yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
+
+ let images = panel.getElementsByTagName("image");
+ is(images.length, 1, "Tooltip contains an image");
+
+ ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri in the computed-view too");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-closes-on-new-selection.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-closes-on-new-selection.js
new file mode 100644
index 000000000..11ae65f87
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-closes-on-new-selection.js
@@ -0,0 +1,49 @@
+/* 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 if a tooltip is visible when a new selection is made, it closes
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,<div class='one'>el 1</div><div class='two'>el 2</div>");
+
+ let {toolbox, inspector, view} = yield openRuleView();
+ yield selectNode(".one", inspector);
+
+ info("Testing rule view tooltip closes on new selection");
+ yield testRuleView(view, inspector);
+
+ info("Testing computed view tooltip closes on new selection");
+ ({view} = yield openComputedView());
+ yield testComputedView(view, inspector);
+});
+
+function* testRuleView(ruleView, inspector) {
+ info("Showing the tooltip");
+ let tooltip = ruleView.tooltips.previewTooltip;
+ let onShown = tooltip.once("shown");
+ tooltip.show();
+ yield onShown;
+
+ info("Selecting a new node");
+ let onHidden = tooltip.once("hidden");
+ yield selectNode(".two", inspector);
+
+ ok(true, "Rule view tooltip closed after a new node got selected");
+}
+
+function* testComputedView(computedView, inspector) {
+ info("Showing the tooltip");
+ let tooltip = computedView.tooltips.previewTooltip;
+ let onShown = tooltip.once("shown");
+ tooltip.show();
+ yield onShown;
+
+ info("Selecting a new node");
+ let onHidden = tooltip.once("hidden");
+ yield selectNode(".one", inspector);
+
+ ok(true, "Computed view tooltip closed after a new node got selected");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-longhand-fontfamily.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-longhand-fontfamily.js
new file mode 100644
index 000000000..d188f13cd
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-longhand-fontfamily.js
@@ -0,0 +1,119 @@
+/* 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 the fontfamily tooltip on longhand properties
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' #testElement {',
+ ' font-family: cursive;',
+ ' color: #333;',
+ ' padding-left: 70px;',
+ ' }',
+ '</style>',
+ '<div id="testElement">test element</div>'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,font family longhand tooltip test");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = PAGE_CONTENT;
+
+ info("Opening the rule view");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test node");
+ yield selectNode("#testElement", inspector);
+
+ yield testRuleView(view, inspector.selection.nodeFront);
+
+ info("Opening the computed view");
+ let onComputedViewReady = inspector.once("computed-view-refreshed");
+ ({toolbox, inspector, view} = yield openComputedView());
+ yield onComputedViewReady;
+
+ yield testComputedView(view, inspector.selection.nodeFront);
+
+ yield testExpandedComputedViewProperty(view, inspector.selection.nodeFront);
+});
+
+function* testRuleView(ruleView, nodeFront) {
+ info("Testing font-family tooltips in the rule view");
+
+ let tooltip = ruleView.tooltips.previewTooltip;
+ let panel = tooltip.panel;
+
+ // Check that the rule view has a tooltip and that a XUL panel has been created
+ ok(tooltip, "Tooltip instance exists");
+ ok(panel, "XUL panel exists");
+
+ // Get the font family property inside the rule view
+ let {valueSpan} = getRuleViewProperty(ruleView, "#testElement", "font-family");
+
+ // And verify that the tooltip gets shown on this property
+ yield assertHoverTooltipOn(tooltip, valueSpan);
+
+ let images = panel.getElementsByTagName("image");
+ is(images.length, 1, "Tooltip contains an image");
+ ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected");
+
+ let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
+ is(images[0].getAttribute("src"), dataURL, "Tooltip contains the correct data-uri image");
+}
+
+function* testComputedView(computedView, nodeFront) {
+ info("Testing font-family tooltips in the computed view");
+
+ let tooltip = computedView.tooltips.previewTooltip;
+ let panel = tooltip.panel;
+ let {valueSpan} = getComputedViewProperty(computedView, "font-family");
+
+ yield assertHoverTooltipOn(tooltip, valueSpan);
+
+ let images = panel.getElementsByTagName("image");
+ is(images.length, 1, "Tooltip contains an image");
+ ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected");
+
+ let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
+ is(images[0].getAttribute("src"), dataURL, "Tooltip contains the correct data-uri image");
+}
+
+function* testExpandedComputedViewProperty(computedView, nodeFront) {
+ info("Testing font-family tooltips in expanded properties of the computed view");
+
+ info("Expanding the font-family property to reveal matched selectors");
+ let propertyView = getPropertyView(computedView, "font-family");
+ propertyView.matchedExpanded = true;
+ yield propertyView.refreshMatchedSelectors();
+
+ let valueSpan = propertyView.matchedSelectorsContainer
+ .querySelector(".bestmatch .other-property-value");
+
+ let tooltip = computedView.tooltips.previewTooltip;
+ let panel = tooltip.panel;
+
+ yield assertHoverTooltipOn(tooltip, valueSpan);
+
+ let images = panel.getElementsByTagName("image");
+ is(images.length, 1, "Tooltip contains an image");
+ ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected");
+
+ let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
+ is(images[0].getAttribute("src"), dataURL, "Tooltip contains the correct data-uri image");
+}
+
+function getPropertyView(computedView, name) {
+ let propertyView = null;
+ computedView.propertyViews.some(function(view) {
+ if (view.name == name) {
+ propertyView = view;
+ return true;
+ }
+ return false;
+ });
+ return propertyView;
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-multiple-background-images.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-multiple-background-images.js
new file mode 100644
index 000000000..ad92b81ed
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-multiple-background-images.js
@@ -0,0 +1,72 @@
+/* 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 for bug 1026921: Ensure the URL of hovered url() node is used instead
+// of the first found from the declaration as there might be multiple urls.
+
+let YELLOW_DOT = "data:image/png;base64," +
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABmJLR0QA/wD/AP+gvaeTAAAACX" +
+ "BIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gYcDCwCr0o5ngAAABl0RVh0Q29tbWVudABDcmVh" +
+ "dGVkIHdpdGggR0lNUFeBDhcAAAANSURBVAjXY/j/n6EeAAd9An7Z55GEAAAAAElFTkSuQmCC";
+
+let BLUE_DOT = "data:image/png;base64," +
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABmJLR0QA/wD/AP+gvaeTAAAACX" +
+ "BIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gYcDCwlCkCM9QAAABl0RVh0Q29tbWVudABDcmVh" +
+ "dGVkIHdpdGggR0lNUFeBDhcAAAANSURBVAjXY2Bg+F8PAAKCAX/tPkrkAAAAAElFTkSuQmCC";
+
+add_task(function* () {
+ let TEST_STYLE = "h1 {background: url(" + YELLOW_DOT + "), url(" + BLUE_DOT + ");}";
+
+ let PAGE_CONTENT = "<style>" + TEST_STYLE + "</style>" +
+ "<h1>browser_styleinspector_tooltip-multiple-background-images.js</h1>";
+
+ yield addTab("data:text/html;charset=utf-8,background image tooltip test");
+ content.document.body.innerHTML = PAGE_CONTENT;
+
+ yield testRuleViewUrls();
+ yield testComputedViewUrls();
+});
+
+function* testRuleViewUrls() {
+ info("Testing tooltips in the rule view");
+
+ let {view, inspector} = yield openRuleView();
+ yield selectNode("h1", inspector);
+
+ let {valueSpan} = getRuleViewProperty(view, "h1", "background");
+ yield performChecks(view, valueSpan);
+}
+
+function* testComputedViewUrls() {
+ info("Testing tooltips in the computed view");
+
+ let {view, inspector} = yield openComputedView();
+ yield inspector.once("computed-view-refreshed");
+ let {valueSpan} = getComputedViewProperty(view, "background-image");
+
+ yield performChecks(view, valueSpan);
+}
+
+/**
+ * A helper that checks url() tooltips contain correct images
+ */
+function* performChecks(view, propertyValue) {
+ function checkTooltip(panel, imageSrc) {
+ let images = panel.getElementsByTagName("image");
+ is(images.length, 1, "Tooltip contains an image");
+ is(images[0].getAttribute("src"), imageSrc, "The image URL is correct");
+ }
+
+ let links = propertyValue.querySelectorAll(".theme-link");
+ let panel = view.tooltips.previewTooltip.panel;
+
+ info("Checking first link tooltip");
+ yield assertHoverTooltipOn(view.tooltips.previewTooltip, links[0]);
+ checkTooltip(panel, YELLOW_DOT);
+
+ info("Checking second link tooltip");
+ yield assertHoverTooltipOn(view.tooltips.previewTooltip, links[1]);
+ checkTooltip(panel, BLUE_DOT);
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-shorthand-fontfamily.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-shorthand-fontfamily.js
new file mode 100644
index 000000000..6be03806b
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-shorthand-fontfamily.js
@@ -0,0 +1,60 @@
+/* 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 the fontfamily tooltip on shorthand properties
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' #testElement {',
+ ' font: italic bold .8em/1.2 Arial;',
+ ' }',
+ '</style>',
+ '<div id="testElement">test element</div>'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,font family shorthand tooltip test");
+
+ info("Creating the test document");
+ content.document.body.innerHTML = PAGE_CONTENT;
+
+ info("Opening the rule view");
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Selecting the test node");
+ yield selectNode("#testElement", inspector);
+
+ yield testRuleView(view, inspector.selection.nodeFront);
+});
+
+function* testRuleView(ruleView, nodeFront) {
+ info("Testing font-family tooltips in the rule view");
+
+ let tooltip = ruleView.tooltips.previewTooltip;
+ let panel = tooltip.panel;
+
+ // Check that the rule view has a tooltip and that a XUL panel has been created
+ ok(tooltip, "Tooltip instance exists");
+ ok(panel, "XUL panel exists");
+
+ // Get the computed font family property inside the font rule view
+ let propertyList = ruleView.element.querySelectorAll(".ruleview-propertylist");
+ let fontExpander = propertyList[1].querySelectorAll(".ruleview-expander")[0];
+ fontExpander.click();
+
+ let rule = getRuleViewRule(ruleView, "#testElement");
+ let valueSpan = rule.querySelector(".ruleview-computed .ruleview-propertyvalue");
+
+ // And verify that the tooltip gets shown on this property
+ yield assertHoverTooltipOn(tooltip, valueSpan);
+
+ let images = panel.getElementsByTagName("image");
+ is(images.length, 1, "Tooltip contains an image");
+ ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected");
+
+ let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
+ is(images[0].getAttribute("src"), dataURL, "Tooltip contains the correct data-uri image");
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-size.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-size.js
new file mode 100644
index 000000000..5b97e9434
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-size.js
@@ -0,0 +1,86 @@
+/* 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";
+
+// Checking tooltips dimensions, to make sure their big enough to display their
+// content
+
+const TEST_PAGE = [
+ 'data:text/html;charset=utf-8,',
+ '<style type="text/css">',
+ ' div {',
+ ' width: 300px;height: 300px;border-radius: 50%;',
+ ' background: red url(chrome://global/skin/icons/warning-64.png);',
+ ' }',
+ '</style>',
+ '<div></div>'
+].join("\n");
+
+add_task(function*() {
+ yield addTab(TEST_PAGE);
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ yield selectNode("div", inspector);
+
+ yield testImageDimension(view);
+ yield testPickerDimension(view);
+});
+
+function* testImageDimension(ruleView) {
+ info("Testing background-image tooltip dimensions");
+
+ let tooltip = ruleView.tooltips.previewTooltip;
+ let panel = tooltip.panel;
+ let {valueSpan} = getRuleViewProperty(ruleView, "div", "background");
+ let uriSpan = valueSpan.querySelector(".theme-link");
+
+ // Make sure there is a hover tooltip for this property, this also will fill
+ // the tooltip with its content
+ yield assertHoverTooltipOn(tooltip, uriSpan);
+
+ info("Showing the tooltip");
+ let onShown = tooltip.once("shown");
+ tooltip.show();
+ yield onShown;
+
+ // Let's not test for a specific size, but instead let's make sure it's at
+ // least as big as the image
+ let imageRect = panel.querySelector("image").getBoundingClientRect();
+ let panelRect = panel.getBoundingClientRect();
+
+ ok(panelRect.width >= imageRect.width,
+ "The panel is wide enough to show the image");
+ ok(panelRect.height >= imageRect.height,
+ "The panel is high enough to show the image");
+
+ let onHidden = tooltip.once("hidden");
+ tooltip.hide();
+ yield onHidden;
+}
+
+function* testPickerDimension(ruleView) {
+ info("Testing color-picker tooltip dimensions");
+
+ let {valueSpan} = getRuleViewProperty(ruleView, "div", "background");
+ let swatch = valueSpan.querySelector(".ruleview-colorswatch");
+ let cPicker = ruleView.tooltips.colorPicker;
+
+ let onShown = cPicker.tooltip.once("shown");
+ swatch.click();
+ yield onShown;
+
+ // The colorpicker spectrum's iframe has a fixed width height, so let's
+ // make sure the tooltip is at least as big as that
+ let w = cPicker.tooltip.panel.querySelector("iframe").width;
+ let h = cPicker.tooltip.panel.querySelector("iframe").height;
+ let panelRect = cPicker.tooltip.panel.getBoundingClientRect();
+
+ ok(panelRect.width >= w, "The panel is wide enough to show the picker");
+ ok(panelRect.height >= h, "The panel is high enough to show the picker");
+
+ let onHidden = cPicker.tooltip.once("hidden");
+ cPicker.hide();
+ yield onHidden;
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-01.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-01.js
new file mode 100644
index 000000000..d9e018bc1
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-01.js
@@ -0,0 +1,44 @@
+/* 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 the css transform highlighter is created only when asked
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body {',
+ ' transform: skew(16deg);',
+ ' }',
+ '</style>',
+ 'Test the css transform highlighter'
+].join("\n");
+
+const TYPE = "CssTransformHighlighter";
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8," + PAGE_CONTENT);
+
+ let {inspector, view: rView} = yield openRuleView();
+ let overlay = rView.highlighters;
+
+ ok(!overlay.highlighters[TYPE], "No highlighter exists in the rule-view");
+ let h = yield overlay._getHighlighter(TYPE);
+ ok(overlay.highlighters[TYPE], "The highlighter has been created in the rule-view");
+ is(h, overlay.highlighters[TYPE], "The right highlighter has been created");
+ let h2 = yield overlay._getHighlighter(TYPE);
+ is(h, h2, "The same instance of highlighter is returned everytime in the rule-view");
+
+ let onComputedViewReady = inspector.once("computed-view-refreshed");
+ let {view: cView} = yield openComputedView();
+ yield onComputedViewReady;
+ overlay = cView.highlighters;
+
+ ok(!overlay.highlighters[TYPE], "No highlighter exists in the computed-view");
+ h = yield overlay._getHighlighter(TYPE);
+ ok(overlay.highlighters[TYPE], "The highlighter has been created in the computed-view");
+ is(h, overlay.highlighters[TYPE], "The right highlighter has been created");
+ h2 = yield overlay._getHighlighter(TYPE);
+ is(h, h2, "The same instance of highlighter is returned everytime in the computed-view");
+});
diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-02.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-02.js
new file mode 100644
index 000000000..3c00d3008
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-02.js
@@ -0,0 +1,64 @@
+/* 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 the css transform highlighter is created when hovering over a
+// transform property
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' body {',
+ ' transform: skew(16deg);',
+ ' color: yellow;',
+ ' }',
+ '</style>',
+ 'Test the css transform highlighter'
+].join("\n");
+
+let TYPE = "CssTransformHighlighter";
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8," + PAGE_CONTENT);
+
+ let {inspector, view: rView} = yield openRuleView();
+ let hs = rView.highlighters;
+
+ ok(!hs.highlighters[TYPE], "No highlighter exists in the rule-view (1)");
+ ok(!hs.promises[TYPE], "No highlighter is being created in the rule-view (1)");
+
+ info("Faking a mousemove on a non-transform property");
+ let {valueSpan} = getRuleViewProperty(rView, "body", "color");
+ hs._onMouseMove({target: valueSpan});
+ ok(!hs.highlighters[TYPE], "No highlighter exists in the rule-view (2)");
+ ok(!hs.promises[TYPE], "No highlighter is being created in the rule-view (2)");
+
+ info("Faking a mousemove on a transform property");
+ ({valueSpan} = getRuleViewProperty(rView, "body", "transform"));
+ hs._onMouseMove({target: valueSpan});
+ ok(hs.promises[TYPE], "The highlighter is being initialized");
+ let h = yield hs.promises[TYPE];
+ is(h, hs.highlighters[TYPE], "The initialized highlighter is the right one");
+
+ let onComputedViewReady = inspector.once("computed-view-refreshed");
+ let {view: cView} = yield openComputedView();
+ yield onComputedViewReady;
+ hs = cView.highlighters;
+
+ ok(!hs.highlighters[TYPE], "No highlighter exists in the computed-view (1)");
+ ok(!hs.promises[TYPE], "No highlighter is being created in the computed-view (1)");
+
+ info("Faking a mousemove on a non-transform property");
+ ({valueSpan} = getComputedViewProperty(cView, "color"));
+ hs._onMouseMove({target: valueSpan});
+ ok(!hs.highlighters[TYPE], "No highlighter exists in the computed-view (2)");
+ ok(!hs.promises[TYPE], "No highlighter is being created in the computed-view (2)");
+
+ info("Faking a mousemove on a transform property");
+ ({valueSpan} = getComputedViewProperty(cView, "transform"));
+ hs._onMouseMove({target: valueSpan});
+ ok(hs.promises[TYPE], "The highlighter is being initialized");
+ h = yield hs.promises[TYPE];
+ is(h, hs.highlighters[TYPE], "The initialized highlighter is the right one");
+});
diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-03.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-03.js
new file mode 100644
index 000000000..7b0a25c2c
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-03.js
@@ -0,0 +1,91 @@
+/* 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 the css transform highlighter is shown when hovering over transform
+// properties
+
+// Note that in this test, we mock the highlighter front, merely testing the
+// behavior of the style-inspector UI for now
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' html {',
+ ' transform: scale(.9);',
+ ' }',
+ ' body {',
+ ' transform: skew(16deg);',
+ ' color: purple;',
+ ' }',
+ '</style>',
+ 'Test the css transform highlighter'
+].join("\n");
+
+const TYPE = "CssTransformHighlighter";
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8," + PAGE_CONTENT);
+
+ let {inspector, view: rView} = yield openRuleView();
+
+ // Mock the highlighter front to get the reference of the NodeFront
+ let HighlighterFront = {
+ isShown: false,
+ nodeFront: null,
+ nbOfTimesShown: 0,
+ show: function(nodeFront) {
+ this.nodeFront = nodeFront;
+ this.isShown = true;
+ this.nbOfTimesShown ++;
+ },
+ hide: function() {
+ this.nodeFront = null;
+ this.isShown = false;
+ }
+ };
+
+ // Inject the mock highlighter in the rule-view
+ rView.highlighters.promises[TYPE] = {
+ then: function(cb) {
+ cb(HighlighterFront);
+ }
+ };
+
+ let {valueSpan} = getRuleViewProperty(rView, "body", "transform");
+
+ info("Checking that the HighlighterFront's show/hide methods are called");
+ rView.highlighters._onMouseMove({target: valueSpan});
+ ok(HighlighterFront.isShown, "The highlighter is shown");
+ rView.highlighters._onMouseLeave();
+ ok(!HighlighterFront.isShown, "The highlighter is hidden");
+
+ info("Checking that hovering several times over the same property doesn't" +
+ " show the highlighter several times");
+ let nb = HighlighterFront.nbOfTimesShown;
+ rView.highlighters._onMouseMove({target: valueSpan});
+ is(HighlighterFront.nbOfTimesShown, nb + 1, "The highlighter was shown once");
+ rView.highlighters._onMouseMove({target: valueSpan});
+ rView.highlighters._onMouseMove({target: valueSpan});
+ is(HighlighterFront.nbOfTimesShown, nb + 1,
+ "The highlighter was shown once, after several mousemove");
+
+ info("Checking that the right NodeFront reference is passed");
+ yield selectNode("html", inspector);
+ ({valueSpan} = getRuleViewProperty(rView, "html", "transform"));
+ rView.highlighters._onMouseMove({target: valueSpan});
+ is(HighlighterFront.nodeFront.tagName, "HTML",
+ "The right NodeFront is passed to the highlighter (1)");
+
+ yield selectNode("body", inspector);
+ ({valueSpan} = getRuleViewProperty(rView, "body", "transform"));
+ rView.highlighters._onMouseMove({target: valueSpan});
+ is(HighlighterFront.nodeFront.tagName, "BODY",
+ "The right NodeFront is passed to the highlighter (2)");
+
+ info("Checking that the highlighter gets hidden when hovering a non-transform property");
+ ({valueSpan} = getRuleViewProperty(rView, "body", "color"));
+ rView.highlighters._onMouseMove({target: valueSpan});
+ ok(!HighlighterFront.isShown, "The highlighter is hidden");
+});
diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-04.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-04.js
new file mode 100644
index 000000000..a70eb0a47
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-04.js
@@ -0,0 +1,62 @@
+/* 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 the css transform highlighter is shown only when hovering over a
+// transform declaration that isn't overriden or disabled
+
+// Note that unlike the other browser_styleinspector_transform-highlighter-N.js
+// tests, this one only tests the rule-view as only this view features disabled
+// and overriden properties
+
+const PAGE_CONTENT = [
+ '<style type="text/css">',
+ ' div {',
+ ' background: purple;',
+ ' width:300px;height:300px;',
+ ' transform: rotate(16deg);',
+ ' }',
+ ' .test {',
+ ' transform: skew(25deg);',
+ ' }',
+ '</style>',
+ '<div class="test"></div>'
+].join("\n");
+
+const TYPE = "CssTransformHighlighter";
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8," + PAGE_CONTENT);
+
+ let {view: rView, inspector} = yield openRuleView();
+ yield selectNode(".test", inspector);
+
+ let hs = rView.highlighters;
+
+ info("Faking a mousemove on the overriden property");
+ let {valueSpan} = getRuleViewProperty(rView, "div", "transform");
+ hs._onMouseMove({target: valueSpan});
+ ok(!hs.highlighters[TYPE], "No highlighter was created for the overriden property");
+ ok(!hs.promises[TYPE], "And no highlighter is being initialized either");
+
+ info("Disabling the applied property");
+ let classRuleEditor = getRuleViewRuleEditor(rView, 1);
+ let propEditor = classRuleEditor.rule.textProps[0].editor;
+ propEditor.enable.click();
+ yield classRuleEditor.rule._applyingModifications;
+
+ info("Faking a mousemove on the disabled property");
+ ({valueSpan} = getRuleViewProperty(rView, ".test", "transform"));
+ hs._onMouseMove({target: valueSpan});
+ ok(!hs.highlighters[TYPE], "No highlighter was created for the disabled property");
+ ok(!hs.promises[TYPE], "And no highlighter is being initialized either");
+
+ info("Faking a mousemove on the now unoverriden property");
+ ({valueSpan} = getRuleViewProperty(rView, "div", "transform"));
+ hs._onMouseMove({target: valueSpan});
+ ok(hs.promises[TYPE], "The highlighter is being initialized now");
+ let h = yield hs.promises[TYPE];
+ is(h, hs.highlighters[TYPE], "The initialized highlighter is the right one");
+});
diff --git a/toolkit/devtools/styleinspector/test/doc_content_stylesheet.html b/toolkit/devtools/styleinspector/test/doc_content_stylesheet.html
new file mode 100644
index 000000000..3d3b132d6
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_content_stylesheet.html
@@ -0,0 +1,33 @@
+<html>
+<head>
+ <title>test</title>
+
+ <link href="./doc_content_stylesheet_linked.css" rel="stylesheet" type="text/css">
+
+ <script>
+ // Load script.css
+ function loadCSS() {
+ var link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.type = 'text/css';
+ link.href = "./doc_content_stylesheet_script.css";
+ document.getElementsByTagName('head')[0].appendChild(link);
+ }
+ </script>
+
+ <style>
+ table {
+ border: 1px solid #000;
+ }
+ </style>
+</head>
+<body onload="loadCSS();">
+ <table id="target">
+ <tr>
+ <td>
+ <h3>Simple test</h3>
+ </td>
+ </tr>
+ </table>
+</body>
+</html>
diff --git a/toolkit/devtools/styleinspector/test/doc_content_stylesheet.xul b/toolkit/devtools/styleinspector/test/doc_content_stylesheet.xul
new file mode 100644
index 000000000..efd53815d
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_content_stylesheet.xul
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/xul.css" type="text/css"?>
+<?xml-stylesheet href="./doc_content_stylesheet_xul.css"
+ type="text/css"?>
+<!DOCTYPE window>
+<window id="testwindow" xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <label id="target" value="Simple XUL document" />
+</window> \ No newline at end of file
diff --git a/toolkit/devtools/styleinspector/test/doc_content_stylesheet_imported.css b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_imported.css
new file mode 100644
index 000000000..ea1a3d986
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_imported.css
@@ -0,0 +1,5 @@
+@import url("./doc_content_stylesheet_imported2.css");
+
+#target {
+ text-decoration: underline;
+}
diff --git a/toolkit/devtools/styleinspector/test/doc_content_stylesheet_imported2.css b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_imported2.css
new file mode 100644
index 000000000..77c73299e
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_imported2.css
@@ -0,0 +1,3 @@
+#target {
+ text-decoration: underline;
+}
diff --git a/toolkit/devtools/styleinspector/test/doc_content_stylesheet_linked.css b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_linked.css
new file mode 100644
index 000000000..712ba78fb
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_linked.css
@@ -0,0 +1,3 @@
+table {
+ border-collapse: collapse;
+}
diff --git a/toolkit/devtools/styleinspector/test/doc_content_stylesheet_script.css b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_script.css
new file mode 100644
index 000000000..5aa5e2c6c
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_script.css
@@ -0,0 +1,5 @@
+@import url("./doc_content_stylesheet_imported.css");
+
+table {
+ opacity: 1;
+}
diff --git a/toolkit/devtools/styleinspector/test/doc_content_stylesheet_xul.css b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_xul.css
new file mode 100644
index 000000000..a14ae7f6f
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_xul.css
@@ -0,0 +1,3 @@
+#target {
+ font-size: 200px;
+}
diff --git a/toolkit/devtools/styleinspector/test/doc_frame_script.js b/toolkit/devtools/styleinspector/test/doc_frame_script.js
new file mode 100644
index 000000000..6ab0916af
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_frame_script.js
@@ -0,0 +1,91 @@
+/* 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";
+
+// A helper frame-script for brower/devtools/styleinspector tests.
+//
+// Most listeners in the script expect "Test:"-namespaced messages from chrome,
+// then execute code upon receiving, and immediately send back a message.
+// This is so that chrome test code can execute code in content and wait for a
+// response this way:
+// let response = yield executeInContent(browser, "Test:MessageName", data, true);
+// The response message should have the same name "Test:MessageName"
+//
+// Some listeners do not send a response message back.
+
+let {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+let {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
+let {CssLogic} = require("devtools/styleinspector/css-logic");
+
+/**
+ * Get a value for a given property name in a css rule in a stylesheet, given
+ * their indexes
+ * @param {Object} data Expects a data object with the following properties
+ * - {Number} styleSheetIndex
+ * - {Number} ruleIndex
+ * - {String} name
+ * @return {String} The value, if found, null otherwise
+ */
+addMessageListener("Test:GetRulePropertyValue", function(msg) {
+ let {name, styleSheetIndex, ruleIndex} = msg.data;
+ let value = null;
+
+ dumpn("Getting the value for property name " + name + " in sheet " +
+ styleSheetIndex + " and rule " + ruleIndex);
+
+ let sheet = content.document.styleSheets[styleSheetIndex];
+ if (sheet) {
+ let rule = sheet.cssRules[ruleIndex];
+ if (rule) {
+ value = rule.style.getPropertyValue(name);
+ }
+ }
+
+ sendAsyncMessage("Test:GetRulePropertyValue", value);
+});
+
+/**
+ * Get information about all the stylesheets that contain rules that apply to
+ * a given node. The information contains the sheet href and whether or not the
+ * sheet is a content sheet or not
+ * @param {Object} objects Expects a 'target' CPOW object
+ * @return {Array} A list of stylesheet info objects
+ */
+addMessageListener("Test:GetStyleSheetsInfoForNode", function(msg) {
+ let target = msg.objects.target;
+ let sheets = [];
+
+ let domUtils = Cc["@mozilla.org/inspector/dom-utils;1"]
+ .getService(Ci.inIDOMUtils);
+ let domRules = domUtils.getCSSStyleRules(target);
+
+ for (let i = 0, n = domRules.Count(); i < n; i++) {
+ let sheet = domRules.GetElementAt(i).parentStyleSheet;
+ sheets.push({
+ href: sheet.href,
+ isContentSheet: CssLogic.isContentStylesheet(sheet)
+ });
+ }
+
+ sendAsyncMessage("Test:GetStyleSheetsInfoForNode", sheets);
+});
+
+/**
+ * Get the property value from the computed style for an element.
+ * @param {Object} data Expects a data object with the following properties
+ * - {String} selector: The selector used to obtain the element.
+ * - {String} pseudo: pseudo id to query, or null.
+ * - {String} name: name of the property
+ * @return {String} The value, if found, null otherwise
+ */
+addMessageListener("Test:GetComputedStylePropertyValue", function(msg) {
+ let {selector, pseudo, name} = msg.data;
+ let element = content.document.querySelector(selector);
+ let value = content.document.defaultView.getComputedStyle(element, pseudo).getPropertyValue(name);
+ sendAsyncMessage("Test:GetComputedStylePropertyValue", value);
+});
+
+let dumpn = msg => dump(msg + "\n");
diff --git a/toolkit/devtools/styleinspector/test/doc_keyframeanimation.css b/toolkit/devtools/styleinspector/test/doc_keyframeanimation.css
new file mode 100644
index 000000000..64582ed35
--- /dev/null
+++ b/toolkit/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;
+ }
+}
diff --git a/toolkit/devtools/styleinspector/test/doc_keyframeanimation.html b/toolkit/devtools/styleinspector/test/doc_keyframeanimation.html
new file mode 100644
index 000000000..4e02c32f0
--- /dev/null
+++ b/toolkit/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>
diff --git a/toolkit/devtools/styleinspector/test/doc_matched_selectors.html b/toolkit/devtools/styleinspector/test/doc_matched_selectors.html
new file mode 100644
index 000000000..8fe007409
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_matched_selectors.html
@@ -0,0 +1,28 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+ <head>
+ <style>
+ .matched1, .matched2, .matched3, .matched4, .matched5 {
+ color: #000;
+ }
+
+ div {
+ position: absolute;
+ top: 40px;
+ left: 20px;
+ border: 1px solid #000;
+ color: #111;
+ width: 100px;
+ height: 50px;
+ }
+ </style>
+ </head>
+ <body>
+ inspectstyle($("test"));
+ <div id="test" class="matched1 matched2 matched3 matched4 matched5">Test div</div>
+ <div id="dummy">
+ <div></div>
+ </div>
+ </body>
+</html>
diff --git a/toolkit/devtools/styleinspector/test/doc_media_queries.html b/toolkit/devtools/styleinspector/test/doc_media_queries.html
new file mode 100644
index 000000000..1adb8bc7a
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_media_queries.html
@@ -0,0 +1,24 @@
+<html>
+<head>
+ <title>test</title>
+ <script type="application/javascript;version=1.7">
+
+ </script>
+ <style>
+ div {
+ width: 1000px;
+ height: 100px;
+ background-color: #f00;
+ }
+
+ @media screen and (min-width: 1px) {
+ div {
+ width: 200px;
+ }
+ }
+ </style>
+</head>
+<body>
+<div></div>
+</body>
+</html>
diff --git a/toolkit/devtools/styleinspector/test/doc_pseudoelement.html b/toolkit/devtools/styleinspector/test/doc_pseudoelement.html
new file mode 100644
index 000000000..6145d4bf1
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_pseudoelement.html
@@ -0,0 +1,131 @@
+<!-- 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;
+}
+
+.box:first-line {
+ color: orange;
+ background: red;
+}
+
+.box:first-letter {
+ color: green;
+}
+
+* {
+ 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;
+}
+
+.topleft:first-line {
+ color: orange;
+}
+.topleft::selection {
+ color: orange;
+}
+
+.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>
diff --git a/toolkit/devtools/styleinspector/test/doc_sourcemaps.css b/toolkit/devtools/styleinspector/test/doc_sourcemaps.css
new file mode 100644
index 000000000..a9b437a40
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_sourcemaps.css
@@ -0,0 +1,7 @@
+div {
+ color: #ff0066; }
+
+span {
+ background-color: #EEE; }
+
+/*# sourceMappingURL=doc_sourcemaps.css.map */ \ No newline at end of file
diff --git a/toolkit/devtools/styleinspector/test/doc_sourcemaps.css.map b/toolkit/devtools/styleinspector/test/doc_sourcemaps.css.map
new file mode 100644
index 000000000..0f7486fd9
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_sourcemaps.css.map
@@ -0,0 +1,7 @@
+{
+"version": 3,
+"mappings": "AAGA,GAAI;EACF,KAAK,EAHU,OAAI;;AAMrB,IAAK;EACH,gBAAgB,EAAE,IAAI",
+"sources": ["doc_sourcemaps.scss"],
+"names": [],
+"file": "doc_sourcemaps.css"
+}
diff --git a/toolkit/devtools/styleinspector/test/doc_sourcemaps.html b/toolkit/devtools/styleinspector/test/doc_sourcemaps.html
new file mode 100644
index 000000000..0014e55fe
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_sourcemaps.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+ <title>testcase for testing CSS source maps</title>
+ <link rel="stylesheet" type="text/css" href="simple.css"/>
+ <link rel="stylesheet" type="text/css" href="doc_sourcemaps.css"/>
+</head>
+<body>
+ <div>source maps <span>testcase</span></div>
+</body>
+</html>
diff --git a/toolkit/devtools/styleinspector/test/doc_sourcemaps.scss b/toolkit/devtools/styleinspector/test/doc_sourcemaps.scss
new file mode 100644
index 000000000..0ff6c471b
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_sourcemaps.scss
@@ -0,0 +1,10 @@
+
+$paulrougetpink: #f06;
+
+div {
+ color: $paulrougetpink;
+}
+
+span {
+ background-color: #EEE;
+} \ No newline at end of file
diff --git a/toolkit/devtools/styleinspector/test/doc_style_editor_link.css b/toolkit/devtools/styleinspector/test/doc_style_editor_link.css
new file mode 100644
index 000000000..e49e1f587
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_style_editor_link.css
@@ -0,0 +1,3 @@
+div {
+ opacity: 1;
+} \ No newline at end of file
diff --git a/toolkit/devtools/styleinspector/test/doc_test_image.png b/toolkit/devtools/styleinspector/test/doc_test_image.png
new file mode 100644
index 000000000..769c63634
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_test_image.png
Binary files differ
diff --git a/toolkit/devtools/styleinspector/test/doc_urls_clickable.css b/toolkit/devtools/styleinspector/test/doc_urls_clickable.css
new file mode 100644
index 000000000..802b580d4
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_urls_clickable.css
@@ -0,0 +1,9 @@
+.relative1 {
+ background-image: url(./doc_test_image.png);
+}
+.absolute {
+ background: url("http://example.com/browser/browser/devtools/styleinspector/test/doc_test_image.png");
+}
+.base64 {
+ background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==');
+} \ No newline at end of file
diff --git a/toolkit/devtools/styleinspector/test/doc_urls_clickable.html b/toolkit/devtools/styleinspector/test/doc_urls_clickable.html
new file mode 100644
index 000000000..b0265a703
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/doc_urls_clickable.html
@@ -0,0 +1,30 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+ <head>
+
+ <link href="./doc_urls_clickable.css" rel="stylesheet" type="text/css">
+
+ <style>
+ .relative2 {
+ background-image: url(doc_test_image.png);
+ }
+ </style>
+ </head>
+ <body>
+
+ <div class="relative1">Background image #1 with relative path (loaded from external css)</div>
+
+ <div class="relative2">Background image #2 with relative path (loaded from style tag)</div>
+
+ <div class="absolute">Background image with absolute path (loaded from external css)</div>
+
+ <div class="base64">Background image with base64 url (loaded from external css)</div>
+
+ <div class="inline" style="background: url(doc_test_image.png);">Background image with relative path (loaded from style attribute)</div>
+
+ <div class="inline-resolved" style="background-image: url(./doc_test_image.png)">Background image with resolved relative path (loaded from style attribute)</div>
+
+ <div class="noimage">No background image :(</div>
+ </body>
+</html>
diff --git a/toolkit/devtools/styleinspector/test/head.js b/toolkit/devtools/styleinspector/test/head.js
new file mode 100644
index 000000000..c030cf577
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/head.js
@@ -0,0 +1,914 @@
+/* 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";
+
+const Cu = Components.utils;
+let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let TargetFactory = devtools.TargetFactory;
+let {CssHtmlTree} = devtools.require("devtools/styleinspector/computed-view");
+let {CssRuleView, _ElementStyle} = devtools.require("devtools/styleinspector/rule-view");
+let {CssLogic, CssSelector} = devtools.require("devtools/styleinspector/css-logic");
+let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+let {editableField, getInplaceEditorForSpan: inplaceEditor} = devtools.require("devtools/shared/inplace-editor");
+let {console} = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {});
+
+// All tests are asynchronous
+waitForExplicitFinish();
+
+const TEST_URL_ROOT = "http://example.com/browser/browser/devtools/styleinspector/test/";
+const TEST_URL_ROOT_SSL = "https://example.com/browser/browser/devtools/styleinspector/test/";
+const ROOT_TEST_DIR = getRootDirectory(gTestPath);
+const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
+
+// Auto clean-up when a test ends
+registerCleanupFunction(function*() {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+});
+
+// Uncomment this pref to dump all devtools emitted events to the console.
+// Services.prefs.setBoolPref("devtools.dump.emit", true);
+
+// Set the testing flag on gDevTools and reset it when the test ends
+gDevTools.testing = true;
+registerCleanupFunction(() => gDevTools.testing = false);
+
+// Clean-up all prefs that might have been changed during a test run
+// (safer here because if the test fails, then the pref is never reverted)
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
+ Services.prefs.clearUserPref("devtools.dump.emit");
+ Services.prefs.clearUserPref("devtools.defaultColorUnit");
+});
+
+/**
+ * The functions found below are here to ease test development and maintenance.
+ * Most of these functions are stateless and will require some form of context
+ * (the instance of the current toolbox, or inspector panel for instance).
+ *
+ * Most of these functions are async too and return promises.
+ *
+ * All tests should follow the following pattern:
+ *
+ * add_task(function*() {
+ * yield addTab(TEST_URI);
+ * let {toolbox, inspector, view} = yield openComputedView();
+ *
+ * yield selectNode("#test", inspector);
+ * yield someAsyncTestFunction(view);
+ * });
+ *
+ * add_task is the way to define the testcase in the test file. It accepts
+ * a single generator-function argument.
+ * The generator function should yield any async call.
+ *
+ * There is no need to clean tabs up at the end of a test as this is done
+ * automatically.
+ *
+ * It is advised not to store any references on the global scope. There shouldn't
+ * be a need to anyway. Thanks to add_task, test steps, even though asynchronous,
+ * can be described in a nice flat way, and if/for/while/... control flow can be
+ * used as in sync code, making it possible to write the outline of the test case
+ * all in add_task, and delegate actual processing and assertions to other
+ * functions.
+ */
+
+/* *********************************************
+ * UTILS
+ * *********************************************
+ * General test utilities.
+ * Add new tabs, open the toolbox and switch to the various panels, select
+ * nodes, get node references, ...
+ */
+
+/**
+ * Add a new test tab in the browser and load the given url.
+ * @param {String} url The url to be loaded in the new tab
+ * @return a promise that resolves to the tab object when the url is loaded
+ */
+function addTab(url) {
+ info("Adding a new tab with URL: '" + url + "'");
+ let def = promise.defer();
+
+ window.focus();
+
+ let tab = window.gBrowser.selectedTab = window.gBrowser.addTab(url);
+ let browser = tab.linkedBrowser;
+
+ info("Loading the helper frame script " + FRAME_SCRIPT_URL);
+ browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
+
+ browser.addEventListener("load", function onload() {
+ browser.removeEventListener("load", onload, true);
+ info("URL '" + url + "' loading complete");
+
+ def.resolve(tab);
+ }, true);
+
+ return def.promise;
+}
+
+/**
+ * Simple DOM node accesor function that takes either a node or a string css
+ * selector as argument and returns the corresponding node
+ * @param {String|DOMNode} nodeOrSelector
+ * @return {DOMNode|CPOW} Note that in e10s mode a CPOW object is returned which
+ * doesn't implement *all* of the DOMNode's properties
+ */
+function getNode(nodeOrSelector) {
+ info("Getting the node for '" + nodeOrSelector + "'");
+ return typeof nodeOrSelector === "string" ?
+ content.document.querySelector(nodeOrSelector) :
+ nodeOrSelector;
+}
+
+/**
+ * Get the NodeFront for a given css selector, via the protocol
+ * @param {String} selector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return {Promise} Resolves to the NodeFront instance
+ */
+function getNodeFront(selector, {walker}) {
+ return walker.querySelector(walker.rootNode, selector);
+}
+
+/**
+ * Highlight a node that matches the given css selector and set the inspector's
+ * current selection to this node.
+ * @param {String} selector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return {Promise} Resolves when the inspector is updated with the new node
+ */
+let selectAndHighlightNode = Task.async(function*(selector, inspector) {
+ info("Highlighting and selecting the node for " + selector);
+
+ let nodeFront = yield getNodeFront(selector, inspector);
+ let updated = inspector.toolbox.once("highlighter-ready");
+ inspector.selection.setNodeFront(nodeFront, "test-highlight");
+ yield updated;
+});
+
+/*
+ * Set the inspector's current selection to a node or to the first match of the
+ * given css selector.
+ * @param {String|NodeFront}
+ * data The node to select
+ * @param {InspectorPanel} inspector
+ * The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @param {String} reason
+ * Defaults to "test" which instructs the inspector not
+ * to highlight the node upon selection
+ * @return {Promise} Resolves when the inspector is updated with the new node
+ */
+let selectNode = Task.async(function*(data, inspector, reason="test") {
+ info("Selecting the node for '" + data + "'");
+ let nodeFront = data;
+ if (!data._form) {
+ nodeFront = yield getNodeFront(data, inspector);
+ }
+ let updated = inspector.once("inspector-updated");
+ inspector.selection.setNodeFront(nodeFront, reason);
+ yield updated;
+});
+
+/**
+ * Set the inspector's current selection to null so that no node is selected
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return a promise that resolves when the inspector is updated
+ */
+function clearCurrentNodeSelection(inspector) {
+ info("Clearing the current selection");
+ let updated = inspector.once("inspector-updated");
+ inspector.selection.setNodeFront(null);
+ return updated;
+}
+
+/**
+ * Open the toolbox, with the inspector tool visible.
+ * @return a promise that resolves when the inspector is ready
+ */
+let openInspector = Task.async(function*() {
+ info("Opening the inspector");
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ let inspector, toolbox;
+
+ // Checking if the toolbox and the inspector are already loaded
+ // The inspector-updated event should only be waited for if the inspector
+ // isn't loaded yet
+ toolbox = gDevTools.getToolbox(target);
+ if (toolbox) {
+ inspector = toolbox.getPanel("inspector");
+ if (inspector) {
+ info("Toolbox and inspector already open");
+ return {
+ toolbox: toolbox,
+ inspector: inspector
+ };
+ }
+ }
+
+ info("Opening the toolbox");
+ toolbox = yield gDevTools.showToolbox(target, "inspector");
+ yield waitForToolboxFrameFocus(toolbox);
+ inspector = toolbox.getPanel("inspector");
+
+ info("Waiting for the inspector to update");
+ yield inspector.once("inspector-updated");
+
+ return {
+ toolbox: toolbox,
+ inspector: inspector
+ };
+});
+
+/**
+ * Wait for the toolbox frame to receive focus after it loads
+ * @param {Toolbox} toolbox
+ * @return a promise that resolves when focus has been received
+ */
+function waitForToolboxFrameFocus(toolbox) {
+ info("Making sure that the toolbox's frame is focused");
+ let def = promise.defer();
+ let win = toolbox.frame.contentWindow;
+ waitForFocus(def.resolve, win);
+ return def.promise;
+}
+
+/**
+ * Open the toolbox, with the inspector tool visible, and the sidebar that
+ * corresponds to the given id selected
+ * @return a promise that resolves when the inspector is ready and the sidebar
+ * view is visible and ready
+ */
+let openInspectorSideBar = Task.async(function*(id) {
+ let {toolbox, inspector} = yield openInspector();
+
+ if (!hasSideBarTab(inspector, id)) {
+ info("Waiting for the " + id + " sidebar to be ready");
+ yield inspector.sidebar.once(id + "-ready");
+ }
+
+ info("Selecting the " + id + " sidebar");
+ inspector.sidebar.select(id);
+
+ return {
+ toolbox: toolbox,
+ inspector: inspector,
+ view: inspector.sidebar.getWindowForTab(id)[id].view
+ };
+});
+
+/**
+ * Open the toolbox, with the inspector tool visible, and the computed-view
+ * sidebar tab selected.
+ * @return a promise that resolves when the inspector is ready and the computed
+ * view is visible and ready
+ */
+function openComputedView() {
+ return openInspectorSideBar("computedview");
+}
+
+/**
+ * Open the toolbox, with the inspector tool visible, and the rule-view
+ * sidebar tab selected.
+ * @return a promise that resolves when the inspector is ready and the rule
+ * view is visible and ready
+ */
+function openRuleView() {
+ return openInspectorSideBar("ruleview");
+}
+
+/**
+ * Wait for eventName on target.
+ * @param {Object} target An observable object that either supports on/off or
+ * addEventListener/removeEventListener
+ * @param {String} eventName
+ * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener
+ * @return A promise that resolves when the event has been handled
+ */
+function once(target, eventName, useCapture=false) {
+ info("Waiting for event: '" + eventName + "' on " + target + ".");
+
+ let deferred = promise.defer();
+
+ for (let [add, remove] of [
+ ["addEventListener", "removeEventListener"],
+ ["addListener", "removeListener"],
+ ["on", "off"]
+ ]) {
+ if ((add in target) && (remove in target)) {
+ target[add](eventName, function onEvent(...aArgs) {
+ target[remove](eventName, onEvent, useCapture);
+ deferred.resolve.apply(deferred, aArgs);
+ }, useCapture);
+ break;
+ }
+ }
+
+ return deferred.promise;
+}
+
+/**
+ * This shouldn't be used in the tests, but is useful when writing new tests or
+ * debugging existing tests in order to introduce delays in the test steps
+ * @param {Number} ms The time to wait
+ * @return A promise that resolves when the time is passed
+ */
+function wait(ms) {
+ let def = promise.defer();
+ content.setTimeout(def.resolve, ms);
+ return def.promise;
+}
+
+/**
+ * Wait for a content -> chrome message on the message manager (the window
+ * messagemanager is used).
+ * @param {String} name The message name
+ * @return {Promise} A promise that resolves to the response data when the
+ * message has been received
+ */
+function waitForContentMessage(name) {
+ info("Expecting message " + name + " from content");
+
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ let def = promise.defer();
+ mm.addMessageListener(name, function onMessage(msg) {
+ mm.removeMessageListener(name, onMessage);
+ def.resolve(msg.data);
+ });
+ return def.promise;
+}
+
+/**
+ * Send an async message to the frame script (chrome -> content) and wait for a
+ * response message with the same name (content -> chrome).
+ * @param {String} name The message name. Should be one of the messages defined
+ * in doc_frame_script.js
+ * @param {Object} data Optional data to send along
+ * @param {Object} objects Optional CPOW objects to send along
+ * @param {Boolean} expectResponse If set to false, don't wait for a response
+ * with the same name from the content script. Defaults to true.
+ * @return {Promise} Resolves to the response data if a response is expected,
+ * immediately resolves otherwise
+ */
+function executeInContent(name, data={}, objects={}, expectResponse=true) {
+ info("Sending message " + name + " to content");
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ mm.sendAsyncMessage(name, data, objects);
+ if (expectResponse) {
+ return waitForContentMessage(name);
+ } else {
+ return promise.resolve();
+ }
+}
+
+/**
+ * Send an async message to the frame script and get back the requested
+ * computed style property.
+ * @param {String} selector: The selector used to obtain the element.
+ * @param {String} pseudo: pseudo id to query, or null.
+ * @param {String} name: name of the property.
+ */
+function* getComputedStyleProperty(selector, pseudo, propName) {
+ return yield executeInContent("Test:GetComputedStylePropertyValue",
+ {selector: selector,
+ pseudo: pseudo,
+ name: propName});
+}
+
+/**
+ * Given an inplace editable element, click to switch it to edit mode, wait for
+ * focus
+ * @return a promise that resolves to the inplace-editor element when ready
+ */
+let focusEditableField = Task.async(function*(editable, xOffset=1, yOffset=1, options={}) {
+ let onFocus = once(editable.parentNode, "focus", true);
+
+ info("Clicking on editable field to turn to edit mode");
+ EventUtils.synthesizeMouse(editable, xOffset, yOffset, options,
+ editable.ownerDocument.defaultView);
+ let event = yield onFocus;
+
+ info("Editable field gained focus, returning the input field now");
+ return inplaceEditor(editable.ownerDocument.activeElement);
+});
+
+/**
+ * Given a tooltip object instance (see Tooltip.js), checks if it is set to
+ * toggle and hover and if so, checks if the given target is a valid hover target.
+ * This won't actually show the tooltip (the less we interact with XUL panels
+ * during test runs, the better).
+ * @return a promise that resolves when the answer is known
+ */
+function isHoverTooltipTarget(tooltip, target) {
+ if (!tooltip._basedNode || !tooltip.panel) {
+ return promise.reject(new Error(
+ "The tooltip passed isn't set to toggle on hover or is not a tooltip"));
+ }
+ return tooltip.isValidHoverTarget(target);
+}
+
+/**
+ * Same as isHoverTooltipTarget except that it will fail the test if there is no
+ * tooltip defined on hover of the given element
+ * @return a promise
+ */
+function assertHoverTooltipOn(tooltip, element) {
+ return isHoverTooltipTarget(tooltip, element).then(() => {
+ ok(true, "A tooltip is defined on hover of the given element");
+ }, () => {
+ ok(false, "No tooltip is defined on hover of the given element");
+ });
+}
+
+/**
+ * Same as assertHoverTooltipOn but fails the test if there is a tooltip defined
+ * on hover of the given element
+ * @return a promise
+ */
+function assertNoHoverTooltipOn(tooltip, element) {
+ return isHoverTooltipTarget(tooltip, element).then(() => {
+ ok(false, "A tooltip is defined on hover of the given element");
+ }, () => {
+ ok(true, "No tooltip is defined on hover of the given element");
+ });
+}
+
+/**
+ * Listen for a new window to open and return a promise that resolves when one
+ * does and completes its load.
+ * Only resolves when the new window topic isn't domwindowopened.
+ * @return a promise that resolves to the window object
+ */
+function waitForWindow() {
+ let def = promise.defer();
+
+ info("Waiting for a window to open");
+ Services.ww.registerNotification(function onWindow(subject, topic) {
+ if (topic != "domwindowopened") {
+ return;
+ }
+ info("A window has been opened");
+ let win = subject.QueryInterface(Ci.nsIDOMWindow);
+ once(win, "load").then(() => {
+ info("The window load completed");
+ Services.ww.unregisterNotification(onWindow);
+ def.resolve(win);
+ });
+ });
+
+ return def.promise;
+}
+
+/**
+ * @see SimpleTest.waitForClipboard
+ * @param {Function} setup Function to execute before checking for the
+ * clipboard content
+ * @param {String|Boolean} expected An expected string or validator function
+ * @return a promise that resolves when the expected string has been found or
+ * the validator function has returned true, rejects otherwise.
+ */
+function waitForClipboard(setup, expected) {
+ let def = promise.defer();
+ SimpleTest.waitForClipboard(expected, setup, def.resolve, def.reject);
+ return def.promise;
+}
+
+/**
+ * Dispatch the copy event on the given element
+ */
+function fireCopyEvent(element) {
+ let evt = element.ownerDocument.createEvent("Event");
+ evt.initEvent("copy", true, true);
+ element.dispatchEvent(evt);
+}
+
+/**
+ * Polls a given function waiting for it to return true.
+ *
+ * @param {Function} validatorFn A validator function that returns a boolean.
+ * This is called every few milliseconds to check if the result is true. When
+ * it is true, the promise resolves. If validatorFn never returns true, then
+ * polling timeouts after several tries and the promise rejects.
+ * @param {String} name Optional name of the test. This is used to generate
+ * the success and failure messages.
+ * @return a promise that resolves when the function returned true or rejects
+ * if the timeout is reached
+ */
+function waitForSuccess(validatorFn, name="untitled") {
+ let def = promise.defer();
+ let start = Date.now();
+
+ function wait(validatorFn) {
+ if (validatorFn()) {
+ ok(true, "Validator function " + name + " returned true");
+ def.resolve();
+ } else {
+ setTimeout(() => wait(validatorFn), 200);
+ }
+ }
+ wait(validatorFn);
+
+ return def.promise;
+}
+
+/**
+ * Create a new style tag containing the given style text and append it to the
+ * document's head node
+ * @param {Document} doc
+ * @param {String} style
+ * @return {DOMNode} The newly created style node
+ */
+function addStyle(doc, style) {
+ info("Adding a new style tag to the document with style content: " +
+ style.substring(0, 50));
+ let node = doc.createElement('style');
+ node.setAttribute("type", "text/css");
+ node.textContent = style;
+ doc.getElementsByTagName("head")[0].appendChild(node);
+ return node;
+}
+
+/**
+ * Checks whether the inspector's sidebar corresponding to the given id already
+ * exists
+ * @param {InspectorPanel}
+ * @param {String}
+ * @return {Boolean}
+ */
+function hasSideBarTab(inspector, id) {
+ return !!inspector.sidebar.getWindowForTab(id);
+}
+
+/**
+ * Get the dataURL for the font family tooltip.
+ * @param {String} font The font family value.
+ * @param {object} nodeFront
+ * The NodeActor that will used to retrieve the dataURL for the
+ * font family tooltip contents.
+ */
+let getFontFamilyDataURL = Task.async(function*(font, nodeFront) {
+ let fillStyle = (Services.prefs.getCharPref("devtools.theme") === "light") ?
+ "black" : "white";
+
+ let {data} = yield nodeFront.getFontFamilyDataURL(font, fillStyle);
+ let dataURL = yield data.string();
+ return dataURL;
+});
+
+/* *********************************************
+ * RULE-VIEW
+ * *********************************************
+ * Rule-view related test utility functions
+ * This object contains functions to get rules, get properties, ...
+ */
+
+/**
+ * Get the DOMNode for a css rule in the rule-view that corresponds to the given
+ * selector
+ * @param {CssRuleView} view The instance of the rule-view panel
+ * @param {String} selectorText The selector in the rule-view for which the rule
+ * object is wanted
+ * @return {DOMNode}
+ */
+function getRuleViewRule(view, selectorText) {
+ let rule;
+ for (let r of view.doc.querySelectorAll(".ruleview-rule")) {
+ let selector = r.querySelector(".ruleview-selector, .ruleview-selector-matched");
+ if (selector && selector.textContent === selectorText) {
+ rule = r;
+ break;
+ }
+ }
+
+ return rule;
+}
+
+/**
+ * Get references to the name and value span nodes corresponding to a given
+ * selector and property name in the rule-view
+ * @param {CssRuleView} view The instance of the rule-view panel
+ * @param {String} selectorText The selector in the rule-view to look for the
+ * property in
+ * @param {String} propertyName The name of the property
+ * @return {Object} An object like {nameSpan: DOMNode, valueSpan: DOMNode}
+ */
+function getRuleViewProperty(view, selectorText, propertyName) {
+ let prop;
+
+ let rule = getRuleViewRule(view, selectorText);
+ if (rule) {
+ // Look for the propertyName in that rule element
+ for (let p of rule.querySelectorAll(".ruleview-property")) {
+ let nameSpan = p.querySelector(".ruleview-propertyname");
+ let valueSpan = p.querySelector(".ruleview-propertyvalue");
+
+ if (nameSpan.textContent === propertyName) {
+ prop = {nameSpan: nameSpan, valueSpan: valueSpan};
+ break;
+ }
+ }
+ }
+ return prop;
+}
+
+/**
+ * Get the text value of the property corresponding to a given selector and name
+ * in the rule-view
+ * @param {CssRuleView} view The instance of the rule-view panel
+ * @param {String} selectorText The selector in the rule-view to look for the
+ * property in
+ * @param {String} propertyName The name of the property
+ * @return {String} The property value
+ */
+function getRuleViewPropertyValue(view, selectorText, propertyName) {
+ return getRuleViewProperty(view, selectorText, propertyName)
+ .valueSpan.textContent;
+}
+
+/**
+ * Get a reference to the selector DOM element corresponding to a given selector
+ * in the rule-view
+ * @param {CssRuleView} view The instance of the rule-view panel
+ * @param {String} selectorText The selector in the rule-view to look for
+ * @return {DOMNode} The selector DOM element
+ */
+function getRuleViewSelector(view, selectorText) {
+ let rule = getRuleViewRule(view, selectorText);
+ return rule.querySelector(".ruleview-selector, .ruleview-selector-matched");
+}
+
+/**
+ * Simulate a color change in a given color picker tooltip, and optionally wait
+ * for a given element in the page to have its style changed as a result
+ * @param {SwatchColorPickerTooltip} colorPicker
+ * @param {Array} newRgba The new color to be set [r, g, b, a]
+ * @param {Object} expectedChange Optional object that needs the following props:
+ * - {DOMNode} element The element in the page that will have its
+ * style changed.
+ * - {String} name The style name that will be changed
+ * - {String} value The expected style value
+ * The style will be checked like so: getComputedStyle(element)[name] === value
+ */
+let simulateColorPickerChange = Task.async(function*(colorPicker, newRgba, expectedChange) {
+ info("Getting the spectrum colorpicker object");
+ let spectrum = yield colorPicker.spectrum;
+ info("Setting the new color");
+ spectrum.rgb = newRgba;
+ info("Applying the change");
+ spectrum.updateUI();
+ spectrum.onChange();
+
+ if (expectedChange) {
+ info("Waiting for the style to be applied on the page");
+ yield waitForSuccess(() => {
+ let {element, name, value} = expectedChange;
+ return content.getComputedStyle(element)[name] === value;
+ }, "Color picker change applied on the page");
+ }
+});
+
+/**
+ * Get a rule-link 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
+ * @return {DOMNode} The link if any at this index
+ */
+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} 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, 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
+ */
+let focusNewRuleViewProperty = Task.async(function*(ruleEditor) {
+ info("Clicking on a close ruleEditor brace to start editing a new property");
+ ruleEditor.closeBrace.scrollIntoView();
+ let editor = yield focusEditableField(ruleEditor.closeBrace);
+
+ is(inplaceEditor(ruleEditor.newPropSpan), editor, "Focused editor is the new property editor.");
+ is(ruleEditor.rule.textProps.length, 0, "Starting with one new text property.");
+ is(ruleEditor.propertyList.children.length, 1, "Starting with two property editors.");
+
+ return editor;
+});
+
+/**
+ * Create a new property name in the rule-view, focusing a new property editor
+ * by clicking on the close brace, and then entering the given text.
+ * Keep in mind that the rule-view knows how to handle strings with multiple
+ * properties, so the input text may be like: "p1:v1;p2:v2;p3:v3".
+ * @param {RuleEditor} ruleEditor The instance of RuleEditor that will receive
+ * the new property(ies)
+ * @param {String} inputValue The text to be entered in the new property name
+ * field
+ * @return a promise that resolves when the new property name has been entered
+ * and once the value field is focused
+ */
+let createNewRuleViewProperty = Task.async(function*(ruleEditor, inputValue) {
+ info("Creating a new property editor");
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+
+ info("Entering the value " + inputValue);
+ editor.input.value = inputValue;
+
+ info("Submitting the new value and waiting for value field focus");
+ let onFocus = once(ruleEditor.element, "focus", true);
+ EventUtils.synthesizeKey("VK_RETURN", {},
+ ruleEditor.element.ownerDocument.defaultView);
+ yield onFocus;
+});
+
+/* *********************************************
+ * COMPUTED-VIEW
+ * *********************************************
+ * Computed-view related utility functions.
+ * Allows to get properties, links, expand properties, ...
+ */
+
+/**
+ * Get references to the name and value span nodes corresponding to a given
+ * property name in the computed-view
+ * @param {CssHtmlTree} view The instance of the computed view panel
+ * @param {String} name The name of the property to retrieve
+ * @return an object {nameSpan, valueSpan}
+ */
+function getComputedViewProperty(view, name) {
+ let prop;
+ for (let property of view.styleDocument.querySelectorAll(".property-view")) {
+ let nameSpan = property.querySelector(".property-name");
+ let valueSpan = property.querySelector(".property-value");
+
+ if (nameSpan.textContent === name) {
+ prop = {nameSpan: nameSpan, valueSpan: valueSpan};
+ break;
+ }
+ }
+ return prop;
+}
+
+/**
+ * Get an instance of PropertyView from the computed-view.
+ * @param {CssHtmlTree} view The instance of the computed view panel
+ * @param {String} name The name of the property to retrieve
+ * @return {PropertyView}
+ */
+function getComputedViewPropertyView(view, name) {
+ let propView;
+ for (let propertyView of view.propertyViews) {
+ if (propertyView._propertyInfo.name === name) {
+ propView = propertyView;
+ break;
+ }
+ }
+ return propView;
+}
+
+/**
+ * Get a reference to the property-content element for a given property name in
+ * the computed-view.
+ * A property-content element always follows (nextSibling) the property itself
+ * and is only shown when the twisty icon is expanded on the property.
+ * A property-content element contains matched rules, with selectors, properties,
+ * values and stylesheet links
+ * @param {CssHtmlTree} view The instance of the computed view panel
+ * @param {String} name The name of the property to retrieve
+ * @return {Promise} A promise that resolves to the property matched rules
+ * container
+ */
+let getComputedViewMatchedRules = Task.async(function*(view, name) {
+ let expander;
+ let propertyContent;
+ for (let property of view.styleDocument.querySelectorAll(".property-view")) {
+ let nameSpan = property.querySelector(".property-name");
+ if (nameSpan.textContent === name) {
+ expander = property.querySelector(".expandable");
+ propertyContent = property.nextSibling;
+ break;
+ }
+ }
+
+ if (!expander.hasAttribute("open")) {
+ // Need to expand the property
+ let onExpand = view.inspector.once("computed-view-property-expanded");
+ expander.click();
+ yield onExpand;
+ }
+
+ return propertyContent;
+});
+
+/**
+ * Get the text value of the property corresponding to a given name in the
+ * computed-view
+ * @param {CssHtmlTree} view The instance of the computed view panel
+ * @param {String} name The name of the property to retrieve
+ * @return {String} The property value
+ */
+function getComputedViewPropertyValue(view, name, propertyName) {
+ return getComputedViewProperty(view, name, propertyName)
+ .valueSpan.textContent;
+}
+
+/**
+ * Expand a given property, given its index in the current property list of
+ * the computed view
+ * @param {CssHtmlTree} view The instance of the computed view panel
+ * @param {Number} index The index of the property to be expanded
+ * @return a promise that resolves when the property has been expanded, or
+ * rejects if the property was not found
+ */
+function expandComputedViewPropertyByIndex(view, index) {
+ info("Expanding property " + index + " in the computed view");
+ let expandos = view.styleDocument.querySelectorAll(".expandable");
+ if (!expandos.length || !expandos[index]) {
+ return promise.reject();
+ }
+
+ let onExpand = view.inspector.once("computed-view-property-expanded");
+ expandos[index].click();
+ return onExpand;
+}
+
+/**
+ * Get a rule-link from the computed-view given its index
+ * @param {CssHtmlTree} view The instance of the computed view panel
+ * @param {Number} index The index of the link to be retrieved
+ * @return {DOMNode} The link at the given index, if one exists, null otherwise
+ */
+function getComputedViewLinkByIndex(view, index) {
+ let links = view.styleDocument.querySelectorAll(".rule-link .link");
+ return links[index];
+}
+
+/* *********************************************
+ * STYLE-EDITOR
+ * *********************************************
+ * Style-editor related utility functions.
+ */
+
+/**
+ * Wait for the toolbox to emit the styleeditor-selected event and when done
+ * wait for the stylesheet identified by href to be loaded in the stylesheet
+ * editor
+ * @param {Toolbox} toolbox
+ * @param {String} href Optional, if not provided, wait for the first editor
+ * to be ready
+ * @return a promise that resolves to the editor when the stylesheet editor is
+ * ready
+ */
+function waitForStyleEditor(toolbox, href) {
+ let def = promise.defer();
+
+ info("Waiting for the toolbox to switch to the styleeditor");
+ toolbox.once("styleeditor-ready").then(() => {
+ let panel = toolbox.getCurrentPanel();
+ ok(panel && panel.UI, "Styleeditor panel switched to front");
+
+ panel.UI.on("editor-selected", function onEditorSelected(event, editor) {
+ let currentHref = editor.styleSheet.href;
+ if (!href || (href && currentHref.endsWith(href))) {
+ info("Stylesheet editor selected");
+ panel.UI.off("editor-selected", onEditorSelected);
+ editor.getSourceEditor().then(editor => {
+ info("Stylesheet editor fully loaded");
+ def.resolve(editor);
+ });
+ }
+ });
+ });
+
+ return def.promise;
+}
diff --git a/toolkit/devtools/styleinspector/test/unit/test_parseDeclarations.js b/toolkit/devtools/styleinspector/test/unit/test_parseDeclarations.js
new file mode 100644
index 000000000..9e33d3f12
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/unit/test_parseDeclarations.js
@@ -0,0 +1,208 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/devtools/Loader.jsm");
+const {parseDeclarations} = devtools.require("devtools/styleinspector/css-parsing-utils");
+
+const TEST_DATA = [
+ // Simple test
+ {
+ input: "p:v;",
+ expected: [{name: "p", value: "v", priority: ""}]
+ },
+ // Simple test
+ {
+ input: "this:is;a:test;",
+ expected: [
+ {name: "this", value: "is", priority: ""},
+ {name: "a", value: "test", priority: ""}
+ ]
+ },
+ // Test a single declaration with semi-colon
+ {
+ input: "name:value;",
+ expected: [{name: "name", value: "value", priority: ""}]
+ },
+ // Test a single declaration without semi-colon
+ {
+ input: "name:value",
+ expected: [{name: "name", value: "value", priority: ""}]
+ },
+ // Test multiple declarations separated by whitespaces and carriage returns and tabs
+ {
+ input: "p1 : v1 ; \t\t \n p2:v2; \n\n\n\n\t p3 : v3;",
+ expected: [
+ {name: "p1", value: "v1", priority: ""},
+ {name: "p2", value: "v2", priority: ""},
+ {name: "p3", value: "v3", priority: ""},
+ ]
+ },
+ // Test simple priority
+ {
+ input: "p1: v1; p2: v2 !important;",
+ expected: [
+ {name: "p1", value: "v1", priority: ""},
+ {name: "p2", value: "v2", priority: "important"}
+ ]
+ },
+ // Test simple priority
+ {
+ input: "p1: v1 !important; p2: v2",
+ expected: [
+ {name: "p1", value: "v1", priority: "important"},
+ {name: "p2", value: "v2", priority: ""}
+ ]
+ },
+ // Test simple priority
+ {
+ input: "p1: v1 ! important; p2: v2 ! important;",
+ expected: [
+ {name: "p1", value: "v1", priority: "important"},
+ {name: "p2", value: "v2", priority: "important"}
+ ]
+ },
+ // Test invalid priority
+ {
+ input: "p1: v1 important;",
+ expected: [
+ {name: "p1", value: "v1 important", priority: ""}
+ ]
+ },
+ // Test various types of background-image urls
+ {
+ input: "background-image: url(../../relative/image.png)",
+ expected: [{name: "background-image", value: "url(\"../../relative/image.png\")", priority: ""}]
+ },
+ {
+ input: "background-image: url(http://site.com/test.png)",
+ expected: [{name: "background-image", value: "url(\"http://site.com/test.png\")", priority: ""}]
+ },
+ {
+ input: "background-image: url(wow.gif)",
+ expected: [{name: "background-image", value: "url(\"wow.gif\")", priority: ""}]
+ },
+ // Test that urls with :;{} characters in them are parsed correctly
+ {
+ input: "background: red url(\"http://site.com/image{}:;.png?id=4#wat\") repeat top right",
+ expected: [
+ {name: "background", value: "red url(\"http://site.com/image{}:;.png?id=4#wat\") repeat top right", priority: ""}
+ ]
+ },
+ // Test that an empty string results in an empty array
+ {input: "", expected: []},
+ // Test that a string comprised only of whitespaces results in an empty array
+ {input: " \n \n \n \n \t \t\t\t ", expected: []},
+ // Test that a null input throws an exception
+ {input: null, throws: true},
+ // Test that a undefined input throws an exception
+ {input: undefined, throws: true},
+ // Test that :;{} characters in quoted content are not parsed as multiple declarations
+ {
+ input: "content: \";color:red;}selector{color:yellow;\"",
+ expected: [
+ {name: "content", value: "\";color:red;}selector{color:yellow;\"", priority: ""}
+ ]
+ },
+ // Test that rules aren't parsed, just declarations. So { and } found after a
+ // property name should be part of the property name, same for values.
+ {
+ input: "body {color:red;} p {color: blue;}",
+ expected: [
+ {name: "body {color", value: "red", priority: ""},
+ {name: "} p {color", value: "blue", priority: ""},
+ {name: "}", value: "", priority: ""}
+ ]
+ },
+ // Test unbalanced : and ;
+ {
+ input: "color :red : font : arial;",
+ expected : [
+ {name: "color", value: "red : font : arial", priority: ""}
+ ]
+ },
+ {input: "background: red;;;;;", expected: [{name: "background", value: "red", priority: ""}]},
+ {input: "background:;", expected: [{name: "background", value: "", priority: ""}]},
+ {input: ";;;;;", expected: []},
+ {input: ":;:;", expected: []},
+ // Test name only
+ {input: "color", expected: [
+ {name: "color", value: "", priority: ""}
+ ]},
+ // Test trailing name without :
+ {input: "color:blue;font", expected: [
+ {name: "color", value: "blue", priority: ""},
+ {name: "font", value: "", priority: ""}
+ ]},
+ // Test trailing name with :
+ {input: "color:blue;font:", expected: [
+ {name: "color", value: "blue", priority: ""},
+ {name: "font", value: "", priority: ""}
+ ]},
+ // Test leading value
+ {input: "Arial;color:blue;", expected: [
+ {name: "", value: "Arial", priority: ""},
+ {name: "color", value: "blue", priority: ""}
+ ]},
+ // Test hex colors
+ {input: "color: #333", expected: [{name: "color", value: "#333", priority: ""}]},
+ {input: "color: #456789", expected: [{name: "color", value: "#456789", priority: ""}]},
+ {input: "wat: #XYZ", expected: [{name: "wat", value: "#XYZ", priority: ""}]},
+ // Test string/url quotes escaping
+ {input: "content: \"this is a 'string'\"", expected: [{name: "content", value: "\"this is a 'string'\"", priority: ""}]},
+ {input: 'content: "this is a \\"string\\""', expected: [{name: "content", value: '\'this is a "string"\'', priority: ""}]},
+ {input: "content: 'this is a \"string\"'", expected: [{name: "content", value: '\'this is a "string"\'', priority: ""}]},
+ {input: "content: 'this is a \\'string\\'", expected: [{name: "content", value: '"this is a \'string\'"', priority: ""}]},
+ {input: "content: 'this \\' is a \" really strange string'", expected: [{name: "content", value: '"this \' is a \" really strange string"', priority: ""}]},
+ {
+ input: "content: \"a not s\\\
+ o very long title\"",
+ expected: [
+ {name: "content", value: '"a not s\
+ o very long title"', priority: ""}
+ ]
+ },
+ // Test calc with nested parentheses
+ {input: "width: calc((100% - 3em) / 2)", expected: [{name: "width", value: "calc((100% - 3em) / 2)", priority: ""}]},
+];
+
+function run_test() {
+ for (let test of TEST_DATA) {
+ do_print("Test input string " + test.input);
+ let output;
+ try {
+ output = parseDeclarations(test.input);
+ } catch (e) {
+ do_print("parseDeclarations threw an exception with the given input string");
+ if (test.throws) {
+ do_print("Exception expected");
+ do_check_true(true);
+ } else {
+ do_print("Exception unexpected\n" + e);
+ do_check_true(false);
+ }
+ }
+ if (output) {
+ assertOutput(output, test.expected);
+ }
+ }
+}
+
+function assertOutput(actual, expected) {
+ if (actual.length === expected.length) {
+ for (let i = 0; i < expected.length; i ++) {
+ do_check_true(!!actual[i]);
+ do_print("Check that the output item has the expected name, value and priority");
+ do_check_eq(expected[i].name, actual[i].name);
+ do_check_eq(expected[i].value, actual[i].value);
+ do_check_eq(expected[i].priority, actual[i].priority);
+ }
+ } else {
+ for (let prop of actual) {
+ do_print("Actual output contained: {name: "+prop.name+", value: "+prop.value+", priority: "+prop.priority+"}");
+ }
+ do_check_eq(actual.length, expected.length);
+ }
+}
diff --git a/toolkit/devtools/styleinspector/test/unit/test_parseSingleValue.js b/toolkit/devtools/styleinspector/test/unit/test_parseSingleValue.js
new file mode 100644
index 000000000..ff39878b7
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/unit/test_parseSingleValue.js
@@ -0,0 +1,76 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/devtools/Loader.jsm");
+const {parseSingleValue} = devtools.require("devtools/styleinspector/css-parsing-utils");
+
+const TEST_DATA = [
+ {input: null, throws: true},
+ {input: undefined, throws: true},
+ {input: "", expected: {value: "", priority: ""}},
+ {input: " \t \t \n\n ", expected: {value: "", priority: ""}},
+ {input: "blue", expected: {value: "blue", priority: ""}},
+ {input: "blue !important", expected: {value: "blue", priority: "important"}},
+ {input: "blue!important", expected: {value: "blue", priority: "important"}},
+ {input: "blue ! important", expected: {value: "blue", priority: "important"}},
+ {input: "blue ! important", expected: {value: "blue", priority: "important"}},
+ {input: "blue !", expected: {value: "blue", priority: ""}},
+ {input: "blue !mportant", expected: {value: "blue !mportant", priority: ""}},
+ {input: " blue !important ", expected: {value: "blue", priority: "important"}},
+ {
+ input: "url(\"http://url.com/whyWouldYouDoThat!important.png\") !important",
+ expected: {
+ value: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
+ priority: "important"
+ }
+ },
+ {
+ input: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
+ expected: {
+ value: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
+ priority: ""
+ }
+ },
+ {
+ input: "\"content!important\" !important",
+ expected: {
+ value: "\"content!important\"",
+ priority: "important"
+ }
+ },
+ {
+ input: "\"content!important\"",
+ expected: {
+ value: "\"content!important\"",
+ priority: ""
+ }
+ }
+];
+
+function run_test() {
+ for (let test of TEST_DATA) {
+ do_print("Test input value " + test.input);
+ try {
+ let output = parseSingleValue(test.input);
+ assertOutput(output, test.expected);
+ } catch (e) {
+ do_print("parseSingleValue threw an exception with the given input value");
+ if (test.throws) {
+ do_print("Exception expected");
+ do_check_true(true);
+ } else {
+ do_print("Exception unexpected\n" + e);
+ do_check_true(false);
+ }
+ }
+ }
+}
+
+function assertOutput(actual, expected) {
+ do_print("Check that the output has the expected value and priority");
+ do_check_eq(expected.value, actual.value);
+ do_check_eq(expected.priority, actual.priority);
+}
diff --git a/toolkit/devtools/styleinspector/test/unit/xpcshell.ini b/toolkit/devtools/styleinspector/test/unit/xpcshell.ini
new file mode 100644
index 000000000..90b0c5d9c
--- /dev/null
+++ b/toolkit/devtools/styleinspector/test/unit/xpcshell.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+head =
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android' || toolkit == 'gonk'
+
+[test_parseDeclarations.js]
+[test_parseSingleValue.js]