summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/rules
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/inspector/rules')
-rw-r--r--devtools/client/inspector/rules/models/element-style.js412
-rw-r--r--devtools/client/inspector/rules/models/moz.build11
-rw-r--r--devtools/client/inspector/rules/models/rule.js686
-rw-r--r--devtools/client/inspector/rules/models/text-property.js215
-rw-r--r--devtools/client/inspector/rules/moz.build16
-rw-r--r--devtools/client/inspector/rules/rules.js1673
-rw-r--r--devtools/client/inspector/rules/test/.eslintrc.js6
-rw-r--r--devtools/client/inspector/rules/test/browser.ini221
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js44
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-cancel_01.js44
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-cancel_02.js34
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-cancel_03.js43
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-commented.js47
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-svg.js22
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property_01.js32
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property_02.js65
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js30
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-button-state.js51
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-edit-selector.js55
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-iframes.js57
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-namespace-elements.js41
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-pseudo-class.js82
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-then-property-edit-selector.js80
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js42
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule.js47
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_authored.js49
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_authored_color.js67
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_authored_override.js53
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js20
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorUnit.js65
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_01.js63
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.js66
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click.js51
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-commit-on-ENTER.js61
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-edit-gradient.js77
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-on-tooltip.js46
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-multiple-changes.js124
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-release-outside-frame.js67
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-revert-on-ESC.js109
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-swatch-displayed.js73
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js139
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js123
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-new-property_01.js102
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js129
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-new-property_03.js47
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-new-property_04.js73
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js131
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-popup-hidden-after-navigation.js41
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_computed-lists_01.js47
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_computed-lists_02.js74
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_content_01.js51
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_content_02.js60
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-01.js96
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js61
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-03.js118
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_copy_styles.js307
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_css-docs-tooltip_closes-on-escape.js51
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cssom.js22
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cubicbezier-appears-on-swatch-click.js70
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cubicbezier-commit-on-ENTER.js66
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cubicbezier-revert-on-ESC.js100
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_custom.js72
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cycle-angle.js93
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cycle-color.js120
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js49
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js46
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-click.js61
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js92
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js89
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js280
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-order.js89
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-remove_01.js67
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-remove_02.js67
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-remove_03.js83
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_01.js93
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_02.js133
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_03.js50
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_04.js85
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_05.js77
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_06.js52
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_07.js50
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_08.js57
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_09.js69
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js88
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector-click.js63
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector-commit.js117
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_01.js62
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_02.js88
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_03.js48
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js69
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_05.js78
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_06.js76
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_07.js62
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_08.js71
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_09.js110
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_10.js64
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_11.js69
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js107
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.js65
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.js69
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_04.js62
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_editable-field-focus_01.js94
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_editable-field-focus_02.js84
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_eyedropper.js123
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_filtereditor-appears-on-swatch-click.js34
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_filtereditor-commit-on-ENTER.js45
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_filtereditor-revert-on-ESC.js118
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.js41
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-reload.js53
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js64
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js73
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js96
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_guessIndentation.js47
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inherited-properties_01.js47
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inherited-properties_02.js34
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inherited-properties_03.js40
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inline-source-map.js26
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_invalid-source-map.js44
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_invalid.js33
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_keybindings.js49
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_keyframeLineNumbers.js25
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js106
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_keyframes-rule_02.js92
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_lineNumbers.js29
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_livepreview.js72
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_01.js56
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_02.js45
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_03.js41
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_04.js36
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_05.js33
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_06.js60
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_07.js72
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mathml-element.js53
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_media-queries.js26
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple-properties-duplicates.js68
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple-properties-priority.js47
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_01.js62
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js71
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple_properties_01.js53
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple_properties_02.js54
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_original-source-link.js85
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js260
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js29
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_pseudo_lock_options.js131
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_refresh-no-flicker.js39
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_01.js61
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_02.js153
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.js38
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_01.js156
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_02.js93
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_03.js49
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_04.js63
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_expander.js92
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-overridden-property.js74
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_01.js91
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_02.js32
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_03.js39
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_04.js76
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_05.js33
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_06.js27
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_07.js62
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_08.js53
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_09.js73
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_10.js84
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js83
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_escape-keypress.js65
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js171
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js38
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter_01.js35
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter_02.js78
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter_03.js78
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter_04.js53
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector_highlight.js144
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_strict-search-filter-computed-list_01.js182
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_strict-search-filter_01.js130
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_strict-search-filter_02.js34
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_strict-search-filter_03.js44
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_style-editor-link.js203
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_urls-clickable.js70
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_user-agent-styles-uneditable.js58
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_user-agent-styles.js183
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_user-property-reset.js90
-rw-r--r--devtools/client/inspector/rules/test/doc_author-sheet.html39
-rw-r--r--devtools/client/inspector/rules/test/doc_blob_stylesheet.html39
-rw-r--r--devtools/client/inspector/rules/test/doc_content_stylesheet.html35
-rw-r--r--devtools/client/inspector/rules/test/doc_content_stylesheet_imported.css5
-rw-r--r--devtools/client/inspector/rules/test/doc_content_stylesheet_imported2.css3
-rw-r--r--devtools/client/inspector/rules/test/doc_content_stylesheet_linked.css3
-rw-r--r--devtools/client/inspector/rules/test/doc_content_stylesheet_script.css5
-rw-r--r--devtools/client/inspector/rules/test/doc_copystyles.css11
-rw-r--r--devtools/client/inspector/rules/test/doc_copystyles.html11
-rw-r--r--devtools/client/inspector/rules/test/doc_cssom.html22
-rw-r--r--devtools/client/inspector/rules/test/doc_custom.html33
-rw-r--r--devtools/client/inspector/rules/test/doc_filter.html13
-rw-r--r--devtools/client/inspector/rules/test/doc_frame_script.js113
-rw-r--r--devtools/client/inspector/rules/test/doc_inline_sourcemap.html18
-rw-r--r--devtools/client/inspector/rules/test/doc_invalid_sourcemap.css3
-rw-r--r--devtools/client/inspector/rules/test/doc_invalid_sourcemap.html11
-rw-r--r--devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html45
-rw-r--r--devtools/client/inspector/rules/test/doc_keyframeanimation.css84
-rw-r--r--devtools/client/inspector/rules/test/doc_keyframeanimation.html13
-rw-r--r--devtools/client/inspector/rules/test/doc_media_queries.html24
-rw-r--r--devtools/client/inspector/rules/test/doc_pseudoelement.html131
-rw-r--r--devtools/client/inspector/rules/test/doc_ruleLineNumbers.html19
-rw-r--r--devtools/client/inspector/rules/test/doc_sourcemaps.css7
-rw-r--r--devtools/client/inspector/rules/test/doc_sourcemaps.css.map7
-rw-r--r--devtools/client/inspector/rules/test/doc_sourcemaps.html11
-rw-r--r--devtools/client/inspector/rules/test/doc_sourcemaps.scss10
-rw-r--r--devtools/client/inspector/rules/test/doc_style_editor_link.css3
-rw-r--r--devtools/client/inspector/rules/test/doc_test_image.pngbin0 -> 580 bytes
-rw-r--r--devtools/client/inspector/rules/test/doc_urls_clickable.css9
-rw-r--r--devtools/client/inspector/rules/test/doc_urls_clickable.html30
-rw-r--r--devtools/client/inspector/rules/test/head.js840
-rw-r--r--devtools/client/inspector/rules/views/moz.build8
-rw-r--r--devtools/client/inspector/rules/views/rule-editor.js620
-rw-r--r--devtools/client/inspector/rules/views/text-property-editor.js880
216 files changed, 19180 insertions, 0 deletions
diff --git a/devtools/client/inspector/rules/models/element-style.js b/devtools/client/inspector/rules/models/element-style.js
new file mode 100644
index 000000000..7f015ba08
--- /dev/null
+++ b/devtools/client/inspector/rules/models/element-style.js
@@ -0,0 +1,412 @@
+/* -*- 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 promise = require("promise");
+const {Rule} = require("devtools/client/inspector/rules/models/rule");
+const {promiseWarn} = require("devtools/client/inspector/shared/utils");
+const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+
+/**
+ * ElementStyle is responsible for the following:
+ * Keeps track of which properties are overridden.
+ * Maintains a list of Rule objects for a given element.
+ *
+ * @param {Element} element
+ * The element whose style we are viewing.
+ * @param {CssRuleView} ruleView
+ * The instance of the rule-view panel.
+ * @param {Object} store
+ * The ElementStyle can use this object to store metadata
+ * that might outlast the rule view, particularly the current
+ * set of disabled properties.
+ * @param {PageStyleFront} pageStyle
+ * Front for the page style actor that will be providing
+ * the style information.
+ * @param {Boolean} showUserAgentStyles
+ * Should user agent styles be inspected?
+ */
+function ElementStyle(element, ruleView, store, pageStyle,
+ showUserAgentStyles) {
+ this.element = element;
+ this.ruleView = ruleView;
+ this.store = store || {};
+ this.pageStyle = pageStyle;
+ this.showUserAgentStyles = showUserAgentStyles;
+ this.rules = [];
+ this.cssProperties = getCssProperties(this.ruleView.inspector.toolbox);
+
+ // 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();
+ }
+}
+
+ElementStyle.prototype = {
+ // The element we're looking at.
+ element: null,
+
+ destroy: function () {
+ if (this.destroyed) {
+ return;
+ }
+ this.destroyed = true;
+
+ for (let rule of this.rules) {
+ if (rule.editor) {
+ rule.editor.destroy();
+ }
+ }
+ },
+
+ /**
+ * 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 promise.resolve(undefined);
+ }
+
+ if (this.populated !== populated) {
+ // Don't care anymore.
+ return promise.resolve(undefined);
+ }
+
+ // Store the current list of rules (if any) during the population
+ // process. They will be reused if possible.
+ let existingRules = this.rules;
+
+ this.rules = [];
+
+ for (let entry of entries) {
+ this._maybeAddRule(entry, existingRules);
+ }
+
+ // Mark overridden computed styles.
+ this.markOverriddenAll();
+
+ this._sortRulesForPseudoElement();
+
+ // We're done with the previous list of rules.
+ for (let r of existingRules) {
+ if (r && r.editor) {
+ r.editor.destroy();
+ }
+ }
+
+ return undefined;
+ }).then(null, e => {
+ // populate is often called after a setTimeout,
+ // the connection may already be closed.
+ if (this.destroyed) {
+ return promise.resolve(undefined);
+ }
+ return promiseWarn(e);
+ });
+ 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} options
+ * Options for creating the Rule, see the Rule constructor.
+ * @param {Array} existingRules
+ * Rules to reuse if possible. If a rule is reused, then it
+ * it will be deleted from this array.
+ * @return {Boolean} true if we added the rule.
+ */
+ _maybeAddRule: function (options, existingRules) {
+ // If we've already included this domRule (for example, when a
+ // common selector is inherited), ignore it.
+ if (options.rule &&
+ this.rules.some(rule => rule.domRule === options.rule)) {
+ return false;
+ }
+
+ if (options.system) {
+ return false;
+ }
+
+ let rule = null;
+
+ // If we're refreshing and the rule previously existed, reuse the
+ // Rule object.
+ if (existingRules) {
+ let ruleIndex = existingRules.findIndex((r) => r.matches(options));
+ if (ruleIndex >= 0) {
+ rule = existingRules[ruleIndex];
+ rule.refresh(options);
+ existingRules.splice(ruleIndex, 1);
+ }
+ }
+
+ // If this is a new rule, create its Rule object.
+ if (!rule) {
+ rule = new Rule(this, options);
+ }
+
+ // Ignore inherited rules with no visible properties.
+ if (options.inherited && !rule.hasAnyVisibleProperties()) {
+ return false;
+ }
+
+ this.rules.push(rule);
+ return true;
+ },
+
+ /**
+ * Calls markOverridden with all supported pseudo elements
+ */
+ markOverriddenAll: function () {
+ this.markOverridden();
+ for (let pseudo of this.cssProperties.pseudoElements) {
+ 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.matchedSelectors.length > 0 ||
+ rule.domRule.type === ELEMENT_STYLE) &&
+ rule.pseudoElement === pseudo && !rule.keyframes) {
+ for (let textProp of rule.textProps.slice(0).reverse()) {
+ if (textProp.enabled) {
+ textProps.push(textProp);
+ }
+ }
+ }
+ }
+
+ // 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];
+
+ // Prevent -webkit-gradient from being selected after unchecking
+ // linear-gradient in this case:
+ // -moz-linear-gradient: ...;
+ // -webkit-linear-gradient: ...;
+ // linear-gradient: ...;
+ if (!computedProp.textProp.isValid()) {
+ computedProp.overridden = true;
+ continue;
+ }
+ let overridden;
+ if (earlier &&
+ computedProp.priority === "important" &&
+ earlier.priority !== "important" &&
+ (earlier.textProp.rule.inherited ||
+ !computedProp.textProp.rule.inherited)) {
+ // 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} prop
+ * The text property to update.
+ * @return {Boolean} true if the TextProperty's overridden state (or any of
+ * its computed properties overridden state) changed.
+ */
+ _updatePropertyOverridden: function (prop) {
+ let overridden = true;
+ let dirty = false;
+ for (let computedProp of prop.computed) {
+ if (!computedProp.overridden) {
+ overridden = false;
+ }
+ dirty = computedProp._overriddenDirty || dirty;
+ delete computedProp._overriddenDirty;
+ }
+
+ dirty = (!!prop.overridden !== overridden) || dirty;
+ prop.overridden = overridden;
+ return dirty;
+ }
+};
+
+/**
+ * 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} style
+ * The CSSStyleDeclaration against which the property is mapped.
+ * @param {String} name
+ * The name of the property to get.
+ * @param {String} value
+ * Default value.
+ * @return {String}
+ * The property value if it has previously been set by the user, null
+ * otherwise.
+ */
+ getProperty: function (style, name, value) {
+ let key = this.getKey(style);
+ let entry = this.map.get(key, null);
+
+ if (entry && name in entry) {
+ return entry[name];
+ }
+ return value;
+ },
+
+ /**
+ * Set a named property for a given CSSStyleDeclaration.
+ *
+ * @param {CSSStyleDeclaration} style
+ * The CSSStyleDeclaration against which the property is to be mapped.
+ * @param {String} bame
+ * The name of the property to set.
+ * @param {String} userValue
+ * The value of the property to set.
+ */
+ setProperty: function (style, bame, userValue) {
+ let key = this.getKey(style, bame);
+ let entry = this.map.get(key, null);
+
+ if (entry) {
+ entry[bame] = userValue;
+ } else {
+ let props = {};
+ props[bame] = userValue;
+ this.map.set(key, props);
+ }
+ },
+
+ /**
+ * Check whether a named property for a given CSSStyleDeclaration is stored.
+ *
+ * @param {CSSStyleDeclaration} style
+ * The CSSStyleDeclaration against which the property would be mapped.
+ * @param {String} name
+ * The name of the property to check.
+ */
+ contains: function (style, name) {
+ let key = this.getKey(style, name);
+ let entry = this.map.get(key, null);
+ return !!entry && name in entry;
+ },
+
+ getKey: function (style, name) {
+ return style.actorID + ":" + name;
+ },
+
+ clear: function () {
+ this.map.clear();
+ }
+};
+
+exports.ElementStyle = ElementStyle;
diff --git a/devtools/client/inspector/rules/models/moz.build b/devtools/client/inspector/rules/models/moz.build
new file mode 100644
index 000000000..1c5c0f89f
--- /dev/null
+++ b/devtools/client/inspector/rules/models/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'element-style.js',
+ 'rule.js',
+ 'text-property.js',
+)
diff --git a/devtools/client/inspector/rules/models/rule.js b/devtools/client/inspector/rules/models/rule.js
new file mode 100644
index 000000000..1a3fa057a
--- /dev/null
+++ b/devtools/client/inspector/rules/models/rule.js
@@ -0,0 +1,686 @@
+/* -*- 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 promise = require("promise");
+const CssLogic = require("devtools/shared/inspector/css-logic");
+const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
+const {TextProperty} =
+ require("devtools/client/inspector/rules/models/text-property");
+const {promiseWarn} = require("devtools/client/inspector/shared/utils");
+const {parseDeclarations} = require("devtools/shared/css/parsing-utils");
+const Services = require("Services");
+
+const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties";
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
+
+/**
+ * Rule is responsible for the following:
+ * Manages a single style declaration or rule.
+ * Applies changes to the properties in a rule.
+ * Maintains a list of TextProperty objects.
+ *
+ * @param {ElementStyle} elementStyle
+ * The ElementStyle to which this rule belongs.
+ * @param {Object} options
+ * 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?
+ * isUnmatched: True if the rule does not match the current selected
+ * element, otherwise, false.
+ */
+function Rule(elementStyle, options) {
+ this.elementStyle = elementStyle;
+ this.domRule = options.rule || null;
+ this.style = options.rule;
+ this.matchedSelectors = options.matchedSelectors || [];
+ this.pseudoElement = options.pseudoElement || "";
+
+ this.isSystem = options.isSystem;
+ this.isUnmatched = options.isUnmatched || false;
+ this.inherited = options.inherited || null;
+ this.keyframes = options.keyframes || null;
+ this._modificationDepth = 0;
+
+ if (this.domRule && this.domRule.mediaText) {
+ this.mediaText = this.domRule.mediaText;
+ }
+
+ this.cssProperties = this.elementStyle.ruleView.cssProperties;
+
+ // Populate the text properties with the style's current authoredText
+ // 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() {
+ let title = CssLogic.shortSource(this.sheet);
+ if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) {
+ title += ":" + this.ruleLine;
+ }
+
+ return title + (this.mediaText ? " @media " + this.mediaText : "");
+ },
+
+ get inheritedSource() {
+ if (this._inheritedSource) {
+ return this._inheritedSource;
+ }
+ this._inheritedSource = "";
+ if (this.inherited) {
+ let eltText = this.inherited.displayName;
+ if (this.inherited.id) {
+ eltText += "#" + this.inherited.id;
+ }
+ this._inheritedSource =
+ STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", eltText);
+ }
+ return this._inheritedSource;
+ },
+
+ get keyframesName() {
+ if (this._keyframesName) {
+ return this._keyframesName;
+ }
+ this._keyframesName = "";
+ if (this.keyframes) {
+ this._keyframesName =
+ STYLE_INSPECTOR_L10N.getFormatStr("rule.keyframe", this.keyframes.name);
+ }
+ 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 : "";
+ },
+
+ /**
+ * 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 () {
+ return this.domRule.getOriginalLocation().then(({href,
+ line, mediaText}) => {
+ let mediaString = mediaText ? " @" + mediaText : "";
+ let linePart = line > 0 ? (":" + line) : "";
+
+ let sourceStrings = {
+ full: (href || CssLogic.l10n("rule.sourceInline")) + linePart +
+ mediaString,
+ short: CssLogic.shortSource({href: href}) + linePart + mediaString
+ };
+
+ return sourceStrings;
+ });
+ },
+
+ /**
+ * Returns true if the rule matches the creation options
+ * specified.
+ *
+ * @param {Object} options
+ * Creation options. See the Rule constructor for documentation.
+ */
+ matches: function (options) {
+ return this.style === options.rule;
+ },
+
+ /**
+ * Create a new TextProperty to include in the rule.
+ *
+ * @param {String} name
+ * The text property name (such as "background" or "border-top").
+ * @param {String} value
+ * The property's value (not including priority).
+ * @param {String} priority
+ * The property's priority (either "important" or an empty string).
+ * @param {Boolean} enabled
+ * True if the property should be enabled.
+ * @param {TextProperty} siblingProp
+ * Optional, property next to which the new property will be added.
+ */
+ createProperty: function (name, value, priority, enabled, siblingProp) {
+ let prop = new TextProperty(this, name, value, priority, enabled);
+
+ let ind;
+ if (siblingProp) {
+ ind = this.textProps.indexOf(siblingProp) + 1;
+ this.textProps.splice(ind, 0, prop);
+ } else {
+ ind = this.textProps.length;
+ this.textProps.push(prop);
+ }
+
+ this.applyProperties((modifications) => {
+ modifications.createProperty(ind, name, value, priority, enabled);
+ // Now that the rule has been updated, the server might have given us data
+ // that changes the state of the property. Update it now.
+ prop.updateEditor();
+ });
+
+ return prop;
+ },
+
+ /**
+ * Helper function for applyProperties that is called when the actor
+ * does not support as-authored styles. Store disabled properties
+ * in the element style's store.
+ */
+ _applyPropertiesNoAuthored: function (modifications) {
+ this.elementStyle.markOverriddenAll();
+
+ let disabledProps = [];
+
+ for (let prop of this.textProps) {
+ if (prop.invisible) {
+ continue;
+ }
+ if (!prop.enabled) {
+ disabledProps.push({
+ name: prop.name,
+ value: prop.value,
+ priority: prop.priority
+ });
+ continue;
+ }
+ if (prop.value.trim() === "") {
+ continue;
+ }
+
+ modifications.setProperty(-1, 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);
+ }
+
+ return modifications.apply().then(() => {
+ let cssProps = {};
+ // Note that even though StyleRuleActors normally provide parsed
+ // declarations already, _applyPropertiesNoAuthored is only used when
+ // connected to older backend that do not provide them. So parse here.
+ for (let cssProp of parseDeclarations(this.cssProperties.isKnown,
+ this.style.authoredText)) {
+ 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;
+ }
+ });
+ },
+
+ /**
+ * A helper for applyProperties that applies properties in the "as
+ * authored" case; that is, when the StyleRuleActor supports
+ * setRuleText.
+ */
+ _applyPropertiesAuthored: function (modifications) {
+ return modifications.apply().then(() => {
+ // The rewriting may have required some other property values to
+ // change, e.g., to insert some needed terminators. Update the
+ // relevant properties here.
+ for (let index in modifications.changedDeclarations) {
+ let newValue = modifications.changedDeclarations[index];
+ this.textProps[index].noticeNewValue(newValue);
+ }
+ // Recompute and redisplay the computed properties.
+ for (let prop of this.textProps) {
+ if (!prop.invisible && prop.enabled) {
+ prop.updateComputed();
+ prop.updateEditor();
+ }
+ }
+ });
+ },
+
+ /**
+ * Reapply all the properties in this rule, and update their
+ * computed styles. Will re-mark overridden properties. Sets the
+ * |_applyingModifications| property to a promise which will resolve
+ * when the edit has completed.
+ *
+ * @param {Function} modifier a function that takes a RuleModificationList
+ * (or RuleRewriter) as an argument and that modifies it
+ * to apply the desired edit
+ * @return {Promise} a promise which will resolve when the edit
+ * is complete
+ */
+ applyProperties: function (modifier) {
+ // If there is already a pending modification, we have to wait
+ // until it settles before applying the next modification.
+ let resultPromise =
+ promise.resolve(this._applyingModifications).then(() => {
+ let modifications = this.style.startModifyingProperties(
+ this.cssProperties);
+ modifier(modifications);
+ if (this.style.canSetRuleText) {
+ return this._applyPropertiesAuthored(modifications);
+ }
+ return this._applyPropertiesNoAuthored(modifications);
+ }).then(() => {
+ this.elementStyle.markOverriddenAll();
+
+ if (resultPromise === this._applyingModifications) {
+ this._applyingModifications = null;
+ this.elementStyle._changed();
+ }
+ }).catch(promiseWarn);
+
+ this._applyingModifications = resultPromise;
+ return resultPromise;
+ },
+
+ /**
+ * Renames a property.
+ *
+ * @param {TextProperty} property
+ * The property to rename.
+ * @param {String} name
+ * The new property name (such as "background" or "border-top").
+ */
+ setPropertyName: function (property, name) {
+ if (name === property.name) {
+ return;
+ }
+
+ let oldName = property.name;
+ property.name = name;
+ let index = this.textProps.indexOf(property);
+ this.applyProperties((modifications) => {
+ modifications.renameProperty(index, oldName, name);
+ });
+ },
+
+ /**
+ * Sets the value and priority of a property, then reapply all properties.
+ *
+ * @param {TextProperty} property
+ * The property to manipulate.
+ * @param {String} value
+ * The property's value (not including priority).
+ * @param {String} priority
+ * The property's priority (either "important" or an empty string).
+ */
+ setPropertyValue: function (property, value, priority) {
+ if (value === property.value && priority === property.priority) {
+ return;
+ }
+
+ property.value = value;
+ property.priority = priority;
+
+ let index = this.textProps.indexOf(property);
+ this.applyProperties((modifications) => {
+ modifications.setProperty(index, property.name, value, priority);
+ });
+ },
+
+ /**
+ * Just sets the value and priority of a property, in order to preview its
+ * effect on the content document.
+ *
+ * @param {TextProperty} property
+ * The property which value will be previewed
+ * @param {String} value
+ * The value to be used for the preview
+ * @param {String} priority
+ * The property's priority (either "important" or an empty string).
+ */
+ previewPropertyValue: function (property, value, priority) {
+ let modifications = this.style.startModifyingProperties(this.cssProperties);
+ modifications.setProperty(this.textProps.indexOf(property),
+ property.name, value, priority);
+ modifications.apply().then(() => {
+ // Ensure dispatching a ruleview-changed event
+ // also for previews
+ this.elementStyle._changed();
+ });
+ },
+
+ /**
+ * Disables or enables given TextProperty.
+ *
+ * @param {TextProperty} property
+ * The property to enable/disable
+ * @param {Boolean} value
+ */
+ setPropertyEnabled: function (property, value) {
+ if (property.enabled === !!value) {
+ return;
+ }
+ property.enabled = !!value;
+ let index = this.textProps.indexOf(property);
+ this.applyProperties((modifications) => {
+ modifications.setPropertyEnabled(index, property.name, property.enabled);
+ });
+ },
+
+ /**
+ * Remove a given TextProperty from the rule and update the rule
+ * accordingly.
+ *
+ * @param {TextProperty} property
+ * The property to be removed
+ */
+ removeProperty: function (property) {
+ let index = this.textProps.indexOf(property);
+ this.textProps.splice(index, 1);
+ // Need to re-apply properties in case removing this TextProperty
+ // exposes another one.
+ this.applyProperties((modifications) => {
+ modifications.removeProperty(index, property.name);
+ });
+ },
+
+ /**
+ * Get the list of TextProperties from the style. Needs
+ * to parse the style's authoredText.
+ */
+ _getTextProperties: function () {
+ let textProps = [];
+ let store = this.elementStyle.store;
+
+ // Starting with FF49, StyleRuleActors provide parsed declarations.
+ let props = this.style.declarations;
+ if (!props.length) {
+ props = parseDeclarations(this.cssProperties.isKnown,
+ this.style.authoredText, true);
+ }
+
+ for (let prop of props) {
+ let name = prop.name;
+ // If the authored text has an invalid property, it will show up
+ // as nameless. Skip these as we don't currently have a good
+ // way to display them.
+ if (!name) {
+ continue;
+ }
+ // In an inherited rule, we only show inherited properties.
+ // However, we must keep all properties in order for rule
+ // rewriting to work properly. So, compute the "invisible"
+ // property here.
+ let invisible = this.inherited && !this.cssProperties.isInherited(name);
+ let value = store.userProperties.getProperty(this.style, name,
+ prop.value);
+ let textProp = new TextProperty(this, name, value, prop.priority,
+ !("commentOffsets" in prop),
+ invisible);
+ 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 (let prop of 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 (options) {
+ this.matchedSelectors = options.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 authoredText. 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} newProp
+ * The current version of the property, as parsed from the
+ * authoredText in Rule._getTextProperties().
+ * @return {Boolean} true if a property was updated, false if no properties
+ * were updated.
+ */
+ _updateTextProperty: function (newProp) {
+ let match = { rank: 0, prop: null };
+
+ for (let prop of this.textProps) {
+ if (prop.name !== newProp.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 === newProp.value) {
+ rank += 2;
+ if (prop.priority === newProp.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(newProp);
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Jump between editable properties in the UI. If the focus direction is
+ * forward, begin editing the next property name if available or focus the
+ * new property editor otherwise. If the focus direction is backward,
+ * begin editing the previous property value or focus the selector editor if
+ * this is the first element in the property list.
+ *
+ * @param {TextProperty} textProperty
+ * The text property that will be left to focus on a sibling.
+ * @param {Number} direction
+ * The move focus direction number.
+ */
+ editClosestTextProperty: function (textProperty, direction) {
+ let index = this.textProps.indexOf(textProperty);
+
+ if (direction === Services.focus.MOVEFOCUS_FORWARD) {
+ for (++index; index < this.textProps.length; ++index) {
+ if (!this.textProps[index].invisible) {
+ break;
+ }
+ }
+ if (index === this.textProps.length) {
+ textProperty.rule.editor.closeBrace.click();
+ } else {
+ this.textProps[index].editor.nameSpan.click();
+ }
+ } else if (direction === Services.focus.MOVEFOCUS_BACKWARD) {
+ for (--index; index >= 0; --index) {
+ if (!this.textProps[index].invisible) {
+ break;
+ }
+ }
+ if (index < 0) {
+ textProperty.editor.ruleEditor.selectorText.click();
+ } else {
+ this.textProps[index].editor.valueSpan.click();
+ }
+ }
+ },
+
+ /**
+ * Return a string representation of the rule.
+ */
+ stringifyRule: function () {
+ let selectorText = this.selectorText;
+ let cssText = "";
+ let terminator = Services.appinfo.OS === "WINNT" ? "\r\n" : "\n";
+
+ for (let textProp of this.textProps) {
+ if (!textProp.invisible) {
+ cssText += "\t" + textProp.stringifyProperty() + terminator;
+ }
+ }
+
+ return selectorText + " {" + terminator + cssText + "}";
+ },
+
+ /**
+ * See whether this rule has any non-invisible properties.
+ * @return {Boolean} true if there is any visible property, or false
+ * if all properties are invisible
+ */
+ hasAnyVisibleProperties: function () {
+ for (let prop of this.textProps) {
+ if (!prop.invisible) {
+ return true;
+ }
+ }
+ return false;
+ }
+};
+
+exports.Rule = Rule;
diff --git a/devtools/client/inspector/rules/models/text-property.js b/devtools/client/inspector/rules/models/text-property.js
new file mode 100644
index 000000000..3bbe6e91d
--- /dev/null
+++ b/devtools/client/inspector/rules/models/text-property.js
@@ -0,0 +1,215 @@
+/* -*- 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 {escapeCSSComment} = require("devtools/shared/css/parsing-utils");
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+
+/**
+ * TextProperty is responsible for the following:
+ * Manages a single property from the authoredText 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.
+ *
+ * @param {Rule} rule
+ * The rule this TextProperty came from.
+ * @param {String} name
+ * The text property name (such as "background" or "border-top").
+ * @param {String} value
+ * The property's value (not including priority).
+ * @param {String} priority
+ * The property's priority (either "important" or an empty string).
+ * @param {Boolean} enabled
+ * Whether the property is enabled.
+ * @param {Boolean} invisible
+ * Whether the property is invisible. An invisible property
+ * does not show up in the UI; these are needed so that the
+ * index of a property in Rule.textProps is the same as the index
+ * coming from parseDeclarations.
+ */
+function TextProperty(rule, name, value, priority, enabled = true,
+ invisible = false) {
+ this.rule = rule;
+ this.name = name;
+ this.value = value;
+ this.priority = priority;
+ this.enabled = !!enabled;
+ this.invisible = invisible;
+ this.panelDoc = this.rule.elementStyle.ruleView.inspector.panelDoc;
+
+ const toolbox = this.rule.elementStyle.ruleView.inspector.toolbox;
+ this.cssProperties = getCssProperties(toolbox);
+
+ 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.ruleView.dummyElement;
+ let dummyStyle = dummyElement.style;
+ dummyStyle.cssText = "";
+ dummyStyle.setProperty(this.name, this.value, this.priority);
+
+ this.computed = [];
+
+ // 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 = this.cssProperties.getSubproperties(this.name);
+
+ for (let prop of subProps) {
+ this.computed.push({
+ textProp: this,
+ name: prop,
+ value: dummyStyle.getPropertyValue(prop),
+ priority: dummyStyle.getPropertyPriority(prop),
+ });
+ }
+ },
+
+ /**
+ * Set all the values from another TextProperty instance into
+ * this TextProperty instance.
+ *
+ * @param {TextProperty} prop
+ * The other TextProperty instance.
+ */
+ set: function (prop) {
+ let changed = false;
+ for (let item of ["name", "value", "priority", "enabled"]) {
+ if (this[item] !== prop[item]) {
+ this[item] = prop[item];
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ this.updateEditor();
+ }
+ },
+
+ setValue: function (value, priority, force = false) {
+ let store = this.rule.elementStyle.store;
+
+ if (this.editor && value !== this.editor.committed.value || force) {
+ store.userProperties.setProperty(this.rule.style, this.name, value);
+ }
+
+ this.rule.setPropertyValue(this, value, priority);
+ this.updateEditor();
+ },
+
+ /**
+ * Called when the property's value has been updated externally, and
+ * the property and editor should update.
+ */
+ noticeNewValue: function (value) {
+ if (value !== this.value) {
+ this.value = value;
+ this.updateEditor();
+ }
+ },
+
+ setName: function (name) {
+ let store = this.rule.elementStyle.store;
+
+ if (name !== this.name) {
+ store.userProperties.setProperty(this.rule.style, name,
+ this.editor.committed.value);
+ }
+
+ this.rule.setPropertyName(this, name);
+ this.updateEditor();
+ },
+
+ setEnabled: function (value) {
+ this.rule.setPropertyEnabled(this, value);
+ this.updateEditor();
+ },
+
+ remove: function () {
+ this.rule.removeProperty(this);
+ },
+
+ /**
+ * Return a string representation of the rule property.
+ */
+ stringifyProperty: function () {
+ // Get the displayed property value
+ let declaration = this.name + ": " + this.editor.valueSpan.textContent +
+ ";";
+
+ // Comment out property declarations that are not enabled
+ if (!this.enabled) {
+ declaration = "/* " + escapeCSSComment(declaration) + " */";
+ }
+
+ return declaration;
+ },
+
+ /**
+ * See whether this property's name is known.
+ *
+ * @return {Boolean} true if the property name is known, false otherwise.
+ */
+ isKnownProperty: function () {
+ return this.cssProperties.isKnown(this.name);
+ },
+
+ /**
+ * Validate this property. Does it make sense for this value to be assigned
+ * to this property name?
+ *
+ * @return {Boolean} true if the property value is valid, false otherwise.
+ */
+ isValid: function () {
+ // Starting with FF49, StyleRuleActors provide a list of parsed
+ // declarations, with data about their validity, but if we don't have this,
+ // compute validity locally (which might not be correct, but better than
+ // nothing).
+ if (!this.rule.domRule.declarations) {
+ return this.cssProperties.isValidOnClient(this.name, this.value, this.panelDoc);
+ }
+
+ let selfIndex = this.rule.textProps.indexOf(this);
+
+ // When adding a new property in the rule-view, the TextProperty object is
+ // created right away before the rule gets updated on the server, so we're
+ // not going to find the corresponding declaration object yet. Default to
+ // true.
+ if (!this.rule.domRule.declarations[selfIndex]) {
+ return true;
+ }
+
+ return this.rule.domRule.declarations[selfIndex].isValid;
+ }
+};
+
+exports.TextProperty = TextProperty;
diff --git a/devtools/client/inspector/rules/moz.build b/devtools/client/inspector/rules/moz.build
new file mode 100644
index 000000000..e826c1414
--- /dev/null
+++ b/devtools/client/inspector/rules/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'models',
+ 'views',
+]
+
+DevToolsModules(
+ 'rules.js',
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/inspector/rules/rules.js b/devtools/client/inspector/rules/rules.js
new file mode 100644
index 000000000..8c5ec7617
--- /dev/null
+++ b/devtools/client/inspector/rules/rules.js
@@ -0,0 +1,1673 @@
+/* -*- 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 promise = require("promise");
+const Services = require("Services");
+const {Task} = require("devtools/shared/task");
+const {Tools} = require("devtools/client/definitions");
+const {l10n} = require("devtools/shared/inspector/css-logic");
+const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
+const {OutputParser} = require("devtools/client/shared/output-parser");
+const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils");
+const {ElementStyle} = require("devtools/client/inspector/rules/models/element-style");
+const {Rule} = require("devtools/client/inspector/rules/models/rule");
+const {RuleEditor} = require("devtools/client/inspector/rules/views/rule-editor");
+const {gDevTools} = require("devtools/client/framework/devtools");
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+const HighlightersOverlay = require("devtools/client/inspector/shared/highlighters-overlay");
+const {
+ VIEW_NODE_SELECTOR_TYPE,
+ VIEW_NODE_PROPERTY_TYPE,
+ VIEW_NODE_VALUE_TYPE,
+ VIEW_NODE_IMAGE_URL_TYPE,
+ VIEW_NODE_LOCATION_TYPE,
+} = require("devtools/client/inspector/shared/node-types");
+const StyleInspectorMenu = require("devtools/client/inspector/shared/style-inspector-menu");
+const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay");
+const {createChild, promiseWarn, throttle} = require("devtools/client/inspector/shared/utils");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+const clipboardHelper = require("devtools/shared/platform/clipboard");
+const {AutocompletePopup} = require("devtools/client/shared/autocomplete-popup");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
+const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit";
+const PREF_ENABLE_MDN_DOCS_TOOLTIP =
+ "devtools.inspector.mdnDocsTooltip.enabled";
+const FILTER_CHANGED_TIMEOUT = 150;
+
+// This is used to parse user input when filtering.
+const FILTER_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*;?$/;
+// This is used to parse the filter search value to see if the filter
+// should be strict or not
+const FILTER_STRICT_RE = /\s*`(.*?)`\s*$/;
+
+/**
+ * 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 authoredText 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.
+ *
+ * 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} inspector
+ * Inspector toolbox panel
+ * @param {Document} document
+ * The document that will contain the rule view.
+ * @param {Object} store
+ * 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} pageStyle
+ * The PageStyleFront for communicating with the remote server.
+ */
+function CssRuleView(inspector, document, store, pageStyle) {
+ this.inspector = inspector;
+ this.styleDocument = document;
+ this.styleWindow = this.styleDocument.defaultView;
+ this.store = store || {};
+ this.pageStyle = pageStyle;
+
+ // Allow tests to override throttling behavior, as this can cause intermittents.
+ this.throttle = throttle;
+
+ this.cssProperties = getCssProperties(inspector.toolbox);
+
+ this._outputParser = new OutputParser(document, this.cssProperties);
+
+ this._onAddRule = this._onAddRule.bind(this);
+ this._onContextMenu = this._onContextMenu.bind(this);
+ this._onCopy = this._onCopy.bind(this);
+ this._onFilterStyles = this._onFilterStyles.bind(this);
+ this._onClearSearch = this._onClearSearch.bind(this);
+ this._onTogglePseudoClassPanel = this._onTogglePseudoClassPanel.bind(this);
+ this._onTogglePseudoClass = this._onTogglePseudoClass.bind(this);
+
+ let doc = this.styleDocument;
+ this.element = doc.getElementById("ruleview-container-focusable");
+ this.addRuleButton = doc.getElementById("ruleview-add-rule-button");
+ this.searchField = doc.getElementById("ruleview-searchbox");
+ this.searchClearButton = doc.getElementById("ruleview-searchinput-clear");
+ this.pseudoClassPanel = doc.getElementById("pseudo-class-panel");
+ this.pseudoClassToggle = doc.getElementById("pseudo-class-panel-toggle");
+ this.hoverCheckbox = doc.getElementById("pseudo-hover-toggle");
+ this.activeCheckbox = doc.getElementById("pseudo-active-toggle");
+ this.focusCheckbox = doc.getElementById("pseudo-focus-toggle");
+
+ this.searchClearButton.hidden = true;
+
+ this.shortcuts = new KeyShortcuts({ window: this.styleWindow });
+ this._onShortcut = this._onShortcut.bind(this);
+ this.shortcuts.on("Escape", this._onShortcut);
+ this.shortcuts.on("Return", this._onShortcut);
+ this.shortcuts.on("Space", this._onShortcut);
+ this.shortcuts.on("CmdOrCtrl+F", this._onShortcut);
+ this.element.addEventListener("copy", this._onCopy);
+ this.element.addEventListener("contextmenu", this._onContextMenu);
+ this.addRuleButton.addEventListener("click", this._onAddRule);
+ this.searchField.addEventListener("input", this._onFilterStyles);
+ this.searchField.addEventListener("contextmenu", this.inspector.onTextBoxContextMenu);
+ this.searchClearButton.addEventListener("click", this._onClearSearch);
+ this.pseudoClassToggle.addEventListener("click",
+ this._onTogglePseudoClassPanel);
+ this.hoverCheckbox.addEventListener("click", this._onTogglePseudoClass);
+ this.activeCheckbox.addEventListener("click", this._onTogglePseudoClass);
+ this.focusCheckbox.addEventListener("click", this._onTogglePseudoClass);
+
+ 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._prefObserver.on(PREF_ENABLE_MDN_DOCS_TOOLTIP, this._handlePrefChange);
+
+ this.showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES);
+ this.enableMdnDocsTooltip =
+ Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP);
+
+ // The popup will be attached to the toolbox document.
+ this.popup = new AutocompletePopup(inspector._toolbox.doc, {
+ autoSelect: true,
+ theme: "auto"
+ });
+
+ this._showEmpty();
+
+ this._contextmenu = new StyleInspectorMenu(this, { isRuleView: true });
+
+ // Add the tooltips and highlighters to the view
+ this.tooltips = new TooltipsOverlay(this);
+ this.tooltips.addToView();
+ this.highlighters = new HighlightersOverlay(this);
+ this.highlighters.addToView();
+
+ EventEmitter.decorate(this);
+}
+
+CssRuleView.prototype = {
+ // The element that we're inspecting.
+ _viewedElement: null,
+
+ // Used for cancelling timeouts in the style filter.
+ _filterChangedTimeout: null,
+
+ // Empty, unconnected element of the same type as this node, used
+ // to figure out how shorthand properties will be parsed.
+ _dummyElement: null,
+
+ // Get the dummy elemenet.
+ get dummyElement() {
+ return this._dummyElement;
+ },
+
+ // Get the filter search value.
+ get searchValue() {
+ return this.searchField.value.toLowerCase();
+ },
+
+ /**
+ * Get an instance of SelectorHighlighter (used to highlight nodes that match
+ * selectors in the rule-view). A new instance is only created the first time
+ * this function is called. The same instance will then be returned.
+ *
+ * @return {Promise} Resolves to the instance of the highlighter.
+ */
+ getSelectorHighlighter: Task.async(function* () {
+ let utils = this.inspector.toolbox.highlighterUtils;
+ if (!utils.supportsCustomHighlighters()) {
+ return null;
+ }
+
+ if (this.selectorHighlighter) {
+ return this.selectorHighlighter;
+ }
+
+ try {
+ let h = yield utils.getHighlighterByType("SelectorHighlighter");
+ this.selectorHighlighter = h;
+ return h;
+ } catch (e) {
+ // The SelectorHighlighter type could not be created in the
+ // current target. It could be an older server, or a XUL page.
+ return null;
+ }
+ }),
+
+ /**
+ * Highlight/unhighlight all the nodes that match a given set of selectors
+ * inside the document of the current selected node.
+ * Only one selector can be highlighted at a time, so calling the method a
+ * second time with a different selector will first unhighlight the previously
+ * highlighted nodes.
+ * Calling the method a second time with the same selector will just
+ * unhighlight the highlighted nodes.
+ *
+ * @param {DOMNode} selectorIcon
+ * The icon that was clicked to toggle the selector. The
+ * class 'highlighted' will be added when the selector is
+ * highlighted.
+ * @param {String} selector
+ * The selector used to find nodes in the page.
+ */
+ toggleSelectorHighlighter: function (selectorIcon, selector) {
+ if (this.lastSelectorIcon) {
+ this.lastSelectorIcon.classList.remove("highlighted");
+ }
+ selectorIcon.classList.remove("highlighted");
+
+ this.unhighlightSelector().then(() => {
+ if (selector !== this.highlighters.selectorHighlighterShown) {
+ this.highlighters.selectorHighlighterShown = selector;
+ selectorIcon.classList.add("highlighted");
+ this.lastSelectorIcon = selectorIcon;
+ this.highlightSelector(selector).then(() => {
+ this.emit("ruleview-selectorhighlighter-toggled", true);
+ }, e => console.error(e));
+ } else {
+ this.highlighters.selectorHighlighterShown = null;
+ this.emit("ruleview-selectorhighlighter-toggled", false);
+ }
+ }, e => console.error(e));
+ },
+
+ highlightSelector: Task.async(function* (selector) {
+ let node = this.inspector.selection.nodeFront;
+
+ let highlighter = yield this.getSelectorHighlighter();
+ if (!highlighter) {
+ return;
+ }
+
+ yield highlighter.show(node, {
+ hideInfoBar: true,
+ hideGuides: true,
+ selector
+ });
+ }),
+
+ unhighlightSelector: Task.async(function* () {
+ let highlighter = yield this.getSelectorHighlighter();
+ if (!highlighter) {
+ return;
+ }
+
+ yield highlighter.hide();
+ }),
+
+ /**
+ * 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
+ * client/inspector/shared/node-types
+ * - 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 = 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,
+ textProperty: prop
+ };
+ } else if (classes.contains("ruleview-propertyvalue") && prop) {
+ type = 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,
+ textProperty: prop
+ };
+ } else if (classes.contains("theme-link") &&
+ !classes.contains("ruleview-rule-source") && prop) {
+ type = 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,
+ textProperty: prop
+ };
+ } else if (classes.contains("ruleview-selector-unmatched") ||
+ classes.contains("ruleview-selector-matched") ||
+ classes.contains("ruleview-selectorcontainer") ||
+ classes.contains("ruleview-selector") ||
+ classes.contains("ruleview-selector-attribute") ||
+ classes.contains("ruleview-selector-pseudo-class") ||
+ classes.contains("ruleview-selector-pseudo-class-lock")) {
+ type = VIEW_NODE_SELECTOR_TYPE;
+ value = this._getRuleEditorForNode(node).selectorText.textContent;
+ } else if (classes.contains("ruleview-rule-source") ||
+ classes.contains("ruleview-rule-source-label")) {
+ type = VIEW_NODE_LOCATION_TYPE;
+ let rule = this._getRuleEditorForNode(node).rule;
+ value = (rule.sheet && rule.sheet.href) ? rule.sheet.href : rule.title;
+ } else {
+ return null;
+ }
+
+ return {type, value};
+ },
+
+ /**
+ * Retrieve the RuleEditor instance that should be stored on
+ * the offset parent of the node
+ */
+ _getRuleEditorForNode: function (node) {
+ if (!node.offsetParent) {
+ // some nodes don't have an offsetParent, but their parentNode does
+ node = node.parentNode;
+ }
+ return node.offsetParent._ruleEditor;
+ },
+
+ /**
+ * Context menu handler.
+ */
+ _onContextMenu: function (event) {
+ this._contextmenu.show(event);
+ },
+
+ /**
+ * Callback for copy event. Copy the selected text.
+ *
+ * @param {Event} event
+ * copy event object.
+ */
+ _onCopy: function (event) {
+ if (event) {
+ this.copySelection(event.target);
+ event.preventDefault();
+ }
+ },
+
+ /**
+ * Copy the current selection. The current target is necessary
+ * if the selection is inside an input or a textarea
+ *
+ * @param {DOMNode} target
+ * DOMNode target of the copy action
+ */
+ copySelection: function (target) {
+ try {
+ let text = "";
+
+ let nodeName = target && target.nodeName;
+ if (nodeName === "input" || nodeName == "textarea") {
+ 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 {
+ text = this.styleWindow.getSelection().toString();
+
+ // Remove any double newlines.
+ text = text.replace(/(\r?\n)\r?\n/g, "$1");
+ }
+
+ clipboardHelper.copyString(text);
+ } catch (e) {
+ console.error(e);
+ }
+ },
+
+ /**
+ * A helper for _onAddRule that handles the case where the actor
+ * does not support as-authored styles.
+ */
+ _onAddNewRuleNonAuthored: function () {
+ let elementStyle = this._elementStyle;
+ let element = elementStyle.element;
+ let rules = elementStyle.rules;
+ let pseudoClasses = element.pseudoClassLocks;
+
+ this.pageStyle.addNewRule(element, pseudoClasses).then(options => {
+ let newRule = new Rule(elementStyle, options);
+ rules.push(newRule);
+ let editor = new RuleEditor(this, newRule);
+ newRule.editor = editor;
+
+ // 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();
+ });
+ },
+
+ /**
+ * Add a new rule to the current element.
+ */
+ _onAddRule: function () {
+ let elementStyle = this._elementStyle;
+ let element = elementStyle.element;
+ let client = this.inspector.target.client;
+ let pseudoClasses = element.pseudoClassLocks;
+
+ if (!client.traits.addNewRule) {
+ return;
+ }
+
+ if (!this.pageStyle.supportsAuthoredStyles) {
+ // We're talking to an old server.
+ this._onAddNewRuleNonAuthored();
+ return;
+ }
+
+ // Adding a new rule with authored styles will cause the actor to
+ // emit an event, which will in turn cause the rule view to be
+ // updated. So, we wait for this update and for the rule creation
+ // request to complete, and then focus the new rule's selector.
+ let eventPromise = this.once("ruleview-refreshed");
+ let newRulePromise = this.pageStyle.addNewRule(element, pseudoClasses);
+ promise.all([eventPromise, newRulePromise]).then((values) => {
+ let options = values[1];
+ // Be sure the reference the correct |rules| here.
+ for (let rule of this._elementStyle.rules) {
+ if (options.rule === rule.domRule) {
+ rule.editor.selectorText.click();
+ elementStyle._changed();
+ break;
+ }
+ }
+ });
+ },
+
+ /**
+ * Disables add rule button when needed
+ */
+ refreshAddRuleButtonState: function () {
+ let shouldBeDisabled = !this._viewedElement ||
+ !this.inspector.selection.isElementNode() ||
+ this.inspector.selection.isAnonymousNode();
+ this.addRuleButton.disabled = shouldBeDisabled;
+ },
+
+ setPageStyle: function (pageStyle) {
+ this.pageStyle = pageStyle;
+ },
+
+ /**
+ * Return {Boolean} true if the rule view currently has an input
+ * editor visible.
+ */
+ get isEditing() {
+ return this.tooltips.isEditing ||
+ this.element.querySelectorAll(".styleinspector-propertyeditor")
+ .length > 0;
+ },
+
+ _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) {
+ this.selectElement(this._viewedElement, true);
+ }
+ },
+
+ /**
+ * Update source links when pref for showing original sources changes
+ */
+ _onSourcePrefChanged: function () {
+ 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");
+ }
+ },
+
+ /**
+ * Set the filter style search value.
+ * @param {String} value
+ * The search value.
+ */
+ setFilterStyles: function (value = "") {
+ this.searchField.value = value;
+ this.searchField.focus();
+ this._onFilterStyles();
+ },
+
+ /**
+ * Called when the user enters a search term in the filter style search box.
+ */
+ _onFilterStyles: function () {
+ if (this._filterChangedTimeout) {
+ clearTimeout(this._filterChangedTimeout);
+ }
+
+ let filterTimeout = (this.searchValue.length > 0) ?
+ FILTER_CHANGED_TIMEOUT : 0;
+ this.searchClearButton.hidden = this.searchValue.length === 0;
+
+ this._filterChangedTimeout = setTimeout(() => {
+ if (this.searchField.value.length > 0) {
+ this.searchField.setAttribute("filled", true);
+ } else {
+ this.searchField.removeAttribute("filled");
+ }
+
+ this.searchData = {
+ searchPropertyMatch: FILTER_PROP_RE.exec(this.searchValue),
+ searchPropertyName: this.searchValue,
+ searchPropertyValue: this.searchValue,
+ strictSearchValue: "",
+ strictSearchPropertyName: false,
+ strictSearchPropertyValue: false,
+ strictSearchAllValues: false
+ };
+
+ if (this.searchData.searchPropertyMatch) {
+ // Parse search value as a single property line and extract the
+ // property name and value. If the parsed property name or value is
+ // contained in backquotes (`), extract the value within the backquotes
+ // and set the corresponding strict search for the property to true.
+ if (FILTER_STRICT_RE.test(this.searchData.searchPropertyMatch[1])) {
+ this.searchData.strictSearchPropertyName = true;
+ this.searchData.searchPropertyName =
+ FILTER_STRICT_RE.exec(this.searchData.searchPropertyMatch[1])[1];
+ } else {
+ this.searchData.searchPropertyName =
+ this.searchData.searchPropertyMatch[1];
+ }
+
+ if (FILTER_STRICT_RE.test(this.searchData.searchPropertyMatch[2])) {
+ this.searchData.strictSearchPropertyValue = true;
+ this.searchData.searchPropertyValue =
+ FILTER_STRICT_RE.exec(this.searchData.searchPropertyMatch[2])[1];
+ } else {
+ this.searchData.searchPropertyValue =
+ this.searchData.searchPropertyMatch[2];
+ }
+
+ // Strict search for stylesheets will match the property line regex.
+ // Extract the search value within the backquotes to be used
+ // in the strict search for stylesheets in _highlightStyleSheet.
+ if (FILTER_STRICT_RE.test(this.searchValue)) {
+ this.searchData.strictSearchValue =
+ FILTER_STRICT_RE.exec(this.searchValue)[1];
+ }
+ } else if (FILTER_STRICT_RE.test(this.searchValue)) {
+ // If the search value does not correspond to a property line and
+ // is contained in backquotes, extract the search value within the
+ // backquotes and set the flag to perform a strict search for all
+ // the values (selector, stylesheet, property and computed values).
+ let searchValue = FILTER_STRICT_RE.exec(this.searchValue)[1];
+ this.searchData.strictSearchAllValues = true;
+ this.searchData.searchPropertyName = searchValue;
+ this.searchData.searchPropertyValue = searchValue;
+ this.searchData.strictSearchValue = searchValue;
+ }
+
+ this._clearHighlight(this.element);
+ this._clearRules();
+ this._createEditors();
+
+ this.inspector.emit("ruleview-filtered");
+
+ this._filterChangeTimeout = null;
+ }, filterTimeout);
+ },
+
+ /**
+ * Called when the user clicks on the clear button in the filter style search
+ * box. Returns true if the search box is cleared and false otherwise.
+ */
+ _onClearSearch: function () {
+ if (this.searchField.value) {
+ this.setFilterStyles("");
+ return true;
+ }
+
+ return false;
+ },
+
+ destroy: function () {
+ this.isDestroyed = true;
+ this.clear();
+
+ this._dummyElement = 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._outputParser = null;
+
+ // Remove context menu
+ if (this._contextmenu) {
+ this._contextmenu.destroy();
+ this._contextmenu = null;
+ }
+
+ this.tooltips.destroy();
+ this.highlighters.destroy();
+
+ // Remove bound listeners
+ this.shortcuts.destroy();
+ this.element.removeEventListener("copy", this._onCopy);
+ this.element.removeEventListener("contextmenu", this._onContextMenu);
+ this.addRuleButton.removeEventListener("click", this._onAddRule);
+ this.searchField.removeEventListener("input", this._onFilterStyles);
+ this.searchField.removeEventListener("contextmenu",
+ this.inspector.onTextBoxContextMenu);
+ this.searchClearButton.removeEventListener("click", this._onClearSearch);
+ this.pseudoClassToggle.removeEventListener("click",
+ this._onTogglePseudoClassPanel);
+ this.hoverCheckbox.removeEventListener("click", this._onTogglePseudoClass);
+ this.activeCheckbox.removeEventListener("click", this._onTogglePseudoClass);
+ this.focusCheckbox.removeEventListener("click", this._onTogglePseudoClass);
+
+ this.searchField = null;
+ this.searchClearButton = null;
+ this.pseudoClassPanel = null;
+ this.pseudoClassToggle = null;
+ this.hoverCheckbox = null;
+ this.activeCheckbox = null;
+ this.focusCheckbox = null;
+
+ this.inspector = null;
+ this.styleDocument = null;
+ this.styleWindow = null;
+
+ if (this.element.parentNode) {
+ this.element.parentNode.removeChild(this.element);
+ }
+
+ if (this._elementStyle) {
+ this._elementStyle.destroy();
+ }
+
+ this.popup.destroy();
+ },
+
+ /**
+ * Mark the view as selecting an element, disabling all interaction, and
+ * visually clearing the view after a few milliseconds to avoid confusion
+ * about which element's styles the rule view shows.
+ */
+ _startSelectingElement: function () {
+ this.element.classList.add("non-interactive");
+ },
+
+ /**
+ * Mark the view as no longer selecting an element, re-enabling interaction.
+ */
+ _stopSelectingElement: function () {
+ this.element.classList.remove("non-interactive");
+ },
+
+ /**
+ * Update the view with a new selected element.
+ *
+ * @param {NodeActor} element
+ * The node whose style rules we'll inspect.
+ * @param {Boolean} allowRefresh
+ * Update the view even if the element is the same as last time.
+ */
+ selectElement: function (element, allowRefresh = false) {
+ let refresh = (this._viewedElement === element);
+ if (refresh && !allowRefresh) {
+ return promise.resolve(undefined);
+ }
+
+ if (this.popup.isOpen) {
+ this.popup.hidePopup();
+ }
+
+ this.clear(false);
+ this._viewedElement = element;
+
+ this.clearPseudoClassPanel();
+ this.refreshAddRuleButtonState();
+
+ if (!this._viewedElement) {
+ this._stopSelectingElement();
+ this._clearRules();
+ this._showEmpty();
+ this.refreshPseudoClassPanel();
+ return promise.resolve(undefined);
+ }
+
+ // 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.
+ let dummyElementPromise = promise.resolve(this.styleDocument).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);
+ }).then(null, promiseWarn);
+
+ let elementStyle = new ElementStyle(element, this, this.store,
+ this.pageStyle, this.showUserAgentStyles);
+ this._elementStyle = elementStyle;
+
+ this._startSelectingElement();
+
+ return dummyElementPromise.then(() => {
+ if (this._elementStyle === elementStyle) {
+ return this._populate();
+ }
+ return undefined;
+ }).then(() => {
+ if (this._elementStyle === elementStyle) {
+ if (!refresh) {
+ this.element.scrollTop = 0;
+ }
+ this._stopSelectingElement();
+ this._elementStyle.onChanged = () => {
+ this._changed();
+ };
+ }
+ }).then(null, e => {
+ if (this._elementStyle === elementStyle) {
+ this._stopSelectingElement();
+ this._clearRules();
+ }
+ console.error(e);
+ });
+ },
+
+ /**
+ * 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 promise.resolve(undefined);
+ }
+
+ // 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();
+ });
+ },
+
+ /**
+ * Clear the pseudo class options panel by removing the checked and disabled
+ * attributes for each checkbox.
+ */
+ clearPseudoClassPanel: function () {
+ this.hoverCheckbox.checked = this.hoverCheckbox.disabled = false;
+ this.activeCheckbox.checked = this.activeCheckbox.disabled = false;
+ this.focusCheckbox.checked = this.focusCheckbox.disabled = false;
+ },
+
+ /**
+ * Update the pseudo class options for the currently highlighted element.
+ */
+ refreshPseudoClassPanel: function () {
+ if (!this._elementStyle || !this.inspector.selection.isElementNode()) {
+ this.hoverCheckbox.disabled = true;
+ this.activeCheckbox.disabled = true;
+ this.focusCheckbox.disabled = true;
+ return;
+ }
+
+ for (let pseudoClassLock of this._elementStyle.element.pseudoClassLocks) {
+ switch (pseudoClassLock) {
+ case ":hover": {
+ this.hoverCheckbox.checked = true;
+ break;
+ }
+ case ":active": {
+ this.activeCheckbox.checked = true;
+ break;
+ }
+ case ":focus": {
+ this.focusCheckbox.checked = true;
+ break;
+ }
+ }
+ }
+ },
+
+ _populate: function () {
+ let elementStyle = this._elementStyle;
+ return this._elementStyle.populate().then(() => {
+ if (this._elementStyle !== elementStyle || this.isDestroyed) {
+ return null;
+ }
+
+ this._clearRules();
+ let onEditorsReady = this._createEditors();
+ this.refreshPseudoClassPanel();
+
+ // Notify anyone that cares that we refreshed.
+ return onEditorsReady.then(() => {
+ this.emit("ruleview-refreshed");
+ }, e => console.error(e));
+ }).then(null, promiseWarn);
+ },
+
+ /**
+ * Show the user that the rule view has no node selected.
+ */
+ _showEmpty: function () {
+ if (this.styleDocument.getElementById("ruleview-no-results")) {
+ return;
+ }
+
+ createChild(this.element, "div", {
+ id: "ruleview-no-results",
+ textContent: l10n("rule.empty")
+ });
+ },
+
+ /**
+ * Clear the rules.
+ */
+ _clearRules: function () {
+ this.element.innerHTML = "";
+ },
+
+ /**
+ * Clear the rule view.
+ */
+ clear: function (clearDom = true) {
+ this.lastSelectorIcon = null;
+
+ if (clearDom) {
+ 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 () {
+ this.emit("ruleview-changed");
+ },
+
+ /**
+ * Text for header that shows above rules for this element
+ */
+ get selectedElementLabel() {
+ if (this._selectedElementLabel) {
+ return this._selectedElementLabel;
+ }
+ this._selectedElementLabel = 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 = 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} label
+ * 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 (label, isPseudo = false) {
+ let header = this.styleDocument.createElementNS(HTML_NS, "div");
+ header.className = this._getRuleViewHeaderClassName(true);
+ header.textContent = label;
+
+ let twisty = this.styleDocument.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.styleDocument.createElementNS(HTML_NS, "div");
+ container.classList.add("ruleview-expandable-container");
+ container.hidden = false;
+ this.element.appendChild(container);
+
+ header.addEventListener("dblclick", () => {
+ this._toggleContainerVisibility(twisty, container, isPseudo,
+ !this.showPseudoElements);
+ }, false);
+
+ twisty.addEventListener("click", () => {
+ this._toggleContainerVisibility(twisty, container, isPseudo,
+ !this.showPseudoElements);
+ }, false);
+
+ if (isPseudo) {
+ this._toggleContainerVisibility(twisty, container, isPseudo,
+ this.showPseudoElements);
+ }
+
+ return container;
+ },
+
+ /**
+ * Toggle the visibility of an expandable container
+ *
+ * @param {DOMNode} twisty
+ * Clickable toggle DOM Node
+ * @param {DOMNode} container
+ * Expandable container DOM Node
+ * @param {Boolean} isPseudo
+ * Whether or not the container will hold pseudo element rules
+ * @param {Boolean} showPseudo
+ * Whether or not pseudo element rules should be displayed
+ */
+ _toggleContainerVisibility: function (twisty, container, isPseudo,
+ showPseudo) {
+ let isOpen = twisty.getAttribute("open");
+
+ if (isPseudo) {
+ this._showPseudoElements = !!showPseudo;
+
+ Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements",
+ this.showPseudoElements);
+
+ container.hidden = !this.showPseudoElements;
+ isOpen = !this.showPseudoElements;
+ } else {
+ container.hidden = !container.hidden;
+ }
+
+ if (isOpen) {
+ twisty.removeAttribute("open");
+ } else {
+ twisty.setAttribute("open", "true");
+ }
+ },
+
+ _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 seenSearchTerm = false;
+ let container = null;
+
+ if (!this._elementStyle.rules) {
+ return promise.resolve();
+ }
+
+ let editorReadyPromises = [];
+ for (let rule of this._elementStyle.rules) {
+ if (rule.domRule.system) {
+ continue;
+ }
+
+ // Initialize rule editor
+ if (!rule.editor) {
+ rule.editor = new RuleEditor(this, rule);
+ editorReadyPromises.push(rule.editor.once("source-link-updated"));
+ }
+
+ // Filter the rules and highlight any matches if there is a search input
+ if (this.searchValue && this.searchData) {
+ if (this.highlightRule(rule)) {
+ seenSearchTerm = true;
+ } else if (rule.domRule.type !== ELEMENT_STYLE) {
+ continue;
+ }
+ }
+
+ // Only print header for this element if there are pseudo elements
+ if (seenPseudoElement && !seenNormalElement && !rule.pseudoElement) {
+ seenNormalElement = true;
+ let div = this.styleDocument.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.styleDocument.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 (container && (rule.pseudoElement || keyframes)) {
+ container.appendChild(rule.editor.element);
+ } else {
+ this.element.appendChild(rule.editor.element);
+ }
+ }
+
+ if (this.searchValue && !seenSearchTerm) {
+ this.searchField.classList.add("devtools-style-searchbox-no-match");
+ } else {
+ this.searchField.classList.remove("devtools-style-searchbox-no-match");
+ }
+
+ return promise.all(editorReadyPromises);
+ },
+
+ /**
+ * Highlight rules that matches the filter search value and returns a
+ * boolean indicating whether or not rules were highlighted.
+ *
+ * @param {Rule} rule
+ * The rule object we're highlighting if its rule selectors or
+ * property values match the search value.
+ * @return {Boolean} true if the rule was highlighted, false otherwise.
+ */
+ highlightRule: function (rule) {
+ let isRuleSelectorHighlighted = this._highlightRuleSelector(rule);
+ let isStyleSheetHighlighted = this._highlightStyleSheet(rule);
+ let isHighlighted = isRuleSelectorHighlighted || isStyleSheetHighlighted;
+
+ // Highlight search matches in the rule properties
+ for (let textProp of rule.textProps) {
+ if (!textProp.invisible && this._highlightProperty(textProp.editor)) {
+ isHighlighted = true;
+ }
+ }
+
+ return isHighlighted;
+ },
+
+ /**
+ * Highlights the rule selector that matches the filter search value and
+ * returns a boolean indicating whether or not the selector was highlighted.
+ *
+ * @param {Rule} rule
+ * The Rule object.
+ * @return {Boolean} true if the rule selector was highlighted,
+ * false otherwise.
+ */
+ _highlightRuleSelector: function (rule) {
+ let isSelectorHighlighted = false;
+
+ let selectorNodes = [...rule.editor.selectorText.childNodes];
+ if (rule.domRule.type === CSSRule.KEYFRAME_RULE) {
+ selectorNodes = [rule.editor.selectorText];
+ } else if (rule.domRule.type === ELEMENT_STYLE) {
+ selectorNodes = [];
+ }
+
+ // Highlight search matches in the rule selectors
+ for (let selectorNode of selectorNodes) {
+ let selector = selectorNode.textContent.toLowerCase();
+ if ((this.searchData.strictSearchAllValues &&
+ selector === this.searchData.strictSearchValue) ||
+ (!this.searchData.strictSearchAllValues &&
+ selector.includes(this.searchValue))) {
+ selectorNode.classList.add("ruleview-highlight");
+ isSelectorHighlighted = true;
+ }
+ }
+
+ return isSelectorHighlighted;
+ },
+
+ /**
+ * Highlights the stylesheet source that matches the filter search value and
+ * returns a boolean indicating whether or not the stylesheet source was
+ * highlighted.
+ *
+ * @return {Boolean} true if the stylesheet source was highlighted, false
+ * otherwise.
+ */
+ _highlightStyleSheet: function (rule) {
+ let styleSheetSource = rule.title.toLowerCase();
+ let isStyleSheetHighlighted = this.searchData.strictSearchValue ?
+ styleSheetSource === this.searchData.strictSearchValue :
+ styleSheetSource.includes(this.searchValue);
+
+ if (isStyleSheetHighlighted) {
+ rule.editor.source.classList.add("ruleview-highlight");
+ }
+
+ return isStyleSheetHighlighted;
+ },
+
+ /**
+ * Highlights the rule properties and computed properties that match the
+ * filter search value and returns a boolean indicating whether or not the
+ * property or computed property was highlighted.
+ *
+ * @param {TextPropertyEditor} editor
+ * The rule property TextPropertyEditor object.
+ * @return {Boolean} true if the property or computed property was
+ * highlighted, false otherwise.
+ */
+ _highlightProperty: function (editor) {
+ let isPropertyHighlighted = this._highlightRuleProperty(editor);
+ let isComputedHighlighted = this._highlightComputedProperty(editor);
+
+ // Expand the computed list if a computed property is highlighted and the
+ // property rule is not highlighted
+ if (!isPropertyHighlighted && isComputedHighlighted &&
+ !editor.computed.hasAttribute("user-open")) {
+ editor.expandForFilter();
+ }
+
+ return isPropertyHighlighted || isComputedHighlighted;
+ },
+
+ /**
+ * Called when TextPropertyEditor is updated and updates the rule property
+ * highlight.
+ *
+ * @param {TextPropertyEditor} editor
+ * The rule property TextPropertyEditor object.
+ */
+ _updatePropertyHighlight: function (editor) {
+ if (!this.searchValue || !this.searchData) {
+ return;
+ }
+
+ this._clearHighlight(editor.element);
+
+ if (this._highlightProperty(editor)) {
+ this.searchField.classList.remove("devtools-style-searchbox-no-match");
+ }
+ },
+
+ /**
+ * Highlights the rule property that matches the filter search value
+ * and returns a boolean indicating whether or not the property was
+ * highlighted.
+ *
+ * @param {TextPropertyEditor} editor
+ * The rule property TextPropertyEditor object.
+ * @return {Boolean} true if the rule property was highlighted,
+ * false otherwise.
+ */
+ _highlightRuleProperty: function (editor) {
+ // Get the actual property value displayed in the rule view
+ let propertyName = editor.prop.name.toLowerCase();
+ let propertyValue = editor.valueSpan.textContent.toLowerCase();
+
+ return this._highlightMatches(editor.container, propertyName,
+ propertyValue);
+ },
+
+ /**
+ * Highlights the computed property that matches the filter search value and
+ * returns a boolean indicating whether or not the computed property was
+ * highlighted.
+ *
+ * @param {TextPropertyEditor} editor
+ * The rule property TextPropertyEditor object.
+ * @return {Boolean} true if the computed property was highlighted, false
+ * otherwise.
+ */
+ _highlightComputedProperty: function (editor) {
+ let isComputedHighlighted = false;
+
+ // Highlight search matches in the computed list of properties
+ editor._populateComputed();
+ for (let computed of editor.prop.computed) {
+ if (computed.element) {
+ // Get the actual property value displayed in the computed list
+ let computedName = computed.name.toLowerCase();
+ let computedValue = computed.parsedValue.toLowerCase();
+
+ isComputedHighlighted = this._highlightMatches(computed.element,
+ computedName, computedValue) ? true : isComputedHighlighted;
+ }
+ }
+
+ return isComputedHighlighted;
+ },
+
+ /**
+ * Helper function for highlightRules that carries out highlighting the given
+ * element if the search terms match the property, and returns a boolean
+ * indicating whether or not the search terms match.
+ *
+ * @param {DOMNode} element
+ * The node to highlight if search terms match
+ * @param {String} propertyName
+ * The property name of a rule
+ * @param {String} propertyValue
+ * The property value of a rule
+ * @return {Boolean} true if the given search terms match the property, false
+ * otherwise.
+ */
+ _highlightMatches: function (element, propertyName, propertyValue) {
+ let {
+ searchPropertyName,
+ searchPropertyValue,
+ searchPropertyMatch,
+ strictSearchPropertyName,
+ strictSearchPropertyValue,
+ strictSearchAllValues,
+ } = this.searchData;
+ let matches = false;
+
+ // If the inputted search value matches a property line like
+ // `font-family: arial`, then check to make sure the name and value match.
+ // Otherwise, just compare the inputted search string directly against the
+ // name and value of the rule property.
+ let hasNameAndValue = searchPropertyMatch &&
+ searchPropertyName &&
+ searchPropertyValue;
+ let isMatch = (value, query, isStrict) => {
+ return isStrict ? value === query : query && value.includes(query);
+ };
+
+ if (hasNameAndValue) {
+ matches =
+ isMatch(propertyName, searchPropertyName, strictSearchPropertyName) &&
+ isMatch(propertyValue, searchPropertyValue, strictSearchPropertyValue);
+ } else {
+ matches =
+ isMatch(propertyName, searchPropertyName,
+ strictSearchPropertyName || strictSearchAllValues) ||
+ isMatch(propertyValue, searchPropertyValue,
+ strictSearchPropertyValue || strictSearchAllValues);
+ }
+
+ if (matches) {
+ element.classList.add("ruleview-highlight");
+ }
+
+ return matches;
+ },
+
+ /**
+ * Clear all search filter highlights in the panel, and close the computed
+ * list if toggled opened
+ */
+ _clearHighlight: function (element) {
+ for (let el of element.querySelectorAll(".ruleview-highlight")) {
+ el.classList.remove("ruleview-highlight");
+ }
+
+ for (let computed of element.querySelectorAll(
+ ".ruleview-computedlist[filter-open]")) {
+ computed.parentNode._textPropertyEditor.collapseForFilter();
+ }
+ },
+
+ /**
+ * Called when the pseudo class panel button is clicked and toggles
+ * the display of the pseudo class panel.
+ */
+ _onTogglePseudoClassPanel: function () {
+ if (this.pseudoClassPanel.hidden) {
+ this.pseudoClassToggle.setAttribute("checked", "true");
+ this.hoverCheckbox.setAttribute("tabindex", "0");
+ this.activeCheckbox.setAttribute("tabindex", "0");
+ this.focusCheckbox.setAttribute("tabindex", "0");
+ } else {
+ this.pseudoClassToggle.removeAttribute("checked");
+ this.hoverCheckbox.setAttribute("tabindex", "-1");
+ this.activeCheckbox.setAttribute("tabindex", "-1");
+ this.focusCheckbox.setAttribute("tabindex", "-1");
+ }
+
+ this.pseudoClassPanel.hidden = !this.pseudoClassPanel.hidden;
+ },
+
+ /**
+ * Called when a pseudo class checkbox is clicked and toggles
+ * the pseudo class for the current selected element.
+ */
+ _onTogglePseudoClass: function (event) {
+ let target = event.currentTarget;
+ this.inspector.togglePseudoClass(target.value);
+ },
+
+ /**
+ * Handle the keypress event in the rule view.
+ */
+ _onShortcut: function (name, event) {
+ if (!event.target.closest("#sidebar-panel-ruleview")) {
+ return;
+ }
+
+ if (name === "CmdOrCtrl+F") {
+ this.searchField.focus();
+ event.preventDefault();
+ } else if ((name === "Return" || name === "Space") &&
+ this.element.classList.contains("non-interactive")) {
+ event.preventDefault();
+ } else if (name === "Escape" &&
+ event.target === this.searchField &&
+ this._onClearSearch()) {
+ // Handle the search box's keypress event. If the escape key is pressed,
+ // clear the search box field.
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+};
+
+/**
+ * Helper functions
+ */
+
+/**
+ * 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;
+ }
+}
+
+function RuleViewTool(inspector, window) {
+ this.inspector = inspector;
+ this.document = window.document;
+
+ this.view = new CssRuleView(this.inspector, this.document);
+
+ this.clearUserProperties = this.clearUserProperties.bind(this);
+ this.refresh = this.refresh.bind(this);
+ this.onLinkClicked = this.onLinkClicked.bind(this);
+ this.onMutations = this.onMutations.bind(this);
+ this.onPanelSelected = this.onPanelSelected.bind(this);
+ this.onPropertyChanged = this.onPropertyChanged.bind(this);
+ this.onResized = this.onResized.bind(this);
+ this.onSelected = this.onSelected.bind(this);
+ this.onViewRefreshed = this.onViewRefreshed.bind(this);
+
+ this.view.on("ruleview-changed", this.onPropertyChanged);
+ this.view.on("ruleview-refreshed", this.onViewRefreshed);
+ this.view.on("ruleview-linked-clicked", this.onLinkClicked);
+
+ this.inspector.selection.on("detached-front", this.onSelected);
+ this.inspector.selection.on("new-node-front", this.onSelected);
+ this.inspector.selection.on("pseudoclass", this.refresh);
+ this.inspector.target.on("navigate", this.clearUserProperties);
+ this.inspector.sidebar.on("ruleview-selected", this.onPanelSelected);
+ this.inspector.pageStyle.on("stylesheet-updated", this.refresh);
+ this.inspector.walker.on("mutations", this.onMutations);
+ this.inspector.walker.on("resize", this.onResized);
+
+ 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 (e, 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 href = rule.nodeHref || rule.href;
+ let toolbox = gDevTools.getToolbox(this.inspector.target);
+ toolbox.viewSource(href, 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 = this.inspector.target;
+ if (Tools.styleEditor.isTargetSupported(target)) {
+ gDevTools.showToolbox(target, "styleeditor").then(function (toolbox) {
+ let url = source || href;
+ toolbox.getCurrentPanel().selectStyleSheet(url, line, column);
+ });
+ }
+ return;
+ });
+ },
+
+ onPropertyChanged: function () {
+ this.inspector.markDirty();
+ },
+
+ onViewRefreshed: function () {
+ this.inspector.emit("rule-view-refreshed");
+ },
+
+ /**
+ * When markup mutations occur, if an attribute of the selected node changes,
+ * we need to refresh the view as that might change the node's styles.
+ */
+ onMutations: function (mutations) {
+ for (let {type, target} of mutations) {
+ if (target === this.inspector.selection.nodeFront &&
+ type === "attributes") {
+ this.refresh();
+ break;
+ }
+ }
+ },
+
+ /**
+ * When the window gets resized, this may cause media-queries to match, and
+ * therefore, different styles may apply.
+ */
+ onResized: function () {
+ this.refresh();
+ },
+
+ destroy: function () {
+ this.inspector.walker.off("mutations", this.onMutations);
+ this.inspector.walker.off("resize", this.onResized);
+ this.inspector.selection.off("detached-front", this.onSelected);
+ 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);
+ if (this.inspector.pageStyle) {
+ this.inspector.pageStyle.off("stylesheet-updated", this.refresh);
+ }
+
+ this.view.off("ruleview-linked-clicked", this.onLinkClicked);
+ this.view.off("ruleview-changed", this.onPropertyChanged);
+ this.view.off("ruleview-refreshed", this.onViewRefreshed);
+
+ this.view.destroy();
+
+ this.view = this.document = this.inspector = null;
+ }
+};
+
+exports.CssRuleView = CssRuleView;
+exports.RuleViewTool = RuleViewTool;
diff --git a/devtools/client/inspector/rules/test/.eslintrc.js b/devtools/client/inspector/rules/test/.eslintrc.js
new file mode 100644
index 000000000..698ae9181
--- /dev/null
+++ b/devtools/client/inspector/rules/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/inspector/rules/test/browser.ini b/devtools/client/inspector/rules/test/browser.ini
new file mode 100644
index 000000000..2c11219fb
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -0,0 +1,221 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ doc_author-sheet.html
+ doc_blob_stylesheet.html
+ doc_content_stylesheet.html
+ doc_content_stylesheet_imported.css
+ doc_content_stylesheet_imported2.css
+ doc_content_stylesheet_linked.css
+ doc_content_stylesheet_script.css
+ doc_copystyles.css
+ doc_copystyles.html
+ doc_cssom.html
+ doc_custom.html
+ doc_filter.html
+ doc_frame_script.js
+ doc_inline_sourcemap.html
+ doc_invalid_sourcemap.css
+ doc_invalid_sourcemap.html
+ doc_keyframeanimation.css
+ doc_keyframeanimation.html
+ doc_keyframeLineNumbers.html
+ doc_media_queries.html
+ doc_pseudoelement.html
+ doc_ruleLineNumbers.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
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+ !/devtools/client/inspector/test/head.js
+ !/devtools/client/inspector/test/shared-head.js
+ !/devtools/client/shared/test/test-actor.js
+ !/devtools/client/shared/test/test-actor-registry.js
+
+[browser_rules_add-property-and-reselect.js]
+[browser_rules_add-property-cancel_01.js]
+[browser_rules_add-property-cancel_02.js]
+[browser_rules_add-property-cancel_03.js]
+[browser_rules_add-property-commented.js]
+[browser_rules_add-property_01.js]
+[browser_rules_add-property_02.js]
+[browser_rules_add-property-svg.js]
+[browser_rules_add-rule-and-property.js]
+[browser_rules_add-rule-button-state.js]
+[browser_rules_add-rule-edit-selector.js]
+[browser_rules_add-rule-iframes.js]
+[browser_rules_add-rule-namespace-elements.js]
+[browser_rules_add-rule-pseudo-class.js]
+[browser_rules_add-rule-then-property-edit-selector.js]
+[browser_rules_add-rule-with-menu.js]
+[browser_rules_add-rule.js]
+[browser_rules_authored.js]
+[browser_rules_authored_color.js]
+[browser_rules_authored_override.js]
+[browser_rules_blob_stylesheet.js]
+[browser_rules_colorpicker-and-image-tooltip_01.js]
+[browser_rules_colorpicker-and-image-tooltip_02.js]
+[browser_rules_colorpicker-appears-on-swatch-click.js]
+[browser_rules_colorpicker-commit-on-ENTER.js]
+[browser_rules_colorpicker-edit-gradient.js]
+[browser_rules_colorpicker-hides-on-tooltip.js]
+[browser_rules_colorpicker-multiple-changes.js]
+[browser_rules_colorpicker-release-outside-frame.js]
+[browser_rules_colorpicker-revert-on-ESC.js]
+[browser_rules_colorpicker-swatch-displayed.js]
+[browser_rules_colorUnit.js]
+[browser_rules_completion-existing-property_01.js]
+[browser_rules_completion-existing-property_02.js]
+[browser_rules_completion-new-property_01.js]
+[browser_rules_completion-new-property_02.js]
+[browser_rules_completion-new-property_03.js]
+[browser_rules_completion-new-property_04.js]
+[browser_rules_completion-new-property_multiline.js]
+[browser_rules_computed-lists_01.js]
+[browser_rules_computed-lists_02.js]
+[browser_rules_completion-popup-hidden-after-navigation.js]
+[browser_rules_content_01.js]
+[browser_rules_content_02.js]
+skip-if = e10s && debug # Bug 1250058 - Docshell leak on debug e10s
+[browser_rules_context-menu-show-mdn-docs-01.js]
+[browser_rules_context-menu-show-mdn-docs-02.js]
+[browser_rules_context-menu-show-mdn-docs-03.js]
+[browser_rules_copy_styles.js]
+subsuite = clipboard
+[browser_rules_cssom.js]
+[browser_rules_cubicbezier-appears-on-swatch-click.js]
+[browser_rules_cubicbezier-commit-on-ENTER.js]
+[browser_rules_cubicbezier-revert-on-ESC.js]
+[browser_rules_custom.js]
+[browser_rules_cycle-angle.js]
+[browser_rules_cycle-color.js]
+[browser_rules_edit-display-grid-property.js]
+[browser_rules_edit-property-cancel.js]
+[browser_rules_edit-property-click.js]
+[browser_rules_edit-property-commit.js]
+[browser_rules_edit-property-computed.js]
+[browser_rules_edit-property-increments.js]
+[browser_rules_edit-property-order.js]
+[browser_rules_edit-property-remove_01.js]
+[browser_rules_edit-property-remove_02.js]
+[browser_rules_edit-property-remove_03.js]
+[browser_rules_edit-property_01.js]
+[browser_rules_edit-property_02.js]
+[browser_rules_edit-property_03.js]
+[browser_rules_edit-property_04.js]
+[browser_rules_edit-property_05.js]
+[browser_rules_edit-property_06.js]
+[browser_rules_edit-property_07.js]
+[browser_rules_edit-property_08.js]
+[browser_rules_edit-property_09.js]
+[browser_rules_edit-selector-click.js]
+[browser_rules_edit-selector-click-on-scrollbar.js]
+skip-if = os == "mac" # Bug 1245996 : click on scrollbar not working on OSX
+[browser_rules_edit-selector-commit.js]
+[browser_rules_edit-selector_01.js]
+[browser_rules_edit-selector_02.js]
+[browser_rules_edit-selector_03.js]
+[browser_rules_edit-selector_04.js]
+[browser_rules_edit-selector_05.js]
+[browser_rules_edit-selector_06.js]
+[browser_rules_edit-selector_07.js]
+[browser_rules_edit-selector_08.js]
+[browser_rules_edit-selector_09.js]
+[browser_rules_edit-selector_10.js]
+[browser_rules_edit-selector_11.js]
+[browser_rules_edit-value-after-name_01.js]
+[browser_rules_edit-value-after-name_02.js]
+[browser_rules_edit-value-after-name_03.js]
+[browser_rules_edit-value-after-name_04.js]
+[browser_rules_editable-field-focus_01.js]
+[browser_rules_editable-field-focus_02.js]
+[browser_rules_eyedropper.js]
+[browser_rules_filtereditor-appears-on-swatch-click.js]
+[browser_rules_filtereditor-commit-on-ENTER.js]
+[browser_rules_filtereditor-revert-on-ESC.js]
+skip-if = (os == "win" && debug) # bug 963492: win.
+[browser_rules_grid-highlighter-on-navigate.js]
+[browser_rules_grid-highlighter-on-reload.js]
+[browser_rules_grid-toggle_01.js]
+[browser_rules_grid-toggle_02.js]
+[browser_rules_grid-toggle_03.js]
+[browser_rules_guessIndentation.js]
+[browser_rules_inherited-properties_01.js]
+[browser_rules_inherited-properties_02.js]
+[browser_rules_inherited-properties_03.js]
+[browser_rules_inline-source-map.js]
+[browser_rules_invalid.js]
+[browser_rules_invalid-source-map.js]
+[browser_rules_keybindings.js]
+[browser_rules_keyframes-rule_01.js]
+[browser_rules_keyframes-rule_02.js]
+[browser_rules_keyframeLineNumbers.js]
+[browser_rules_lineNumbers.js]
+[browser_rules_livepreview.js]
+[browser_rules_mark_overridden_01.js]
+[browser_rules_mark_overridden_02.js]
+[browser_rules_mark_overridden_03.js]
+[browser_rules_mark_overridden_04.js]
+[browser_rules_mark_overridden_05.js]
+[browser_rules_mark_overridden_06.js]
+[browser_rules_mark_overridden_07.js]
+[browser_rules_mathml-element.js]
+[browser_rules_media-queries.js]
+[browser_rules_multiple-properties-duplicates.js]
+[browser_rules_multiple-properties-priority.js]
+[browser_rules_multiple-properties-unfinished_01.js]
+[browser_rules_multiple-properties-unfinished_02.js]
+[browser_rules_multiple_properties_01.js]
+[browser_rules_multiple_properties_02.js]
+[browser_rules_original-source-link.js]
+[browser_rules_pseudo-element_01.js]
+[browser_rules_pseudo-element_02.js]
+[browser_rules_pseudo_lock_options.js]
+[browser_rules_refresh-no-flicker.js]
+[browser_rules_refresh-on-attribute-change_01.js]
+[browser_rules_refresh-on-attribute-change_02.js]
+[browser_rules_refresh-on-style-change.js]
+[browser_rules_search-filter-computed-list_01.js]
+[browser_rules_search-filter-computed-list_02.js]
+[browser_rules_search-filter-computed-list_03.js]
+[browser_rules_search-filter-computed-list_04.js]
+[browser_rules_search-filter-computed-list_expander.js]
+[browser_rules_search-filter-overridden-property.js]
+[browser_rules_search-filter_01.js]
+[browser_rules_search-filter_02.js]
+[browser_rules_search-filter_03.js]
+[browser_rules_search-filter_04.js]
+[browser_rules_search-filter_05.js]
+[browser_rules_search-filter_06.js]
+[browser_rules_search-filter_07.js]
+[browser_rules_search-filter_08.js]
+[browser_rules_search-filter_09.js]
+[browser_rules_search-filter_10.js]
+[browser_rules_search-filter_context-menu.js]
+subsuite = clipboard
+[browser_rules_search-filter_escape-keypress.js]
+[browser_rules_select-and-copy-styles.js]
+subsuite = clipboard
+[browser_rules_selector-highlighter-on-navigate.js]
+[browser_rules_selector-highlighter_01.js]
+[browser_rules_selector-highlighter_02.js]
+[browser_rules_selector-highlighter_03.js]
+[browser_rules_selector-highlighter_04.js]
+[browser_rules_selector_highlight.js]
+[browser_rules_strict-search-filter-computed-list_01.js]
+[browser_rules_strict-search-filter_01.js]
+[browser_rules_strict-search-filter_02.js]
+[browser_rules_strict-search-filter_03.js]
+[browser_rules_style-editor-link.js]
+[browser_rules_urls-clickable.js]
+[browser_rules_user-agent-styles.js]
+[browser_rules_user-agent-styles-uneditable.js]
+[browser_rules_user-property-reset.js]
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js b/devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js
new file mode 100644
index 000000000..492739abe
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.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 that adding properties to rules work and reselecting the element still
+// show them.
+
+const TEST_URI = URL_ROOT + "doc_content_stylesheet.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#target", inspector);
+
+ info("Setting a font-weight property on all rules");
+ yield setPropertyOnAllRules(view);
+
+ info("Reselecting the element");
+ yield selectNode("body", inspector);
+ yield selectNode("#target", inspector);
+
+ checkPropertyOnAllRules(view);
+});
+
+function* setPropertyOnAllRules(view) {
+ // Wait for the properties to be properly created on the backend and for the
+ // view to be updated.
+ let onRefreshed = view.once("ruleview-refreshed");
+ for (let rule of view._elementStyle.rules) {
+ rule.editor.addProperty("font-weight", "bold", "", true);
+ }
+ yield onRefreshed;
+}
+
+function checkPropertyOnAllRules(view) {
+ for (let rule of view._elementStyle.rules) {
+ let lastRule = rule.textProps[rule.textProps.length - 1];
+
+ is(lastRule.name, "font-weight", "Last rule name is font-weight");
+ is(lastRule.value, "bold", "Last rule value is bold");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_01.js b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_01.js
new file mode 100644
index 000000000..78b3a4c91
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_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";
+
+// Tests adding a new property and escapes the new empty property name editor.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ background-color: blue;
+ }
+ .testclass {
+ background-color: green;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let elementRuleEditor = getRuleViewRuleEditor(view, 0);
+ let editor = yield focusNewRuleViewProperty(elementRuleEditor);
+ is(inplaceEditor(elementRuleEditor.newPropSpan), editor,
+ "The new property editor got focused");
+
+ info("Escape the new property editor");
+ let onBlur = once(editor.input, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ yield onBlur;
+
+ info("Checking the state of cancelling a new property name editor");
+ is(elementRuleEditor.rule.textProps.length, 0,
+ "Should have cancelled creating a new text property.");
+ ok(!elementRuleEditor.propertyList.hasChildNodes(),
+ "Should not have any properties.");
+
+ is(view.styleDocument.activeElement, view.styleDocument.body,
+ "Correct element has focus");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_02.js b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_02.js
new file mode 100644
index 000000000..7f4d1564c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_02.js
@@ -0,0 +1,34 @@
+/* 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 adding a new property and escapes the new empty property value editor.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Test creating a new property and escaping");
+ yield addProperty(view, 1, "color", "red", "VK_ESCAPE", false);
+
+ is(view.styleDocument.activeElement, view.styleDocument.body,
+ "Correct element has focus");
+
+ let elementRuleEditor = getRuleViewRuleEditor(view, 1);
+ is(elementRuleEditor.rule.textProps.length, 1,
+ "Removed the new text property.");
+ is(elementRuleEditor.propertyList.children.length, 1,
+ "Removed the property editor.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_03.js b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_03.js
new file mode 100644
index 000000000..4f8b42009
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_03.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 adding a new property and escapes the property name editor with a
+// value.
+
+const TEST_URI = `
+ <style type='text/css'>
+ div {
+ background-color: blue;
+ }
+ </style>
+ <div>Test node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ // Add a property to the element's style declaration, add some text,
+ // then press escape.
+
+ let elementRuleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = yield focusNewRuleViewProperty(elementRuleEditor);
+
+ is(inplaceEditor(elementRuleEditor.newPropSpan), editor,
+ "Next focused editor should be the new property editor.");
+
+ EventUtils.sendString("background", view.styleWindow);
+
+ let onBlur = once(editor.input, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield onBlur;
+
+ is(elementRuleEditor.rule.textProps.length, 1,
+ "Should have canceled creating a new text property.");
+ is(view.styleDocument.activeElement, view.styleDocument.body,
+ "Correct element has focus");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-commented.js b/devtools/client/inspector/rules/test/browser_rules_add-property-commented.js
new file mode 100644
index 000000000..eacf5db5a
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-commented.js
@@ -0,0 +1,47 @@
+/* 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 commented properties can be added and are disabled.
+
+const TEST_URI = "<div id='testid'></div>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testCreateNewSetOfCommentedAndUncommentedProperties(view);
+});
+
+function* testCreateNewSetOfCommentedAndUncommentedProperties(view) {
+ info("Test creating a new set of commented and uncommented properties");
+
+ info("Focusing a new property name in the rule-view");
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let editor = yield focusEditableField(view, ruleEditor.closeBrace);
+ is(inplaceEditor(ruleEditor.newPropSpan), editor,
+ "The new property editor has focus");
+
+ info(
+ "Entering a commented property/value pair into the property name editor");
+ let input = editor.input;
+ input.value = `color: blue;
+ /* background-color: yellow; */
+ width: 200px;
+ height: 100px;
+ /* padding-bottom: 1px; */`;
+
+ info("Pressing return to commit and focus the new value field");
+ let onModifications = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onModifications;
+
+ let textProps = ruleEditor.rule.textProps;
+ ok(textProps[0].enabled, "The 'color' property is enabled.");
+ ok(!textProps[1].enabled, "The 'background-color' property is disabled.");
+ ok(textProps[2].enabled, "The 'width' property is enabled.");
+ ok(textProps[3].enabled, "The 'height' property is enabled.");
+ ok(!textProps[4].enabled, "The 'padding-bottom' property is disabled.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-svg.js b/devtools/client/inspector/rules/test/browser_rules_add-property-svg.js
new file mode 100644
index 000000000..a53421db3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-svg.js
@@ -0,0 +1,22 @@
+/* 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 editing SVG styles using the rules view.
+
+var TEST_URL = "chrome://global/skin/icons/warning.svg";
+var TEST_SELECTOR = "path";
+
+add_task(function* () {
+ yield addTab(TEST_URL);
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(TEST_SELECTOR, inspector);
+
+ info("Test creating a new property");
+ yield addProperty(view, 0, "fill", "red");
+
+ is((yield getComputedStyleProperty(TEST_SELECTOR, null, "fill")),
+ "rgb(255, 0, 0)", "The fill was changed to red");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property_01.js b/devtools/client/inspector/rules/test/browser_rules_add-property_01.js
new file mode 100644
index 000000000..1d7068d54
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property_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 adding an invalid property.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ }
+ .testclass {
+ background-color: green;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Test creating a new property");
+ let textProp = yield addProperty(view, 0, "background-color", "#XYZ");
+
+ is(textProp.value, "#XYZ", "Text prop should have been changed.");
+ is(textProp.overridden, true, "Property should be overridden");
+ is(textProp.editor.isValid(), false, "#XYZ should not be a valid entry");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property_02.js b/devtools/client/inspector/rules/test/browser_rules_add-property_02.js
new file mode 100644
index 000000000..6f6bef0f7
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property_02.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";
+
+// Test adding a valid property to a CSS rule, and navigating through the fields
+// by pressing ENTER.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ color: blue;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Focus the new property name field");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+ let input = editor.input;
+
+ is(inplaceEditor(ruleEditor.newPropSpan), editor,
+ "Next focused editor should be the new property editor.");
+ ok(input.selectionStart === 0 && input.selectionEnd === input.value.length,
+ "Editor contents are selected.");
+
+ // Try clicking on the editor's input again, shouldn't cause trouble
+ // (see bug 761665).
+ EventUtils.synthesizeMouse(input, 1, 1, {}, view.styleWindow);
+ input.select();
+
+ info("Entering the property name");
+ editor.input.value = "background-color";
+
+ info("Pressing RETURN and waiting for the value field focus");
+ let onNameAdded = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+
+ yield onNameAdded;
+
+ editor = inplaceEditor(view.styleDocument.activeElement);
+
+ 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.");
+ let textProp = ruleEditor.rule.textProps[1];
+ is(editor, inplaceEditor(textProp.editor.valueSpan),
+ "Should be editing the value span now.");
+
+ info("Entering the property value");
+ let onValueAdded = view.once("ruleview-changed");
+ editor.input.value = "purple";
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onValueAdded;
+
+ is(textProp.value, "purple", "Text prop should have been changed.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js
new file mode 100644
index 000000000..1cf04a275
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js
@@ -0,0 +1,30 @@
+/* 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 adding a new rule and a new property in this rule.
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8,<div id='testid'>Styled Node</div>");
+ let {inspector, view} = yield openRuleView();
+
+ info("Selecting the test node");
+ yield selectNode("#testid", inspector);
+
+ info("Adding a new rule for this node and blurring the new selector field");
+ yield addNewRuleAndDismissEditor(inspector, view, "#testid", 1);
+
+ info("Adding a new property for this rule");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ let onRuleViewChanged = view.once("ruleview-changed");
+ ruleEditor.addProperty("font-weight", "bold", "", true);
+ yield onRuleViewChanged;
+
+ let textProps = ruleEditor.rule.textProps;
+ let prop = textProps[textProps.length - 1];
+ is(prop.name, "font-weight", "The last property name is font-weight");
+ is(prop.value, "bold", "The last property value is bold");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-button-state.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-button-state.js
new file mode 100644
index 000000000..1441213b3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-button-state.js
@@ -0,0 +1,51 @@
+/* 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 if the `Add rule` button disables itself properly for non-element nodes
+// and anonymous element.
+
+const TEST_URI = `
+ <style type="text/css">
+ #pseudo::before {
+ content: "before";
+ }
+ </style>
+ <div id="pseudo"></div>
+ <div id="testid">Test Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield testDisabledButton(inspector, view);
+});
+
+function* testDisabledButton(inspector, view) {
+ let node = "#testid";
+
+ info("Selecting a real element");
+ yield selectNode(node, inspector);
+ ok(!view.addRuleButton.disabled, "Add rule button should be enabled");
+
+ info("Select a null element");
+ yield view.selectElement(null);
+ ok(view.addRuleButton.disabled, "Add rule button should be disabled");
+
+ info("Selecting a real element");
+ yield selectNode(node, inspector);
+ ok(!view.addRuleButton.disabled, "Add rule button should be enabled");
+
+ info("Selecting a pseudo element");
+ let pseudo = yield getNodeFront("#pseudo", inspector);
+ let children = yield inspector.walker.children(pseudo);
+ let before = children.nodes[0];
+ yield selectNode(before, inspector);
+ ok(view.addRuleButton.disabled, "Add rule button should be disabled");
+
+ info("Selecting a real element");
+ yield selectNode(node, inspector);
+ ok(!view.addRuleButton.disabled, "Add rule button should be enabled");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-edit-selector.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-edit-selector.js
new file mode 100644
index 000000000..b59f317a5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-edit-selector.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";
+
+// Tests the behaviour of adding a new rule to the rule view and editing
+// its selector.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ text-align: center;
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+ <span>This is a span</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ yield addNewRule(inspector, view);
+ yield testEditSelector(view, "span");
+
+ info("Selecting the modified element with the new rule");
+ 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 update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+}
+
+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/devtools/client/inspector/rules/test/browser_rules_add-rule-iframes.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-iframes.js
new file mode 100644
index 000000000..7b0ba7812
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-iframes.js
@@ -0,0 +1,57 @@
+/* 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 adding a rule on elements nested in iframes.
+
+const TEST_URI =
+ `<div>outer</div>
+ <iframe id="frame1" src="data:text/html;charset=utf-8,<div>inner1</div>">
+ </iframe>
+ <iframe id="frame2" src="data:text/html;charset=utf-8,<div>inner2</div>">
+ </iframe>`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+ yield addNewRuleAndDismissEditor(inspector, view, "div", 1);
+ yield addNewProperty(view, 1, "color", "red");
+
+ let innerFrameDiv1 = yield getNodeFrontInFrame("div", "#frame1", inspector);
+ yield selectNode(innerFrameDiv1, inspector);
+ yield addNewRuleAndDismissEditor(inspector, view, "div", 1);
+ yield addNewProperty(view, 1, "color", "blue");
+
+ let innerFrameDiv2 = yield getNodeFrontInFrame("div", "#frame2", inspector);
+ yield selectNode(innerFrameDiv2, inspector);
+ yield addNewRuleAndDismissEditor(inspector, view, "div", 1);
+ yield addNewProperty(view, 1, "color", "green");
+});
+
+/**
+ * Add a new property in the rule at the provided index in the rule view.
+ *
+ * @param {RuleView} view
+ * @param {Number} index
+ * The index of the rule in which we should add a new property.
+ * @param {String} name
+ * The name of the new property.
+ * @param {String} value
+ * The value of the new property.
+ */
+function* addNewProperty(view, index, name, value) {
+ let idRuleEditor = getRuleViewRuleEditor(view, index);
+ info(`Adding new property "${name}: ${value};"`);
+
+ let onRuleViewChanged = view.once("ruleview-changed");
+ idRuleEditor.addProperty(name, value, "", true);
+ yield onRuleViewChanged;
+
+ let textProps = idRuleEditor.rule.textProps;
+ let lastProperty = textProps[textProps.length - 1];
+ is(lastProperty.name, name, "Last property has the expected name");
+ is(lastProperty.value, value, "Last property has the expected value");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-namespace-elements.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-namespace-elements.js
new file mode 100644
index 000000000..98e34e69f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-namespace-elements.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 the behaviour of adding a new rule using the add rule button
+// on namespaced elements.
+
+const XHTML = `
+ <!DOCTYPE html>
+ <html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <body>
+ <svg:svg width="100" height="100">
+ <svg:clipPath>
+ <svg:rect x="0" y="0" width="10" height="5"></svg:rect>
+ </svg:clipPath>
+ <svg:circle cx="0" cy="0" r="5"></svg:circle>
+ </svg:svg>
+ </body>
+ </html>
+`;
+const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML);
+
+const TEST_DATA = [
+ { node: "clipPath", expected: "clipPath" },
+ { node: "rect", expected: "rect" },
+ { node: "circle", expected: "circle" }
+];
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+
+ for (let data of TEST_DATA) {
+ let {node, expected} = data;
+ yield selectNode(node, inspector);
+ yield addNewRuleAndDismissEditor(inspector, view, expected, 1);
+ }
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-pseudo-class.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-pseudo-class.js
new file mode 100644
index 000000000..39f773c13
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-pseudo-class.js
@@ -0,0 +1,82 @@
+/* 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 adding a rule with pseudo class locks on.
+
+const TEST_URI = "<p id='element'>Test element</p>";
+
+const EXPECTED_SELECTOR = "#element";
+const TEST_DATA = [
+ [],
+ [":hover"],
+ [":hover", ":active"],
+ [":hover", ":active", ":focus"],
+ [":active"],
+ [":active", ":focus"],
+ [":focus"]
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#element", inspector);
+
+ for (let data of TEST_DATA) {
+ yield runTestData(inspector, view, data);
+ }
+});
+
+function* runTestData(inspector, view, pseudoClasses) {
+ yield setPseudoLocks(inspector, view, pseudoClasses);
+
+ let expected = EXPECTED_SELECTOR + pseudoClasses.join("");
+ yield addNewRuleAndDismissEditor(inspector, view, expected, 1);
+
+ yield resetPseudoLocks(inspector, view);
+}
+
+function* setPseudoLocks(inspector, view, pseudoClasses) {
+ if (pseudoClasses.length == 0) {
+ return;
+ }
+
+ for (let pseudoClass of pseudoClasses) {
+ switch (pseudoClass) {
+ case ":hover":
+ view.hoverCheckbox.click();
+ yield inspector.once("rule-view-refreshed");
+ break;
+ case ":active":
+ view.activeCheckbox.click();
+ yield inspector.once("rule-view-refreshed");
+ break;
+ case ":focus":
+ view.focusCheckbox.click();
+ yield inspector.once("rule-view-refreshed");
+ break;
+ }
+ }
+}
+
+function* resetPseudoLocks(inspector, view) {
+ if (!view.hoverCheckbox.checked &&
+ !view.activeCheckbox.checked &&
+ !view.focusCheckbox.checked) {
+ return;
+ }
+ if (view.hoverCheckbox.checked) {
+ view.hoverCheckbox.click();
+ yield inspector.once("rule-view-refreshed");
+ }
+ if (view.activeCheckbox.checked) {
+ view.activeCheckbox.click();
+ yield inspector.once("rule-view-refreshed");
+ }
+ if (view.focusCheckbox.checked) {
+ view.focusCheckbox.click();
+ yield inspector.once("rule-view-refreshed");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-then-property-edit-selector.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-then-property-edit-selector.js
new file mode 100644
index 000000000..294eb67e4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-then-property-edit-selector.js
@@ -0,0 +1,80 @@
+/* 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.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ text-align: center;
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+ <span>This is a span</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ yield addNewRuleAndDismissEditor(inspector, view, "#testid", 1);
+
+ info("Adding a new property to the new rule");
+ yield testAddingProperty(view, 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* testAddingProperty(view, index) {
+ let ruleEditor = getRuleViewRuleEditor(view, index);
+ ruleEditor.addProperty("font-weight", "bold", "", true);
+ let textProps = ruleEditor.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(view, 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 update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ is(view._elementStyle.rules.length, 3, "Should have 3 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/devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js
new file mode 100644
index 000000000..976fc9643
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.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";
+
+// Tests the a new CSS rule can be added using the context menu.
+
+const TEST_URI = '<div id="testid">Test Node</div>';
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("#testid", inspector);
+ yield addNewRuleFromContextMenu(inspector, view);
+ yield testNewRule(view);
+});
+
+function* addNewRuleFromContextMenu(inspector, view) {
+ info("Waiting for context menu to be shown");
+
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, view.element);
+ let menuitemAddRule = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.addNewRule"));
+
+ ok(menuitemAddRule.visible, "Add rule is visible");
+
+ info("Adding the new rule and expecting a ruleview-changed event");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ menuitemAddRule.click();
+ yield onRuleViewChanged;
+}
+
+function* testNewRule(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = ruleEditor.selectorText.ownerDocument.activeElement;
+ is(editor.value, "#testid", "Selector editor value is as expected");
+
+ info("Escaping from the selector field the change");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule.js b/devtools/client/inspector/rules/test/browser_rules_add-rule.js
new file mode 100644
index 000000000..296105c85
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule.js
@@ -0,0 +1,47 @@
+/* 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 adding a new rule using the add rule button.
+
+const TEST_URI = `
+ <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>
+ <span class="class3 class4">Multiple classes</span>
+ <p>Empty<p>
+ <h1 class="asd@@@@a!!!!:::@asd">Invalid characters in class</h1>
+ <h2 id="asd@@@a!!2a">Invalid characters in id</h2>
+ <svg viewBox="0 0 10 10">
+ <circle cx="5" cy="5" r="5" fill="blue"></circle>
+ </svg>
+`;
+
+const TEST_DATA = [
+ { node: "#testid", expected: "#testid" },
+ { node: ".testclass2", expected: ".testclass2" },
+ { node: ".class1.class2", expected: ".class1.class2" },
+ { node: ".class3.class4", expected: ".class3.class4" },
+ { node: "p", expected: "p" },
+ { node: "h1", expected: ".asd\\@\\@\\@\\@a\\!\\!\\!\\!\\:\\:\\:\\@asd" },
+ { node: "h2", expected: "#asd\\@\\@\\@a\\!\\!2a" },
+ { node: "circle", expected: "circle" }
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ for (let data of TEST_DATA) {
+ let {node, expected} = data;
+ yield selectNode(node, inspector);
+ yield addNewRuleAndDismissEditor(inspector, view, expected, 1);
+ }
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_authored.js b/devtools/client/inspector/rules/test/browser_rules_authored.js
new file mode 100644
index 000000000..cb0dd1186
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_authored.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 for as-authored styles.
+
+function* createTestContent(style) {
+ let html = `<style type="text/css">
+ ${style}
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>`;
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(html));
+
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ return view;
+}
+
+add_task(function* () {
+ let view = yield createTestContent("#testid {" +
+ // Invalid property.
+ " something: random;" +
+ // Invalid value.
+ " color: orang;" +
+ // Override.
+ " background-color: blue;" +
+ " background-color: #f0c;" +
+ "} ");
+
+ let elementStyle = view._elementStyle;
+
+ let expected = [
+ {name: "something", overridden: true},
+ {name: "color", overridden: true},
+ {name: "background-color", overridden: true},
+ {name: "background-color", overridden: false}
+ ];
+
+ let rule = elementStyle.rules[1];
+
+ for (let i = 0; i < expected.length; ++i) {
+ let prop = rule.textProps[i];
+ is(prop.name, expected[i].name, "test name for prop " + i);
+ is(prop.overridden, expected[i].overridden,
+ "test overridden for prop " + i);
+ }
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_authored_color.js b/devtools/client/inspector/rules/test/browser_rules_authored_color.js
new file mode 100644
index 000000000..4c5cab206
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_authored_color.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";
+
+// Test for as-authored color styles.
+
+/**
+ * Array of test color objects:
+ * {String} name: name of the used & expected color format.
+ * {String} id: id of the element that will be created to test this color.
+ * {String} color: initial value of the color property applied to the test element.
+ * {String} result: expected value of the color property after edition.
+ */
+const colors = [
+ {name: "hex", id: "test1", color: "#f0c", result: "#0f0"},
+ {name: "rgb", id: "test2", color: "rgb(0,128,250)", result: "rgb(0, 255, 0)"},
+ // Test case preservation.
+ {name: "hex", id: "test3", color: "#F0C", result: "#0F0"},
+];
+
+add_task(function* () {
+ Services.prefs.setCharPref("devtools.defaultColorUnit", "authored");
+
+ let html = "";
+ for (let {color, id} of colors) {
+ html += `<div id="${id}" style="color: ${color}">Styled Node</div>`;
+ }
+
+ let tab = yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(html));
+
+ let {inspector, view} = yield openRuleView();
+
+ for (let color of colors) {
+ let cPicker = view.tooltips.colorPicker;
+ let selector = "#" + color.id;
+ yield selectNode(selector, inspector);
+
+ let swatch = getRuleViewProperty(view, "element", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+ let onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ yield simulateColorPickerChange(view, cPicker, [0, 255, 0, 1], {
+ selector,
+ name: "color",
+ value: "rgb(0, 255, 0)"
+ });
+
+ let spectrum = cPicker.spectrum;
+ let onHidden = cPicker.tooltip.once("hidden");
+ // Validating the color change ends up updating the rule view twice
+ let onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2);
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+ yield onHidden;
+ yield onRuleViewChanged;
+
+ is(getRuleViewPropertyValue(view, "element", "color"), color.result,
+ "changing the color preserved the unit for " + color.name);
+ }
+
+ let target = TargetFactory.forTab(tab);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_authored_override.js b/devtools/client/inspector/rules/test/browser_rules_authored_override.js
new file mode 100644
index 000000000..7305e5712
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_authored_override.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 for as-authored styles.
+
+function* createTestContent(style) {
+ let html = `<style type="text/css">
+ ${style}
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>`;
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(html));
+
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ return view;
+}
+
+add_task(function* () {
+ let gradientText1 = "(orange, blue);";
+ let gradientText2 = "(pink, teal);";
+
+ let view =
+ yield createTestContent("#testid {" +
+ " background-image: linear-gradient" +
+ gradientText1 +
+ " background-image: -ms-linear-gradient" +
+ gradientText2 +
+ " background-image: linear-gradient" +
+ gradientText2 +
+ "} ");
+
+ let elementStyle = view._elementStyle;
+ let rule = elementStyle.rules[1];
+
+ // Initially the last property should be active.
+ for (let i = 0; i < 3; ++i) {
+ let prop = rule.textProps[i];
+ is(prop.name, "background-image", "check the property name");
+ is(prop.overridden, i !== 2, "check overridden for " + i);
+ }
+
+ yield togglePropStatus(view, rule.textProps[2]);
+
+ // Now the first property should be active.
+ for (let i = 0; i < 3; ++i) {
+ let prop = rule.textProps[i];
+ is(prop.overridden || !prop.enabled, i !== 0,
+ "post-change check overridden for " + i);
+ }
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js b/devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js
new file mode 100644
index 000000000..adc8eb2ee
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js
@@ -0,0 +1,20 @@
+/* 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 for stylesheet generated
+// with createObjectURL(cssBlob)
+const TEST_URL = URL_ROOT + "doc_blob_stylesheet.html";
+
+add_task(function* () {
+ yield addTab(TEST_URL);
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("h1", inspector);
+ is(view.element.querySelectorAll("#noResults").length, 0,
+ "The no-results element is not displayed");
+
+ is(view.element.querySelectorAll(".ruleview-rule").length, 2,
+ "There are 2 displayed rules");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorUnit.js b/devtools/client/inspector/rules/test/browser_rules_colorUnit.js
new file mode 100644
index 000000000..138f68365
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorUnit.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";
+
+// Test that color selection respects the user pref.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ color: blue;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ let TESTS = [
+ {name: "hex", result: "#0f0"},
+ {name: "rgb", result: "rgb(0, 255, 0)"}
+ ];
+
+ for (let {name, result} of TESTS) {
+ info("starting test for " + name);
+ Services.prefs.setCharPref("devtools.defaultColorUnit", name);
+
+ let tab = yield addTab("data:text/html;charset=utf-8," +
+ encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("#testid", inspector);
+ yield basicTest(view, name, result);
+
+ let target = TargetFactory.forTab(tab);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+ }
+});
+
+function* basicTest(view, name, result) {
+ let cPicker = view.tooltips.colorPicker;
+ let swatch = getRuleViewProperty(view, "#testid", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+ let onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ yield simulateColorPickerChange(view, cPicker, [0, 255, 0, 1], {
+ selector: "#testid",
+ name: "color",
+ value: "rgb(0, 255, 0)"
+ });
+
+ let spectrum = cPicker.spectrum;
+ let onHidden = cPicker.tooltip.once("hidden");
+ // Validating the color change ends up updating the rule view twice
+ let onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2);
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+ yield onHidden;
+ yield onRuleViewChanged;
+
+ is(getRuleViewPropertyValue(view, "#testid", "color"), result,
+ "changing the color used the " + name + " unit");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_01.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_01.js
new file mode 100644
index 000000000..a8d2fd5f1
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_01.js
@@ -0,0 +1,63 @@
+/* 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 after a color change, the image preview tooltip in the same
+// property is displayed and positioned correctly.
+// See bug 979292
+
+const TEST_URI = `
+ <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!
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+ let value = getRuleViewProperty(view, "body", "background").valueSpan;
+ let swatch = value.querySelectorAll(".ruleview-colorswatch")[0];
+ 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 onColorPickerReady = picker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ yield simulateColorPickerChange(ruleView, picker, [0, 0, 0, 1], {
+ selector: "body",
+ name: "background-image",
+ value: 'url("chrome://global/skin/icons/warning-64.png"), linear-gradient(rgb(0, 0, 0), rgb(255, 0, 102) 400px)'
+ });
+
+ let spectrum = picker.spectrum;
+ let onHidden = picker.tooltip.once("hidden");
+ let onModifications = ruleView.once("ruleview-changed");
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+ yield onHidden;
+ yield onModifications;
+
+ 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/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.js
new file mode 100644
index 000000000..743ad5180
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.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";
+
+// Tests that after a color change, opening another tooltip, like the image
+// preview doesn't revert the color change in the rule view.
+// This used to happen when the activeSwatch wasn't reset when the colorpicker
+// would hide.
+// See bug 979292
+
+const TEST_URI = `
+ <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!
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {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 onColorPickerReady = picker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ yield simulateColorPickerChange(ruleView, picker, [0, 0, 0, 1], {
+ selector: "body",
+ name: "background-color",
+ value: "rgb(0, 0, 0)"
+ });
+
+ let spectrum = picker.spectrum;
+
+ let onModifications = waitForNEvents(ruleView, "ruleview-changed", 2);
+ let onHidden = picker.tooltip.once("hidden");
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+ yield onHidden;
+ yield onModifications;
+
+ info("Open the image preview tooltip");
+ let value = getRuleViewProperty(ruleView, "body", "background").valueSpan;
+ let url = value.querySelector(".theme-link");
+ let 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, "black",
+ "The swatch's color is correct");
+ is(swatch.nextSibling.textContent, "black", "The color name is correct");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click.js
new file mode 100644
index 000000000..383ffed6c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click.js
@@ -0,0 +1,51 @@
+/* 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 color pickers appear when clicking on color swatches.
+
+const TEST_URI = `
+ <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!
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+
+ let propertiesToTest = ["color", "background-color", "border"];
+
+ for (let property of propertiesToTest) {
+ info("Testing that the colorpicker appears on swatch click");
+ let value = getRuleViewProperty(view, "body", property).valueSpan;
+ let swatch = value.querySelector(".ruleview-colorswatch");
+ 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 onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ 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");
+
+ yield hideTooltipAndWaitForRuleViewChanged(cPicker, view);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-commit-on-ENTER.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-commit-on-ENTER.js
new file mode 100644
index 000000000..129e8f245
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-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";
+
+// Tests that a color change in the color picker is committed when ENTER is
+// pressed.
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ border: 2em solid rgba(120, 120, 120, .5);
+ }
+ </style>
+ Testing the color picker tooltip!
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {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 onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ yield simulateColorPickerChange(ruleView, cPicker, [0, 255, 0, .5], {
+ selector: "body",
+ name: "border-left-color",
+ 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 onModified = ruleView.once("ruleview-changed");
+ let spectrum = cPicker.spectrum;
+ let onHidden = cPicker.tooltip.once("hidden");
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+ yield onHidden;
+ yield onModified;
+
+ is((yield getComputedStyleProperty("body", null, "border-left-color")),
+ "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/devtools/client/inspector/rules/test/browser_rules_colorpicker-edit-gradient.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-edit-gradient.js
new file mode 100644
index 000000000..71ceb14c3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-edit-gradient.js
@@ -0,0 +1,77 @@
+/* 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 changing a color in a gradient css declaration using the tooltip
+// color picker works.
+
+const TEST_URI = `
+ <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
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {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("Get the color picker tooltip and clicking on the swatch to show it");
+ let cPicker = view.tooltips.colorPicker;
+ let onColorPickerReady = cPicker.once("ready");
+ swatchEl.click();
+ yield onColorPickerReady;
+
+ let change = {
+ selector: "body",
+ name: "background-image",
+ value: "linear-gradient(to left, rgb(1, 1, 1) 25%, " +
+ "rgb(51, 51, 51) 95%, rgb(0, 0, 0) 100%)"
+ };
+ yield simulateColorPickerChange(view, cPicker, [1, 1, 1, 1], change);
+
+ 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((yield getComputedStyleProperty("body", null, "background-image")),
+ "linear-gradient(to left, rgb(1, 1, 1) 25%, rgb(51, 51, 51) 95%, " +
+ "rgb(0, 0, 0) 100%)",
+ "The gradient has been updated correctly");
+
+ yield hideTooltipAndWaitForRuleViewChanged(cPicker, view);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-on-tooltip.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-on-tooltip.js
new file mode 100644
index 000000000..b50c63605
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-on-tooltip.js
@@ -0,0 +1,46 @@
+/* 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 color picker tooltip hides when an image tooltip appears.
+
+const TEST_URI = `
+ <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!
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+
+ let swatch = getRuleViewProperty(view, "body", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ let bgImageSpan = getRuleViewProperty(view, "body", "background-image").valueSpan;
+ let uriSpan = bgImageSpan.querySelector(".theme-link");
+
+ let colorPicker = view.tooltips.colorPicker;
+ info("Showing the color picker tooltip by clicking on the color swatch");
+ let onColorPickerReady = colorPicker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ info("Now showing the image preview tooltip to hide the color picker");
+ let onHidden = colorPicker.tooltip.once("hidden");
+ // Hiding the color picker refreshes the value.
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
+ yield onHidden;
+ yield onRuleViewChanged;
+
+ ok(true, "The color picker closed when the image preview tooltip appeared");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-multiple-changes.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-multiple-changes.js
new file mode 100644
index 000000000..06fab72d6
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-multiple-changes.js
@@ -0,0 +1,124 @@
+/* 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 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 TEST_URI = `
+ <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>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {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 onColorPickerReady = picker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ 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(ruleView, picker, rgba, {
+ selector: "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 onColorPickerReady = picker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ 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(ruleView, picker, rgba, {
+ selector: "body",
+ name: "background-color",
+ value: computed
+ });
+ }
+
+ info("Closing the color picker");
+ yield hideTooltipAndWaitForRuleViewChanged(picker, ruleView);
+}
+
+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 onColorPickerReady = picker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ 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(ruleView, picker, rgba, {
+ selector: "body",
+ name: "color",
+ value: computed
+ });
+ is((yield getComputedStyleProperty("p", null, "color")),
+ "rgb(200, 200, 200)", "The color of the P tag is still correct");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-release-outside-frame.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-release-outside-frame.js
new file mode 100644
index 000000000..ef6ca02b1
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-release-outside-frame.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";
+
+// Tests that color pickers stops following the pointer if the pointer is
+// released outside the tooltip frame (bug 1160720).
+
+const TEST_URI = "<body style='color: red'>Test page for bug 1160720";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+
+ let cSwatch = getRuleViewProperty(view, "element", "color").valueSpan
+ .querySelector(".ruleview-colorswatch");
+
+ let picker = yield openColorPickerForSwatch(cSwatch, view);
+ let spectrum = picker.spectrum;
+ let change = spectrum.once("changed");
+
+ info("Pressing mouse down over color picker.");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeMouseAtCenter(spectrum.dragger, {
+ type: "mousedown",
+ }, spectrum.dragger.ownerDocument.defaultView);
+ yield onRuleViewChanged;
+
+ let value = yield change;
+ info(`Color changed to ${value} on mousedown.`);
+
+ // If the mousemove below fails to detect that the button is no longer pressed
+ // the spectrum will update and emit changed event synchronously after calling
+ // synthesizeMouse so this handler is executed before the test ends.
+ spectrum.once("changed", (event, newValue) => {
+ is(newValue, value, "Value changed on mousemove without a button pressed.");
+ });
+
+ // Releasing the button pressed by mousedown above on top of a different frame
+ // does not make sense in this test as EventUtils doesn't preserve the context
+ // i.e. the buttons that were pressed down between events.
+
+ info("Moving mouse over color picker without any buttons pressed.");
+
+ EventUtils.synthesizeMouse(spectrum.dragger, 10, 10, {
+ // -1 = no buttons are pressed down
+ button: -1,
+ type: "mousemove",
+ }, spectrum.dragger.ownerDocument.defaultView);
+});
+
+function* openColorPickerForSwatch(swatch, view) {
+ 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 onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ ok(true, "The color picker was shown on click of the color swatch");
+
+ return cPicker;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-revert-on-ESC.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-revert-on-ESC.js
new file mode 100644
index 000000000..e244d429c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-revert-on-ESC.js
@@ -0,0 +1,109 @@
+/* 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 a color change in the color picker is reverted when ESC is
+// pressed.
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ background-color: #EDEDED;
+ }
+ </style>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+ yield testPressingEscapeRevertsChanges(view);
+ yield testPressingEscapeRevertsChangesAndDisables(view);
+});
+
+function* testPressingEscapeRevertsChanges(view) {
+ let {swatch, propEditor, cPicker} = yield openColorPickerAndSelectColor(view,
+ 1, 0, [0, 0, 0, 1], {
+ selector: "body",
+ name: "background-color",
+ value: "rgb(0, 0, 0)"
+ });
+
+ is(swatch.style.backgroundColor, "rgb(0, 0, 0)",
+ "The color swatch's background was updated");
+ is(propEditor.valueSpan.textContent, "#000",
+ "The text of the background-color css property was updated");
+
+ let spectrum = cPicker.spectrum;
+
+ info("Pressing ESCAPE to close the tooltip");
+ let onHidden = cPicker.tooltip.once("hidden");
+ let onModifications = view.once("ruleview-changed");
+ EventUtils.sendKey("ESCAPE", spectrum.element.ownerDocument.defaultView);
+ yield onHidden;
+ yield onModifications;
+
+ yield waitForComputedStyleProperty("body", null, "background-color",
+ "rgb(237, 237, 237)");
+ is(propEditor.valueSpan.textContent, "#EDEDED",
+ "Got expected property value.");
+}
+
+function* testPressingEscapeRevertsChangesAndDisables(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Disabling background-color property");
+ let textProp = ruleEditor.rule.textProps[0];
+ yield togglePropStatus(view, textProp);
+
+ ok(textProp.editor.element.classList.contains("ruleview-overridden"),
+ "property is overridden.");
+ is(textProp.editor.enable.style.visibility, "visible",
+ "property enable checkbox is visible.");
+ ok(!textProp.editor.enable.getAttribute("checked"),
+ "property enable checkbox is not checked.");
+ ok(!textProp.editor.prop.enabled,
+ "background-color property is disabled.");
+ let newValue = yield getRulePropertyValue("background-color");
+ is(newValue, "", "background-color should have been unset.");
+
+ let {cPicker} = yield openColorPickerAndSelectColor(view,
+ 1, 0, [0, 0, 0, 1]);
+
+ ok(!textProp.editor.element.classList.contains("ruleview-overridden"),
+ "property overridden is not displayed.");
+ is(textProp.editor.enable.style.visibility, "hidden",
+ "property enable checkbox is hidden.");
+
+ let spectrum = cPicker.spectrum;
+
+ info("Pressing ESCAPE to close the tooltip");
+ let onHidden = cPicker.tooltip.once("hidden");
+ let onModifications = view.once("ruleview-changed");
+ EventUtils.sendKey("ESCAPE", spectrum.element.ownerDocument.defaultView);
+ yield onHidden;
+ yield onModifications;
+
+ ok(textProp.editor.element.classList.contains("ruleview-overridden"),
+ "property is overridden.");
+ is(textProp.editor.enable.style.visibility, "visible",
+ "property enable checkbox is visible.");
+ ok(!textProp.editor.enable.getAttribute("checked"),
+ "property enable checkbox is not checked.");
+ ok(!textProp.editor.prop.enabled,
+ "background-color property is disabled.");
+ newValue = yield getRulePropertyValue("background-color");
+ is(newValue, "", "background-color should have been unset.");
+ is(textProp.editor.valueSpan.textContent, "#EDEDED",
+ "Got expected property value.");
+}
+
+function* getRulePropertyValue(name) {
+ let propValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: name
+ });
+ return propValue;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-swatch-displayed.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-swatch-displayed.js
new file mode 100644
index 000000000..b06ff37df
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-swatch-displayed.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 color swatches are displayed next to colors in the rule-view.
+
+const TEST_URI = `
+ <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;
+ background: linear-gradient(
+ to right,
+ #f00,
+ #f008,
+ #00ff00,
+ #00ff0080,
+ rgb(31,170,217),
+ rgba(31,170,217,.5),
+ hsl(5, 5%, 5%),
+ hsla(5, 5%, 5%, 0.25),
+ #F00,
+ #F008,
+ #00FF00,
+ #00FF0080,
+ RGB(31,170,217),
+ RGBA(31,170,217,.5),
+ HSL(5, 5%, 5%),
+ HSLA(5, 5%, 5%, 0.25));
+ box-shadow: inset 0 0 2px 20px red, inset 0 0 2px 40px blue;
+ }
+ </style>
+ Testing the color picker tooltip!
+`;
+
+// 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: "background", nb: 16},
+ {selector: "*", propertyName: "box-shadow", nb: 2},
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {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/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js
new file mode 100644
index 000000000..566bae259
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_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";
+
+// Tests that CSS property names are autocompleted and cycled correctly when
+// editing an existing property in the rule view.
+
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// is the popup open,
+// is a suggestion selected in the popup,
+// ]
+
+const OPEN = true, SELECTED = true;
+var testData = [
+ ["VK_RIGHT", "font", !OPEN, !SELECTED],
+ ["-", "font-size", OPEN, SELECTED],
+ ["f", "font-family", OPEN, SELECTED],
+ ["VK_BACK_SPACE", "font-f", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "font-", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "font", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "fon", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "fo", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "f", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "", !OPEN, !SELECTED],
+ ["d", "display", OPEN, SELECTED],
+ ["VK_DOWN", "dominant-baseline", OPEN, SELECTED],
+ ["VK_DOWN", "direction", OPEN, SELECTED],
+ ["VK_DOWN", "display", OPEN, SELECTED],
+ ["VK_UP", "direction", OPEN, SELECTED],
+ ["VK_UP", "dominant-baseline", OPEN, SELECTED],
+ ["VK_UP", "display", OPEN, SELECTED],
+ ["VK_BACK_SPACE", "d", !OPEN, !SELECTED],
+ ["i", "display", OPEN, SELECTED],
+ ["s", "display", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "dis", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "di", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "d", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "", !OPEN, !SELECTED],
+ ["VK_HOME", "", !OPEN, !SELECTED],
+ ["VK_END", "", !OPEN, !SELECTED],
+ ["VK_PAGE_UP", "", !OPEN, !SELECTED],
+ ["VK_PAGE_DOWN", "", !OPEN, !SELECTED],
+ ["d", "display", OPEN, SELECTED],
+ ["VK_HOME", "display", !OPEN, !SELECTED],
+ ["VK_END", "display", !OPEN, !SELECTED],
+ // Press right key to ensure caret move to end of the input on Mac OS since
+ // Mac OS doesn't move caret after pressing HOME / END.
+ ["VK_RIGHT", "display", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "displa", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "displ", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "disp", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "dis", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "di", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "d", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "", !OPEN, !SELECTED],
+ ["f", "font-size", OPEN, SELECTED],
+ ["i", "filter", OPEN, SELECTED],
+ ["VK_LEFT", "filter", !OPEN, !SELECTED],
+ ["VK_LEFT", "filter", !OPEN, !SELECTED],
+ ["i", "fiilter", !OPEN, !SELECTED],
+ ["VK_ESCAPE", null, !OPEN, !SELECTED],
+];
+
+const TEST_URI = "<h1 style='font: 24px serif'>Header</h1>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view, testActor} = yield openRuleView();
+
+ info("Test autocompletion after 1st page load");
+ yield runAutocompletionTest(toolbox, inspector, view);
+
+ info("Test autocompletion after page navigation");
+ yield reloadPage(inspector, testActor);
+ yield runAutocompletionTest(toolbox, inspector, view);
+});
+
+function* runAutocompletionTest(toolbox, inspector, view) {
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing the css property editable field");
+ let propertyName = view.styleDocument.querySelectorAll(".ruleview-propertyname")[0];
+ let editor = yield focusEditableField(view, 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, open, selected],
+ editor, view) {
+ info("Pressing key " + key);
+ info("Expecting " + completion);
+ info("Is popup opened: " + open);
+ info("Is item selected: " + selected);
+
+ // Listening for the right event that will tell us when the key has been
+ // entered and processed.
+ let onSuggest;
+ if (/(left|right|back_space|escape|home|end|page_up|page_down)/ig.test(key)) {
+ info("Adding event listener for " +
+ "left|right|back_space|escape|home|end|page_up|page_down keys");
+ onSuggest = once(editor.input, "keypress");
+ } else {
+ info("Waiting for after-suggest event on the editor");
+ onSuggest = editor.once("after-suggest");
+ }
+
+ // Also listening for popup opened/closed events if needed.
+ let popupEvent = open ? "popup-opened" : "popup-closed";
+ let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null;
+
+ info("Synthesizing key " + key);
+ EventUtils.synthesizeKey(key, {}, view.styleWindow);
+
+ // Flush the throttle for the preview text.
+ view.throttle.flush();
+
+ yield onSuggest;
+ yield onPopupEvent;
+
+ info("Checking the state");
+ if (completion !== null) {
+ is(editor.input.value, completion, "Correct value is autocompleted");
+ }
+ if (!open) {
+ ok(!(editor.popup && editor.popup.isOpen), "Popup is closed");
+ } else {
+ ok(editor.popup.isOpen, "Popup is open");
+ is(editor.popup.selectedIndex !== -1, selected, "An item is selected");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js
new file mode 100644
index 000000000..fde8f5d12
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_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";
+
+// Tests 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,
+// is the popup open,
+// is a suggestion selected in the popup,
+// expect ruleview-changed,
+// ]
+
+const OPEN = true, SELECTED = true, CHANGE = true;
+var testData = [
+ ["b", {}, "beige", OPEN, SELECTED, CHANGE],
+ ["l", {}, "black", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "blanchedalmond", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "blue", OPEN, SELECTED, CHANGE],
+ ["VK_RIGHT", {}, "blue", !OPEN, !SELECTED, !CHANGE],
+ [" ", {}, "blue aliceblue", OPEN, SELECTED, CHANGE],
+ ["!", {}, "blue !important", !OPEN, !SELECTED, CHANGE],
+ ["VK_BACK_SPACE", {}, "blue !", !OPEN, !SELECTED, CHANGE],
+ ["VK_BACK_SPACE", {}, "blue ", !OPEN, !SELECTED, CHANGE],
+ ["VK_BACK_SPACE", {}, "blue", !OPEN, !SELECTED, CHANGE],
+ ["VK_TAB", {shiftKey: true}, "color", !OPEN, !SELECTED, CHANGE],
+ ["VK_BACK_SPACE", {}, "", !OPEN, !SELECTED, !CHANGE],
+ ["d", {}, "display", OPEN, SELECTED, !CHANGE],
+ ["VK_TAB", {}, "blue", !OPEN, !SELECTED, CHANGE],
+ ["n", {}, "none", !OPEN, !SELECTED, CHANGE],
+ ["VK_RETURN", {}, null, !OPEN, !SELECTED, CHANGE]
+];
+
+const TEST_URI = "<h1 style='color: red'>Header</h1>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view, testActor} = yield openRuleView();
+
+ info("Test autocompletion after 1st page load");
+ yield runAutocompletionTest(toolbox, inspector, view);
+
+ info("Test autocompletion after page navigation");
+ yield reloadPage(inspector, testActor);
+ yield runAutocompletionTest(toolbox, inspector, view);
+});
+
+function* runAutocompletionTest(toolbox, inspector, view) {
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ let rule = getRuleViewRuleEditor(view, 0).rule;
+ let prop = rule.textProps[0];
+
+ info("Focusing the css property editable value");
+ let editor = yield focusEditableField(view, prop.editor.valueSpan);
+
+ 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.styleDocument.activeElement);
+ yield testCompletion(testData[i], editor, view);
+ }
+}
+
+function* testCompletion([key, modifiers, completion, open, selected, change],
+ editor, view) {
+ info("Pressing key " + key);
+ info("Expecting " + completion);
+ info("Is popup opened: " + open);
+ info("Is item selected: " + selected);
+
+ let onDone;
+ if (change) {
+ // If the key triggers a ruleview-changed, wait for that event, it will
+ // always be the last to be triggered and tells us when the preview has
+ // been done.
+ onDone = view.once("ruleview-changed");
+ } else {
+ // Otherwise, expect an after-suggest event (except if the popup gets
+ // closed).
+ onDone = key !== "VK_RIGHT" && key !== "VK_BACK_SPACE"
+ ? editor.once("after-suggest")
+ : null;
+ }
+
+ info("Synthesizing key " + key + ", modifiers: " + Object.keys(modifiers));
+
+ // Also listening for popup opened/closed events if needed.
+ let popupEvent = open ? "popup-opened" : "popup-closed";
+ let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null;
+
+ EventUtils.synthesizeKey(key, modifiers, view.styleWindow);
+
+ // Flush the throttle for the preview text.
+ view.throttle.flush();
+
+ yield onDone;
+ yield onPopupEvent;
+
+ // The key might have been a TAB or shift-TAB, in which case the editor will
+ // be a new one
+ editor = inplaceEditor(view.styleDocument.activeElement);
+
+ info("Checking the state");
+ if (completion !== null) {
+ is(editor.input.value, completion, "Correct value is autocompleted");
+ }
+
+ if (!open) {
+ ok(!(editor.popup && editor.popup.isOpen), "Popup is closed");
+ } else {
+ ok(editor.popup.isOpen, "Popup is open");
+ is(editor.popup.selectedIndex !== -1, selected, "An item is selected");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_01.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_01.js
new file mode 100644
index 000000000..86ff9ca03
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_01.js
@@ -0,0 +1,102 @@
+/* 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 CSS property names are autocompleted and cycled correctly when
+// creating a new property in the rule view.
+
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// is the popup open,
+// is a suggestion selected in the popup,
+// ]
+const OPEN = true, SELECTED = true;
+var testData = [
+ ["d", "display", OPEN, SELECTED],
+ ["VK_DOWN", "dominant-baseline", OPEN, SELECTED],
+ ["VK_DOWN", "direction", OPEN, SELECTED],
+ ["VK_DOWN", "display", OPEN, SELECTED],
+ ["VK_UP", "direction", OPEN, SELECTED],
+ ["VK_UP", "dominant-baseline", OPEN, SELECTED],
+ ["VK_UP", "display", OPEN, SELECTED],
+ ["VK_BACK_SPACE", "d", !OPEN, !SELECTED],
+ ["i", "display", OPEN, SELECTED],
+ ["s", "display", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "dis", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "di", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "d", !OPEN, !SELECTED],
+ ["VK_BACK_SPACE", "", !OPEN, !SELECTED],
+ ["f", "font-size", OPEN, SELECTED],
+ ["i", "filter", OPEN, SELECTED],
+ ["VK_ESCAPE", null, !OPEN, !SELECTED],
+];
+
+const TEST_URI = "<h1 style='border: 1px solid red'>Header</h1>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view, testActor} = yield openRuleView();
+
+ info("Test autocompletion after 1st page load");
+ yield runAutocompletionTest(toolbox, inspector, view);
+
+ info("Test autocompletion after page navigation");
+ yield reloadPage(inspector, testActor);
+ yield runAutocompletionTest(toolbox, inspector, view);
+});
+
+function* runAutocompletionTest(toolbox, inspector, view) {
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing the css property editable field");
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+
+ 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, open, isSelected], editor, view) {
+ info("Pressing key " + key);
+ info("Expecting " + completion);
+ info("Is popup opened: " + open);
+ info("Is item selected: " + isSelected);
+
+ 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");
+ }
+
+ // Also listening for popup opened/closed events if needed.
+ let popupEvent = open ? "popup-opened" : "popup-closed";
+ let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null;
+
+ info("Synthesizing key " + key);
+ EventUtils.synthesizeKey(key, {}, view.styleWindow);
+
+ yield onSuggest;
+ yield onPopupEvent;
+
+ info("Checking the state");
+ if (completion !== null) {
+ is(editor.input.value, completion, "Correct value is autocompleted");
+ }
+ if (!open) {
+ ok(!(editor.popup && editor.popup.isOpen), "Popup is closed");
+ } else {
+ ok(editor.popup.isOpen, "Popup is open");
+ is(editor.popup.selectedIndex !== -1, isSelected, "An item is selected");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js
new file mode 100644
index 000000000..d89e5129d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.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 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,
+// is the popup open,
+// is a suggestion selected in the popup,
+// expect ruleview-changed,
+// ]
+
+const OPEN = true, SELECTED = true, CHANGE = true;
+const testData = [
+ ["d", {}, "display", OPEN, SELECTED, !CHANGE],
+ ["VK_TAB", {}, "", OPEN, !SELECTED, CHANGE],
+ ["VK_DOWN", {}, "block", OPEN, SELECTED, CHANGE],
+ ["n", {}, "none", !OPEN, !SELECTED, CHANGE],
+ ["VK_TAB", {shiftKey: true}, "display", !OPEN, !SELECTED, CHANGE],
+ ["VK_BACK_SPACE", {}, "", !OPEN, !SELECTED, !CHANGE],
+ ["o", {}, "overflow", OPEN, SELECTED, !CHANGE],
+ ["u", {}, "outline", OPEN, SELECTED, !CHANGE],
+ ["VK_DOWN", {}, "outline-color", OPEN, SELECTED, !CHANGE],
+ ["VK_TAB", {}, "none", !OPEN, !SELECTED, CHANGE],
+ ["r", {}, "rebeccapurple", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "red", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "rgb", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "rgba", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "rosybrown", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "royalblue", OPEN, SELECTED, CHANGE],
+ ["VK_RIGHT", {}, "royalblue", !OPEN, !SELECTED, !CHANGE],
+ [" ", {}, "royalblue aliceblue", OPEN, SELECTED, CHANGE],
+ ["!", {}, "royalblue !important", !OPEN, !SELECTED, CHANGE],
+ ["VK_ESCAPE", {}, null, !OPEN, !SELECTED, CHANGE]
+];
+
+const TEST_URI = `
+ <style type="text/css">
+ h1 {
+ border: 1px solid red;
+ }
+ </style>
+ <h1>Test element</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view, testActor} = yield openRuleView();
+
+ info("Test autocompletion after 1st page load");
+ yield runAutocompletionTest(toolbox, inspector, view);
+
+ info("Test autocompletion after page navigation");
+ yield reloadPage(inspector, testActor);
+ yield runAutocompletionTest(toolbox, inspector, view);
+});
+
+function* runAutocompletionTest(toolbox, inspector, view) {
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing a new css property editable property");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+
+ 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.styleDocument.activeElement);
+ yield testCompletion(testData[i], editor, view);
+ }
+}
+
+function* testCompletion([key, modifiers, completion, open, selected, change],
+ editor, view) {
+ info("Pressing key " + key);
+ info("Expecting " + completion);
+ info("Is popup opened: " + open);
+ info("Is item selected: " + selected);
+
+ let onDone;
+ if (change) {
+ // If the key triggers a ruleview-changed, wait for that event, it will
+ // always be the last to be triggered and tells us when the preview has
+ // been done.
+ onDone = view.once("ruleview-changed");
+ } else {
+ // Otherwise, expect an after-suggest event (except if the popup gets
+ // closed).
+ onDone = key !== "VK_RIGHT" && key !== "VK_BACK_SPACE"
+ ? editor.once("after-suggest")
+ : null;
+ }
+
+ // Also listening for popup opened/closed events if needed.
+ let popupEvent = open ? "popup-opened" : "popup-closed";
+ let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null;
+
+ info("Synthesizing key " + key + ", modifiers: " + Object.keys(modifiers));
+ EventUtils.synthesizeKey(key, modifiers, view.styleWindow);
+
+ // Flush the throttle for the preview text.
+ view.throttle.flush();
+
+ yield onDone;
+ yield onPopupEvent;
+
+ 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.styleDocument.activeElement);
+ is(editor.input.value, completion, "Correct value is autocompleted");
+ }
+ if (!open) {
+ ok(!(editor.popup && editor.popup.isOpen), "Popup is closed");
+ } else {
+ ok(editor.popup.isOpen, "Popup is open");
+ is(editor.popup.selectedIndex !== -1, selected, "An item is selected");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_03.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_03.js
new file mode 100644
index 000000000..a5072429c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_03.js
@@ -0,0 +1,47 @@
+/* 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";
+
+// Regression test for a case where completing gave the wrong answer.
+// See bug 1179318.
+
+const TEST_URI = "<h1 style='color: red'>Header</h1>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ info("Test autocompletion for background-color");
+ yield runAutocompletionTest(toolbox, inspector, view);
+});
+
+function* runAutocompletionTest(toolbox, inspector, view) {
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing the new property editable field");
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+
+ info("Sending \"background\" to the editable field");
+ for (let key of "background") {
+ let onSuggest = editor.once("after-suggest");
+ EventUtils.synthesizeKey(key, {}, view.styleWindow);
+ yield onSuggest;
+ }
+
+ const itemIndex = 4;
+
+ let bgcItem = editor.popup.getItemAtIndex(itemIndex);
+ is(bgcItem.label, "background-color",
+ "check the expected completion element");
+
+ editor.popup.selectedIndex = itemIndex;
+
+ let node = editor.popup._list.childNodes[itemIndex];
+ EventUtils.synthesizeMouseAtCenter(node, {}, editor.popup._window);
+
+ is(editor.input.value, "background-color", "Correct value is autocompleted");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_04.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_04.js
new file mode 100644
index 000000000..e19794e1b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_04.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";
+
+// Test that a new property editor supports the following flow:
+// - type first character of property name
+// - select an autocomplete suggestion !!with a mouse click!!
+// - press RETURN to move to the property value
+// - blur the input to commit
+
+const TEST_URI = "<style>.title {color: red;}</style>" +
+ "<h1 class=title>Header</h1>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let { inspector, view} = yield openRuleView();
+
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing the new property editable field");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+
+ info("Sending \"background\" to the editable field.");
+ for (let key of "background") {
+ let onSuggest = editor.once("after-suggest");
+ EventUtils.synthesizeKey(key, {}, view.styleWindow);
+ yield onSuggest;
+ }
+
+ const itemIndex = 4;
+ let bgcItem = editor.popup.getItemAtIndex(itemIndex);
+ is(bgcItem.label, "background-color",
+ "Check the expected completion element is background-color.");
+ editor.popup.selectedIndex = itemIndex;
+
+ info("Select the background-color suggestion with a mouse click.");
+ let onSuggest = editor.once("after-suggest");
+ let node = editor.popup.elements.get(bgcItem);
+ EventUtils.synthesizeMouseAtCenter(node, {}, editor.popup._window);
+
+ yield onSuggest;
+ is(editor.input.value, "background-color", "Correct value is autocompleted");
+
+ info("Press RETURN to move the focus to a property value editor.");
+ let onModifications = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+
+ yield onModifications;
+
+ // Getting the new value editor after focus
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ let textProp = ruleEditor.rule.textProps[1];
+
+ is(ruleEditor.rule.textProps.length, 2,
+ "Created a new text property.");
+ is(ruleEditor.propertyList.children.length, 2,
+ "Created a property editor.");
+ is(editor, inplaceEditor(textProp.editor.valueSpan),
+ "Editing the value span now.");
+
+ info("Entering a value and blurring the field to expect a rule change");
+ editor.input.value = "#F00";
+
+ onModifications = view.once("ruleview-changed");
+ editor.input.blur();
+ yield onModifications;
+
+ is(textProp.value, "#F00", "Text prop should have been changed.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js
new file mode 100644
index 000000000..ec939eafc
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js
@@ -0,0 +1,131 @@
+/* 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 behaviour of the CSS autocomplete for CSS value displayed on
+// multiple lines. Expected behavior is:
+// - UP/DOWN should navigate in the input and not increment/decrement numbers
+// - typing a new value should still trigger the autocomplete
+// - UP/DOWN when the autocomplete popup is displayed should cycle through
+// suggestions
+
+const LONG_CSS_VALUE =
+ "transparent linear-gradient(0deg, blue 0%, white 5%, red 10%, blue 15%, " +
+ "white 20%, red 25%, blue 30%, white 35%, red 40%, blue 45%, white 50%, " +
+ "red 55%, blue 60%, white 65%, red 70%, blue 75%, white 80%, red 85%, " +
+ "blue 90%, white 95% ) repeat scroll 0% 0%";
+
+const EXPECTED_CSS_VALUE = LONG_CSS_VALUE.replace("95%", "95%, red");
+
+const TEST_URI =
+ `<style>
+ .title {
+ background: ${LONG_CSS_VALUE};
+ }
+ </style>
+ <h1 class=title>Header</h1>`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let { inspector, view} = yield openRuleView();
+
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing the property editable field");
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[0];
+
+ // Calculate offsets to click in the middle of the first box quad.
+ let rect = prop.editor.valueSpan.getBoundingClientRect();
+ let firstQuad = prop.editor.valueSpan.getBoxQuads()[0];
+ // For a multiline value, the first quad left edge is not aligned with the
+ // bounding rect left edge. The offsets expected by focusEditableField are
+ // relative to the bouding rectangle, so we need to translate the x-offset.
+ let x = firstQuad.bounds.left - rect.left + firstQuad.bounds.width / 2;
+ // The first quad top edge is aligned with the bounding top edge, no
+ // translation needed here.
+ let y = firstQuad.bounds.height / 2;
+
+ info("Focusing the css property editable value");
+ let editor = yield focusEditableField(view, prop.editor.valueSpan, x, y);
+
+ info("Moving the caret next to a number");
+ let pos = editor.input.value.indexOf("0deg") + 1;
+ editor.input.setSelectionRange(pos, pos);
+ is(editor.input.value[editor.input.selectionStart - 1], "0",
+ "Input caret is after a 0");
+
+ info("Check that UP/DOWN navigates in the input, even when next to a number");
+ EventUtils.synthesizeKey("VK_DOWN", {}, view.styleWindow);
+ ok(editor.input.selectionStart !== pos, "Input caret moved");
+ is(editor.input.value, LONG_CSS_VALUE, "Input value was not decremented.");
+
+ info("Move the caret to the end of the gradient definition.");
+ pos = editor.input.value.indexOf("95%") + 3;
+ editor.input.setSelectionRange(pos, pos);
+
+ info("Sending \", re\" to the editable field.");
+ for (let key of ", re") {
+ yield synthesizeKeyForAutocomplete(key, editor, view.styleWindow);
+ }
+
+ info("Check the autocomplete can still be displayed.");
+ ok(editor.popup && editor.popup.isOpen, "Autocomplete popup is displayed.");
+ is(editor.popup.selectedIndex, 0,
+ "Autocomplete has an item selected by default");
+
+ let item = editor.popup.getItemAtIndex(editor.popup.selectedIndex);
+ is(item.label, "rebeccapurple",
+ "Check autocomplete displays expected value.");
+
+ info("Check autocomplete suggestions can be cycled using UP/DOWN arrows.");
+
+ yield synthesizeKeyForAutocomplete("VK_DOWN", editor, view.styleWindow);
+ ok(editor.popup.selectedIndex, 1, "Using DOWN cycles autocomplete values.");
+ yield synthesizeKeyForAutocomplete("VK_DOWN", editor, view.styleWindow);
+ ok(editor.popup.selectedIndex, 2, "Using DOWN cycles autocomplete values.");
+ yield synthesizeKeyForAutocomplete("VK_UP", editor, view.styleWindow);
+ is(editor.popup.selectedIndex, 1, "Using UP cycles autocomplete values.");
+ item = editor.popup.getItemAtIndex(editor.popup.selectedIndex);
+ is(item.label, "red", "Check autocomplete displays expected value.");
+
+ info("Select the background-color suggestion with a mouse click.");
+ let onRuleviewChanged = view.once("ruleview-changed");
+ let onSuggest = editor.once("after-suggest");
+
+ let node = editor.popup._list.childNodes[editor.popup.selectedIndex];
+ EventUtils.synthesizeMouseAtCenter(node, {}, editor.popup._window);
+
+ view.throttle.flush();
+ yield onSuggest;
+ yield onRuleviewChanged;
+
+ is(editor.input.value, EXPECTED_CSS_VALUE,
+ "Input value correctly autocompleted");
+
+ info("Press ESCAPE to leave the input.");
+ onRuleviewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ yield onRuleviewChanged;
+});
+
+/**
+ * Send the provided key to the currently focused input of the provided window.
+ * Wait for the editor to emit "after-suggest" to make sure the autocompletion
+ * process is finished.
+ *
+ * @param {String} key
+ * The key to send to the input.
+ * @param {InplaceEditor} editor
+ * The inplace editor which owns the focused input.
+ * @param {Window} win
+ * Window in which the key event will be dispatched.
+ */
+function* synthesizeKeyForAutocomplete(key, editor, win) {
+ let onSuggest = editor.once("after-suggest");
+ EventUtils.synthesizeKey(key, {}, win);
+ yield onSuggest;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-popup-hidden-after-navigation.js b/devtools/client/inspector/rules/test/browser_rules_completion-popup-hidden-after-navigation.js
new file mode 100644
index 000000000..84f119606
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-popup-hidden-after-navigation.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 the ruleview autocomplete popup is hidden after page navigation.
+
+const TEST_URI = "<h1 style='font: 24px serif'></h1>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = yield openRuleView();
+
+ info("Test autocompletion popup is hidden after page navigation");
+
+ info("Selecting the test node");
+ yield selectNode("h1", inspector);
+
+ info("Focusing the css property editable field");
+ let propertyName = view.styleDocument
+ .querySelectorAll(".ruleview-propertyname")[0];
+ let editor = yield focusEditableField(view, propertyName);
+
+ info("Pressing key VK_DOWN");
+ let onSuggest = once(editor.input, "keypress");
+ let onPopupOpened = once(editor.popup, "popup-opened");
+
+ EventUtils.synthesizeKey("VK_DOWN", {}, view.styleWindow);
+
+ info("Waiting for autocomplete popup to be displayed");
+ yield onSuggest;
+ yield onPopupOpened;
+
+ ok(view.popup && view.popup.isOpen, "Popup should be opened");
+
+ info("Reloading the page");
+ yield reloadPage(inspector, testActor);
+
+ ok(!(view.popup && view.popup.isOpen), "Popup should be closed");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_computed-lists_01.js b/devtools/client/inspector/rules/test/browser_rules_computed-lists_01.js
new file mode 100644
index 000000000..5acebd562
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_computed-lists_01.js
@@ -0,0 +1,47 @@
+/* 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 shows expanders for properties with computed lists.
+
+var TEST_URI = `
+ <style type="text/css">
+ #testid {
+ margin: 4px;
+ top: 0px;
+ }
+ </style>
+ <h1 id="testid">Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testExpandersShown(inspector, view);
+});
+
+function* testExpandersShown(inspector, view) {
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+
+ info("Check that the correct rules are visible");
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ is(rule.textProps[0].name, "margin", "First property is margin.");
+ is(rule.textProps[1].name, "top", "Second property is top.");
+
+ info("Check that the expanders are shown correctly");
+ is(rule.textProps[0].editor.expander.style.visibility, "visible",
+ "margin expander is visible.");
+ is(rule.textProps[1].editor.expander.style.visibility, "hidden",
+ "top expander is hidden.");
+ ok(!rule.textProps[0].editor.expander.hasAttribute("open"),
+ "margin computed list is closed.");
+ ok(!rule.textProps[1].editor.expander.hasAttribute("open"),
+ "top computed list is closed.");
+ ok(!rule.textProps[0].editor.computed.hasChildNodes(),
+ "margin computed list is empty before opening.");
+ ok(!rule.textProps[1].editor.computed.hasChildNodes(),
+ "top computed list is empty.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_computed-lists_02.js b/devtools/client/inspector/rules/test/browser_rules_computed-lists_02.js
new file mode 100644
index 000000000..d6dc82d5f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_computed-lists_02.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";
+
+// Tests that the rule view computed lists can be expanded/collapsed,
+// and contain the right subproperties.
+
+var TEST_URI = `
+ <style type="text/css">
+ #testid {
+ margin: 0px 1px 2px 3px;
+ top: 0px;
+ }
+ </style>
+ <h1 id="testid">Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testComputedList(inspector, view);
+});
+
+function* testComputedList(inspector, view) {
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let propEditor = rule.textProps[0].editor;
+ let expander = propEditor.expander;
+
+ ok(!expander.hasAttribute("open"), "margin computed list is closed");
+
+ info("Opening the computed list of margin property");
+ expander.click();
+ ok(expander.hasAttribute("open"), "margin computed list is open");
+
+ let computed = propEditor.prop.computed;
+ let computedDom = propEditor.computed;
+ let propNames = [
+ "margin-top",
+ "margin-right",
+ "margin-bottom",
+ "margin-left"
+ ];
+
+ is(computed.length, propNames.length, "There should be 4 computed values");
+ is(computedDom.children.length, propNames.length,
+ "There should be 4 nodes in the DOM");
+
+ propNames.forEach((propName, i) => {
+ let propValue = i + "px";
+ is(computed[i].name, propName,
+ "Computed property #" + i + " has name " + propName);
+ is(computed[i].value, propValue,
+ "Computed property #" + i + " has value " + propValue);
+ is(computedDom.querySelectorAll(".ruleview-propertyname")[i].textContent,
+ propName,
+ "Computed property #" + i + " in DOM has correct name");
+ is(computedDom.querySelectorAll(".ruleview-propertyvalue")[i].textContent,
+ propValue,
+ "Computed property #" + i + " in DOM has correct value");
+ });
+
+ info("Closing the computed list of margin property");
+ expander.click();
+ ok(!expander.hasAttribute("open"), "margin computed list is closed");
+
+ info("Opening the computed list of margin property");
+ expander.click();
+ ok(expander.hasAttribute("open"), "margin computed list is open");
+ is(computed.length, propNames.length, "Still 4 computed values");
+ is(computedDom.children.length, propNames.length, "Still 4 nodes in the DOM");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_content_01.js b/devtools/client/inspector/rules/test/browser_rules_content_01.js
new file mode 100644
index 000000000..8695d9b8d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_content_01.js
@@ -0,0 +1,51 @@
+/* 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
+
+const TEST_URI = `
+ <style type="text/css">
+ @media screen and (min-width: 10px) {
+ #testid {
+ background-color: blue;
+ }
+ }
+ .testclass, .unmatched {
+ background-color: green;
+ }
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>
+ <div id="testid2">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("#testid", inspector);
+ is(view.element.querySelectorAll("#ruleview-no-results").length, 0,
+ "After a highlight, no longer has a no-results element.");
+
+ yield clearCurrentNodeSelection(inspector);
+ is(view.element.querySelectorAll("#ruleview-no-results").length, 1,
+ "After highlighting null, has a no-results element again.");
+
+ yield selectNode("#testid", inspector);
+
+ let linkText = getRuleViewLinkTextByIndex(view, 1);
+ is(linkText, "inline:3 @screen and (min-width: 10px)",
+ "link text at index 1 contains media query text.");
+
+ linkText = getRuleViewLinkTextByIndex(view, 2);
+ is(linkText, "inline:7",
+ "link text at index 2 contains no media query text.");
+
+ let selector = getRuleViewRuleEditor(view, 2).selectorText;
+ is(selector.querySelector(".ruleview-selector-matched").textContent,
+ ".testclass", ".textclass should be matched.");
+ is(selector.querySelector(".ruleview-selector-unmatched").textContent,
+ ".unmatched", ".unmatched should not be matched.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_content_02.js b/devtools/client/inspector/rules/test/browser_rules_content_02.js
new file mode 100644
index 000000000..253f374b4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_content_02.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/ */
+/* globals getTestActorWithoutToolbox */
+"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>
+`;
+
+add_task(function* () {
+ let tab = yield addTab("data:text/html;charset=utf-8," + CONTENT);
+
+ let testActor = yield getTestActorWithoutToolbox(tab);
+ let inspector = yield clickOnInspectMenuItem(testActor, "span");
+
+ checkRuleViewContent(inspector.ruleview.view);
+});
+
+function checkRuleViewContent({styleDocument}) {
+ info("Making sure the rule-view contains the expected content");
+
+ let headers = [...styleDocument.querySelectorAll(".ruleview-header")];
+ is(headers.length, 3, "There are 3 headers for inherited rules");
+
+ is(headers[0].textContent,
+ STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", "p"),
+ "The first header is correct");
+ is(headers[1].textContent,
+ STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", "div"),
+ "The second header is correct");
+ is(headers[2].textContent,
+ STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", "body"),
+ "The third header is correct");
+
+ let rules = styleDocument.querySelectorAll(".ruleview-rule");
+ is(rules.length, 4, "There are 4 rules in the view");
+
+ for (let rule of rules) {
+ let selector = rule.querySelector(".ruleview-selectorcontainer");
+ is(selector.textContent, STYLE_INSPECTOR_L10N.getStr("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/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-01.js b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-01.js
new file mode 100644
index 000000000..b81bb8013
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-01.js
@@ -0,0 +1,96 @@
+/* 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/ */
+
+/**
+ * This file tests the code that integrates the Style Inspector's rule view
+ * with the MDN docs tooltip.
+ *
+ * If you display the context click on a property name in the rule view, you
+ * should see a menu item "Show MDN Docs". If you click that item, the MDN
+ * docs tooltip should be shown, containing docs from MDN for that property.
+ *
+ * This file tests that the context menu item is shown when it should be
+ * shown and hidden when it should be hidden.
+ */
+
+"use strict";
+
+/**
+ * The test document tries to confuse the context menu
+ * code by having a tag called "padding" and a property
+ * value called "margin".
+ */
+const TEST_URI = `
+ <html>
+ <head>
+ <style>
+ padding {font-family: margin;}
+ </style>
+ </head>
+
+ <body>
+ <padding>MDN tooltip testing</padding>
+ </body>
+ </html>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("padding", inspector);
+ yield testMdnContextMenuItemVisibility(view);
+});
+
+/**
+ * Tests that the MDN context menu item is shown when it should be,
+ * and hidden when it should be.
+ * - iterate through every node in the rule view
+ * - set that node as popupNode (the node that the context menu
+ * is shown for)
+ * - update the context menu's state
+ * - test that the MDN context menu item is hidden, or not,
+ * depending on popupNode
+ */
+function* testMdnContextMenuItemVisibility(view) {
+ info("Test that MDN context menu item is shown only when it should be.");
+
+ let root = rootElement(view);
+ for (let node of iterateNodes(root)) {
+ info("Setting " + node + " as popupNode");
+ info("Creating context menu with " + node + " as popupNode");
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, node);
+ let menuitemShowMdnDocs = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.showMdnDocs"));
+
+ let isVisible = menuitemShowMdnDocs.visible;
+ let shouldBeVisible = isPropertyNameNode(node);
+ let message = shouldBeVisible ? "shown" : "hidden";
+ is(isVisible, shouldBeVisible,
+ "The MDN context menu item is " + message + " ; content : " +
+ node.textContent + " ; type : " + node.nodeType);
+ }
+}
+
+/**
+ * Check if a node is a property name.
+ */
+function isPropertyNameNode(node) {
+ return node.textContent === "font-family";
+}
+
+/**
+ * A generator that iterates recursively through 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 rule view.
+ */
+var rootElement = view => (view.element) ? view.element : view.styleDocument;
diff --git a/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js
new file mode 100644
index 000000000..e0d08d28a
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-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/ */
+
+/**
+ * This file tests the code that integrates the Style Inspector's rule view
+ * with the MDN docs tooltip.
+ *
+ * If you display the context click on a property name in the rule view, you
+ * should see a menu item "Show MDN Docs". If you click that item, the MDN
+ * docs tooltip should be shown, containing docs from MDN for that property.
+ *
+ * This file tests that:
+ * - clicking the context menu item shows the tooltip
+ * - the tooltip content matches the property name for which the context menu was opened
+ */
+
+"use strict";
+
+const {setBaseCssDocsUrl} =
+ require("devtools/client/shared/widgets/MdnDocsWidget");
+
+const PROPERTYNAME = "color";
+
+const TEST_DOC = `
+ <html>
+ <body>
+ <div style="color: red">
+ Test "Show MDN Docs" context menu option
+ </div>
+ </body>
+ </html>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_DOC));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ setBaseCssDocsUrl(URL_ROOT);
+
+ info("Setting the popupNode for the MDN docs tooltip");
+
+ let {nameSpan} = getRuleViewProperty(view, "element", PROPERTYNAME);
+
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, nameSpan.firstChild);
+ let menuitemShowMdnDocs = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.showMdnDocs"));
+
+ let cssDocs = view.tooltips.cssDocs;
+
+ info("Showing the MDN docs tooltip");
+ let onShown = cssDocs.tooltip.once("shown");
+ menuitemShowMdnDocs.click();
+ yield onShown;
+ ok(true, "The MDN docs tooltip was shown");
+
+ info("Quick check that the tooltip contents are set");
+ let h1 = cssDocs.tooltip.container.querySelector(".mdn-property-name");
+ is(h1.textContent, PROPERTYNAME, "The MDN docs tooltip h1 is correct");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-03.js b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-03.js
new file mode 100644
index 000000000..d1089fcf6
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-03.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/ */
+
+/**
+ * This file tests the "devtools.inspector.mdnDocsTooltip.enabled" preference,
+ * that we use to enable/disable the MDN tooltip in the Inspector.
+ *
+ * The desired behavior is:
+ * - if the preference is true, show the "Show MDN Docs" context menu item
+ * - if the preference is false, don't show the item
+ * - listen for changes to the pref, so we can show/hide the item dynamically
+ */
+
+"use strict";
+
+const { PrefObserver } = require("devtools/client/styleeditor/utils");
+const PREF_ENABLE_MDN_DOCS_TOOLTIP =
+ "devtools.inspector.mdnDocsTooltip.enabled";
+const PROPERTY_NAME_CLASS = "ruleview-propertyname";
+
+const TEST_DOC = `
+ <html>
+ <body>
+ <div style="color: red">
+ Test the pref to enable/disable the "Show MDN Docs" context menu option
+ </div>
+ </body>
+ </html>
+`;
+
+add_task(function* () {
+ info("Ensure the pref is true to begin with");
+ let initial = Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP);
+ if (initial != true) {
+ setBooleanPref(PREF_ENABLE_MDN_DOCS_TOOLTIP, true);
+ }
+
+ yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_DOC));
+
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+ yield testMdnContextMenuItemVisibility(view, true);
+
+ yield setBooleanPref(PREF_ENABLE_MDN_DOCS_TOOLTIP, false);
+ yield testMdnContextMenuItemVisibility(view, false);
+
+ info("Close the Inspector");
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+
+ ({inspector, view} = yield openRuleView());
+ yield selectNode("div", inspector);
+ yield testMdnContextMenuItemVisibility(view, false);
+
+ yield setBooleanPref(PREF_ENABLE_MDN_DOCS_TOOLTIP, true);
+ yield testMdnContextMenuItemVisibility(view, true);
+
+ info("Ensure the pref is reset to its initial value");
+ let eventual = Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP);
+ if (eventual != initial) {
+ setBooleanPref(PREF_ENABLE_MDN_DOCS_TOOLTIP, initial);
+ }
+});
+
+/**
+ * Set a boolean pref, and wait for the pref observer to
+ * trigger, so that code listening for the pref change
+ * has had a chance to update itself.
+ *
+ * @param pref {string} Name of the pref to change
+ * @param state {boolean} Desired value of the pref.
+ *
+ * Note that if the pref already has the value in `state`,
+ * then the prefObserver will not trigger. So you should only
+ * call this function if you know the pref's current value is
+ * not `state`.
+ */
+function* setBooleanPref(pref, state) {
+ let oncePrefChanged = defer();
+ let prefObserver = new PrefObserver("devtools.");
+ prefObserver.on(pref, oncePrefChanged.resolve);
+
+ info("Set the pref " + pref + " to: " + state);
+ Services.prefs.setBoolPref(pref, state);
+
+ info("Wait for prefObserver to call back so the UI can update");
+ yield oncePrefChanged.promise;
+ prefObserver.off(pref, oncePrefChanged.resolve);
+}
+
+/**
+ * Test whether the MDN tooltip context menu item is visible when it should be.
+ *
+ * @param view The rule view
+ * @param shouldBeVisible {boolean} Whether we expect the context
+ * menu item to be visible or not.
+ */
+function* testMdnContextMenuItemVisibility(view, shouldBeVisible) {
+ let message = shouldBeVisible ? "shown" : "hidden";
+ info("Test that MDN context menu item is " + message);
+
+ info("Set a CSS property name as popupNode");
+ let root = rootElement(view);
+ let node = root.querySelector("." + PROPERTY_NAME_CLASS).firstChild;
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, node);
+ let menuitemShowMdnDocs = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.showMdnDocs"));
+
+ let isVisible = menuitemShowMdnDocs.visible;
+ is(isVisible, shouldBeVisible,
+ "The MDN context menu item is " + message);
+}
+
+/**
+ * Returns the root element for the rule view.
+ */
+var rootElement = view => (view.element) ? view.element : view.styleDocument;
diff --git a/devtools/client/inspector/rules/test/browser_rules_copy_styles.js b/devtools/client/inspector/rules/test/browser_rules_copy_styles.js
new file mode 100644
index 000000000..a6f991a60
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_copy_styles.js
@@ -0,0 +1,307 @@
+/* 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 the copy styles context menu items in the rule
+ * view.
+ */
+
+const osString = Services.appinfo.OS;
+
+const TEST_URI = URL_ROOT + "doc_copystyles.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let { inspector, view } = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ let data = [
+ {
+ desc: "Test Copy Property Name",
+ node: ruleEditor.rule.textProps[0].editor.nameSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyPropertyName",
+ expectedPattern: "color",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: true,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ desc: "Test Copy Property Value",
+ node: ruleEditor.rule.textProps[2].editor.valueSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyPropertyValue",
+ expectedPattern: "12px",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: false,
+ copyPropertyValue: true,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ desc: "Test Copy Property Value with Priority",
+ node: ruleEditor.rule.textProps[3].editor.valueSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyPropertyValue",
+ expectedPattern: "#00F !important",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: false,
+ copyPropertyValue: true,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ desc: "Test Copy Property Declaration",
+ node: ruleEditor.rule.textProps[2].editor.nameSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyPropertyDeclaration",
+ expectedPattern: "font-size: 12px;",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: true,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ desc: "Test Copy Property Declaration with Priority",
+ node: ruleEditor.rule.textProps[3].editor.nameSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyPropertyDeclaration",
+ expectedPattern: "border-color: #00F !important;",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: true,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ desc: "Test Copy Rule",
+ node: ruleEditor.rule.textProps[2].editor.nameSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyRule",
+ expectedPattern: "#testid {[\\r\\n]+" +
+ "\tcolor: #F00;[\\r\\n]+" +
+ "\tbackground-color: #00F;[\\r\\n]+" +
+ "\tfont-size: 12px;[\\r\\n]+" +
+ "\tborder-color: #00F !important;[\\r\\n]+" +
+ "\t--var: \"\\*/\";[\\r\\n]+" +
+ "}",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: true,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ desc: "Test Copy Selector",
+ node: ruleEditor.selectorText,
+ menuItemLabel: "styleinspector.contextmenu.copySelector",
+ expectedPattern: "html, body, #testid",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: false,
+ copyPropertyName: false,
+ copyPropertyValue: false,
+ copySelector: true,
+ copyRule: true
+ }
+ },
+ {
+ desc: "Test Copy Location",
+ node: ruleEditor.source,
+ menuItemLabel: "styleinspector.contextmenu.copyLocation",
+ expectedPattern: "http://example.com/browser/devtools/client/" +
+ "inspector/rules/test/doc_copystyles.css",
+ visible: {
+ copyLocation: true,
+ copyPropertyDeclaration: false,
+ copyPropertyName: false,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ setup: function* () {
+ yield disableProperty(view, 0);
+ },
+ desc: "Test Copy Rule with Disabled Property",
+ node: ruleEditor.rule.textProps[2].editor.nameSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyRule",
+ expectedPattern: "#testid {[\\r\\n]+" +
+ "\t\/\\* color: #F00; \\*\/[\\r\\n]+" +
+ "\tbackground-color: #00F;[\\r\\n]+" +
+ "\tfont-size: 12px;[\\r\\n]+" +
+ "\tborder-color: #00F !important;[\\r\\n]+" +
+ "\t--var: \"\\*/\";[\\r\\n]+" +
+ "}",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: true,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ setup: function* () {
+ yield disableProperty(view, 4);
+ },
+ desc: "Test Copy Rule with Disabled Property with Comment",
+ node: ruleEditor.rule.textProps[2].editor.nameSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyRule",
+ expectedPattern: "#testid {[\\r\\n]+" +
+ "\t\/\\* color: #F00; \\*\/[\\r\\n]+" +
+ "\tbackground-color: #00F;[\\r\\n]+" +
+ "\tfont-size: 12px;[\\r\\n]+" +
+ "\tborder-color: #00F !important;[\\r\\n]+" +
+ "\t/\\* --var: \"\\*\\\\\/\"; \\*\/[\\r\\n]+" +
+ "}",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: true,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ {
+ desc: "Test Copy Property Declaration with Disabled Property",
+ node: ruleEditor.rule.textProps[0].editor.nameSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyPropertyDeclaration",
+ expectedPattern: "\/\\* color: #F00; \\*\/",
+ visible: {
+ copyLocation: false,
+ copyPropertyDeclaration: true,
+ copyPropertyName: true,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true
+ }
+ },
+ ];
+
+ for (let { setup, desc, node, menuItemLabel, expectedPattern, visible } of data) {
+ if (setup) {
+ yield setup();
+ }
+
+ info(desc);
+ yield checkCopyStyle(view, node, menuItemLabel, expectedPattern, visible);
+ }
+});
+
+function* checkCopyStyle(view, node, menuItemLabel, expectedPattern, visible) {
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, node);
+ let menuItem = allMenuItems.find(item =>
+ item.label === STYLE_INSPECTOR_L10N.getStr(menuItemLabel));
+ let menuitemCopy = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy"));
+ let menuitemCopyLocation = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyLocation"));
+ let menuitemCopyPropertyDeclaration = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyDeclaration"));
+ let menuitemCopyPropertyName = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyName"));
+ let menuitemCopyPropertyValue = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyValue"));
+ let menuitemCopySelector = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copySelector"));
+ let menuitemCopyRule = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyRule"));
+
+ ok(menuitemCopy.disabled,
+ "Copy disabled is as expected: true");
+ ok(menuitemCopy.visible,
+ "Copy visible is as expected: true");
+
+ is(menuitemCopyLocation.visible,
+ visible.copyLocation,
+ "Copy Location visible attribute is as expected: " +
+ visible.copyLocation);
+
+ is(menuitemCopyPropertyDeclaration.visible,
+ visible.copyPropertyDeclaration,
+ "Copy Property Declaration visible attribute is as expected: " +
+ visible.copyPropertyDeclaration);
+
+ is(menuitemCopyPropertyName.visible,
+ visible.copyPropertyName,
+ "Copy Property Name visible attribute is as expected: " +
+ visible.copyPropertyName);
+
+ is(menuitemCopyPropertyValue.visible,
+ visible.copyPropertyValue,
+ "Copy Property Value visible attribute is as expected: " +
+ visible.copyPropertyValue);
+
+ is(menuitemCopySelector.visible,
+ visible.copySelector,
+ "Copy Selector visible attribute is as expected: " +
+ visible.copySelector);
+
+ is(menuitemCopyRule.visible,
+ visible.copyRule,
+ "Copy Rule visible attribute is as expected: " +
+ visible.copyRule);
+
+ try {
+ yield waitForClipboardPromise(() => menuItem.click(),
+ () => checkClipboardData(expectedPattern));
+ } catch (e) {
+ failedClipboard(expectedPattern);
+ }
+}
+
+function* disableProperty(view, index) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let textProp = ruleEditor.rule.textProps[index];
+ yield togglePropStatus(view, textProp);
+}
+
+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();
+
+ ok(false, "Clipboard text does not match expected " +
+ "results (escaped for accurate comparison):\n");
+ info("Actual: " + escape(actual));
+ info("Expected: " + escape(expectedPattern));
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_css-docs-tooltip_closes-on-escape.js b/devtools/client/inspector/rules/test/browser_rules_css-docs-tooltip_closes-on-escape.js
new file mode 100644
index 000000000..f386f45b4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_css-docs-tooltip_closes-on-escape.js
@@ -0,0 +1,51 @@
+/* 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/ */
+
+/**
+ * Test that the CssDocs tooltip of the ruleview can be closed when pressing the Escape
+ * key.
+ */
+
+"use strict";
+
+const {setBaseCssDocsUrl} =
+ require("devtools/client/shared/widgets/MdnDocsWidget");
+
+const PROPERTYNAME = "color";
+
+const TEST_URI = `
+ <html>
+ <body>
+ <div style="color: red">
+ Test "Show MDN Docs" closes on escape
+ </div>
+ </body>
+ </html>
+`;
+
+/**
+ * Test that the tooltip is hidden when we press Escape
+ */
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ setBaseCssDocsUrl(URL_ROOT);
+
+ info("Retrieve a valid anchor for the CssDocs tooltip");
+ let {nameSpan} = getRuleViewProperty(view, "element", PROPERTYNAME);
+
+ info("Showing the MDN docs tooltip");
+ let onShown = view.tooltips.cssDocs.tooltip.once("shown");
+ view.tooltips.cssDocs.show(nameSpan, PROPERTYNAME);
+ yield onShown;
+ ok(true, "The MDN docs tooltip was shown");
+
+ info("Simulate pressing the 'Escape' key");
+ let onHidden = view.tooltips.cssDocs.tooltip.once("hidden");
+ EventUtils.sendKey("escape");
+ yield onHidden;
+ ok(true, "The MDN docs tooltip was hidden on pressing 'escape'");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_cssom.js b/devtools/client/inspector/rules/test/browser_rules_cssom.js
new file mode 100644
index 000000000..d20e85192
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cssom.js
@@ -0,0 +1,22 @@
+/* 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 to ensure that CSSOM doesn't make the rule view blow up.
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1224121
+
+const TEST_URI = URL_ROOT + "doc_cssom.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#target", inspector);
+
+ let elementStyle = view._elementStyle;
+ let rule = elementStyle.rules[1];
+
+ is(rule.textProps.length, 1, "rule should have one property");
+ is(rule.textProps[0].name, "color", "the property should be 'color'");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_cubicbezier-appears-on-swatch-click.js b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-appears-on-swatch-click.js
new file mode 100644
index 000000000..18099894b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_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";
+
+// Tests that cubic-bezier pickers appear when clicking on cubic-bezier
+// swatches.
+
+const TEST_URI = `
+ <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>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {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 onBezierWidgetReady = bezier.once("ready");
+ swatch.click();
+ yield onBezierWidgetReady;
+
+ 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");
+ yield hideTooltipAndWaitForRuleViewChanged(bezier, view);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_cubicbezier-commit-on-ENTER.js b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-commit-on-ENTER.js
new file mode 100644
index 000000000..5dc43d1c9
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-commit-on-ENTER.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";
+
+// Tests that a curve change in the cubic-bezier tooltip is committed when ENTER
+// is pressed.
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ transition: top 2s linear;
+ }
+ </style>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {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 onBezierWidgetReady = bezierTooltip.once("ready");
+ swatch.click();
+ yield onBezierWidgetReady;
+
+ 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(function* () {
+ let func = yield getComputedStyleProperty("body", null,
+ "transition-timing-function");
+ return func === 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");
+ // Pressing RETURN ends up doing 2 rule-view updates, one for the preview and
+ // one for the commit when the tooltip closes.
+ let onRuleViewChanged = waitForNEvents(ruleView, "ruleview-changed", 2);
+ focusAndSendKey(widget.parent.ownerDocument.defaultView, "RETURN");
+ yield onRuleViewChanged;
+
+ let style = yield getComputedStyleProperty("body", null,
+ "transition-timing-function");
+ is(style, expected, "The element's timing-function was kept after RETURN");
+
+ let ruleViewStyle = getRuleViewProperty(ruleView, "body", "transition")
+ .valueSpan.textContent.indexOf("cubic-bezier(") !== -1;
+ ok(ruleViewStyle, "The text of the timing-function was kept after RETURN");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_cubicbezier-revert-on-ESC.js b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-revert-on-ESC.js
new file mode 100644
index 000000000..826d8a5aa
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-revert-on-ESC.js
@@ -0,0 +1,100 @@
+/* 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 changes made to the cubic-bezier timing-function in the
+// cubic-bezier tooltip are reverted when ESC is pressed.
+
+const TEST_URI = `
+ <style type='text/css'>
+ body {
+ animation-timing-function: linear;
+ }
+ </style>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+ yield testPressingEscapeRevertsChanges(view);
+ yield testPressingEscapeRevertsChangesAndDisables(view);
+});
+
+function* testPressingEscapeRevertsChanges(view) {
+ let {propEditor} = yield openCubicBezierAndChangeCoords(view, 1, 0,
+ [0.1, 2, 0.9, -1], {
+ selector: "body",
+ name: "animation-timing-function",
+ value: "cubic-bezier(0.1, 2, 0.9, -1)"
+ });
+
+ is(propEditor.valueSpan.textContent, "cubic-bezier(.1,2,.9,-1)",
+ "Got expected property value.");
+
+ yield escapeTooltip(view);
+
+ yield waitForComputedStyleProperty("body", null, "animation-timing-function",
+ "linear");
+ is(propEditor.valueSpan.textContent, "linear",
+ "Got expected property value.");
+}
+
+function* testPressingEscapeRevertsChangesAndDisables(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let textProp = ruleEditor.rule.textProps[0];
+ let propEditor = textProp.editor;
+
+ info("Disabling animation-timing-function property");
+ yield togglePropStatus(view, textProp);
+
+ ok(propEditor.element.classList.contains("ruleview-overridden"),
+ "property is overridden.");
+ is(propEditor.enable.style.visibility, "visible",
+ "property enable checkbox is visible.");
+ ok(!propEditor.enable.getAttribute("checked"),
+ "property enable checkbox is not checked.");
+ ok(!propEditor.prop.enabled,
+ "animation-timing-function property is disabled.");
+ let newValue = yield getRulePropertyValue("animation-timing-function");
+ is(newValue, "", "animation-timing-function should have been unset.");
+
+ yield openCubicBezierAndChangeCoords(view, 1, 0, [0.1, 2, 0.9, -1]);
+
+ yield escapeTooltip(view);
+
+ ok(propEditor.element.classList.contains("ruleview-overridden"),
+ "property is overridden.");
+ is(propEditor.enable.style.visibility, "visible",
+ "property enable checkbox is visible.");
+ ok(!propEditor.enable.getAttribute("checked"),
+ "property enable checkbox is not checked.");
+ ok(!propEditor.prop.enabled,
+ "animation-timing-function property is disabled.");
+ newValue = yield getRulePropertyValue("animation-timing-function");
+ is(newValue, "", "animation-timing-function should have been unset.");
+ is(propEditor.valueSpan.textContent, "linear",
+ "Got expected property value.");
+}
+
+function* getRulePropertyValue(name) {
+ let propValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: name
+ });
+ return propValue;
+}
+
+function* escapeTooltip(view) {
+ info("Pressing ESCAPE to close the tooltip");
+
+ let bezierTooltip = view.tooltips.cubicBezier;
+ let widget = yield bezierTooltip.widget;
+ let onHidden = bezierTooltip.tooltip.once("hidden");
+ let onModifications = view.once("ruleview-changed");
+ focusAndSendKey(widget.parent.ownerDocument.defaultView, "ESCAPE");
+ yield onHidden;
+ yield onModifications;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_custom.js b/devtools/client/inspector/rules/test/browser_rules_custom.js
new file mode 100644
index 000000000..7c941af6f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_custom.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";
+
+const TEST_URI = URL_ROOT + "doc_custom.html";
+
+// Tests the display of custom declarations in the rule-view.
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+
+ yield simpleCustomOverride(inspector, view);
+ yield importantCustomOverride(inspector, view);
+ yield disableCustomOverride(inspector, view);
+});
+
+function* simpleCustomOverride(inspector, view) {
+ yield selectNode("#testidSimple", inspector);
+
+ let idRule = getRuleViewRuleEditor(view, 1).rule;
+ let idRuleProp = idRule.textProps[0];
+
+ is(idRuleProp.name, "--background-color",
+ "First ID prop should be --background-color");
+ ok(!idRuleProp.overridden, "ID prop should not be overridden.");
+
+ let classRule = getRuleViewRuleEditor(view, 2).rule;
+ let classRuleProp = classRule.textProps[0];
+
+ is(classRuleProp.name, "--background-color",
+ "First class prop should be --background-color");
+ ok(classRuleProp.overridden, "Class property should be overridden.");
+
+ // Override --background-color by changing the element style.
+ let elementProp = yield addProperty(view, 0, "--background-color", "purple");
+
+ is(classRuleProp.name, "--background-color",
+ "First element prop should now be --background-color");
+ ok(!elementProp.overridden,
+ "Element style property should not be overridden");
+ ok(idRuleProp.overridden, "ID property should be overridden");
+ ok(classRuleProp.overridden, "Class property should be overridden");
+}
+
+function* importantCustomOverride(inspector, view) {
+ yield selectNode("#testidImportant", inspector);
+
+ let idRule = getRuleViewRuleEditor(view, 1).rule;
+ let idRuleProp = idRule.textProps[0];
+ ok(idRuleProp.overridden, "Not-important rule should be overridden.");
+
+ let classRule = getRuleViewRuleEditor(view, 2).rule;
+ let classRuleProp = classRule.textProps[0];
+ ok(!classRuleProp.overridden, "Important rule should not be overridden.");
+}
+
+function* disableCustomOverride(inspector, view) {
+ yield selectNode("#testidDisable", inspector);
+
+ let idRule = getRuleViewRuleEditor(view, 1).rule;
+ let idRuleProp = idRule.textProps[0];
+
+ yield togglePropStatus(view, idRuleProp);
+
+ let classRule = getRuleViewRuleEditor(view, 2).rule;
+ let classRuleProp = classRule.textProps[0];
+ ok(!classRuleProp.overridden,
+ "Class prop should not be overridden after id prop was disabled.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_cycle-angle.js b/devtools/client/inspector/rules/test/browser_rules_cycle-angle.js
new file mode 100644
index 000000000..fa135f937
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cycle-angle.js
@@ -0,0 +1,93 @@
+/* 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 cycling angle units in the rule view.
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ image-orientation: 1turn;
+ }
+ div {
+ image-orientation: 180deg;
+ }
+ </style>
+ <body><div>Test</div>cycling angle units in the rule view!</body>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ let container = getRuleViewProperty(
+ view, "body", "image-orientation").valueSpan;
+ yield checkAngleCycling(container, view);
+ yield checkAngleCyclingPersist(inspector, view);
+});
+
+function* checkAngleCycling(container, view) {
+ let valueNode = container.querySelector(".ruleview-angle");
+ let win = view.styleWindow;
+
+ // turn
+ is(valueNode.textContent, "1turn", "Angle displayed as a turn value.");
+
+ let tests = [{
+ value: "360deg",
+ comment: "Angle displayed as a degree value."
+ }, {
+ value: `${Math.round(Math.PI * 2 * 10000) / 10000}rad`,
+ comment: "Angle displayed as a radian value."
+ }, {
+ value: "400grad",
+ comment: "Angle displayed as a gradian value."
+ }, {
+ value: "1turn",
+ comment: "Angle displayed as a turn value again."
+ }];
+
+ for (let test of tests) {
+ yield checkSwatchShiftClick(container, win, test.value, test.comment);
+ }
+}
+
+function* checkAngleCyclingPersist(inspector, view) {
+ yield selectNode("div", inspector);
+ let container = getRuleViewProperty(
+ view, "div", "image-orientation").valueSpan;
+ let valueNode = container.querySelector(".ruleview-angle");
+ let win = view.styleWindow;
+
+ is(valueNode.textContent, "180deg", "Angle displayed as a degree value.");
+
+ yield checkSwatchShiftClick(container, win,
+ `${Math.round(Math.PI * 10000) / 10000}rad`,
+ "Angle displayed as a radian value.");
+
+ // Select the body and reselect the div to see
+ // if the new angle unit persisted
+ yield selectNode("body", inspector);
+ yield selectNode("div", inspector);
+
+ // We have to query for the container and the swatch because
+ // they've been re-generated
+ container = getRuleViewProperty(view, "div", "image-orientation").valueSpan;
+ valueNode = container.querySelector(".ruleview-angle");
+ is(valueNode.textContent, `${Math.round(Math.PI * 10000) / 10000}rad`,
+ "Angle still displayed as a radian value.");
+}
+
+function* checkSwatchShiftClick(container, win, expectedValue, comment) {
+ let swatch = container.querySelector(".ruleview-angleswatch");
+ let valueNode = container.querySelector(".ruleview-angle");
+
+ let onUnitChange = swatch.once("unit-change");
+ EventUtils.synthesizeMouseAtCenter(swatch, {
+ type: "mousedown",
+ shiftKey: true
+ }, win);
+ yield onUnitChange;
+ is(valueNode.textContent, expectedValue, comment);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_cycle-color.js b/devtools/client/inspector/rules/test/browser_rules_cycle-color.js
new file mode 100644
index 000000000..e31ffa133
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cycle-color.js
@@ -0,0 +1,120 @@
+/* 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 cycling color types in the rule view.
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ color: #f00;
+ }
+ span {
+ color: blue;
+ border-color: #ff000080;
+ }
+ </style>
+ <body><span>Test</span> cycling color types in the rule view!</body>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ let container = getRuleViewProperty(view, "body", "color").valueSpan;
+ yield checkColorCycling(container, view);
+ yield checkAlphaColorCycling(inspector, view);
+ yield checkColorCyclingPersist(inspector, view);
+});
+
+function* checkColorCycling(container, view) {
+ let valueNode = container.querySelector(".ruleview-color");
+ let win = view.styleWindow;
+
+ // Hex
+ is(valueNode.textContent, "#f00", "Color displayed as a hex value.");
+
+ let tests = [{
+ value: "hsl(0, 100%, 50%)",
+ comment: "Color displayed as an HSL value."
+ }, {
+ value: "rgb(255, 0, 0)",
+ comment: "Color displayed as an RGB value."
+ }, {
+ value: "red",
+ comment: "Color displayed as a color name."
+ }, {
+ value: "#f00",
+ comment: "Color displayed as an authored value."
+ }, {
+ value: "hsl(0, 100%, 50%)",
+ comment: "Color displayed as an HSL value again."
+ }];
+
+ for (let test of tests) {
+ yield checkSwatchShiftClick(container, win, test.value, test.comment);
+ }
+}
+
+function* checkAlphaColorCycling(inspector, view) {
+ yield selectNode("span", inspector);
+ let container = getRuleViewProperty(view, "span", "border-color").valueSpan;
+ let valueNode = container.querySelector(".ruleview-color");
+ let win = view.styleWindow;
+
+ is(valueNode.textContent, "#ff000080",
+ "Color displayed as an alpha hex value.");
+
+ let tests = [{
+ value: "hsla(0, 100%, 50%, 0.5)",
+ comment: "Color displayed as an HSLa value."
+ }, {
+ value: "rgba(255, 0, 0, 0.5)",
+ comment: "Color displayed as an RGBa value."
+ }, {
+ value: "#ff000080",
+ comment: "Color displayed as an alpha hex value again."
+ }];
+
+ for (let test of tests) {
+ yield checkSwatchShiftClick(container, win, test.value, test.comment);
+ }
+}
+
+function* checkColorCyclingPersist(inspector, view) {
+ yield selectNode("span", inspector);
+ let container = getRuleViewProperty(view, "span", "color").valueSpan;
+ let valueNode = container.querySelector(".ruleview-color");
+ let win = view.styleWindow;
+
+ is(valueNode.textContent, "blue", "Color displayed as a color name.");
+
+ yield checkSwatchShiftClick(container, win, "#00f",
+ "Color displayed as a hex value.");
+
+ // Select the body and reselect the span to see
+ // if the new color unit persisted
+ yield selectNode("body", inspector);
+ yield selectNode("span", inspector);
+
+ // We have to query for the container and the swatch because
+ // they've been re-generated
+ container = getRuleViewProperty(view, "span", "color").valueSpan;
+ valueNode = container.querySelector(".ruleview-color");
+ is(valueNode.textContent, "#00f",
+ "Color is still displayed as a hex value.");
+}
+
+function* checkSwatchShiftClick(container, win, expectedValue, comment) {
+ let swatch = container.querySelector(".ruleview-colorswatch");
+ let valueNode = container.querySelector(".ruleview-color");
+
+ let onUnitChange = swatch.once("unit-change");
+ EventUtils.synthesizeMouseAtCenter(swatch, {
+ type: "mousedown",
+ shiftKey: true
+ }, win);
+ yield onUnitChange;
+ is(valueNode.textContent, expectedValue, comment);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js b/devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js
new file mode 100644
index 000000000..18522b527
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.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 toggling the grid highlighter in the rule view and modifying the 'display: grid'
+// declaration.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ let highlighters = view.highlighters;
+
+ yield selectNode("#grid", inspector);
+ let container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ let gridToggle = container.querySelector(".ruleview-grid");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ let onHighlighterShown = highlighters.once("highlighter-shown");
+ gridToggle.click();
+ yield onHighlighterShown;
+
+ info("Edit the 'grid' property value to 'block'.");
+ let editor = yield focusEditableField(view, container);
+ let onHighlighterHidden = highlighters.once("highlighter-hidden");
+ let onDone = view.once("ruleview-changed");
+ editor.input.value = "block;";
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onHighlighterHidden;
+ yield onDone;
+
+ info("Check the grid highlighter and grid toggle button are hidden.");
+ gridToggle = container.querySelector(".ruleview-grid");
+ ok(!gridToggle, "Grid highlighter toggle is not visible.");
+ ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js
new file mode 100644
index 000000000..af1a6fbc0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js
@@ -0,0 +1,46 @@
+/* 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 editing a property name or value and escaping will revert the
+// changes and restore the original value.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: #00F;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+
+ yield focusEditableField(view, propEditor.nameSpan);
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element,
+ ["DELETE", "ESCAPE"]);
+
+ is(propEditor.nameSpan.textContent, "background-color",
+ "'background-color' property name is correctly set.");
+ is((yield getComputedStyleProperty("#testid", null, "background-color")),
+ "rgb(0, 0, 255)", "#00F background color is set.");
+
+ yield focusEditableField(view, propEditor.valueSpan);
+ let onValueDeleted = view.once("ruleview-changed");
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element,
+ ["DELETE", "ESCAPE"]);
+ yield onValueDeleted;
+
+ is(propEditor.valueSpan.textContent, "#00F",
+ "'#00F' property value is correctly set.");
+ is((yield getComputedStyleProperty("#testid", null, "background-color")),
+ "rgb(0, 0, 255)", "#00F background color is set.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-click.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-click.js
new file mode 100644
index 000000000..08a5ee786
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-click.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 that the property name and value editors can be triggered when
+// clicking on the property-name, the property-value, the colon or semicolon.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ margin: 0;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testEditPropertyAndCancel(inspector, view);
+});
+
+function* testEditPropertyAndCancel(inspector, view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+
+ info("Test editor is created when clicking on property name");
+ yield focusEditableField(view, propEditor.nameSpan);
+ ok(propEditor.nameSpan.inplaceEditor, "Editor created for property name");
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]);
+
+ info("Test editor is created when clicking on ':' next to property name");
+ let nameRect = propEditor.nameSpan.getBoundingClientRect();
+ yield focusEditableField(view, propEditor.nameSpan, nameRect.width + 1);
+ ok(propEditor.nameSpan.inplaceEditor, "Editor created for property name");
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]);
+
+ info("Test editor is created when clicking on property value");
+ yield focusEditableField(view, propEditor.valueSpan);
+ ok(propEditor.valueSpan.inplaceEditor, "Editor created for property value");
+ // When cancelling a value edition, the text-property-editor will trigger
+ // a modification to make sure the property is back to its original value
+ // => need to wait on "ruleview-changed" to avoid unhandled promises
+ let onRuleviewChanged = view.once("ruleview-changed");
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]);
+ yield onRuleviewChanged;
+
+ info("Test editor is created when clicking on ';' next to property value");
+ let valueRect = propEditor.valueSpan.getBoundingClientRect();
+ yield focusEditableField(view, propEditor.valueSpan, valueRect.width + 1);
+ ok(propEditor.valueSpan.inplaceEditor, "Editor created for property value");
+ // When cancelling a value edition, the text-property-editor will trigger
+ // a modification to make sure the property is back to its original value
+ // => need to wait on "ruleview-changed" to avoid unhandled promises
+ onRuleviewChanged = view.once("ruleview-changed");
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]);
+ yield onRuleviewChanged;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js
new file mode 100644
index 000000000..8e16601c7
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js
@@ -0,0 +1,92 @@
+/* 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 TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ color: #00F;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+// 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: "#00F"
+ },
+ {
+ 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," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ for (let data of testData) {
+ yield runTestData(view, data);
+ }
+});
+
+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(view, propEditor.valueSpan);
+ is(inplaceEditor(propEditor.valueSpan), editor,
+ "Focused editor should be the value span.");
+
+ info("Entering test data " + value);
+ let onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.sendString(value, view.styleWindow);
+ view.throttle.flush();
+ yield onRuleViewChanged;
+
+ info("Entering the commit key " + commitKey + " " + modifiers);
+ onRuleViewChanged = view.once("ruleview-changed");
+ let onBlur = once(editor.input, "blur");
+ EventUtils.synthesizeKey(commitKey, modifiers);
+ yield onBlur;
+ yield onRuleViewChanged;
+
+ if (commitKey === "VK_ESCAPE") {
+ is(propEditor.valueSpan.textContent, expected,
+ "Value is as expected: " + expected);
+ } else {
+ is(propEditor.valueSpan.textContent, expected,
+ "Value is as expected: " + expected);
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js
new file mode 100644
index 000000000..ee0a1fa74
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js
@@ -0,0 +1,89 @@
+/* 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 values of a style (the shorthand expansion) are
+// properly updated after the style is changed.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ padding: 10px;
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield editAndCheck(view);
+});
+
+function* editAndCheck(view) {
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let prop = idRuleEditor.rule.textProps[0];
+ let propEditor = prop.editor;
+ let newPaddingValue = "20px";
+
+ info("Focusing the inplace editor field");
+ let editor = yield focusEditableField(view, propEditor.valueSpan);
+ is(inplaceEditor(propEditor.valueSpan), editor,
+ "Focused editor should be the value span.");
+
+ let onPropertyChange = waitForComputedStyleProperty("#testid", null,
+ "padding-top", newPaddingValue);
+ let onRefreshAfterPreview = once(view, "ruleview-changed");
+
+ info("Entering a new value");
+ EventUtils.sendString(newPaddingValue, view.styleWindow);
+
+ info("Waiting for the throttled previewValue to apply the " +
+ "changes to document");
+
+ view.throttle.flush();
+ yield onPropertyChange;
+
+ info("Waiting for ruleview-refreshed after previewValue was applied.");
+ yield onRefreshAfterPreview;
+
+ let onBlur = once(editor.input, "blur");
+
+ info("Entering the commit key and finishing edit");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ info("Waiting for blur on the field");
+ yield onBlur;
+
+ info("Waiting for the style changes to be applied");
+ yield once(view, "ruleview-changed");
+
+ let computed = prop.computed;
+ let propNames = [
+ "padding-top",
+ "padding-right",
+ "padding-bottom",
+ "padding-left"
+ ];
+
+ is(computed.length, propNames.length, "There should be 4 computed values");
+ propNames.forEach((propName, i) => {
+ is(computed[i].name, propName,
+ "Computed property #" + i + " has name " + propName);
+ is(computed[i].value, newPaddingValue,
+ "Computed value of " + propName + " is as expected");
+ });
+
+ propEditor.expander.click();
+ let computedDom = propEditor.computed;
+ is(computedDom.children.length, propNames.length,
+ "There should be 4 nodes in the DOM");
+ propNames.forEach((propName, i) => {
+ is(computedDom.getElementsByClassName("ruleview-propertyvalue")[i]
+ .textContent, newPaddingValue,
+ "Computed value of " + propName + " in DOM is as expected");
+ });
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js
new file mode 100644
index 000000000..ca63cedcc
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js
@@ -0,0 +1,280 @@
+/* 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.
+
+// Bug 1275446 - This test happen to hit the default timeout on linux32
+requestLongerTimeout(2);
+
+const TEST_URI = `
+ <style>
+ #test {
+ margin-top: 0px;
+ padding-top: 0px;
+ color: #000000;
+ background-color: #000000;
+ background: none;
+ transition: initial;
+ z-index: 0;
+ }
+ </style>
+ <div id="test"></div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#test", inspector);
+
+ yield testMarginIncrements(view);
+ yield testVariousUnitIncrements(view);
+ yield testHexIncrements(view);
+ yield testAlphaHexIncrements(view);
+ yield testRgbIncrements(view);
+ yield testShorthandIncrements(view);
+ yield testOddCases(view);
+ yield testZeroValueIncrements(view);
+});
+
+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},
+ 9: {start: "0", end: "1px", selectAll: true},
+ 10: {down: true, start: "0", end: "-1px", 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},
+ 10: {start: "0", end: "1px", selectAll: true},
+ 11: {down: true, start: "0", end: "-1px", 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* testAlphaHexIncrements(view) {
+ info("Testing keyboard increments with alpha hex colors");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let hexColorPropEditor = idRuleEditor.rule.textProps[2].editor;
+
+ yield runIncrementTest(hexColorPropEditor, view, {
+ 1: {start: "#CCCCCCAA", end: "#CDCDCDAB", selectAll: true},
+ 2: {shift: true, start: "#CCCCCCAA", end: "#DCDCDCBA", selectAll: true},
+ 3: {start: "#CCCCCCAA", end: "#CDCCCCAA", selection: [1, 3]},
+ 4: {shift: true, start: "#CCCCCCAA", end: "#DCCCCCAA", selection: [1, 3]},
+ 5: {start: "#FFFFFFFF", end: "#FFFFFFFF", selectAll: true},
+ 6: {down: true, shift: true, start: "#00000000", end: "#00000000",
+ 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: "1px"},
+ 4: {down: true, start: "0", end: "-1px"},
+ 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* testZeroValueIncrements(view) {
+ info("Testing a valid unit is added when incrementing from 0");
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+ let backgroundPropEditor = idRuleEditor.rule.textProps[4].editor;
+ yield runIncrementTest(backgroundPropEditor, view, {
+ 1: { start: "url(test-0.png) no-repeat 0 0",
+ end: "url(test-0.png) no-repeat 1px 0", selection: [26, 26] },
+ 2: { start: "url(test-0.png) no-repeat 0 0",
+ end: "url(test-0.png) no-repeat 0 1px", selection: [28, 28] },
+ 3: { start: "url(test-0.png) no-repeat center/0",
+ end: "url(test-0.png) no-repeat center/1px", selection: [34, 34] },
+ 4: { start: "url(test-0.png) no-repeat 0 0",
+ end: "url(test-1.png) no-repeat 0 0", selection: [10, 10] },
+ 5: { start: "linear-gradient(0, red 0, blue 0)",
+ end: "linear-gradient(1deg, red 0, blue 0)", selection: [17, 17] },
+ 6: { start: "linear-gradient(1deg, red 0, blue 0)",
+ end: "linear-gradient(1deg, red 1px, blue 0)", selection: [27, 27] },
+ 7: { start: "linear-gradient(1deg, red 0, blue 0)",
+ end: "linear-gradient(1deg, red 0, blue 1px)", selection: [35, 35] },
+ });
+
+ let transitionPropEditor = idRuleEditor.rule.textProps[5].editor;
+ yield runIncrementTest(transitionPropEditor, view, {
+ 1: { start: "all 0 ease-out", end: "all 1s ease-out", selection: [5, 5] },
+ 2: { start: "margin 4s, color 0",
+ end: "margin 4s, color 1s", selection: [18, 18] },
+ });
+
+ let zIndexPropEditor = idRuleEditor.rule.textProps[6].editor;
+ yield runIncrementTest(zIndexPropEditor, view, {
+ 1: {start: "0", end: "1", selection: [1, 1]},
+ });
+}
+
+function* runIncrementTest(propertyEditor, view, tests) {
+ let editor = yield focusEditableField(view, propertyEditor.valueSpan);
+
+ for (let test in tests) {
+ yield testIncrement(editor, tests[test], view, propertyEditor);
+ }
+
+ // Blur the field to put back the UI in its initial state (and avoid pending
+ // requests when the test ends).
+ let onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ view.throttle.flush();
+ yield onRuleViewChanged;
+}
+
+function* testIncrement(editor, options, view) {
+ 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 onRuleViewChanged = view.once("ruleview-changed");
+ let onKeyUp = once(input, "keyup");
+
+ let key;
+ key = options.down ? "VK_DOWN" : "VK_UP";
+ if (options.pageDown) {
+ key = "VK_PAGE_DOWN";
+ } else if (options.pageUp) {
+ key = "VK_PAGE_UP";
+ }
+
+ EventUtils.synthesizeKey(key, {altKey: options.alt, shiftKey: options.shift},
+ view.styleWindow);
+
+ yield onKeyUp;
+
+ // Only expect a change if the value actually changed!
+ if (options.start !== options.end) {
+ view.throttle.flush();
+ yield onRuleViewChanged;
+ }
+
+ is(input.value, options.end, "Value changed to " + options.end);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-order.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-order.js
new file mode 100644
index 000000000..b4a86c194
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-order.js
@@ -0,0 +1,89 @@
+/* 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.
+
+const TEST_URI = "<style>#testid {}</style><div id='testid'>Styled Node</div>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let elementStyle = view._elementStyle;
+ let elementRule = elementStyle.rules[1];
+
+ info("Checking rules insertion order and checking the applied style");
+ let firstProp = yield addProperty(view, 1, "background-color", "green");
+ let secondProp = yield addProperty(view, 1, "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.");
+
+ // rgb(0, 0, 255) = blue
+ is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)",
+ "Second property should have been used.");
+
+ info("Removing the second property and checking the applied style again");
+ yield removeProperty(view, secondProp);
+ // rgb(0, 128, 0) = green
+ is((yield getValue("#testid", "background-color")), "rgb(0, 128, 0)",
+ "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 = yield addProperty(view, 1, "background-color", "blue");
+
+ is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)",
+ "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");
+ yield togglePropStatus(view, secondProp);
+
+ is((yield getValue("#testid", "background-color")), "rgb(0, 128, 0)",
+ "After disabling second property, first value should be used");
+
+ info("Disabling the first property too and checking the applied style");
+ yield togglePropStatus(view, firstProp);
+
+ is((yield getValue("#testid", "background-color")), "transparent",
+ "After disabling both properties, value should be empty.");
+
+ info("Re-enabling the second propertyt and checking the applied style");
+ yield togglePropStatus(view, secondProp);
+
+ is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)",
+ "Value should be set correctly after re-enabling");
+
+ info("Re-enabling the first property and checking the insertion order " +
+ "is still respected");
+ yield togglePropStatus(view, firstProp);
+
+ is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)",
+ "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");
+ yield setProperty(view, firstProp, "purple");
+
+ is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)",
+ "Modifying an earlier property shouldn't override a later property.");
+});
+
+function* getValue(selector, propName) {
+ let value = yield getComputedStyleProperty(selector, null, propName);
+ return value;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_01.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_01.js
new file mode 100644
index 000000000..0aed2f5c8
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_01.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";
+
+// Tests removing a property by clearing the property name and pressing the
+// return key, and checks if the focus is moved to the appropriate editable
+// field.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: #00F;
+ color: #00F;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Getting the first property in the #testid rule");
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[0];
+
+ info("Deleting the name of that property to remove the property");
+ yield removeProperty(view, prop, false);
+
+ let newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "background-color"
+ });
+ is(newValue, "", "background-color should have been unset.");
+
+ info("Getting the new first property in the rule");
+ prop = rule.textProps[0];
+
+ let editor = inplaceEditor(view.styleDocument.activeElement);
+ is(inplaceEditor(prop.editor.nameSpan), editor,
+ "Focus should have moved to the next property name");
+
+ info("Deleting the name of that property to remove the property");
+ view.styleDocument.activeElement.blur();
+ yield removeProperty(view, prop, false);
+
+ newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "color"
+ });
+ is(newValue, "", "color should have been unset.");
+
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ is(inplaceEditor(rule.editor.newPropSpan), editor,
+ "Focus should have moved to the new property span");
+ is(rule.textProps.length, 0,
+ "All properties should have been removed.");
+ is(rule.editor.propertyList.children.length, 1,
+ "Should have the new property span.");
+
+ view.styleDocument.activeElement.blur();
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_02.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_02.js
new file mode 100644
index 000000000..5690e7c2d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_02.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";
+
+// Tests removing a property by clearing the property value and pressing the
+// return key, and checks if the focus is moved to the appropriate editable
+// field.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: #00F;
+ color: #00F;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Getting the first property in the rule");
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[0];
+
+ info("Clearing the property value");
+ yield setProperty(view, prop, null, false);
+
+ let newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "background-color"
+ });
+ is(newValue, "", "background-color should have been unset.");
+
+ info("Getting the new first property in the rule");
+ prop = rule.textProps[0];
+
+ let editor = inplaceEditor(view.styleDocument.activeElement);
+ is(inplaceEditor(prop.editor.nameSpan), editor,
+ "Focus should have moved to the next property name");
+ view.styleDocument.activeElement.blur();
+
+ info("Clearing the property value");
+ yield setProperty(view, prop, null, false);
+
+ newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "color"
+ });
+ is(newValue, "", "color should have been unset.");
+
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ is(inplaceEditor(rule.editor.newPropSpan), editor,
+ "Focus should have moved to the new property span");
+ is(rule.textProps.length, 0,
+ "All properties should have been removed.");
+ is(rule.editor.propertyList.children.length, 1,
+ "Should have the new property span.");
+
+ view.styleDocument.activeElement.blur();
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_03.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_03.js
new file mode 100644
index 000000000..21a1063c2
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_03.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";
+
+// Tests removing a property by clearing the property name and pressing shift
+// and tab keys, and checks if the focus is moved to the appropriate editable
+// field.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: #00F;
+ color: #00F;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Getting the second property in the rule");
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[1];
+
+ info("Clearing the property value and pressing shift-tab");
+ let editor = yield focusEditableField(view, prop.editor.valueSpan);
+ let onValueDone = view.once("ruleview-changed");
+ editor.input.value = "";
+ EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, view.styleWindow);
+ yield onValueDone;
+
+ let newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "color"
+ });
+ is(newValue, "", "color should have been unset.");
+ is(prop.editor.valueSpan.textContent, "",
+ "'' property value is correctly set.");
+
+ info("Pressing shift-tab again to focus the previous property value");
+ let onValueFocused = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, view.styleWindow);
+ yield onValueFocused;
+
+ info("Getting the first property in the rule");
+ prop = rule.textProps[0];
+
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ is(inplaceEditor(prop.editor.valueSpan), editor,
+ "Focus should have moved to the previous property value");
+
+ info("Pressing shift-tab again to focus the property name");
+ let onNameFocused = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, view.styleWindow);
+ yield onNameFocused;
+
+ info("Removing the name and pressing shift-tab to focus the selector");
+ let onNameDeleted = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow);
+ EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, view.styleWindow);
+ yield onNameDeleted;
+
+ newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "background-color"
+ });
+ is(newValue, "", "background-color should have been unset.");
+
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ is(inplaceEditor(rule.editor.selectorText), editor,
+ "Focus should have moved to the selector text.");
+ is(rule.textProps.length, 0,
+ "All properties should have been removed.");
+ ok(!rule.editor.propertyList.hasChildNodes(),
+ "Should not have any properties.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_01.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_01.js
new file mode 100644
index 000000000..6f4c49e20
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_01.js
@@ -0,0 +1,93 @@
+/* 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 adding new properties via the inplace-editors in the rule
+// view.
+// FIXME: some of the inplace-editor focus/blur/commit/revert stuff
+// should be factored out in head.js
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ color: red;
+ background-color: blue;
+ }
+ .testclass, .unmatched {
+ background-color: green;
+ }
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>
+ <div id="testid2">Styled Node</div>
+`;
+
+var BACKGROUND_IMAGE_URL = 'url("' + URL_ROOT + 'doc_test_image.png")';
+
+var TEST_DATA = [
+ { name: "border-color", value: "red", isValid: true },
+ { name: "background-image", value: BACKGROUND_IMAGE_URL, isValid: true },
+ { name: "border", value: "solid 1px foo", isValid: false },
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ for (let {name, value, isValid} of TEST_DATA) {
+ yield testEditProperty(view, rule, name, value, isValid);
+ }
+});
+
+function* testEditProperty(view, rule, name, value, isValid) {
+ info("Test editing existing property name/value fields");
+
+ let doc = rule.editor.doc;
+ let prop = rule.textProps[0];
+
+ info("Focusing an existing property name in the rule-view");
+ let editor = yield focusEditableField(view, prop.editor.nameSpan, 32, 1);
+
+ is(inplaceEditor(prop.editor.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(rule.editor.element, "focus", true);
+ let onNameDone = view.once("ruleview-changed");
+ EventUtils.sendString(name + ":", doc.defaultView);
+ yield onValueFocus;
+ yield onNameDone;
+
+ // Getting the value editor after focus
+ editor = inplaceEditor(doc.activeElement);
+ input = editor.input;
+ is(inplaceEditor(prop.editor.valueSpan), editor, "Focus moved to the value.");
+
+ info("Entering a new value, including ; to commit and blur the value");
+ let onValueDone = view.once("ruleview-changed");
+ let onBlur = once(input, "blur");
+ EventUtils.sendString(value + ";", doc.defaultView);
+ yield onBlur;
+ yield onValueDone;
+
+ is(prop.editor.isValid(), isValid,
+ value + " is " + isValid ? "valid" : "invalid");
+
+ info("Checking that the style property was changed on the content page");
+ let propValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name
+ });
+
+ if (isValid) {
+ is(propValue, value, name + " should have been set.");
+ } else {
+ isnot(propValue, value, name + " shouldn't have been set.");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js
new file mode 100644
index 000000000..7e6315236
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js
@@ -0,0 +1,133 @@
+/* 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
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ background-color: blue;
+ }
+ .testclass, .unmatched {
+ background-color: green;
+ }
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>
+ <div id="testid2">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ yield testEditProperty(inspector, view);
+ yield testDisableProperty(inspector, view);
+ yield testPropertyStillMarkedDirty(inspector, view);
+});
+
+function* testEditProperty(inspector, ruleView) {
+ let idRule = getRuleViewRuleEditor(ruleView, 1).rule;
+ let prop = idRule.textProps[0];
+
+ let editor = yield focusEditableField(ruleView, prop.editor.nameSpan);
+ let input = editor.input;
+ is(inplaceEditor(prop.editor.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.styleWindow);
+ input.select();
+
+ info("Entering property name \"border-color\" followed by a colon to " +
+ "focus the value");
+ let onNameDone = ruleView.once("ruleview-changed");
+ let onFocus = once(idRule.editor.element, "focus", true);
+ EventUtils.sendString("border-color:", ruleView.styleWindow);
+ yield onFocus;
+ yield onNameDone;
+
+ info("Verifying that the focused field is the valueSpan");
+ editor = inplaceEditor(ruleView.styleDocument.activeElement);
+ input = editor.input;
+ is(inplaceEditor(prop.editor.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");
+ // Use sendChar() to pass each character as a string so that we can test
+ // prop.editor.warning.hidden after each character.
+ for (let ch of "red;") {
+ let onPreviewDone = ruleView.once("ruleview-changed");
+ EventUtils.sendChar(ch, ruleView.styleWindow);
+ ruleView.throttle.flush();
+ yield onPreviewDone;
+ is(prop.editor.warning.hidden, true,
+ "warning triangle is hidden or shown as appropriate");
+ }
+ yield onBlur;
+
+ let newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "border-color"
+ });
+ is(newValue, "red", "border-color should have been set.");
+
+ ruleView.styleDocument.activeElement.blur();
+ yield addProperty(ruleView, 1, "color", "red", ";");
+
+ 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 idRule = getRuleViewRuleEditor(ruleView, 1).rule;
+ let prop = idRule.textProps[0];
+
+ info("Disabling a property");
+ yield togglePropStatus(ruleView, prop);
+
+ 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");
+ yield togglePropStatus(ruleView, prop);
+
+ 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/devtools/client/inspector/rules/test/browser_rules_edit-property_03.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_03.js
new file mode 100644
index 000000000..a5771b41e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_03.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";
+
+// Tests that emptying out an existing value removes the property and
+// doesn't cause any other issues. See also Bug 1150780.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ color: red;
+ background-color: blue;
+ font-size: 12px;
+ }
+ .testclass, .unmatched {
+ background-color: green;
+ }
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>
+ <div id="testid2">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[1].editor;
+
+ yield focusEditableField(view, propEditor.valueSpan);
+
+ info("Deleting all the text out of a value field");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element,
+ ["DELETE", "RETURN"]);
+ yield onRuleViewChanged;
+
+ info("Pressing enter a couple times to cycle through editors");
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["RETURN"]);
+ onRuleViewChanged = view.once("ruleview-changed");
+ yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["RETURN"]);
+ yield onRuleViewChanged;
+
+ isnot(ruleEditor.rule.textProps[1].editor.nameSpan.style.display, "none",
+ "The name span is visible");
+ is(ruleEditor.rule.textProps.length, 2, "Correct number of props");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_04.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_04.js
new file mode 100644
index 000000000..7460db4cd
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_04.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";
+
+// Tests that a disabled property remains disabled when the escaping out of
+// the property editor.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[0];
+
+ info("Disabling a property");
+ yield togglePropStatus(view, prop);
+
+ let newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "background-color"
+ });
+ is(newValue, "", "background-color should have been unset.");
+
+ yield testEditDisableProperty(view, rule, prop, "name", "VK_ESCAPE");
+ yield testEditDisableProperty(view, rule, prop, "value", "VK_ESCAPE");
+ yield testEditDisableProperty(view, rule, prop, "value", "VK_TAB");
+ yield testEditDisableProperty(view, rule, prop, "value", "VK_RETURN");
+});
+
+function* testEditDisableProperty(view, rule, prop, fieldType, commitKey) {
+ let field = fieldType === "name" ? prop.editor.nameSpan
+ : prop.editor.valueSpan;
+
+ let editor = yield focusEditableField(view, field);
+
+ ok(!prop.editor.element.classList.contains("ruleview-overridden"),
+ "property is not overridden.");
+ is(prop.editor.enable.style.visibility, "hidden",
+ "property enable checkbox is hidden.");
+
+ let newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "background-color"
+ });
+ is(newValue, "", "background-color should remain unset.");
+
+ let onChangeDone;
+ if (fieldType === "value") {
+ onChangeDone = view.once("ruleview-changed");
+ }
+
+ let onBlur = once(editor.input, "blur");
+ EventUtils.synthesizeKey(commitKey, {}, view.styleWindow);
+ yield onBlur;
+ yield onChangeDone;
+
+ ok(!prop.enabled, "property is disabled.");
+ ok(prop.editor.element.classList.contains("ruleview-overridden"),
+ "property is overridden.");
+ is(prop.editor.enable.style.visibility, "visible",
+ "property enable checkbox is visible.");
+ ok(!prop.editor.enable.getAttribute("checked"),
+ "property enable checkbox is not checked.");
+
+ newValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: "background-color"
+ });
+ is(newValue, "", "background-color should remain unset.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_05.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_05.js
new file mode 100644
index 000000000..3d37c81d5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_05.js
@@ -0,0 +1,77 @@
+/* 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 a disabled property is re-enabled if the property name or value is
+// modified
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[0];
+
+ info("Disabling background-color property");
+ yield togglePropStatus(view, prop);
+
+ let newValue = yield getRulePropertyValue("background-color");
+ is(newValue, "", "background-color should have been unset.");
+
+ info("Entering a new property name, including : to commit and " +
+ "focus the value");
+
+ yield focusEditableField(view, prop.editor.nameSpan);
+ let onNameDone = view.once("ruleview-changed");
+ EventUtils.sendString("border-color:", view.styleWindow);
+ yield onNameDone;
+
+ info("Escape editing the property value");
+ let onValueDone = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ yield onValueDone;
+
+ newValue = yield getRulePropertyValue("border-color");
+ is(newValue, "blue", "border-color should have been set.");
+
+ ok(prop.enabled, "border-color property is enabled.");
+ ok(!prop.editor.element.classList.contains("ruleview-overridden"),
+ "border-color is not overridden");
+
+ info("Disabling border-color property");
+ yield togglePropStatus(view, prop);
+
+ newValue = yield getRulePropertyValue("border-color");
+ is(newValue, "", "border-color should have been unset.");
+
+ info("Enter a new property value for the border-color property");
+ yield setProperty(view, prop, "red");
+
+ newValue = yield getRulePropertyValue("border-color");
+ is(newValue, "red", "new border-color should have been set.");
+
+ ok(prop.enabled, "border-color property is enabled.");
+ ok(!prop.editor.element.classList.contains("ruleview-overridden"),
+ "border-color is not overridden");
+});
+
+function* getRulePropertyValue(name) {
+ let propValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: name
+ });
+ return propValue;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_06.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_06.js
new file mode 100644
index 000000000..95211f1d0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_06.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";
+
+// Tests that editing a property's priority is behaving correctly, and disabling
+// and editing the property will re-enable the property.
+
+const TEST_URI = `
+ <style type='text/css'>
+ body {
+ background-color: green !important;
+ }
+ body {
+ background-color: red;
+ }
+ </style>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("body", inspector);
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[0];
+
+ is((yield getComputedStyleProperty("body", null, "background-color")),
+ "rgb(0, 128, 0)", "green background color is set.");
+
+ yield setProperty(view, prop, "red !important");
+
+ is(prop.editor.valueSpan.textContent, "red !important",
+ "'red !important' property value is correctly set.");
+ is((yield getComputedStyleProperty("body", null, "background-color")),
+ "rgb(255, 0, 0)", "red background color is set.");
+
+ info("Disabling red background color property");
+ yield togglePropStatus(view, prop);
+
+ is((yield getComputedStyleProperty("body", null, "background-color")),
+ "rgb(0, 128, 0)", "green background color is set.");
+
+ yield setProperty(view, prop, "red");
+
+ is(prop.editor.valueSpan.textContent, "red",
+ "'red' property value is correctly set.");
+ ok(prop.enabled, "red background-color property is enabled.");
+ is((yield getComputedStyleProperty("body", null, "background-color")),
+ "rgb(0, 128, 0)", "green background color is set.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_07.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_07.js
new file mode 100644
index 000000000..40314819f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_07.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";
+
+// Tests that adding multiple values will enable the property even if the
+// property does not change, and that the extra values are added correctly.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: #f00;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[0];
+
+ info("Disabling red background color property");
+ yield togglePropStatus(view, prop);
+ ok(!prop.enabled, "red background-color property is disabled.");
+
+ let editor = yield focusEditableField(view, prop.editor.valueSpan);
+ let onDone = view.once("ruleview-changed");
+ editor.input.value = "red; color: red;";
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onDone;
+
+ is(prop.editor.valueSpan.textContent, "red",
+ "'red' property value is correctly set.");
+ ok(prop.enabled, "red background-color property is enabled.");
+ is((yield getComputedStyleProperty("#testid", null, "background-color")),
+ "rgb(255, 0, 0)", "red background color is set.");
+
+ let propEditor = rule.textProps[1].editor;
+ is(propEditor.nameSpan.textContent, "color",
+ "new 'color' property name is correctly set.");
+ is(propEditor.valueSpan.textContent, "red",
+ "new 'red' property value is correctly set.");
+ is((yield getComputedStyleProperty("#testid", null, "color")),
+ "rgb(255, 0, 0)", "red color is set.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_08.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_08.js
new file mode 100644
index 000000000..1becd40d9
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_08.js
@@ -0,0 +1,57 @@
+/* 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 renaming a property works.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ color: #FFF;
+ }
+ </style>
+ <div style='color: red' id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Get the color property editor");
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+ is(ruleEditor.rule.textProps[0].name, "color");
+
+ info("Focus the property name field");
+ yield focusEditableField(ruleEditor.ruleView, propEditor.nameSpan, 32, 1);
+
+ info("Rename the property to background-color");
+ // Expect 3 events: the value editor being focused, the ruleview-changed event
+ // which signals that the new value has been previewed (fires once when the
+ // value gets focused), and the markupmutation event since we're modifying an
+ // inline style.
+ let onValueFocus = once(ruleEditor.element, "focus", true);
+ let onRuleViewChanged = ruleEditor.ruleView.once("ruleview-changed");
+ let onMutation = inspector.once("markupmutation");
+ EventUtils.sendString("background-color:", ruleEditor.doc.defaultView);
+ yield onValueFocus;
+ yield onRuleViewChanged;
+ yield onMutation;
+
+ is(ruleEditor.rule.textProps[0].name, "background-color");
+ yield waitForComputedStyleProperty("#testid", null, "background-color",
+ "rgb(255, 0, 0)");
+
+ is((yield getComputedStyleProperty("#testid", null, "color")),
+ "rgb(255, 255, 255)", "color is white");
+
+ // The value field is still focused. Blur it now and wait for the
+ // ruleview-changed event to avoid pending requests.
+ onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield onRuleViewChanged;
+});
+
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_09.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_09.js
new file mode 100644
index 000000000..51f714021
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_09.js
@@ -0,0 +1,69 @@
+/* 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 newProperty editor is only created if no other editor was
+// previously displayed.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testClickOnEmptyAreaToCloseEditor(inspector, view);
+});
+
+function synthesizeMouseOnEmptyArea(ruleEditor, view) {
+ // any text property editor will do
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+ let valueContainer = propEditor.valueContainer;
+ let valueRect = valueContainer.getBoundingClientRect();
+ // click right next to the ";" at the end of valueContainer
+ EventUtils.synthesizeMouse(valueContainer, valueRect.width + 1, 1, {},
+ view.styleWindow);
+}
+
+function* testClickOnEmptyAreaToCloseEditor(inspector, view) {
+ // Start at the beginning: start to add a rule to the element's style
+ // declaration, add some text, then press escape.
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+
+ info("Create a property value editor");
+ let editor = yield focusEditableField(view, propEditor.valueSpan);
+ ok(editor.input, "The inplace-editor field is ready");
+
+ info("Close the property value editor by clicking on an empty area " +
+ "in the rule editor");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ let onBlur = once(editor.input, "blur");
+ synthesizeMouseOnEmptyArea(ruleEditor, view);
+ yield onBlur;
+ yield onRuleViewChanged;
+ ok(!view.isEditing, "No inplace editor should be displayed in the ruleview");
+
+ info("Create new newProperty editor by clicking again on the empty area");
+ let onFocus = once(ruleEditor.element, "focus", true);
+ synthesizeMouseOnEmptyArea(ruleEditor, view);
+ yield onFocus;
+ editor = inplaceEditor(ruleEditor.element.ownerDocument.activeElement);
+ is(inplaceEditor(ruleEditor.newPropSpan), editor,
+ "New property editor was created");
+
+ info("Close the newProperty editor by clicking again on the empty area");
+ onBlur = once(editor.input, "blur");
+ synthesizeMouseOnEmptyArea(ruleEditor, view);
+ yield onBlur;
+
+ ok(!view.isEditing, "No inplace editor should be displayed in the ruleview");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js
new file mode 100644
index 000000000..1846df60d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js
@@ -0,0 +1,88 @@
+/* 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 ruleview inplace-editor is not blurred when clicking on the ruleview
+// container scrollbar.
+
+const TEST_URI = `
+ <style type="text/css">
+ div.testclass {
+ color: black;
+ }
+ .a {
+ color: #aaa;
+ }
+ .b {
+ color: #bbb;
+ }
+ .c {
+ color: #ccc;
+ }
+ .d {
+ color: #ddd;
+ }
+ .e {
+ color: #eee;
+ }
+ .f {
+ color: #fff;
+ }
+ </style>
+ <div class="testclass a b c d e f">Styled Node</div>
+`;
+
+add_task(function* () {
+ info("Toolbox height should be small enough to force scrollbars to appear");
+ yield new Promise(done => {
+ let options = {"set": [
+ ["devtools.toolbox.footer.height", 200],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".testclass", inspector);
+
+ info("Check we have an overflow on the ruleview container.");
+ let container = view.element;
+ let hasScrollbar = container.offsetHeight < container.scrollHeight;
+ ok(hasScrollbar, "The rule view container should have a vertical scrollbar.");
+
+ info("Focusing an existing selector name in the rule-view.");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "The selector editor is focused.");
+
+ info("Click on the scrollbar element.");
+ yield clickOnRuleviewScrollbar(view);
+
+ is(editor.input, view.styleDocument.activeElement,
+ "The editor input should still be focused.");
+
+ info("Check a new value can still be committed in the editable field");
+ let newValue = ".testclass.a.b.c.d.e.f";
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Enter new value and commit.");
+ editor.input.value = newValue;
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+ ok(getRuleViewRule(view, newValue), "Rule with '" + newValue + " 'exists.");
+});
+
+function* clickOnRuleviewScrollbar(view) {
+ let container = view.element.parentNode;
+ let onScroll = once(container, "scroll");
+ let rect = container.getBoundingClientRect();
+ // click 5 pixels before the bottom-right corner should hit the scrollbar
+ EventUtils.synthesizeMouse(container, rect.width - 5, rect.height - 5,
+ {}, view.styleWindow);
+ yield onScroll;
+
+ ok(true, "The rule view container scrolled after clicking on the scrollbar.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector-click.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click.js
new file mode 100644
index 000000000..7a3b6d467
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click.js
@@ -0,0 +1,63 @@
+/* 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 remains available and focused after clicking
+// in its input.
+
+const TEST_URI = `
+ <style type="text/css">
+ .testclass {
+ text-align: center;
+ }
+ </style>
+ <div class="testclass">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".testclass", inspector);
+ yield testClickOnSelectorEditorInput(view);
+});
+
+function* testClickOnSelectorEditorInput(view) {
+ info("Test clicking inside the selector editor input");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+ let editorInput = editor.input;
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Click inside the editor input");
+ let onClick = once(editorInput, "click");
+ EventUtils.synthesizeMouse(editor.input, 2, 1, {}, view.styleWindow);
+ yield onClick;
+ is(editor.input, view.styleDocument.activeElement,
+ "The editor input should still be focused");
+ ok(!ruleEditor.newPropSpan, "No newProperty editor was created");
+
+ info("Doubleclick inside the editor input");
+ let onDoubleClick = once(editorInput, "dblclick");
+ EventUtils.synthesizeMouse(editor.input, 2, 1, { clickCount: 2 },
+ view.styleWindow);
+ yield onDoubleClick;
+ is(editor.input, view.styleDocument.activeElement,
+ "The editor input should still be focused");
+ ok(!ruleEditor.newPropSpan, "No newProperty editor was created");
+
+ info("Click outside the editor input");
+ let onBlur = once(editorInput, "blur");
+ let rect = editorInput.getBoundingClientRect();
+ EventUtils.synthesizeMouse(editorInput, rect.width + 5, rect.height / 2, {},
+ view.styleWindow);
+ yield onBlur;
+
+ isnot(editorInput, view.styleDocument.activeElement,
+ "The editor input should no longer be focused");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector-commit.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector-commit.js
new file mode 100644
index 000000000..f7058371f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector-commit.js
@@ -0,0 +1,117 @@
+/* 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
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid1 {
+ text-align: center;
+ }
+ #testid2 {
+ text-align: center;
+ }
+ #testid3 {
+ }
+ </style>
+ <div id='testid1'>Styled Node</div>
+ <div id='testid2'>Styled Node</div>
+ <div id='testid3'>Styled Node</div>
+`;
+
+const TEST_DATA = [
+ {
+ node: "#testid1",
+ value: ".testclass",
+ commitKey: "VK_ESCAPE",
+ modifiers: {},
+ expected: "#testid1",
+
+ },
+ {
+ node: "#testid1",
+ value: ".testclass1",
+ commitKey: "VK_RETURN",
+ modifiers: {},
+ expected: ".testclass1"
+ },
+ {
+ node: "#testid2",
+ value: ".testclass2",
+ commitKey: "VK_TAB",
+ modifiers: {},
+ expected: ".testclass2"
+ },
+ {
+ node: "#testid3",
+ value: ".testclass3",
+ commitKey: "VK_TAB",
+ modifiers: {shiftKey: true},
+ expected: ".testclass3"
+ }
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let { inspector, view } = yield openRuleView();
+
+ 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(view, 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);
+
+ let activeElement = view.styleDocument.activeElement;
+
+ if (commitKey === "VK_ESCAPE") {
+ is(idRuleEditor.rule.selectorText, expected,
+ "Value is as expected: " + expected);
+ is(idRuleEditor.isEditing, false, "Selector is not being edited.");
+ is(idRuleEditor.selectorText, activeElement,
+ "Focus is on selector span.");
+ return;
+ }
+
+ yield once(view, "ruleview-changed");
+
+ ok(getRuleViewRule(view, expected),
+ "Rule with " + expected + " selector exists.");
+
+ if (modifiers.shiftKey) {
+ idRuleEditor = getRuleViewRuleEditor(view, 0);
+ }
+
+ let rule = idRuleEditor.rule;
+ if (rule.textProps.length > 0) {
+ is(inplaceEditor(rule.textProps[0].editor.nameSpan).input, activeElement,
+ "Focus is on the first property name span.");
+ } else {
+ is(inplaceEditor(idRuleEditor.newPropSpan).input, activeElement,
+ "Focus is on the new property span.");
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_01.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_01.js
new file mode 100644
index 000000000..af228094b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_01.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";
+
+// Testing selector inplace-editor behaviors in the rule-view
+
+const TEST_URI = `
+ <style type="text/css">
+ .testclass {
+ text-align: center;
+ }
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>
+ <span>This is a span</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ info("Selecting the test element");
+ yield selectNode("#testid", inspector);
+ yield testEditSelector(view, "span");
+
+ info("Selecting the modified element with the new rule");
+ yield selectNode("span", 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(view, 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 update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+ ok(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"),
+ "Rule with " + name + " does not match the current element.");
+}
+
+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/devtools/client/inspector/rules/test/browser_rules_edit-selector_02.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_02.js
new file mode 100644
index 000000000..503f91efa
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_02.js
@@ -0,0 +1,88 @@
+/* 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.
+
+const TEST_URI = `
+ <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>
+`;
+
+const PSEUDO_PREF = "devtools.inspector.show_pseudo_elements";
+
+add_task(function* () {
+ // Expand the pseudo-elements section by default.
+ Services.prefs.setBoolPref(PSEUDO_PREF, true);
+
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ info("Selecting the test element");
+ yield selectNode(".testclass", inspector);
+ yield testEditSelector(view, "div:nth-child(1)");
+
+ info("Selecting the modified element");
+ yield selectNode("#testid", inspector);
+ yield checkModifiedElement(view, "div:nth-child(1)");
+
+ 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");
+
+ // Reset the pseudo-elements section pref to its default value.
+ Services.prefs.clearUserPref(PSEUDO_PREF);
+});
+
+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(view, 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 update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rule.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+
+ let newRuleEditor = getRuleViewRuleEditor(view, 1) ||
+ getRuleViewRuleEditor(view, 1, 0);
+ ok(newRuleEditor.element.getAttribute("unmatched"),
+ "Rule with " + name + " does not match the current element.");
+}
+
+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/devtools/client/inspector/rules/test/browser_rules_edit-selector_03.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_03.js
new file mode 100644
index 000000000..c6834f6ee
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_03.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";
+
+// Testing selector inplace-editor behaviors in the rule-view with invalid
+// selectors
+
+const TEST_URI = `
+ <style type="text/css">
+ .testclass {
+ text-align: center;
+ }
+ </style>
+ <div class="testclass">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".testclass", inspector);
+ yield testEditSelector(view, "asd@:::!");
+});
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Entering a new selector name and committing");
+ editor.input.value = name;
+ let onRuleViewChanged = once(view, "ruleview-invalid-selector");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ is(getRuleViewRule(view, name), undefined,
+ "Rule with " + name + " selector should not exist.");
+ ok(getRuleViewRule(view, ".testclass"),
+ "Rule with .testclass selector exists.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js
new file mode 100644
index 000000000..09b6ad841
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js
@@ -0,0 +1,69 @@
+/* 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 selector highlighter is removed when modifying a selector and
+// the selector highlighter works for the newly added unmatched rule.
+
+const TEST_URI = `
+ <style type="text/css">
+ p {
+ background: red;
+ }
+ </style>
+ <p>Test the selector highlighter</p>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("p", inspector);
+
+ ok(!view.selectorHighlighter,
+ "No selectorhighlighter exist in the rule-view");
+
+ yield testSelectorHighlight(view, "p");
+ yield testEditSelector(view, "body");
+ yield testSelectorHighlight(view, "body");
+});
+
+function* testSelectorHighlight(view, name) {
+ info("Test creating selector highlighter");
+
+ info("Clicking on a selector icon");
+ let icon = getRuleViewSelectorHighlighterIcon(view, name);
+
+ let onToggled = view.once("ruleview-selectorhighlighter-toggled");
+ EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow);
+ let isVisible = yield onToggled;
+
+ ok(view.selectorHighlighter, "The selectorhighlighter instance was created");
+ ok(isVisible, "The toggle event says the highlighter is visible");
+}
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Waiting for rule view to update");
+ let onToggled = view.once("ruleview-selectorhighlighter-toggled");
+
+ info("Entering a new selector name and committing");
+ editor.input.value = name;
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ let isVisible = yield onToggled;
+
+ ok(!view.highlighters.selectorHighlighterShown,
+ "The selectorHighlighterShown instance was removed");
+ ok(!isVisible, "The toggle event says the highlighter is not visible");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_05.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_05.js
new file mode 100644
index 000000000..cd996b4b0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_05.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 that adding a new property of an unmatched rule works properly.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ }
+ .testclass {
+ background-color: white;
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+ <span class="testclass">This is a span</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ info("Selecting the test element");
+ yield selectNode("#testid", inspector);
+ yield testEditSelector(view, "span");
+ yield testAddProperty(view);
+
+ info("Selecting the modified element with the new rule");
+ yield selectNode("span", inspector);
+ yield checkModifiedElement(view, "span");
+});
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ is(inplaceEditor(ruleEditor.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 update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+ ok(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"),
+ "Rule with " + name + " does not match the current element.");
+
+ // Escape the new property editor after editing the selector
+ let onBlur = once(view.styleDocument.activeElement, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ yield onBlur;
+}
+
+function* checkModifiedElement(view, name) {
+ is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+}
+
+function* testAddProperty(view) {
+ info("Test creating a new property");
+ let textProp = yield addProperty(view, 1, "text-align", "center");
+
+ is(textProp.value, "center", "Text prop should have been changed.");
+ ok(!textProp.overridden, "Property should not be overridden");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_06.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_06.js
new file mode 100644
index 000000000..7d782a309
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_06.js
@@ -0,0 +1,76 @@
+/* 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 unmatched
+// selectors
+
+const TEST_URI = `
+ <style type="text/css">
+ .testclass {
+ text-align: center;
+ }
+ div {
+ }
+ </style>
+ <div class="testclass">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".testclass", inspector);
+ yield testEditClassSelector(view);
+ yield testEditDivSelector(view);
+});
+
+function* testEditClassSelector(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ editor.input.value = "body";
+ let onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ // Get the new rule editor that replaced the original
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+
+ info("Check that the correct rules are visible");
+ is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
+ ok(ruleEditor.element.getAttribute("unmatched"), "Rule editor is unmatched.");
+ is(getRuleViewRule(view, ".testclass"), undefined,
+ "Rule with .testclass selector should not exist.");
+ ok(getRuleViewRule(view, "body"),
+ "Rule with body selector exists.");
+ is(inplaceEditor(propEditor.nameSpan),
+ inplaceEditor(view.styleDocument.activeElement),
+ "Focus should have moved to the property name.");
+}
+
+function* testEditDivSelector(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 2);
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ editor.input.value = "asdf";
+ let onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ // Get the new rule editor that replaced the original
+ ruleEditor = getRuleViewRuleEditor(view, 2);
+
+ info("Check that the correct rules are visible");
+ is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
+ ok(ruleEditor.element.getAttribute("unmatched"), "Rule editor is unmatched.");
+ is(getRuleViewRule(view, "div"), undefined,
+ "Rule with div selector should not exist.");
+ ok(getRuleViewRule(view, "asdf"),
+ "Rule with asdf selector exists.");
+ is(inplaceEditor(ruleEditor.newPropSpan),
+ inplaceEditor(view.styleDocument.activeElement),
+ "Focus should have moved to the property name.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_07.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_07.js
new file mode 100644
index 000000000..81c7aad72
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_07.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule view overridden search filter does not appear for an
+// unmatched rule.
+
+const TEST_URI = `
+ <style type="text/css">
+ div {
+ height: 0px;
+ }
+ #testid {
+ height: 1px;
+ }
+ .testclass {
+ height: 10px;
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+ <span class="testclass">This is a span</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("#testid", inspector);
+ yield testEditSelector(view, "span");
+});
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Entering a new selector name and committing");
+ editor.input.value = name;
+
+ info("Entering the commit key");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ // Get the new rule editor that replaced the original
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+ let rule = ruleEditor.rule;
+ let textPropEditor = rule.textProps[0].editor;
+
+ is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+ ok(ruleEditor.element.getAttribute("unmatched"),
+ "Rule with " + name + " does not match the current element.");
+ ok(textPropEditor.filterProperty.hidden, "Overridden search is hidden.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_08.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_08.js
new file mode 100644
index 000000000..33382e0de
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_08.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 reverting a selector edit does the right thing.
+// Bug 1241046.
+
+const TEST_URI = `
+ <style type="text/css">
+ span {
+ color: chartreuse;
+ }
+ </style>
+ <span>
+ <div id="testid" class="testclass">Styled Node</div>
+ </span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ info("Selecting the test element");
+ yield selectNode("#testid", inspector);
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 2);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, idRuleEditor.selectorText);
+
+ is(inplaceEditor(idRuleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Entering a new selector name and committing");
+ editor.input.value = "pre";
+
+ info("Waiting for rule view to update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ info("Re-focusing the selector name in the rule-view");
+ idRuleEditor = getRuleViewRuleEditor(view, 2);
+ editor = yield focusEditableField(view, idRuleEditor.selectorText);
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, "pre"), "Rule with pre selector exists.");
+ is(getRuleViewRuleEditor(view, 2).element.getAttribute("unmatched"),
+ "true",
+ "Rule with pre does not match the current element.");
+
+ // Now change it back.
+ info("Re-entering original selector name and committing");
+ editor.input.value = "span";
+
+ info("Waiting for rule view to update");
+ onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, "span"), "Rule with span selector exists.");
+ is(getRuleViewRuleEditor(view, 2).element.getAttribute("unmatched"),
+ "false", "Rule with span matches the current element.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_09.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_09.js
new file mode 100644
index 000000000..a18ddc5ef
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_09.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";
+
+// Tests that editing a selector to an unmatched rule does set up the correct
+// property on the rule, and that settings property in said rule does not
+// lead to overriding properties from matched rules.
+// Test that having a rule with both matched and unmatched selectors does work
+// correctly.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ color: black;
+ }
+ .testclass {
+ background-color: white;
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+ <span class="testclass">This is a span</span>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("#testid", inspector);
+ yield testEditSelector(view, "span");
+ yield testAddImportantProperty(view);
+ yield testAddMatchedRule(view, "span, div");
+});
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ is(inplaceEditor(ruleEditor.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 update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+ ok(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"),
+ "Rule with " + name + " does not match the current element.");
+
+ // Escape the new property editor after editing the selector
+ let onBlur = once(view.styleDocument.activeElement, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ yield onBlur;
+}
+
+function* testAddImportantProperty(view) {
+ info("Test creating a new property with !important");
+ let textProp = yield addProperty(view, 1, "color", "red !important");
+
+ is(textProp.value, "red", "Text prop should have been changed.");
+ is(textProp.priority, "important",
+ "Text prop has an \"important\" priority.");
+ ok(!textProp.overridden, "Property should not be overridden");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let prop = ruleEditor.rule.textProps[0];
+ ok(!prop.overridden,
+ "Existing property on matched rule should not be overridden");
+}
+
+function* testAddMatchedRule(view, name) {
+ info("Test adding a matching selector");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ is(inplaceEditor(ruleEditor.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 update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ is(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"), "false",
+ "Rule with " + name + " does match the current element.");
+
+ // Escape the new property editor after editing the selector
+ let onBlur = once(view.styleDocument.activeElement, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ yield onBlur;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_10.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_10.js
new file mode 100644
index 000000000..d878dd516
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_10.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";
+
+// Regression test for bug 1293616: make sure that editing a selector
+// keeps the rule in the proper position.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid span, #testid p {
+ background: aqua;
+ }
+ span {
+ background: fuchsia;
+ }
+ </style>
+ <div id="testid">
+ <span class="pickme">
+ Styled Node
+ </span>
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".pickme", inspector);
+ yield testEditSelector(view);
+});
+
+function* testEditSelector(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ editor.input.value = "#testid span";
+ let onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ // Escape the new property editor after editing the selector
+ let onBlur = once(view.styleDocument.activeElement, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ yield onBlur;
+
+ // Get the new rule editor that replaced the original
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Check that the correct rules are visible");
+ is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
+ is(ruleEditor.element.getAttribute("unmatched"), "false", "Rule editor is matched.");
+
+ let props = ruleEditor.rule.textProps;
+ is(props.length, 1, "Rule has correct number of properties");
+ is(props[0].name, "background", "Found background property");
+ ok(!props[0].overridden, "Background property is not overridden");
+
+ ruleEditor = getRuleViewRuleEditor(view, 2);
+ props = ruleEditor.rule.textProps;
+ is(props.length, 1, "Rule has correct number of properties");
+ is(props[0].name, "background", "Found background property");
+ ok(props[0].overridden, "Background property is overridden");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_11.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_11.js
new file mode 100644
index 000000000..9a1bdc8fa
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_11.js
@@ -0,0 +1,69 @@
+/* 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";
+
+// Regression test for bug 1293616, where editing a selector should
+// change the relative priority of the rule.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ background: aqua;
+ }
+ .pickme {
+ background: seagreen;
+ }
+ span {
+ background: fuchsia;
+ }
+ </style>
+ <div>
+ <span id="testid" class="pickme">
+ Styled Node
+ </span>
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".pickme", inspector);
+ yield testEditSelector(view);
+});
+
+function* testEditSelector(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ editor.input.value = ".pickme";
+ let onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ // Escape the new property editor after editing the selector
+ let onBlur = once(view.styleDocument.activeElement, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ yield onBlur;
+
+ // Get the new rule editor that replaced the original
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Check that the correct rules are visible");
+ is(view._elementStyle.rules.length, 4, "Should have 4 rules.");
+ is(ruleEditor.element.getAttribute("unmatched"), "false", "Rule editor is matched.");
+
+ let props = ruleEditor.rule.textProps;
+ is(props.length, 1, "Rule has correct number of properties");
+ is(props[0].name, "background", "Found background property");
+ is(props[0].value, "aqua", "Background property is aqua");
+ ok(props[0].overridden, "Background property is overridden");
+
+ ruleEditor = getRuleViewRuleEditor(view, 2);
+ props = ruleEditor.rule.textProps;
+ is(props.length, 1, "Rule has correct number of properties");
+ is(props[0].name, "background", "Found background property");
+ is(props[0].value, "seagreen", "Background property is seagreen");
+ ok(!props[0].overridden, "Background property is not overridden");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js
new file mode 100644
index 000000000..dbf59cba9
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js
@@ -0,0 +1,107 @@
+/* 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 clicking on swatch-preceeded value while editing the property name
+// will result in editing the property value. Also tests that the value span is updated
+// only if the property name has changed. See also Bug 1248274.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ color: red;
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("#testid", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+
+ yield testColorValueSpanClickWithoutNameChange(propEditor, view);
+ yield testColorValueSpanClickAfterNameChange(propEditor, view);
+});
+
+function* testColorValueSpanClickWithoutNameChange(propEditor, view) {
+ info("Test click on color span while focusing property name editor");
+ let colorSpan = propEditor.valueSpan.querySelector(".ruleview-color");
+
+ info("Focus the color name span");
+ yield focusEditableField(view, propEditor.nameSpan);
+ let editor = inplaceEditor(propEditor.doc.activeElement);
+
+ // We add a click event to make sure the color span won't be cleared
+ // on nameSpan blur (which would lead to the click event not being triggered)
+ let onColorSpanClick = once(colorSpan, "click");
+
+ // The property-value-updated is emitted when the valueSpan markup is being
+ // re-populated, which should not be the case when not modifying the property name
+ let onPropertyValueUpdated = function () {
+ ok(false, "The \"property-value-updated\" should not be emitted");
+ };
+ view.on("property-value-updated", onPropertyValueUpdated);
+
+ info("blur propEditor.nameSpan by clicking on the color span");
+ EventUtils.synthesizeMouse(colorSpan, 1, 1, {}, propEditor.doc.defaultView);
+
+ info("wait for the click event on the color span");
+ yield onColorSpanClick;
+ ok(true, "Expected click event was emitted");
+
+ editor = inplaceEditor(propEditor.doc.activeElement);
+ is(inplaceEditor(propEditor.valueSpan), editor,
+ "The property value editor got focused");
+
+ // We remove this listener in order to not cause unwanted conflict in the next test
+ view.off("property-value-updated", onPropertyValueUpdated);
+
+ info("blur valueSpan editor to trigger ruleview-changed event and prevent " +
+ "having pending request");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ editor.input.blur();
+ yield onRuleViewChanged;
+}
+
+function* testColorValueSpanClickAfterNameChange(propEditor, view) {
+ info("Test click on color span after property name change");
+ let colorSpan = propEditor.valueSpan.querySelector(".ruleview-color");
+
+ info("Focus the color name span");
+ yield focusEditableField(view, propEditor.nameSpan);
+ let editor = inplaceEditor(propEditor.doc.activeElement);
+
+ info("Modify the property to border-color to trigger the " +
+ "property-value-updated event");
+ editor.input.value = "border-color";
+
+ let onRuleViewChanged = view.once("ruleview-changed");
+ let onPropertyValueUpdate = view.once("property-value-updated");
+
+ info("blur propEditor.nameSpan by clicking on the color span");
+ EventUtils.synthesizeMouse(colorSpan, 1, 1, {}, propEditor.doc.defaultView);
+
+ info("wait for ruleview-changed event to be triggered to prevent pending requests");
+ yield onRuleViewChanged;
+
+ info("wait for the property value to be updated");
+ yield onPropertyValueUpdate;
+ ok(true, "Expected \"property-value-updated\" event was emitted");
+
+ editor = inplaceEditor(propEditor.doc.activeElement);
+ is(inplaceEditor(propEditor.valueSpan), editor,
+ "The property value editor got focused");
+
+ info("blur valueSpan editor to trigger ruleview-changed event and prevent " +
+ "having pending request");
+ onRuleViewChanged = view.once("ruleview-changed");
+ editor.input.blur();
+ yield onRuleViewChanged;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.js b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.js
new file mode 100644
index 000000000..372ed7477
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.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 hitting shift + click on color swatch while editing the property
+// name will only change the color unit and not lead to edit the property value.
+// See also Bug 1248274.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ color: red;
+ background: linear-gradient(
+ 90deg,
+ rgb(183,222,237),
+ rgb(33,180,226),
+ rgb(31,170,217),
+ rgba(200,170,140,0.5));
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ info("Test shift + click on color swatch while editing property name");
+
+ yield selectNode("#testid", inspector);
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[1].editor;
+ let swatchSpan = propEditor.valueSpan.querySelectorAll(".ruleview-colorswatch")[2];
+
+ info("Focus the background name span");
+ yield focusEditableField(view, propEditor.nameSpan);
+ let editor = inplaceEditor(propEditor.doc.activeElement);
+
+ info("Modify the property to background-image to trigger the " +
+ "property-value-updated event");
+ editor.input.value = "background-image";
+
+ let onPropertyValueUpdate = view.once("property-value-updated");
+ let onSwatchUnitChange = swatchSpan.once("unit-change");
+ let onRuleViewChanged = view.once("ruleview-changed");
+
+ info("blur propEditor.nameSpan by clicking on the color swatch");
+ EventUtils.synthesizeMouseAtCenter(swatchSpan, {shiftKey: true},
+ propEditor.doc.defaultView);
+
+ info("wait for ruleview-changed event to be triggered to prevent pending requests");
+ yield onRuleViewChanged;
+
+ info("wait for the color unit to change");
+ yield onSwatchUnitChange;
+ ok(true, "the color unit was changed");
+
+ info("wait for the property value to be updated");
+ yield onPropertyValueUpdate;
+
+ ok(!inplaceEditor(propEditor.valueSpan), "The inplace editor wasn't shown " +
+ "as a result of the color swatch shift + click");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.js b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.js
new file mode 100644
index 000000000..041a45a3e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.js
@@ -0,0 +1,69 @@
+/* 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 clicking on color swatch while editing the property name
+// will show the color tooltip with the correct value. See also Bug 1248274.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ color: red;
+ background: linear-gradient(
+ 90deg,
+ rgb(183,222,237),
+ rgb(33,180,226),
+ rgb(31,170,217),
+ rgba(200,170,140,0.5));
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ info("Test click on color swatch while editing property name");
+
+ yield selectNode("#testid", inspector);
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[1].editor;
+ let swatchSpan = propEditor.valueSpan.querySelectorAll(
+ ".ruleview-colorswatch")[3];
+ let colorPicker = view.tooltips.colorPicker;
+
+ info("Focus the background name span");
+ yield focusEditableField(view, propEditor.nameSpan);
+ let editor = inplaceEditor(propEditor.doc.activeElement);
+
+ info("Modify the background property to background-image to trigger the " +
+ "property-value-updated event");
+ editor.input.value = "background-image";
+
+ let onRuleViewChanged = view.once("ruleview-changed");
+ let onPropertyValueUpdate = view.once("property-value-updated");
+ let onReady = colorPicker.once("ready");
+
+ info("blur propEditor.nameSpan by clicking on the color swatch");
+ EventUtils.synthesizeMouseAtCenter(swatchSpan, {},
+ propEditor.doc.defaultView);
+
+ info("wait for ruleview-changed event to be triggered to prevent pending requests");
+ yield onRuleViewChanged;
+
+ info("wait for the property value to be updated");
+ yield onPropertyValueUpdate;
+
+ info("wait for the color picker to be shown");
+ yield onReady;
+
+ ok(true, "The color picker was shown on click of the color swatch");
+ ok(!inplaceEditor(propEditor.valueSpan),
+ "The inplace editor wasn't shown as a result of the color swatch click");
+
+ let spectrum = colorPicker.spectrum;
+ is(spectrum.rgb, "200,170,140,0.5", "The correct color picker was shown");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_04.js b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_04.js
new file mode 100644
index 000000000..fa4d8e6e2
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_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";
+
+// Tests that clicking on a property's value URL while editing the property name
+// will open the link in a new tab. See also Bug 1248274.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ background: url("chrome://global/skin/icons/warning-64.png"), linear-gradient(white, #F06 400px);
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ info("Test click on background-image url while editing property name");
+
+ yield selectNode("#testid", inspector);
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+ let anchor = propEditor.valueSpan.querySelector(".ruleview-propertyvalue .theme-link");
+
+ info("Focus the background name span");
+ yield focusEditableField(view, propEditor.nameSpan);
+ let editor = inplaceEditor(propEditor.doc.activeElement);
+
+ info("Modify the property to background to trigger the " +
+ "property-value-updated event");
+ editor.input.value = "background-image";
+
+ let onRuleViewChanged = view.once("ruleview-changed");
+ let onPropertyValueUpdate = view.once("property-value-updated");
+ let onTabOpened = waitForTab();
+
+ info("blur propEditor.nameSpan by clicking on the link");
+ // The url can be wrapped across multiple lines, and so we click the lower left corner
+ // of the anchor to make sure to target the link.
+ let rect = anchor.getBoundingClientRect();
+ EventUtils.synthesizeMouse(anchor, 2, rect.height - 2, {}, propEditor.doc.defaultView);
+
+ info("wait for ruleview-changed event to be triggered to prevent pending requests");
+ yield onRuleViewChanged;
+
+ info("wait for the property value to be updated");
+ yield onPropertyValueUpdate;
+
+ info("wait for the image to be open in a new tab");
+ let tab = yield onTabOpened;
+ ok(true, "A new tab opened");
+
+ is(tab.linkedBrowser.currentURI.spec, anchor.href,
+ "The URL for the new tab is correct");
+
+ gBrowser.removeTab(tab);
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_01.js b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_01.js
new file mode 100644
index 000000000..c9c7cd3d2
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_01.js
@@ -0,0 +1,94 @@
+/* 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 correct editable fields are focused when tabbing and entering
+// through the rule view.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ color: red;
+ margin: 0;
+ padding: 0;
+ }
+ div {
+ border-color: red
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testEditableFieldFocus(inspector, view, "VK_RETURN");
+ yield testEditableFieldFocus(inspector, view, "VK_TAB");
+});
+
+function* testEditableFieldFocus(inspector, view, commitKey) {
+ info("Click on the selector of the inline style ('element')");
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let onFocus = once(ruleEditor.element, "focus", true);
+ ruleEditor.selectorText.click();
+ yield onFocus;
+ assertEditor(view, ruleEditor.newPropSpan,
+ "Focus should be in the element property span");
+
+ info("Focus the next field with " + commitKey);
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+ yield focusNextEditableField(view, ruleEditor, commitKey);
+ assertEditor(view, ruleEditor.selectorText,
+ "Focus should have moved to the next rule selector");
+
+ for (let i = 0; i < ruleEditor.rule.textProps.length; i++) {
+ let textProp = ruleEditor.rule.textProps[i];
+ let propEditor = textProp.editor;
+
+ info("Focus the next field with " + commitKey);
+ // Expect a ruleview-changed event if we are moving from a property value
+ // to the next property name (which occurs after the first iteration, as for
+ // i=0, the previous field is the selector).
+ let onRuleViewChanged = i > 0 ? view.once("ruleview-changed") : null;
+ yield focusNextEditableField(view, ruleEditor, commitKey);
+ yield onRuleViewChanged;
+ assertEditor(view, propEditor.nameSpan,
+ "Focus should have moved to the property name");
+
+ info("Focus the next field with " + commitKey);
+ yield focusNextEditableField(view, ruleEditor, commitKey);
+ assertEditor(view, propEditor.valueSpan,
+ "Focus should have moved to the property value");
+ }
+
+ // Expect a ruleview-changed event again as we're bluring a property value.
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield focusNextEditableField(view, ruleEditor, commitKey);
+ yield onRuleViewChanged;
+ assertEditor(view, ruleEditor.newPropSpan,
+ "Focus should have moved to the new property span");
+
+ ruleEditor = getRuleViewRuleEditor(view, 2);
+
+ yield focusNextEditableField(view, ruleEditor, commitKey);
+ assertEditor(view, ruleEditor.selectorText,
+ "Focus should have moved to the next rule selector");
+
+ info("Blur the selector field");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+}
+
+function* focusNextEditableField(view, ruleEditor, commitKey) {
+ let onFocus = once(ruleEditor.element, "focus", true);
+ EventUtils.synthesizeKey(commitKey, {}, view.styleWindow);
+ yield onFocus;
+}
+
+function assertEditor(view, element, message) {
+ let editor = inplaceEditor(view.styleDocument.activeElement);
+ is(inplaceEditor(element), editor, message);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_02.js b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_02.js
new file mode 100644
index 000000000..13ad221f0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_02.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";
+
+// Tests that the correct editable fields are focused when shift tabbing
+// through the rule view.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ color: red;
+ margin: 0;
+ padding: 0;
+ }
+ div {
+ border-color: red
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testEditableFieldFocus(inspector, view, "VK_TAB", { shiftKey: true });
+});
+
+function* testEditableFieldFocus(inspector, view, commitKey, options = {}) {
+ let ruleEditor = getRuleViewRuleEditor(view, 2);
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "Focus should be in the 'div' rule selector");
+
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ yield focusNextField(view, ruleEditor, commitKey, options);
+ assertEditor(view, ruleEditor.newPropSpan,
+ "Focus should have moved to the new property span");
+
+ for (let textProp of ruleEditor.rule.textProps.slice(0).reverse()) {
+ let propEditor = textProp.editor;
+
+ yield focusNextField(view, ruleEditor, commitKey, options);
+ yield assertEditor(view, propEditor.valueSpan,
+ "Focus should have moved to the property value");
+
+ yield focusNextFieldAndExpectChange(view, ruleEditor, commitKey, options);
+ yield assertEditor(view, propEditor.nameSpan,
+ "Focus should have moved to the property name");
+ }
+
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ yield focusNextField(view, ruleEditor, commitKey, options);
+ yield assertEditor(view, ruleEditor.selectorText,
+ "Focus should have moved to the '#testid' rule selector");
+
+ ruleEditor = getRuleViewRuleEditor(view, 0);
+
+ yield focusNextField(view, ruleEditor, commitKey, options);
+ assertEditor(view, ruleEditor.newPropSpan,
+ "Focus should have moved to the new property span");
+}
+
+function* focusNextFieldAndExpectChange(view, ruleEditor, commitKey, options) {
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield focusNextField(view, ruleEditor, commitKey, options);
+ yield onRuleViewChanged;
+}
+
+function* focusNextField(view, ruleEditor, commitKey, options) {
+ let onFocus = once(ruleEditor.element, "focus", true);
+ EventUtils.synthesizeKey(commitKey, options, view.styleWindow);
+ yield onFocus;
+}
+
+function* assertEditor(view, element, message) {
+ let editor = inplaceEditor(view.styleDocument.activeElement);
+ is(inplaceEditor(element), editor, message);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_eyedropper.js b/devtools/client/inspector/rules/test/browser_rules_eyedropper.js
new file mode 100644
index 000000000..0762066e3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_eyedropper.js
@@ -0,0 +1,123 @@
+/* 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";
+
+// Test opening the eyedropper from the color picker. Pressing escape to close it, and
+// clicking the page to select a color.
+
+const TEST_URI = `
+ <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>
+`;
+
+// #f09
+const ORIGINAL_COLOR = "rgb(255, 0, 153)";
+// #ff5
+const EXPECTED_COLOR = "rgb(255, 255, 85)";
+
+add_task(function* () {
+ info("Add the test tab, open the rule-view and select the test node");
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {testActor, inspector, view} = yield openRuleView();
+ yield selectNode("#div2", inspector);
+
+ info("Get the background-color property from the rule-view");
+ 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");
+
+ info("Open the eyedropper from the colorpicker tooltip");
+ yield openEyedropper(view, swatch);
+
+ let tooltip = view.tooltips.colorPicker.tooltip;
+ ok(!tooltip.isVisible(), "color picker tooltip is closed after opening eyedropper");
+
+ info("Test that pressing escape dismisses the eyedropper");
+ yield testESC(swatch, inspector, testActor);
+
+ info("Open the eyedropper again");
+ yield openEyedropper(view, swatch);
+
+ info("Test that a color can be selected with the eyedropper");
+ yield testSelect(view, swatch, inspector, testActor);
+
+ let onHidden = tooltip.once("hidden");
+ tooltip.hide();
+ yield onHidden;
+ ok(!tooltip.isVisible(), "color picker tooltip is closed");
+
+ yield waitForTick();
+});
+
+function* testESC(swatch, inspector, testActor) {
+ info("Press escape");
+ let onCanceled = new Promise(resolve => {
+ inspector.inspector.once("color-pick-canceled", resolve);
+ });
+ yield testActor.synthesizeKey({key: "VK_ESCAPE", options: {}});
+ yield onCanceled;
+
+ let color = swatch.style.backgroundColor;
+ is(color, ORIGINAL_COLOR, "swatch didn't change after pressing ESC");
+}
+
+function* testSelect(view, swatch, inspector, testActor) {
+ info("Click at x:10px y:10px");
+ let onPicked = new Promise(resolve => {
+ inspector.inspector.once("color-picked", resolve);
+ });
+ // The change to the content is done async after rule view change
+ let onRuleViewChanged = view.once("ruleview-changed");
+
+ yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10,
+ options: {type: "mousemove"}});
+ yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10,
+ options: {type: "mousedown"}});
+ yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10,
+ options: {type: "mouseup"}});
+
+ yield onPicked;
+ yield onRuleViewChanged;
+
+ let color = swatch.style.backgroundColor;
+ is(color, EXPECTED_COLOR, "swatch changed colors");
+
+ is((yield getComputedStyleProperty("div", null, "background-color")),
+ EXPECTED_COLOR,
+ "div's color set to body color after dropper");
+}
+
+function* openEyedropper(view, swatch) {
+ let tooltip = view.tooltips.colorPicker.tooltip;
+
+ info("Click on the swatch");
+ let onColorPickerReady = view.tooltips.colorPicker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ let dropperButton = tooltip.doc.querySelector("#eyedropper-button");
+
+ info("Click on the eyedropper icon");
+ let onOpened = tooltip.once("eyedropper-opened");
+ dropperButton.click();
+ yield onOpened;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_filtereditor-appears-on-swatch-click.js b/devtools/client/inspector/rules/test/browser_rules_filtereditor-appears-on-swatch-click.js
new file mode 100644
index 000000000..21eeebb36
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_filtereditor-appears-on-swatch-click.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the that Filter Editor Tooltip opens by clicking on filter swatches
+
+const TEST_URL = URL_ROOT + "doc_filter.html";
+
+add_task(function* () {
+ yield addTab(TEST_URL);
+
+ let {view} = yield openRuleView();
+
+ info("Getting the filter swatch element");
+ let swatch = getRuleViewProperty(view, "body", "filter").valueSpan
+ .querySelector(".ruleview-filterswatch");
+
+ let filterTooltip = view.tooltips.filterEditor;
+ // Clicking on a cssfilter swatch sets the current filter value in the tooltip
+ // which, in turn, makes the FilterWidget emit an "updated" event that causes
+ // the rule-view to refresh. So we must wait for the ruleview-changed event.
+ let onRuleViewChanged = view.once("ruleview-changed");
+ swatch.click();
+ yield onRuleViewChanged;
+
+ ok(true, "The shown event was emitted after clicking on swatch");
+ ok(!inplaceEditor(swatch.parentNode),
+ "The inplace editor wasn't shown as a result of the filter swatch click");
+
+ yield hideTooltipAndWaitForRuleViewChanged(filterTooltip, view);
+
+ yield waitForTick();
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_filtereditor-commit-on-ENTER.js b/devtools/client/inspector/rules/test/browser_rules_filtereditor-commit-on-ENTER.js
new file mode 100644
index 000000000..127a20843
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_filtereditor-commit-on-ENTER.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Tooltip committing changes on ENTER
+
+const TEST_URL = URL_ROOT + "doc_filter.html";
+
+add_task(function* () {
+ yield addTab(TEST_URL);
+ let {view} = yield openRuleView();
+
+ info("Get the filter swatch element");
+ let swatch = getRuleViewProperty(view, "body", "filter").valueSpan
+ .querySelector(".ruleview-filterswatch");
+
+ info("Click on the filter swatch element");
+ // Clicking on a cssfilter swatch sets the current filter value in the tooltip
+ // which, in turn, makes the FilterWidget emit an "updated" event that causes
+ // the rule-view to refresh. So we must wait for the ruleview-changed event.
+ let onRuleViewChanged = view.once("ruleview-changed");
+ swatch.click();
+ yield onRuleViewChanged;
+
+ info("Get the cssfilter widget instance");
+ let filterTooltip = view.tooltips.filterEditor;
+ let widget = filterTooltip.widget;
+
+ info("Set a new value in the cssfilter widget");
+ onRuleViewChanged = view.once("ruleview-changed");
+ widget.setCssValue("blur(2px)");
+ yield waitForComputedStyleProperty("body", null, "filter", "blur(2px)");
+ yield onRuleViewChanged;
+ ok(true, "Changes previewed on the element");
+
+ info("Press RETURN to commit changes");
+ // Pressing return in the cssfilter tooltip triggeres 2 ruleview-changed
+ onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2);
+ EventUtils.sendKey("RETURN", widget.styleWindow);
+ yield onRuleViewChanged;
+
+ is((yield getComputedStyleProperty("body", null, "filter")), "blur(2px)",
+ "The elemenet's filter was kept after RETURN");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_filtereditor-revert-on-ESC.js b/devtools/client/inspector/rules/test/browser_rules_filtereditor-revert-on-ESC.js
new file mode 100644
index 000000000..0302f40a9
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_filtereditor-revert-on-ESC.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that changes made to the Filter Editor Tooltip are reverted when
+// ESC is pressed
+
+const TEST_URL = URL_ROOT + "doc_filter.html";
+
+add_task(function* () {
+ yield addTab(TEST_URL);
+ let {view} = yield openRuleView();
+ yield testPressingEscapeRevertsChanges(view);
+ yield testPressingEscapeRevertsChangesAndDisables(view);
+});
+
+function* testPressingEscapeRevertsChanges(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+ let swatch = propEditor.valueSpan.querySelector(".ruleview-filterswatch");
+
+ yield clickOnFilterSwatch(swatch, view);
+ yield setValueInFilterWidget("blur(2px)", view);
+
+ yield waitForComputedStyleProperty("body", null, "filter", "blur(2px)");
+ is(propEditor.valueSpan.textContent, "blur(2px)",
+ "Got expected property value.");
+
+ yield pressEscapeToCloseTooltip(view);
+
+ yield waitForComputedStyleProperty("body", null, "filter",
+ "blur(2px) contrast(2)");
+ is(propEditor.valueSpan.textContent, "blur(2px) contrast(2)",
+ "Got expected property value.");
+}
+
+function* testPressingEscapeRevertsChangesAndDisables(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+
+ info("Disabling filter property");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ propEditor.enable.click();
+ yield onRuleViewChanged;
+
+ ok(propEditor.element.classList.contains("ruleview-overridden"),
+ "property is overridden.");
+ is(propEditor.enable.style.visibility, "visible",
+ "property enable checkbox is visible.");
+ ok(!propEditor.enable.getAttribute("checked"),
+ "property enable checkbox is not checked.");
+ ok(!propEditor.prop.enabled,
+ "filter property is disabled.");
+ let newValue = yield getRulePropertyValue("filter");
+ is(newValue, "", "filter should have been unset.");
+
+ let swatch = propEditor.valueSpan.querySelector(".ruleview-filterswatch");
+ yield clickOnFilterSwatch(swatch, view);
+
+ ok(!propEditor.element.classList.contains("ruleview-overridden"),
+ "property overridden is not displayed.");
+ is(propEditor.enable.style.visibility, "hidden",
+ "property enable checkbox is hidden.");
+
+ yield setValueInFilterWidget("blur(2px)", view);
+ yield pressEscapeToCloseTooltip(view);
+
+ ok(propEditor.element.classList.contains("ruleview-overridden"),
+ "property is overridden.");
+ is(propEditor.enable.style.visibility, "visible",
+ "property enable checkbox is visible.");
+ ok(!propEditor.enable.getAttribute("checked"),
+ "property enable checkbox is not checked.");
+ ok(!propEditor.prop.enabled, "filter property is disabled.");
+ newValue = yield getRulePropertyValue("filter");
+ is(newValue, "", "filter should have been unset.");
+ is(propEditor.valueSpan.textContent, "blur(2px) contrast(2)",
+ "Got expected property value.");
+}
+
+function* getRulePropertyValue(name) {
+ let propValue = yield executeInContent("Test:GetRulePropertyValue", {
+ styleSheetIndex: 0,
+ ruleIndex: 0,
+ name: name
+ });
+ return propValue;
+}
+
+function* clickOnFilterSwatch(swatch, view) {
+ info("Clicking on a css filter swatch to open the tooltip");
+
+ // Clicking on a cssfilter swatch sets the current filter value in the tooltip
+ // which, in turn, makes the FilterWidget emit an "updated" event that causes
+ // the rule-view to refresh. So we must wait for the ruleview-changed event.
+ let onRuleViewChanged = view.once("ruleview-changed");
+ swatch.click();
+ yield onRuleViewChanged;
+}
+
+function* setValueInFilterWidget(value, view) {
+ info("Setting the CSS filter value in the tooltip");
+
+ let filterTooltip = view.tooltips.filterEditor;
+ let onRuleViewChanged = view.once("ruleview-changed");
+ filterTooltip.widget.setCssValue(value);
+ yield onRuleViewChanged;
+}
+
+function* pressEscapeToCloseTooltip(view) {
+ info("Pressing ESCAPE to close the tooltip");
+
+ let filterTooltip = view.tooltips.filterEditor;
+ let onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.sendKey("ESCAPE", filterTooltip.widget.styleWindow);
+ yield onRuleViewChanged;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.js b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.js
new file mode 100644
index 000000000..617eb00da
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.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 grid highlighter is hidden on page navigation.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+const TEST_URI_2 = "data:text/html,<html><body>test</body></html>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ let highlighters = view.highlighters;
+
+ yield selectNode("#grid", inspector);
+ let container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ let gridToggle = container.querySelector(".ruleview-grid");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ let onHighlighterShown = highlighters.once("highlighter-shown");
+ gridToggle.click();
+ yield onHighlighterShown;
+
+ ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+ yield navigateTo(inspector, TEST_URI_2);
+ ok(!highlighters.gridHighlighterShown, "CSS grid highlighter is hidden.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-reload.js b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-reload.js
new file mode 100644
index 000000000..a6780a94a
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-reload.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 a grid highlighter showing grid gaps can be displayed after reloading the
+// page (Bug 1342051).
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ grid-gap: 10px;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ info("Check that the grid highlighter can be displayed");
+ yield checkGridHighlighter();
+
+ info("Close the toolbox before reloading the tab");
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+
+ yield refreshTab(gBrowser.selectedTab);
+
+ info("Check that the grid highlighter can be displayed after reloading the page");
+ yield checkGridHighlighter();
+});
+
+function* checkGridHighlighter() {
+ let {inspector, view} = yield openRuleView();
+ let {highlighters} = view;
+
+ yield selectNode("#grid", inspector);
+ let container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ let gridToggle = container.querySelector(".ruleview-grid");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ let onHighlighterShown = highlighters.once("highlighter-shown");
+ gridToggle.click();
+ yield onHighlighterShown;
+
+ ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js
new file mode 100644
index 000000000..04534522b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.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 toggling the grid highlighter in the rule view and the display of the
+// grid highlighter.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+const HIGHLIGHTER_TYPE = "CssGridHighlighter";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ let highlighters = view.highlighters;
+
+ yield selectNode("#grid", inspector);
+ let container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ let gridToggle = container.querySelector(".ruleview-grid");
+
+ info("Checking the initial state of the CSS grid toggle in the rule-view.");
+ ok(gridToggle, "Grid highlighter toggle is visible.");
+ ok(!gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active.");
+ ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
+ "No CSS grid highlighter exists in the rule-view.");
+ ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ let onHighlighterShown = highlighters.once("highlighter-shown");
+ gridToggle.click();
+ yield onHighlighterShown;
+
+ info("Checking the CSS grid highlighter is created and toggle button is active in " +
+ "the rule-view.");
+ ok(gridToggle.classList.contains("active"),
+ "Grid highlighter toggle is active.");
+ ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
+ "CSS grid highlighter created in the rule-view.");
+ ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+ info("Toggling OFF the CSS grid highlighter from the rule-view.");
+ let onHighlighterHidden = highlighters.once("highlighter-hidden");
+ gridToggle.click();
+ yield onHighlighterHidden;
+
+ info("Checking the CSS grid highlighter is not shown and toggle button is not active " +
+ "in the rule-view.");
+ ok(!gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active.");
+ ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js
new file mode 100644
index 000000000..5c339e892
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.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";
+
+// Test toggling the grid highlighter in the rule view from an overridden 'display: grid'
+// declaration.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ div, ul {
+ display: grid;
+ }
+ </style>
+ <ul id="grid">
+ <li id="cell1">cell1</li>
+ <li id="cell2">cell2</li>
+ </ul>
+`;
+
+const HIGHLIGHTER_TYPE = "CssGridHighlighter";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ let highlighters = view.highlighters;
+
+ yield selectNode("#grid", inspector);
+ let container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ let gridToggle = container.querySelector(".ruleview-grid");
+ let overriddenContainer = getRuleViewProperty(view, "div, ul", "display").valueSpan;
+ let overriddenGridToggle = overriddenContainer.querySelector(".ruleview-grid");
+
+ info("Checking the initial state of the CSS grid toggle in the rule-view.");
+ ok(gridToggle && overriddenGridToggle, "Grid highlighter toggles are visible.");
+ ok(!gridToggle.classList.contains("active") &&
+ !overriddenGridToggle.classList.contains("active"),
+ "Grid highlighter toggle buttons are not active.");
+ ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
+ "No CSS grid highlighter exists in the rule-view.");
+ ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+
+ info("Toggling ON the CSS grid highlighter from the overridden rule in the rule-view.");
+ let onHighlighterShown = highlighters.once("highlighter-shown");
+ overriddenGridToggle.click();
+ yield onHighlighterShown;
+
+ info("Checking the CSS grid highlighter is created and toggle buttons are active in " +
+ "the rule-view.");
+ ok(gridToggle.classList.contains("active") &&
+ overriddenGridToggle.classList.contains("active"),
+ "Grid highlighter toggle is active.");
+ ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
+ "CSS grid highlighter created in the rule-view.");
+ ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+ info("Toggling off the CSS grid highlighter from the normal grid declaration in the " +
+ "rule-view.");
+ let onHighlighterHidden = highlighters.once("highlighter-hidden");
+ gridToggle.click();
+ yield onHighlighterHidden;
+
+ info("Checking the CSS grid highlighter is not shown and toggle buttons are not " +
+ "active in the rule-view.");
+ ok(!gridToggle.classList.contains("active") &&
+ !overriddenGridToggle.classList.contains("active"),
+ "Grid highlighter toggle buttons are not active.");
+ ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js
new file mode 100644
index 000000000..a908d6a97
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js
@@ -0,0 +1,96 @@
+/* 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 toggling the grid highlighter in the rule view with multiple grids in the page.
+
+const TEST_URI = `
+ <style type='text/css'>
+ .grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid1" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+ <div id="grid2" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+`;
+
+const HIGHLIGHTER_TYPE = "CssGridHighlighter";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ let highlighters = view.highlighters;
+
+ info("Selecting the first grid container.");
+ yield selectNode("#grid1", inspector);
+ let container = getRuleViewProperty(view, ".grid", "display").valueSpan;
+ let gridToggle = container.querySelector(".ruleview-grid");
+
+ info("Checking the state of the CSS grid toggle for the first grid container in the " +
+ "rule-view.");
+ ok(gridToggle, "Grid highlighter toggle is visible.");
+ ok(!gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active.");
+ ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
+ "No CSS grid highlighter exists in the rule-view.");
+ ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+
+ info("Toggling ON the CSS grid highlighter for the first grid container from the " +
+ "rule-view.");
+ let onHighlighterShown = highlighters.once("highlighter-shown");
+ gridToggle.click();
+ yield onHighlighterShown;
+
+ info("Checking the CSS grid highlighter is created and toggle button is active in " +
+ "the rule-view.");
+ ok(gridToggle.classList.contains("active"),
+ "Grid highlighter toggle is active.");
+ ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
+ "CSS grid highlighter created in the rule-view.");
+ ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+ info("Selecting the second grid container.");
+ yield selectNode("#grid2", inspector);
+ let firstGridHighterShown = highlighters.gridHighlighterShown;
+ container = getRuleViewProperty(view, ".grid", "display").valueSpan;
+ gridToggle = container.querySelector(".ruleview-grid");
+
+ info("Checking the state of the CSS grid toggle for the second grid container in the " +
+ "rule-view.");
+ ok(gridToggle, "Grid highlighter toggle is visible.");
+ ok(!gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active.");
+ ok(highlighters.gridHighlighterShown, "CSS grid highlighter is still shown.");
+
+ info("Toggling ON the CSS grid highlighter for the second grid container from the " +
+ "rule-view.");
+ onHighlighterShown = highlighters.once("highlighter-shown");
+ gridToggle.click();
+ yield onHighlighterShown;
+
+ info("Checking the CSS grid highlighter is created for the second grid container and " +
+ "toggle button is active in the rule-view.");
+ ok(gridToggle.classList.contains("active"),
+ "Grid highlighter toggle is active.");
+ ok(highlighters.gridHighlighterShown != firstGridHighterShown,
+ "Grid highlighter for the second grid container is shown.");
+
+ info("Selecting the first grid container.");
+ yield selectNode("#grid1", inspector);
+ container = getRuleViewProperty(view, ".grid", "display").valueSpan;
+ gridToggle = container.querySelector(".ruleview-grid");
+
+ info("Checking the state of the CSS grid toggle for the first grid container in the " +
+ "rule-view.");
+ ok(gridToggle, "Grid highlighter toggle is visible.");
+ ok(!gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_guessIndentation.js b/devtools/client/inspector/rules/test/browser_rules_guessIndentation.js
new file mode 100644
index 000000000..ba2a1d7fb
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_guessIndentation.js
@@ -0,0 +1,47 @@
+/* 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 can guess indentation from a style sheet, not just a
+// rule.
+
+// Use a weird indentation depth to avoid accidental success.
+const TEST_URI = `
+ <style type='text/css'>
+div {
+ background-color: blue;
+}
+
+* {
+}
+</style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+const expectedText = `
+div {
+ background-color: blue;
+}
+
+* {
+ color: chartreuse;
+}
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Add a new property in the rule-view");
+ yield addProperty(view, 2, "color", "chartreuse");
+
+ info("Switch to the style-editor");
+ let { UI } = yield toolbox.selectTool("styleeditor");
+
+ let styleEditor = yield UI.editors[0].getSourceEditor();
+ let text = styleEditor.sourceEditor.getText();
+ is(text, expectedText, "style inspector changes are synced");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_inherited-properties_01.js b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_01.js
new file mode 100644
index 000000000..d1f6d7f45
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_01.js
@@ -0,0 +1,47 @@
+/* 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 for a nested element in the
+// rule view.
+
+const TEST_URI = `
+ <style type="text/css">
+ #test2 {
+ background-color: green;
+ color: purple;
+ }
+ </style>
+ <div id="test2"><div id="test1">Styled Node</div></div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#test1", inspector);
+ yield simpleInherit(inspector, view);
+});
+
+function* simpleInherit(inspector, view) {
+ 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, 2,
+ "Rule should have two styles");
+ let bgcProp = inheritRule.textProps[0];
+ is(bgcProp.name, "background-color",
+ "background-color property should exist");
+ ok(bgcProp.invisible, "background-color property should be invisible");
+ let inheritProp = inheritRule.textProps[1];
+ is(inheritProp.name, "color", "color should have been inherited.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_inherited-properties_02.js b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_02.js
new file mode 100644
index 000000000..db9662eee
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_02.js
@@ -0,0 +1,34 @@
+/* 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 no inherited properties appear when the property does not apply
+// to the nested element.
+
+const TEST_URI = `
+ <style type="text/css">
+ #test2 {
+ background-color: green;
+ }
+ </style>
+ <div id="test2"><div id="test1">Styled Node</div></div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#test1", inspector);
+ yield emptyInherit(inspector, view);
+});
+
+function* emptyInherit(inspector, view) {
+ // No inheritable styles, this rule shouldn't show up.
+ 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.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_inherited-properties_03.js b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_03.js
new file mode 100644
index 000000000..d6075f6f4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_03.js
@@ -0,0 +1,40 @@
+/* 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 inline inherited properties appear in the nested element.
+
+var {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
+
+const TEST_URI = `
+ <div id="test2" style="color: red">
+ <div id="test1">Styled Node</div>
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#test1", inspector);
+ yield elementStyleInherit(inspector, view);
+});
+
+function* elementStyleInherit(inspector, view) {
+ 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/devtools/client/inspector/rules/test/browser_rules_inline-source-map.js b/devtools/client/inspector/rules/test/browser_rules_inline-source-map.js
new file mode 100644
index 000000000..05109d8c6
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inline-source-map.js
@@ -0,0 +1,26 @@
+/* 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 when a source map comment appears in an inline stylesheet, the
+// rule-view still appears correctly.
+// Bug 1255787.
+
+const TESTCASE_URI = URL_ROOT + "doc_inline_sourcemap.html";
+const PREF = "devtools.styleeditor.source-maps-enabled";
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ yield addTab(TESTCASE_URI);
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("div", inspector);
+
+ let ruleEl = getRuleViewRule(view, "div");
+ ok(ruleEl, "The 'div' rule exists in the rule-view");
+
+ Services.prefs.clearUserPref(PREF);
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_invalid-source-map.js b/devtools/client/inspector/rules/test/browser_rules_invalid-source-map.js
new file mode 100644
index 000000000..825f48a96
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_invalid-source-map.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 when a source map is missing/invalid, the rule view still loads
+// correctly.
+
+const TESTCASE_URI = URL_ROOT + "doc_invalid_sourcemap.html";
+const PREF = "devtools.styleeditor.source-maps-enabled";
+const CSS_LOC = "doc_invalid_sourcemap.css:1";
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ yield addTab(TESTCASE_URI);
+ let {inspector, view} = yield openRuleView();
+
+ yield selectNode("div", inspector);
+
+ let ruleEl = getRuleViewRule(view, "div");
+ ok(ruleEl, "The 'div' rule exists in the rule-view");
+
+ let prop = getRuleViewProperty(view, "div", "color");
+ ok(prop, "The 'color' property exists in this rule");
+
+ let value = getRuleViewPropertyValue(view, "div", "color");
+ is(value, "gold", "The 'color' property has the right value");
+
+ yield verifyLinkText(view, CSS_LOC);
+
+ Services.prefs.clearUserPref(PREF);
+});
+
+function verifyLinkText(view, text) {
+ info("Verifying that the rule-view stylesheet link is " + text);
+ let label = getRuleViewLinkByIndex(view, 1)
+ .querySelector(".ruleview-rule-source-label");
+ return waitForSuccess(
+ () => label.textContent == text,
+ "Link text changed to display correct location: " + text
+ );
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_invalid.js b/devtools/client/inspector/rules/test/browser_rules_invalid.js
new file mode 100644
index 000000000..e664f68ac
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_invalid.js
@@ -0,0 +1,33 @@
+/* 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 an invalid property still lets us display the rule view
+// Bug 1235603.
+
+const TEST_URI = `
+ <style>
+ div {
+ background: #fff;
+ font-family: sans-serif;
+ url(display-table.min.htc);
+ }
+ </style>
+ <body>
+ <div id="testid" class="testclass">Styled Node</div>
+ </body>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ // Have to actually get the rule in order to ensure that the
+ // elements were created.
+ ok(getRuleViewRule(view, "div"), "Rule with div selector exists");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_keybindings.js b/devtools/client/inspector/rules/test/browser_rules_keybindings.js
new file mode 100644
index 000000000..84fdeff85
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_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 {inspector, view} = yield openRuleView();
+ yield selectNode("h1", inspector);
+
+ info("Getting the ruleclose brace element");
+ let brace = view.styleDocument.querySelector(".ruleview-ruleclose");
+
+ info("Focus the new property editable field to create a color property");
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+ editor.input.value = "color";
+
+ info("Typing ENTER to focus the next field: property value");
+ let onFocus = once(brace.parentNode, "focus", true);
+ let onRuleViewChanged = view.once("ruleview-changed");
+
+ EventUtils.sendKey("return");
+
+ yield onFocus;
+ yield onRuleViewChanged;
+ 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);
+ onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.sendKey("return");
+ yield onFocus;
+ yield onRuleViewChanged;
+ ok(true, "The new property name field was focused");
+ getCurrentInplaceEditor(view).input.blur();
+});
+
+function getCurrentInplaceEditor(view) {
+ return inplaceEditor(view.styleDocument.activeElement);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_keyframeLineNumbers.js b/devtools/client/inspector/rules/test/browser_rules_keyframeLineNumbers.js
new file mode 100644
index 000000000..ebbde08ac
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_keyframeLineNumbers.js
@@ -0,0 +1,25 @@
+/* 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 editing a rule will update the line numbers of subsequent
+// rules in the rule view.
+
+const TESTCASE_URI = URL_ROOT + "doc_keyframeLineNumbers.html";
+
+add_task(function* () {
+ yield addTab(TESTCASE_URI);
+ let { inspector, view } = yield openRuleView();
+ yield selectNode("#outer", inspector);
+
+ info("Insert a new property, which will affect the line numbers");
+ yield addProperty(view, 1, "font-size", "72px");
+
+ yield selectNode("#inner", inspector);
+
+ let value = getRuleViewLinkTextByIndex(view, 3);
+ // Note that this is relative to the <style>.
+ is(value.slice(-3), ":27", "rule line number is 27");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js
new file mode 100644
index 000000000..8d4b436c5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js
@@ -0,0 +1,106 @@
+/* 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 = URL_ROOT + "doc_keyframeanimation.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {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");
+
+ yield assertKeyframeRules("#pacman", inspector, view, {
+ elementRulesNb: 2,
+ keyframeRulesNb: 2,
+ keyframesRules: ["pacman", "pacman"],
+ keyframeRules: ["100%", "100%"]
+ });
+
+ assertGutters(view, {
+ guttersNbs: 2,
+ gutterHeading: ["Keyframes pacman", "Keyframes pacman"]
+ });
+}
+
+function* testBoxy(inspector, view) {
+ info("Test content and gutter in the keyframes rule of #boxy");
+
+ yield assertKeyframeRules("#boxy", inspector, view, {
+ elementRulesNb: 3,
+ keyframeRulesNb: 3,
+ keyframesRules: ["boxy", "boxy", "boxy"],
+ keyframeRules: ["10%", "20%", "100%"]
+ });
+
+ assertGutters(view, {
+ guttersNbs: 1,
+ gutterHeading: ["Keyframes boxy"]
+ });
+}
+
+function* testMoxy(inspector, view) {
+ info("Test content and gutter in the keyframes rule of #moxy");
+
+ yield assertKeyframeRules("#moxy", inspector, view, {
+ elementRulesNb: 3,
+ keyframeRulesNb: 4,
+ keyframesRules: ["boxy", "boxy", "boxy", "moxy"],
+ keyframeRules: ["10%", "20%", "100%", "100%"]
+ });
+
+ assertGutters(view, {
+ guttersNbs: 2,
+ gutterHeading: ["Keyframes boxy", "Keyframes moxy"]
+ });
+}
+
+function* assertKeyframeRules(selector, inspector, view, expected) {
+ yield selectNode(selector, inspector);
+ let elementStyle = view._elementStyle;
+
+ 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++;
+ }
+}
+
+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/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_02.js b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_02.js
new file mode 100644
index 000000000..b7652ecaa
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_02.js
@@ -0,0 +1,92 @@
+/* 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 = URL_ROOT + "doc_keyframeanimation.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {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 = 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
+ // If this part of the test is ever enabled again, it should be changed to
+ // use addProperty (in head.js) and stop using _applyingModifications
+
+ // 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", true);
+
+ // 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 = 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) {
+ 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;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_lineNumbers.js b/devtools/client/inspector/rules/test/browser_rules_lineNumbers.js
new file mode 100644
index 000000000..3b09209f5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_lineNumbers.js
@@ -0,0 +1,29 @@
+/* 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 editing a rule will update the line numbers of subsequent
+// rules in the rule view.
+
+const TESTCASE_URI = URL_ROOT + "doc_ruleLineNumbers.html";
+
+add_task(function* () {
+ yield addTab(TESTCASE_URI);
+ let { inspector, view } = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let bodyRuleEditor = getRuleViewRuleEditor(view, 3);
+ let value = getRuleViewLinkTextByIndex(view, 2);
+ // Note that this is relative to the <style>.
+ is(value.slice(-2), ":6", "initial rule line number is 6");
+
+ let onLocationChanged = once(bodyRuleEditor.rule.domRule, "location-changed");
+ yield addProperty(view, 1, "font-size", "23px");
+ yield onLocationChanged;
+
+ let newBodyTitle = getRuleViewLinkTextByIndex(view, 2);
+ // Note that this is relative to the <style>.
+ is(newBodyTitle.slice(-2), ":7", "updated rule line number is 7");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_livepreview.js b/devtools/client/inspector/rules/test/browser_rules_livepreview.js
new file mode 100644
index 000000000..1f1302a70
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_livepreview.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 that changes are previewed when editing a property value.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ display:block;
+ }
+ </style>
+ <div id="testid">Styled Node</div><span>inline element</span>
+`;
+
+// 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," + encodeURIComponent(TEST_URI));
+ let {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 rule = getRuleViewRuleEditor(ruleView, 1).rule;
+ let propEditor = rule.textProps[0].editor;
+
+ info("Focusing the property value inplace-editor");
+ let editor = yield focusEditableField(ruleView, propEditor.valueSpan);
+ is(inplaceEditor(propEditor.valueSpan), editor,
+ "The focused editor is the value");
+
+ info("Entering value in the editor: " + data.value);
+ let onPreviewDone = ruleView.once("ruleview-changed");
+ EventUtils.sendString(data.value, ruleView.styleWindow);
+ ruleView.throttle.flush();
+ yield onPreviewDone;
+
+ let onValueDone = ruleView.once("ruleview-changed");
+ if (data.escape) {
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ } else {
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }
+ yield onValueDone;
+
+ // 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/devtools/client/inspector/rules/test/browser_rules_mark_overridden_01.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_01.js
new file mode 100644
index 000000000..ab10fadfe
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_01.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";
+
+// Tests that the rule view marks overridden rules correctly based on the
+// specificity of the rule.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ }
+ .testclass {
+ background-color: green;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let idRule = getRuleViewRuleEditor(view, 1).rule;
+ let idProp = idRule.textProps[0];
+ is(idProp.name, "background-color",
+ "First ID property should be background-color");
+ is(idProp.value, "blue", "First ID property value should be blue");
+ ok(!idProp.overridden, "ID prop should not be overridden.");
+ ok(!idProp.editor.element.classList.contains("ruleview-overridden"),
+ "ID property editor should not have ruleview-overridden class");
+
+ let classRule = getRuleViewRuleEditor(view, 2).rule;
+ let classProp = classRule.textProps[0];
+ is(classProp.name, "background-color",
+ "First class prop should be background-color");
+ is(classProp.value, "green", "First class property value should be green");
+ ok(classProp.overridden, "Class property should be overridden.");
+ ok(classProp.editor.element.classList.contains("ruleview-overridden"),
+ "Class property editor should have ruleview-overridden class");
+
+ // Override background-color by changing the element style.
+ let elementProp = yield addProperty(view, 0, "background-color", "purple");
+
+ ok(!elementProp.overridden,
+ "Element style property should not be overridden");
+ ok(idProp.overridden, "ID property should be overridden");
+ ok(idProp.editor.element.classList.contains("ruleview-overridden"),
+ "ID property editor should have ruleview-overridden class");
+ ok(classProp.overridden, "Class property should be overridden");
+ ok(classProp.editor.element.classList.contains("ruleview-overridden"),
+ "Class property editor should have ruleview-overridden class");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_02.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_02.js
new file mode 100644
index 000000000..c71fc7211
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_02.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";
+
+// Tests that the rule view marks overridden rules correctly for short hand
+// properties and the computed list properties
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ margin-left: 1px;
+ }
+ .testclass {
+ margin: 2px;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testMarkOverridden(inspector, view);
+});
+
+function* testMarkOverridden(inspector, view) {
+ 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.");
+ }
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_03.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_03.js
new file mode 100644
index 000000000..b99bab8b4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_03.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 the rule view marks overridden rules correctly based on the
+// priority for the rule
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ }
+ .testclass {
+ background-color: green !important;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let idRule = getRuleViewRuleEditor(view, 1).rule;
+ let idProp = idRule.textProps[0];
+ ok(idProp.overridden, "Not-important rule should be overridden.");
+
+ let classRule = getRuleViewRuleEditor(view, 2).rule;
+ let classProp = classRule.textProps[0];
+ ok(!classProp.overridden, "Important rule should not be overridden.");
+
+ ok(idProp.overridden, "ID property should be overridden.");
+
+ // FIXME: re-enable these 2 assertions when bug 1247737 is fixed.
+ // let elementProp = yield addProperty(view, 0, "background-color", "purple");
+ // ok(!elementProp.overridden, "New important prop should not be overriden.");
+ // ok(classProp.overridden, "Class property should be overridden.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_04.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_04.js
new file mode 100644
index 000000000..fbce1ebf4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_04.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";
+
+// Tests that the rule view marks overridden rules correctly if a property gets
+// disabled
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: blue;
+ }
+ .testclass {
+ background-color: green;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let idRule = getRuleViewRuleEditor(view, 1).rule;
+ let idProp = idRule.textProps[0];
+
+ yield togglePropStatus(view, idProp);
+
+ let classRule = getRuleViewRuleEditor(view, 2).rule;
+ let classProp = classRule.textProps[0];
+ ok(!classProp.overridden,
+ "Class prop should not be overridden after id prop was disabled.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_05.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_05.js
new file mode 100644
index 000000000..11ecd72ff
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_05.js
@@ -0,0 +1,33 @@
+/* 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 marks overridden rules correctly based on the
+// order of the property.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ background-color: green;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+
+ yield addProperty(view, 1, "background-color", "red");
+
+ let firstProp = rule.textProps[0];
+ let secondProp = rule.textProps[1];
+
+ ok(firstProp.overridden, "First property should be overridden.");
+ ok(!secondProp.overridden, "Second property should not be overridden.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_06.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_06.js
new file mode 100644
index 000000000..c2e71fe49
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_06.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";
+
+// Tests that the rule view marks overridden rules correctly after
+// editing the selector.
+
+const TEST_URI = `
+ <style type='text/css'>
+ div {
+ background-color: blue;
+ background-color: chartreuse;
+ }
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testMarkOverridden(inspector, view);
+});
+
+function* testMarkOverridden(inspector, view) {
+ let elementStyle = view._elementStyle;
+ let rule = elementStyle.rules[1];
+ checkProperties(rule);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+ info("Entering a new selector name and committing");
+ editor.input.value = "div[class]";
+
+ let onRuleViewChanged = once(view, "ruleview-changed");
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ view.searchField.focus();
+ checkProperties(rule);
+}
+
+// A helper to perform a repeated set of checks.
+function checkProperties(rule) {
+ let prop = rule.textProps[0];
+ is(prop.name, "background-color",
+ "First property should be background-color");
+ is(prop.value, "blue", "First property value should be blue");
+ ok(prop.overridden, "prop should be overridden.");
+ prop = rule.textProps[1];
+ is(prop.name, "background-color",
+ "Second property should be background-color");
+ is(prop.value, "chartreuse", "First property value should be chartreuse");
+ ok(!prop.overridden, "prop should not be overridden.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_07.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_07.js
new file mode 100644
index 000000000..9480ddd47
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_07.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";
+
+// Tests that the rule view marks overridden rules correctly based on the
+// specificity of the rule.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ margin-left: 23px;
+ }
+
+ div {
+ margin-right: 23px;
+ margin-left: 1px !important;
+ }
+
+ body {
+ margin-right: 1px !important;
+ font-size: 79px;
+ }
+
+ span {
+ font-size: 12px;
+ }
+ </style>
+ <body>
+ <span>
+ <div id='testid' class='testclass'>Styled Node</div>
+ </span>
+ </body>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testMarkOverridden(inspector, view);
+});
+
+function* testMarkOverridden(inspector, view) {
+ let elementStyle = view._elementStyle;
+
+ let RESULTS = [
+ // We skip the first element
+ [],
+ [{name: "margin-left", value: "23px", overridden: true}],
+ [{name: "margin-right", value: "23px", overridden: false},
+ {name: "margin-left", value: "1px", overridden: false}],
+ [{name: "font-size", value: "12px", overridden: false}],
+ [{name: "margin-right", value: "1px", overridden: true},
+ {name: "font-size", value: "79px", overridden: true}]
+ ];
+
+ for (let i = 1; i < RESULTS.length; ++i) {
+ let idRule = elementStyle.rules[i];
+
+ for (let propIndex in RESULTS[i]) {
+ let expected = RESULTS[i][propIndex];
+ let prop = idRule.textProps[propIndex];
+
+ info("Checking rule " + i + ", property " + propIndex);
+
+ is(prop.name, expected.name, "check property name");
+ is(prop.value, expected.value, "check property value");
+ is(prop.overridden, expected.overridden, "check property overridden");
+ }
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_mathml-element.js b/devtools/client/inspector/rules/test/browser_rules_mathml-element.js
new file mode 100644
index 000000000..f8a1e8572
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mathml-element.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";
+
+// Tests that the rule-view displays correctly on MathML elements.
+
+const TEST_URI = `
+ <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>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {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/devtools/client/inspector/rules/test/browser_rules_media-queries.js b/devtools/client/inspector/rules/test/browser_rules_media-queries.js
new file mode 100644
index 000000000..57ab19163
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_media-queries.js
@@ -0,0 +1,26 @@
+/* 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 = 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 inline = STYLE_INSPECTOR_L10N.getStr("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 +
+ ":9 @media screen and (min-width: 1px)", "check rule 1 title");
+ is(elementStyle.rules[2].title, inline + ":2", "check rule 2 title");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-duplicates.js b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-duplicates.js
new file mode 100644
index 000000000..c820dd73f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-duplicates.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 that the rule-view behaves correctly when entering mutliple and/or
+// unfinished properties/values in inplace-editors
+
+const TEST_URI = "<div>Test Element</div>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ // Note that we wait for a markup mutation here because this new rule will end
+ // up creating a style attribute on the node shown in the markup-view.
+ // (we also wait for the rule-view to refresh).
+ let onMutation = inspector.once("markupmutation");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield createNewRuleViewProperty(ruleEditor,
+ "color:red;color:orange;color:yellow;color:green;color:blue;color:indigo;" +
+ "color:violet;");
+ yield onMutation;
+ yield onRuleViewChanged;
+
+ 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");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-priority.js b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-priority.js
new file mode 100644
index 000000000..f7d98b768
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-priority.js
@@ -0,0 +1,47 @@
+/* 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.
+
+const TEST_URI = "<div>Test Element</div>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ // Note that we wait for a markup mutation here because this new rule will end
+ // up creating a style attribute on the node shown in the markup-view.
+ // (we also wait for the rule-view to refresh).
+ let onMutation = inspector.once("markupmutation");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield createNewRuleViewProperty(ruleEditor,
+ "color:red;width:100px;height: 100px;");
+ yield onMutation;
+ yield onRuleViewChanged;
+
+ 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");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_01.js b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_01.js
new file mode 100644
index 000000000..deaf16029
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_01.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 rule-view behaves correctly when entering multiple and/or
+// unfinished properties/values in inplace-editors
+
+const TEST_URI = "<div>Test Element</div>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+ yield testCreateNewMultiUnfinished(inspector, view);
+});
+
+function* testCreateNewMultiUnfinished(inspector, view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let onMutation = inspector.once("markupmutation");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield createNewRuleViewProperty(ruleEditor,
+ "color:blue;background : orange ; text-align:center; border-color: ");
+ yield onMutation;
+ yield onRuleViewChanged;
+
+ is(ruleEditor.rule.textProps.length, 4,
+ "Should have created new text properties.");
+ is(ruleEditor.propertyList.children.length, 4,
+ "Should have created property editors.");
+
+ EventUtils.sendString("red", view.styleWindow);
+ onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onRuleViewChanged;
+
+ 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");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js
new file mode 100644
index 000000000..dd1360b96
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.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 the rule-view behaves correctly when entering mutliple and/or
+// unfinished properties/values in inplace-editors
+
+const TEST_URI = "<div>Test Element</div>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ // Turn off throttling, which can cause intermittents. Throttling is used by
+ // the TextPropertyEditor.
+ view.throttle = () => {};
+
+ yield selectNode("div", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ // Note that we wait for a markup mutation here because this new rule will end
+ // up creating a style attribute on the node shown in the markup-view.
+ // (we also wait for the rule-view to refresh).
+ let onMutation = inspector.once("markupmutation");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield createNewRuleViewProperty(ruleEditor, "width: 100px; heig");
+ yield onMutation;
+ yield onRuleViewChanged;
+
+ 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
+ onMutation = inspector.once("markupmutation");
+ onRuleViewChanged = view.once("ruleview-changed");
+ let valueEditor = ruleEditor.propertyList.children[1]
+ .querySelector(".styleinspector-propertyeditor");
+ valueEditor.value = "10px;background:orangered;color: black;";
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onMutation;
+ yield onRuleViewChanged;
+
+ 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");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple_properties_01.js b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_01.js
new file mode 100644
index 000000000..2801df652
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_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.
+
+const TEST_URI = "<div>Test Element</div>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ // Note that we wait for a markup mutation here because this new rule will end
+ // up creating a style attribute on the node shown in the markup-view.
+ // (we also wait for the rule-view to refresh).
+ let onMutation = inspector.once("markupmutation");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ yield createNewRuleViewProperty(ruleEditor,
+ "color:blue;background : orange ; text-align:center; " +
+ "border-color: green;");
+ yield onMutation;
+ yield onRuleViewChanged;
+
+ 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");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple_properties_02.js b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_02.js
new file mode 100644
index 000000000..ce6f1909f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_02.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
+
+const TEST_URI = "<div>Test Element</div>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let onDone = view.once("ruleview-changed");
+ yield createNewRuleViewProperty(ruleEditor, "width:");
+ yield onDone;
+
+ 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
+ onDone = view.once("ruleview-changed");
+ let onMutation = inspector.once("markupmutation");
+ let input = view.styleDocument.activeElement;
+ input.value = "height: 10px;color:blue";
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onMutation;
+ yield onDone;
+
+ 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.styleWindow);
+ 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");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_original-source-link.js b/devtools/client/inspector/rules/test/browser_rules_original-source-link.js
new file mode 100644
index 000000000..09dad9a86
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_original-source-link.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 stylesheet links in the rule view are correct when source maps
+// are involved.
+
+const TESTCASE_URI = 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);
+
+ 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 = 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} = 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(".ruleview-rule-source-label");
+ return waitForSuccess(function* () {
+ return label.textContent == text;
+ }, "Link text changed to display correct location: " + text);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js
new file mode 100644
index 000000000..e98b5437c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js
@@ -0,0 +1,260 @@
+/* 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 = URL_ROOT + "doc_pseudoelement.html";
+const PSEUDO_PREF = "devtools.inspector.show_pseudo_elements";
+
+add_task(function* () {
+ yield pushPref(PSEUDO_PREF, true);
+
+ yield addTab(TEST_URI);
+ let {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 id = "#topleft";
+ let rules = yield assertPseudoElementRulesNumbers(id,
+ inspector, view, {
+ elementRulesNb: 4,
+ firstLineRulesNb: 2,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 0,
+ afterRulesNb: 1,
+ beforeRulesNb: 2
+ }
+ );
+
+ let gutters = assertGutters(view);
+
+ info("Make sure that clicking on the twisty hides pseudo elements");
+ let expander = gutters[0].querySelector(".ruleview-expander");
+ ok(!view.element.children[1].hidden, "Pseudo Elements are expanded");
+
+ expander.click();
+ ok(view.element.children[1].hidden,
+ "Pseudo Elements are collapsed by twisty");
+
+ expander.click();
+ ok(!view.element.children[1].hidden, "Pseudo Elements are expanded again");
+
+ info("Make sure that dblclicking on the header container also toggles " +
+ "the pseudo elements");
+ EventUtils.synthesizeMouseAtCenter(gutters[0], {clickCount: 2},
+ view.styleWindow);
+ ok(view.element.children[1].hidden,
+ "Pseudo Elements are collapsed by dblclicking");
+
+ let elementRuleView = getRuleViewRuleEditor(view, 3);
+
+ let elementFirstLineRule = rules.firstLineRules[0];
+ let elementFirstLineRuleView =
+ [...view.element.children[1].children].filter(e => {
+ return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule;
+ })[0]._ruleEditor;
+
+ is(convertTextPropsToString(elementFirstLineRule.textProps),
+ "color: orange",
+ "TopLeft firstLine properties are correct");
+
+ let onAdded = view.once("ruleview-changed");
+ let firstProp = elementFirstLineRuleView.addProperty("background-color",
+ "rgb(0, 255, 0)", "", true);
+ yield onAdded;
+
+ onAdded = view.once("ruleview-changed");
+ let secondProp = elementFirstLineRuleView.addProperty("font-style",
+ "italic", "", true);
+ yield onAdded;
+
+ 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");
+
+ is((yield getComputedStyleProperty(id, ":first-line", "background-color")),
+ "rgb(0, 255, 0)", "Added property should have been used.");
+ is((yield getComputedStyleProperty(id, ":first-line", "font-style")),
+ "italic", "Added property should have been used.");
+ is((yield getComputedStyleProperty(id, null, "text-decoration")),
+ "none", "Added property should not apply to element");
+
+ yield togglePropStatus(view, firstProp);
+
+ is((yield getComputedStyleProperty(id, ":first-line", "background-color")),
+ "rgb(255, 0, 0)", "Disabled property should now have been used.");
+ is((yield getComputedStyleProperty(id, null, "background-color")),
+ "rgb(221, 221, 221)", "Added property should not apply to element");
+
+ yield togglePropStatus(view, firstProp);
+
+ is((yield getComputedStyleProperty(id, ":first-line", "background-color")),
+ "rgb(0, 255, 0)", "Added property should have been used.");
+ is((yield getComputedStyleProperty(id, null, "text-decoration")),
+ "none", "Added property should not apply to element");
+
+ onAdded = view.once("ruleview-changed");
+ firstProp = elementRuleView.addProperty("background-color",
+ "rgb(0, 0, 255)", "", true);
+ yield onAdded;
+
+ is((yield getComputedStyleProperty(id, null, "background-color")),
+ "rgb(0, 0, 255)", "Added property should have been used.");
+ is((yield getComputedStyleProperty(id, ":first-line", "background-color")),
+ "rgb(0, 255, 0)", "Added prop does not apply to pseudo");
+}
+
+function* testTopRight(inspector, view) {
+ yield assertPseudoElementRulesNumbers("#topright", inspector, view, {
+ elementRulesNb: 4,
+ firstLineRulesNb: 1,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 0,
+ beforeRulesNb: 2,
+ afterRulesNb: 1
+ });
+
+ 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.children[1].hidden,
+ "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,
+ beforeRulesNb: 3,
+ afterRulesNb: 1
+ });
+}
+
+function* testBottomLeft(inspector, view) {
+ yield assertPseudoElementRulesNumbers("#bottomleft", inspector, view, {
+ elementRulesNb: 4,
+ firstLineRulesNb: 1,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 0,
+ beforeRulesNb: 2,
+ afterRulesNb: 1
+ });
+}
+
+function* testParagraph(inspector, view) {
+ let rules =
+ yield assertPseudoElementRulesNumbers("#bottomleft p", inspector, view, {
+ elementRulesNb: 3,
+ firstLineRulesNb: 1,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 1,
+ beforeRulesNb: 0,
+ afterRulesNb: 0
+ });
+
+ assertGutters(view);
+
+ let elementFirstLineRule = rules.firstLineRules[0];
+ is(convertTextPropsToString(elementFirstLineRule.textProps),
+ "background: blue",
+ "Paragraph first-line properties are correct");
+
+ let elementFirstLetterRule = rules.firstLetterRules[0];
+ is(convertTextPropsToString(elementFirstLetterRule.textProps),
+ "color: red; font-size: 130%",
+ "Paragraph first-letter properties are correct");
+
+ let elementSelectionRule = rules.selectionRules[0];
+ is(convertTextPropsToString(elementSelectionRule.textProps),
+ "color: white; background: black",
+ "Paragraph first-letter properties are correct");
+}
+
+function* testBody(inspector, view) {
+ yield testNode("body", inspector, view);
+
+ let gutters = getGutters(view);
+ 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) {
+ yield selectNode(selector, inspector);
+ let elementStyle = view._elementStyle;
+ return elementStyle;
+}
+
+function* assertPseudoElementRulesNumbers(selector, inspector, view, ruleNbs) {
+ let 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"),
+ beforeRules: elementStyle.rules.filter(rule =>
+ rule.pseudoElement === ":before"),
+ afterRules: elementStyle.rules.filter(rule =>
+ rule.pseudoElement === ":after"),
+ };
+
+ 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");
+ is(rules.beforeRules.length, ruleNbs.beforeRulesNb,
+ selector + " has the correct number of :before rules");
+ is(rules.afterRules.length, ruleNbs.afterRulesNb,
+ selector + " has the correct number of :after rules");
+
+ return rules;
+}
+
+function getGutters(view) {
+ return view.element.querySelectorAll(".theme-gutter");
+}
+
+function assertGutters(view) {
+ let gutters = getGutters(view);
+
+ 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/devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js
new file mode 100644
index 000000000..f69c328db
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js
@@ -0,0 +1,29 @@
+/* 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 markup view.
+
+const TEST_URI = URL_ROOT + "doc_pseudoelement.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector} = yield openRuleView();
+
+ let node = yield getNodeFront("#topleft", inspector);
+ 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/devtools/client/inspector/rules/test/browser_rules_pseudo_lock_options.js b/devtools/client/inspector/rules/test/browser_rules_pseudo_lock_options.js
new file mode 100644
index 000000000..d795ba5f3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_pseudo_lock_options.js
@@ -0,0 +1,131 @@
+/* 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 pseudo lock options work properly.
+
+const TEST_URI = `
+ <style type='text/css'>
+ div {
+ color: red;
+ }
+ div:hover {
+ color: blue;
+ }
+ div:active {
+ color: yellow;
+ }
+ div:focus {
+ color: green;
+ }
+ </style>
+ <div>test div</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ yield assertPseudoPanelClosed(view);
+
+ info("Toggle the pseudo class panel open");
+ view.pseudoClassToggle.click();
+ yield assertPseudoPanelOpened(view);
+
+ info("Toggle each pseudo lock and check that the pseudo lock is added");
+ yield togglePseudoClass(inspector, view.hoverCheckbox);
+ yield assertPseudoAdded(inspector, view, ":hover", 3, 1);
+ yield togglePseudoClass(inspector, view.hoverCheckbox);
+ yield assertPseudoRemoved(inspector, view, 2);
+
+ yield togglePseudoClass(inspector, view.activeCheckbox);
+ yield assertPseudoAdded(inspector, view, ":active", 3, 1);
+ yield togglePseudoClass(inspector, view.activeCheckbox);
+ yield assertPseudoRemoved(inspector, view, 2);
+
+ yield togglePseudoClass(inspector, view.focusCheckbox);
+ yield assertPseudoAdded(inspector, view, ":focus", 3, 1);
+ yield togglePseudoClass(inspector, view.focusCheckbox);
+ yield assertPseudoRemoved(inspector, view, 2);
+
+ info("Toggle all pseudo lock and check that the pseudo lock is added");
+ yield togglePseudoClass(inspector, view.hoverCheckbox);
+ yield togglePseudoClass(inspector, view.activeCheckbox);
+ yield togglePseudoClass(inspector, view.focusCheckbox);
+ yield assertPseudoAdded(inspector, view, ":focus", 5, 1);
+ yield assertPseudoAdded(inspector, view, ":active", 5, 2);
+ yield assertPseudoAdded(inspector, view, ":hover", 5, 3);
+ yield togglePseudoClass(inspector, view.hoverCheckbox);
+ yield togglePseudoClass(inspector, view.activeCheckbox);
+ yield togglePseudoClass(inspector, view.focusCheckbox);
+ yield assertPseudoRemoved(inspector, view, 2);
+
+ info("Select a null element");
+ yield view.selectElement(null);
+ ok(!view.hoverCheckbox.checked && view.hoverCheckbox.disabled,
+ ":hover checkbox is unchecked and disabled");
+ ok(!view.activeCheckbox.checked && view.activeCheckbox.disabled,
+ ":active checkbox is unchecked and disabled");
+ ok(!view.focusCheckbox.checked && view.focusCheckbox.disabled,
+ ":focus checkbox is unchecked and disabled");
+
+ info("Toggle the pseudo class panel close");
+ view.pseudoClassToggle.click();
+ yield assertPseudoPanelClosed(view);
+});
+
+function* togglePseudoClass(inspector, pseudoClassOption) {
+ info("Toggle the pseudoclass, wait for it to be applied");
+ let onRefresh = inspector.once("rule-view-refreshed");
+ pseudoClassOption.click();
+ yield onRefresh;
+}
+
+function* assertPseudoAdded(inspector, view, pseudoClass, numRules,
+ childIndex) {
+ info("Check that the ruleview contains the pseudo-class rule");
+ is(view.element.children.length, numRules,
+ "Should have " + numRules + " rules.");
+ is(getRuleViewRuleEditor(view, childIndex).rule.selectorText,
+ "div" + pseudoClass, "rule view is showing " + pseudoClass + " rule");
+}
+
+function* assertPseudoRemoved(inspector, view, numRules) {
+ info("Check that the ruleview no longer contains the pseudo-class rule");
+ is(view.element.children.length, numRules,
+ "Should have " + numRules + " rules.");
+ is(getRuleViewRuleEditor(view, 1).rule.selectorText, "div",
+ "Second rule is div");
+}
+
+function* assertPseudoPanelOpened(view) {
+ info("Check the opened state of the pseudo class panel");
+
+ ok(!view.pseudoClassPanel.hidden, "Pseudo Class Panel Opened");
+ ok(!view.hoverCheckbox.disabled, ":hover checkbox is not disabled");
+ ok(!view.activeCheckbox.disabled, ":active checkbox is not disabled");
+ ok(!view.focusCheckbox.disabled, ":focus checkbox is not disabled");
+
+ is(view.hoverCheckbox.getAttribute("tabindex"), "0",
+ ":hover checkbox has a tabindex of 0");
+ is(view.activeCheckbox.getAttribute("tabindex"), "0",
+ ":active checkbox has a tabindex of 0");
+ is(view.focusCheckbox.getAttribute("tabindex"), "0",
+ ":focus checkbox has a tabindex of 0");
+}
+
+function* assertPseudoPanelClosed(view) {
+ info("Check the closed state of the pseudo clas panel");
+
+ ok(view.pseudoClassPanel.hidden, "Pseudo Class Panel Hidden");
+
+ is(view.hoverCheckbox.getAttribute("tabindex"), "-1",
+ ":hover checkbox has a tabindex of -1");
+ is(view.activeCheckbox.getAttribute("tabindex"), "-1",
+ ":active checkbox has a tabindex of -1");
+ is(view.focusCheckbox.getAttribute("tabindex"), "-1",
+ ":focus checkbox has a tabindex of -1");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_refresh-no-flicker.js b/devtools/client/inspector/rules/test/browser_rules_refresh-no-flicker.js
new file mode 100644
index 000000000..25ea3d972
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_refresh-no-flicker.js
@@ -0,0 +1,39 @@
+/* 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 does not go blank while selecting a new node.
+
+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 rule view and selecting the test node");
+ let {inspector, view} = yield openRuleView();
+ let testdiv = yield getNodeFront("#testdiv", inspector);
+ yield selectNode(testdiv, inspector);
+
+ let htmlBefore = view.element.innerHTML;
+ ok(htmlBefore.indexOf("font-size") > -1,
+ "The rule view should contain a font-size property.");
+
+ // Do the selectNode call manually, because otherwise it's hard to guarantee
+ // that we can make the below checks at a reasonable time.
+ info("refreshing the node");
+ let p = view.selectElement(testdiv, true);
+ is(view.element.innerHTML, htmlBefore,
+ "The rule view is unchanged during selection.");
+ ok(view.element.classList.contains("non-interactive"),
+ "The rule view is marked non-interactive.");
+ yield p;
+
+ info("node refreshed");
+ ok(!view.element.classList.contains("non-interactive"),
+ "The rule view is marked interactive again.");
+});
+
diff --git a/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_01.js b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_01.js
new file mode 100644
index 000000000..381a6bda2
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_01.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 changing the current element's attributes refreshes the rule-view
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ background-color: blue;
+ }
+ .testclass {
+ background-color: green;
+ }
+ </style>
+ <div id="testid" class="testclass" style="margin-top: 1px; padding-top: 5px;">
+ Styled Node
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = 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");
+ yield testActor.setAttribute("#testid", "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");
+ yield testActor.setAttribute("#differentid", "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.styleDocument
+ .querySelectorAll(".ruleview-selectorcontainer");
+
+ 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/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_02.js b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_02.js
new file mode 100644
index 000000000..6ee385faa
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_02.js
@@ -0,0 +1,153 @@
+/* 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
+
+const TEST_URI = `
+ <div id="testid" class="testclass" style="margin-top: 1px; padding-top: 5px;">
+ Styled Node
+ </div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ yield testPropertyChanges(inspector, view);
+ yield testPropertyChange0(inspector, view, "#testid", testActor);
+ yield testPropertyChange1(inspector, view, "#testid", testActor);
+ yield testPropertyChange2(inspector, view, "#testid", testActor);
+ yield testPropertyChange3(inspector, view, "#testid", testActor);
+ yield testPropertyChange4(inspector, view, "#testid", testActor);
+ yield testPropertyChange5(inspector, view, "#testid", testActor);
+ yield testPropertyChange6(inspector, view, "#testid", testActor);
+});
+
+function* testPropertyChanges(inspector, ruleView) {
+ 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", "", true);
+ yield onRefreshed;
+
+ let rule = ruleView._elementStyle.rules[0];
+ validateTextProp(rule.textProps[0], false, "margin-top", "1px",
+ "Original margin property active");
+}
+
+function* testPropertyChange0(inspector, ruleView, selector, testActor) {
+ yield changeElementStyle(selector, "margin-top: 1px; padding-top: 5px",
+ inspector, testActor);
+
+ 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, selector, testActor) {
+ info("Now set it back to 5px, the 5px value should be re-enabled.");
+ yield changeElementStyle(selector, "margin-top: 5px; padding-top: 5px;",
+ inspector, testActor);
+
+ 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, selector, testActor) {
+ 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(selector, "margin-top: 15px; padding-top: 5px;",
+ inspector, testActor);
+
+ 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, selector, testActor) {
+ info("Remove the padding-top attribute. Should disable the padding " +
+ "property but not remove it.");
+ yield changeElementStyle(selector, "margin-top: 5px;", inspector, testActor);
+
+ 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, selector, testActor) {
+ info("Put the padding-top attribute back in, should re-enable the " +
+ "padding property.");
+ yield changeElementStyle(selector, "margin-top: 5px; padding-top: 25px",
+ inspector, testActor);
+
+ 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, selector, testActor) {
+ info("Add an entirely new property");
+ yield changeElementStyle(selector,
+ "margin-top: 5px; padding-top: 25px; padding-left: 20px;",
+ inspector, testActor);
+
+ 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, selector, testActor) {
+ info("Add an entirely new property again");
+ yield changeElementStyle(selector, "background: red " +
+ "url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%",
+ inspector, testActor);
+
+ 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");
+}
+
+function* changeElementStyle(selector, style, inspector, testActor) {
+ let onRefreshed = inspector.once("rule-view-refreshed");
+ yield testActor.setAttribute(selector, "style", style);
+ yield onRefreshed;
+}
+
+function validateTextProp(prop, enabled, name, value, desc) {
+ is(prop.enabled, enabled, desc + ": enabled.");
+ is(prop.name, name, desc + ": name.");
+ is(prop.value, value, desc + ": value.");
+
+ is(prop.editor.enable.hasAttribute("checked"), enabled,
+ desc + ": enabled checkbox.");
+ is(prop.editor.nameSpan.textContent, name, desc + ": name span.");
+ is(prop.editor.valueSpan.textContent,
+ value, desc + ": value span.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.js b/devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.js
new file mode 100644
index 000000000..81ff9d4d5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.js
@@ -0,0 +1,38 @@
+/* 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 TEST_URI = "<div id='testdiv' style='font-size: 10px;''>Test div!</div>";
+
+add_task(function* () {
+ Services.prefs.setCharPref("devtools.defaultColorUnit", "name");
+
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = 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");
+ yield testActor.setAttribute("#testdiv", "style",
+ "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/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_01.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_01.js
new file mode 100644
index 000000000..f4c47bba0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_01.js
@@ -0,0 +1,156 @@
+/* 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 search filter and clear button works properly in
+// the computed list.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ margin: 4px 0px;
+ }
+ .testclass {
+ background-color: red;
+ }
+ </style>
+ <h1 id="testid" class="testclass">Styled Node</h1>
+`;
+
+const TEST_DATA = [
+ {
+ desc: "Tests that the search filter works properly in the computed list " +
+ "for property names",
+ search: "margin",
+ isExpanderOpen: false,
+ isFilterOpen: false,
+ isMarginHighlighted: true,
+ isMarginTopHighlighted: true,
+ isMarginRightHighlighted: true,
+ isMarginBottomHighlighted: true,
+ isMarginLeftHighlighted: true
+ },
+ {
+ desc: "Tests that the search filter works properly in the computed list " +
+ "for property values",
+ search: "0px",
+ isExpanderOpen: false,
+ isFilterOpen: false,
+ isMarginHighlighted: true,
+ isMarginTopHighlighted: false,
+ isMarginRightHighlighted: true,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: true
+ },
+ {
+ desc: "Tests that the search filter works properly in the computed list " +
+ "for property line input",
+ search: "margin-top:4px",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: true,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: false
+ },
+ {
+ desc: "Tests that the search filter works properly in the computed list " +
+ "for parsed name",
+ search: "margin-top:",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: true,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: false
+ },
+ {
+ desc: "Tests that the search filter works properly in the computed list " +
+ "for parsed property value",
+ search: ":4px",
+ isExpanderOpen: false,
+ isFilterOpen: false,
+ isMarginHighlighted: true,
+ isMarginTopHighlighted: true,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: true,
+ isMarginLeftHighlighted: false
+ }
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ for (let data of TEST_DATA) {
+ info(data.desc);
+ yield setSearchFilter(view, data.search);
+ yield checkRules(view, data);
+ yield clearSearchAndCheckRules(view);
+ }
+}
+
+function* checkRules(view, data) {
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let textPropEditor = rule.textProps[0].editor;
+ let computed = textPropEditor.computed;
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ is(!!textPropEditor.expander.getAttribute("open"), data.isExpanderOpen,
+ "Got correct expander state.");
+ is(computed.hasAttribute("filter-open"), data.isFilterOpen,
+ "Got correct expanded state for margin computed list.");
+ is(textPropEditor.container.classList.contains("ruleview-highlight"),
+ data.isMarginHighlighted,
+ "Got correct highlight for margin text property.");
+
+ is(computed.children[0].classList.contains("ruleview-highlight"),
+ data.isMarginTopHighlighted,
+ "Got correct highlight for margin-top computed property.");
+ is(computed.children[1].classList.contains("ruleview-highlight"),
+ data.isMarginRightHighlighted,
+ "Got correct highlight for margin-right computed property.");
+ is(computed.children[2].classList.contains("ruleview-highlight"),
+ data.isMarginBottomHighlighted,
+ "Got correct highlight for margin-bottom computed property.");
+ is(computed.children[3].classList.contains("ruleview-highlight"),
+ data.isMarginLeftHighlighted,
+ "Got correct highlight for margin-left computed property.");
+}
+
+function* clearSearchAndCheckRules(view) {
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ let searchClearButton = view.searchClearButton;
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let textPropEditor = rule.textProps[0].editor;
+ let computed = textPropEditor.computed;
+
+ info("Clearing the search filter");
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
+ yield view.inspector.once("ruleview-filtered");
+
+ info("Check the search filter is cleared and no rules are highlighted");
+ is(view.element.children.length, 3, "Should have 3 rules.");
+ ok(!searchField.value, "Search filter is cleared");
+ ok(!view.styleDocument.querySelectorAll(".ruleview-highlight").length,
+ "No rules are higlighted");
+
+ ok(!textPropEditor.expander.getAttribute("open"), "Expander is closed.");
+ ok(!computed.hasAttribute("filter-open"),
+ "margin computed list is closed.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_02.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_02.js
new file mode 100644
index 000000000..911f09ff3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_02.js
@@ -0,0 +1,93 @@
+/* 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 search filter works properly in the computed list
+// when modifying the existing search filter value
+
+const SEARCH = "margin-";
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ margin: 4px 0px;
+ }
+ .testclass {
+ background-color: red;
+ }
+ </style>
+ <h1 id="testid" class="testclass">Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+ yield testRemoveTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let ruleEditor = rule.textProps[0].editor;
+ let computed = ruleEditor.computed;
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(ruleEditor.expander.getAttribute("open"), "Expander is open.");
+ ok(!ruleEditor.container.classList.contains("ruleview-highlight"),
+ "margin text property is not highlighted.");
+ ok(computed.hasAttribute("filter-open"), "margin computed list is open.");
+
+ ok(computed.children[0].classList.contains("ruleview-highlight"),
+ "margin-top computed property is correctly highlighted.");
+ ok(computed.children[1].classList.contains("ruleview-highlight"),
+ "margin-right computed property is correctly highlighted.");
+ ok(computed.children[2].classList.contains("ruleview-highlight"),
+ "margin-bottom computed property is correctly highlighted.");
+ ok(computed.children[3].classList.contains("ruleview-highlight"),
+ "margin-left computed property is correctly highlighted.");
+}
+
+function* testRemoveTextInFilter(inspector, view) {
+ info("Press backspace and set filter text to \"margin\"");
+
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+
+ searchField.focus();
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win);
+ yield inspector.once("ruleview-filtered");
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let ruleEditor = rule.textProps[0].editor;
+ let computed = ruleEditor.computed;
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(!ruleEditor.expander.getAttribute("open"), "Expander is closed.");
+ ok(ruleEditor.container.classList.contains("ruleview-highlight"),
+ "margin text property is correctly highlighted.");
+ ok(!computed.hasAttribute("filter-open"), "margin computed list is closed.");
+
+ ok(computed.children[0].classList.contains("ruleview-highlight"),
+ "margin-top computed property is correctly highlighted.");
+ ok(computed.children[1].classList.contains("ruleview-highlight"),
+ "margin-right computed property is correctly highlighted.");
+ ok(computed.children[2].classList.contains("ruleview-highlight"),
+ "margin-bottom computed property is correctly highlighted.");
+ ok(computed.children[3].classList.contains("ruleview-highlight"),
+ "margin-left computed property is correctly highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_03.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_03.js
new file mode 100644
index 000000000..1d8063419
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_03.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";
+
+// Tests that the rule view search filter works properly in the computed list
+// for color values.
+
+// The color format here is chosen to match the default returned by
+// CssColor.toString.
+const SEARCH = "background-color: rgb(243, 243, 243)";
+
+const TEST_URI = `
+ <style type="text/css">
+ .testclass {
+ background: rgb(243, 243, 243) none repeat scroll 0% 0%;
+ }
+ </style>
+ <div class="testclass">Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".testclass", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let ruleEditor = rule.textProps[0].editor;
+ let computed = ruleEditor.computed;
+
+ is(rule.selectorText, ".testclass", "Second rule is .testclass.");
+ ok(ruleEditor.expander.getAttribute("open"), "Expander is open.");
+ ok(!ruleEditor.container.classList.contains("ruleview-highlight"),
+ "background property is not highlighted.");
+ ok(computed.hasAttribute("filter-open"), "background computed list is open.");
+ ok(computed.children[0].classList.contains("ruleview-highlight"),
+ "background-color computed property is highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_04.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_04.js
new file mode 100644
index 000000000..05b8b01eb
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_04.js
@@ -0,0 +1,63 @@
+/* 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 search filter works properly in the computed list
+// for newly modified property values.
+
+const SEARCH = "0px";
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ margin: 4px;
+ top: 0px;
+ }
+ </style>
+ <h1 id='testid'>Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testModifyPropertyValueFilter(inspector, view);
+});
+
+function* testModifyPropertyValueFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let propEditor = rule.textProps[0].editor;
+ let computed = propEditor.computed;
+ let editor = yield focusEditableField(view, propEditor.valueSpan);
+
+ info("Check that the correct rules are visible");
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(!propEditor.container.classList.contains("ruleview-highlight"),
+ "margin text property is not highlighted.");
+ ok(rule.textProps[1].editor.container.classList
+ .contains("ruleview-highlight"),
+ "top text property is correctly highlighted.");
+
+ let onBlur = once(editor.input, "blur");
+ let onModification = view.once("ruleview-changed");
+ EventUtils.sendString("4px 0px", view.styleWindow);
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onBlur;
+ yield onModification;
+
+ ok(propEditor.container.classList.contains("ruleview-highlight"),
+ "margin text property is correctly highlighted.");
+ ok(!computed.hasAttribute("filter-open"), "margin computed list is closed.");
+ ok(!computed.children[0].classList.contains("ruleview-highlight"),
+ "margin-top computed property is not highlighted.");
+ ok(computed.children[1].classList.contains("ruleview-highlight"),
+ "margin-right computed property is correctly highlighted.");
+ ok(!computed.children[2].classList.contains("ruleview-highlight"),
+ "margin-bottom computed property is not highlighted.");
+ ok(computed.children[3].classList.contains("ruleview-highlight"),
+ "margin-left computed property is correctly highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_expander.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_expander.js
new file mode 100644
index 000000000..c8b1e0869
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_expander.js
@@ -0,0 +1,92 @@
+/* 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 expanded computed list for a property remains open after
+// clearing the rule view search filter.
+
+const SEARCH = "0px";
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ margin: 4px 0px;
+ }
+ .testclass {
+ background-color: red;
+ }
+ </style>
+ <h1 id="testid" class="testclass">Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testOpenExpanderAndAddTextInFilter(inspector, view);
+ yield testClearSearchFilter(inspector, view);
+});
+
+function* testOpenExpanderAndAddTextInFilter(inspector, view) {
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let ruleEditor = rule.textProps[0].editor;
+ let computed = ruleEditor.computed;
+
+ info("Opening the computed list of margin property");
+ ruleEditor.expander.click();
+
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(ruleEditor.expander.getAttribute("open"), "Expander is open.");
+ ok(ruleEditor.container.classList.contains("ruleview-highlight"),
+ "margin text property is correctly highlighted.");
+ ok(!computed.hasAttribute("filter-open"),
+ "margin computed list does not contain filter-open class.");
+ ok(computed.hasAttribute("user-open"),
+ "margin computed list contains user-open attribute.");
+
+ ok(!computed.children[0].classList.contains("ruleview-highlight"),
+ "margin-top computed property is not highlighted.");
+ ok(computed.children[1].classList.contains("ruleview-highlight"),
+ "margin-right computed property is correctly highlighted.");
+ ok(!computed.children[2].classList.contains("ruleview-highlight"),
+ "margin-bottom computed property is not highlighted.");
+ ok(computed.children[3].classList.contains("ruleview-highlight"),
+ "margin-left computed property is correctly highlighted.");
+}
+
+function* testClearSearchFilter(inspector, view) {
+ info("Clearing the search filter");
+
+ let searchField = view.searchField;
+ let searchClearButton = view.searchClearButton;
+ let onRuleViewFiltered = inspector.once("ruleview-filtered");
+
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {},
+ view.styleWindow);
+
+ yield onRuleViewFiltered;
+
+ info("Check the search filter is cleared and no rules are highlighted");
+ is(view.element.children.length, 3, "Should have 3 rules.");
+ ok(!searchField.value, "Search filter is cleared");
+ ok(!view.styleDocument.querySelectorAll(".ruleview-highlight").length,
+ "No rules are higlighted");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1).rule.textProps[0].editor;
+ let computed = ruleEditor.computed;
+
+ ok(ruleEditor.expander.getAttribute("open"), "Expander is open.");
+ ok(!computed.hasAttribute("filter-open"),
+ "margin computed list does not contain filter-open class.");
+ ok(computed.hasAttribute("user-open"),
+ "margin computed list contains user-open attribute.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-overridden-property.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-overridden-property.js
new file mode 100644
index 000000000..3e634b76e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-overridden-property.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";
+
+// Tests that the rule view overriden search filter works properly for
+// overridden properties.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ width: 100%;
+ }
+ h1 {
+ width: 50%;
+ }
+ </style>
+ <h1 id='testid' class='testclass'>Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testFilterOverriddenProperty(inspector, view);
+});
+
+function* testFilterOverriddenProperty(inspector, ruleView) {
+ info("Check that the correct rules are visible");
+ is(ruleView.element.children.length, 3, "Should have 3 rules.");
+
+ let rule = getRuleViewRuleEditor(ruleView, 1).rule;
+ let textPropEditor = rule.textProps[0].editor;
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(!textPropEditor.element.classList.contains("ruleview-overridden"),
+ "width property is not overridden.");
+ ok(textPropEditor.filterProperty.hidden,
+ "Overridden search button is hidden.");
+
+ rule = getRuleViewRuleEditor(ruleView, 2).rule;
+ textPropEditor = rule.textProps[0].editor;
+ is(rule.selectorText, "h1", "Third rule is h1.");
+ ok(textPropEditor.element.classList.contains("ruleview-overridden"),
+ "width property is overridden.");
+ ok(!textPropEditor.filterProperty.hidden,
+ "Overridden search button is not hidden.");
+
+ let searchField = ruleView.searchField;
+ let onRuleViewFiltered = inspector.once("ruleview-filtered");
+
+ info("Click the overridden search");
+ textPropEditor.filterProperty.click();
+ yield onRuleViewFiltered;
+
+ info("Check that the overridden search is applied");
+ is(searchField.value, "`width`", "The search field value is width.");
+
+ rule = getRuleViewRuleEditor(ruleView, 1).rule;
+ textPropEditor = rule.textProps[0].editor;
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(textPropEditor.container.classList.contains("ruleview-highlight"),
+ "width property is correctly highlighted.");
+
+ rule = getRuleViewRuleEditor(ruleView, 2).rule;
+ textPropEditor = rule.textProps[0].editor;
+ is(rule.selectorText, "h1", "Third rule is h1.");
+ ok(textPropEditor.container.classList.contains("ruleview-highlight"),
+ "width property is correctly highlighted.");
+ ok(textPropEditor.element.classList.contains("ruleview-overridden"),
+ "width property is overridden.");
+ ok(!textPropEditor.filterProperty.hidden,
+ "Overridden search button is not hidden.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_01.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_01.js
new file mode 100644
index 000000000..4dd1c951d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_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 that the rule view search filter and clear button works properly.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid, h1 {
+ background-color: #00F !important;
+ }
+ .testclass {
+ width: 100%;
+ }
+ </style>
+ <h1 id="testid" class="testclass">Styled Node</h1>
+`;
+
+const TEST_DATA = [
+ {
+ desc: "Tests that the search filter works properly for property names",
+ search: "color"
+ },
+ {
+ desc: "Tests that the search filter works properly for property values",
+ search: "00F"
+ },
+ {
+ desc: "Tests that the search filter works properly for property line input",
+ search: "background-color:#00F"
+ },
+ {
+ desc: "Tests that the search filter works properly for parsed property " +
+ "names",
+ search: "background:"
+ },
+ {
+ desc: "Tests that the search filter works properly for parsed property " +
+ "values",
+ search: ":00F"
+ },
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ for (let data of TEST_DATA) {
+ info(data.desc);
+ yield setSearchFilter(view, data.search);
+ yield checkRules(view);
+ yield clearSearchAndCheckRules(view);
+ }
+}
+
+function* checkRules(view) {
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+
+ is(rule.selectorText, "#testid, h1", "Second rule is #testid, h1.");
+ ok(rule.textProps[0].editor.container.classList
+ .contains("ruleview-highlight"),
+ "background-color text property is correctly highlighted.");
+}
+
+function* clearSearchAndCheckRules(view) {
+ let doc = view.styleDocument;
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ let searchClearButton = view.searchClearButton;
+
+ info("Clearing the search filter");
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
+ yield view.inspector.once("ruleview-filtered");
+
+ info("Check the search filter is cleared and no rules are highlighted");
+ is(view.element.children.length, 3, "Should have 3 rules.");
+ ok(!searchField.value, "Search filter is cleared.");
+ ok(!doc.querySelectorAll(".ruleview-highlight").length,
+ "No rules are higlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_02.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_02.js
new file mode 100644
index 000000000..c23e7be62
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_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";
+
+// Tests that the rule view search filter works properly for keyframe rule
+// selectors.
+
+const SEARCH = "20%";
+const TEST_URI = URL_ROOT + "doc_keyframeanimation.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#boxy", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 2, 0);
+
+ is(ruleEditor.rule.domRule.keyText, "20%", "Second rule is 20%.");
+ ok(ruleEditor.selectorText.classList.contains("ruleview-highlight"),
+ "20% selector is highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_03.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_03.js
new file mode 100644
index 000000000..89280f0eb
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_03.js
@@ -0,0 +1,39 @@
+/* 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 search filter works properly for inline styles.
+
+const SEARCH = "color";
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ width: 100%;
+ }
+ </style>
+ <div id="testid" style="background-color:aliceblue">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 1, "Should have 1 rule.");
+
+ let rule = getRuleViewRuleEditor(view, 0).rule;
+
+ is(rule.selectorText, "element", "First rule is inline element.");
+ ok(rule.textProps[0].editor.container.classList
+ .contains("ruleview-highlight"),
+ "background-color text property is correctly highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_04.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_04.js
new file mode 100644
index 000000000..5804d74ac
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_04.js
@@ -0,0 +1,76 @@
+/* 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 search filter works properly when modifying the
+// existing search filter value.
+
+const SEARCH = "00F";
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ background-color: #00F;
+ }
+ .testclass {
+ width: 100%;
+ }
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+ yield testRemoveTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(rule.textProps[0].editor.container.classList
+ .contains("ruleview-highlight"),
+ "background-color text property is correctly highlighted.");
+}
+
+function* testRemoveTextInFilter(inspector, view) {
+ info("Press backspace and set filter text to \"00\"");
+
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+
+ searchField.focus();
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win);
+ yield inspector.once("ruleview-filtered");
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 3, "Should have 3 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(rule.textProps[0].editor.container.classList
+ .contains("ruleview-highlight"),
+ "background-color text property is correctly highlighted.");
+
+ rule = getRuleViewRuleEditor(view, 2).rule;
+
+ is(rule.selectorText, ".testclass", "Second rule is .testclass.");
+ ok(rule.textProps[0].editor.container.classList
+ .contains("ruleview-highlight"),
+ "width text property is correctly highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_05.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_05.js
new file mode 100644
index 000000000..9388dd47e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_05.js
@@ -0,0 +1,33 @@
+/* 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 search filter works properly for stylesheet source.
+
+const SEARCH = "doc_urls_clickable.css";
+const TEST_URI = URL_ROOT + "doc_urls_clickable.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".relative1", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let source = rule.textProps[0].editor.ruleEditor.source;
+
+ is(rule.selectorText, ".relative1", "Second rule is .relative1.");
+ ok(source.classList.contains("ruleview-highlight"),
+ "stylesheet source is correctly highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_06.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_06.js
new file mode 100644
index 000000000..67b02ab73
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_06.js
@@ -0,0 +1,27 @@
+/* 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 search filter does not highlight the source with
+// input that could be parsed as a property line.
+
+const SEARCH = "doc_urls_clickable.css: url";
+const TEST_URI = URL_ROOT + "doc_urls_clickable.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".relative1", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 1, "Should have 1 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_07.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_07.js
new file mode 100644
index 000000000..16b047d8d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_07.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 that the rule view search filter works properly for newly modified
+// property name.
+
+const SEARCH = "e";
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ width: 100%;
+ height: 50%;
+ }
+ </style>
+ <h1 id='testid'>Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Enter the test value in the search filter");
+ yield setSearchFilter(view, SEARCH);
+
+ info("Focus the width property name");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let rule = ruleEditor.rule;
+ let propEditor = rule.textProps[0].editor;
+ yield focusEditableField(view, propEditor.nameSpan);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(!propEditor.container.classList.contains("ruleview-highlight"),
+ "width text property is not highlighted.");
+ ok(rule.textProps[1].editor.container.classList
+ .contains("ruleview-highlight"),
+ "height text property is correctly highlighted.");
+
+ info("Change the width property to margin-left");
+ EventUtils.sendString("margin-left", view.styleWindow);
+
+ info("Submit the change");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ ok(propEditor.container.classList.contains("ruleview-highlight"),
+ "margin-left text property is correctly highlighted.");
+
+ // After pressing return on the property name, the value has been focused
+ // automatically. Blur it now and wait for the rule-view to refresh to avoid
+ // pending requests.
+ onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield onRuleViewChanged;
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_08.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_08.js
new file mode 100644
index 000000000..1a3c0de59
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_08.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";
+
+// Tests that the rule view search filter works properly for newly modified
+// property value.
+
+const SEARCH = "100%";
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ width: 100%;
+ height: 50%;
+ }
+ </style>
+ <h1 id='testid'>Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Enter the test value in the search filter");
+ yield setSearchFilter(view, SEARCH);
+
+ info("Focus the height property value");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let rule = ruleEditor.rule;
+ let propEditor = rule.textProps[1].editor;
+ yield focusEditableField(view, propEditor.valueSpan);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(rule.textProps[0].editor.container.classList
+ .contains("ruleview-highlight"),
+ "width text property is correctly highlighted.");
+ ok(!propEditor.container.classList.contains("ruleview-highlight"),
+ "height text property is not highlighted.");
+
+ info("Change the height property value to 100%");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.sendString("100%", view.styleWindow);
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ ok(propEditor.container.classList.contains("ruleview-highlight"),
+ "height text property is correctly highlighted.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js
new file mode 100644
index 000000000..620e5d336
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_09.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 rule view search filter works properly for newly added
+// property.
+
+const SEARCH = "100%";
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ width: 100%;
+ height: 50%;
+ }
+ </style>
+ <h1 id='testid'>Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+
+ info("Enter the test value in the search filter");
+ yield setSearchFilter(view, SEARCH);
+
+ info("Start entering a new property in the rule");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ let rule = ruleEditor.rule;
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(rule.textProps[0].editor.container.classList
+ .contains("ruleview-highlight"),
+ "width text property is correctly highlighted.");
+ ok(!rule.textProps[1].editor.container.classList
+ .contains("ruleview-highlight"),
+ "height text property is not highlighted.");
+
+ info("Test creating a new property");
+
+ info("Entering margin-left in the property name editor");
+ // Changing the value doesn't cause a rule-view refresh, no need to wait for
+ // ruleview-changed here.
+ editor.input.value = "margin-left";
+
+ info("Pressing return to commit and focus the new value field");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onRuleViewChanged;
+
+ // Getting the new value editor after focus
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ let propEditor = ruleEditor.rule.textProps[2].editor;
+
+ info("Entering a value and bluring the field to expect a rule change");
+ onRuleViewChanged = view.once("ruleview-changed");
+ editor.input.value = "100%";
+ view.throttle.flush();
+ yield onRuleViewChanged;
+
+ onRuleViewChanged = view.once("ruleview-changed");
+ editor.input.blur();
+ yield onRuleViewChanged;
+
+ ok(propEditor.container.classList.contains("ruleview-highlight"),
+ "margin-left text property is correctly highlighted.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_10.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_10.js
new file mode 100644
index 000000000..ac336591d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_10.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";
+
+// Tests that the rule view search filter works properly for rule selectors.
+
+const TEST_URI = `
+ <style type="text/css">
+ html, body, div {
+ background-color: #00F;
+ }
+ #testid {
+ width: 100%;
+ }
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>
+`;
+
+const TEST_DATA = [
+ {
+ desc: "Tests that the search filter works properly for a single rule " +
+ "selector",
+ search: "#test",
+ selectorText: "#testid",
+ index: 0
+ },
+ {
+ desc: "Tests that the search filter works properly for multiple rule " +
+ "selectors",
+ search: "body",
+ selectorText: "html, body, div",
+ index: 2
+ }
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ for (let data of TEST_DATA) {
+ info(data.desc);
+ yield setSearchFilter(view, data.search);
+ yield checkRules(view, data);
+ yield clearSearchAndCheckRules(view);
+ }
+}
+
+function* checkRules(view, data) {
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ is(ruleEditor.rule.selectorText, data.selectorText,
+ "Second rule is " + data.selectorText + ".");
+ ok(ruleEditor.selectorText.children[data.index].classList
+ .contains("ruleview-highlight"),
+ data.selectorText + " selector is highlighted.");
+}
+
+function* clearSearchAndCheckRules(view) {
+ let doc = view.styleDocument;
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ let searchClearButton = view.searchClearButton;
+
+ info("Clearing the search filter");
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
+ yield view.inspector.once("ruleview-filtered");
+
+ info("Check the search filter is cleared and no rules are highlighted");
+ is(view.element.children.length, 3, "Should have 3 rules.");
+ ok(!searchField.value, "Search filter is cleared.");
+ ok(!doc.querySelectorAll(".ruleview-highlight").length,
+ "No rules are higlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js
new file mode 100644
index 000000000..349f1b9b3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.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 rule view search filter context menu works properly.
+
+const TEST_INPUT = "h1";
+const TEST_URI = "<h1>test filter context menu</h1>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view} = yield openRuleView();
+ yield selectNode("h1", inspector);
+
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ let searchContextMenu = toolbox.textBoxContextMenuPopup;
+ ok(searchContextMenu,
+ "The search filter context menu is loaded in the rule view");
+
+ let cmdUndo = searchContextMenu.querySelector("[command=cmd_undo]");
+ let cmdDelete = searchContextMenu.querySelector("[command=cmd_delete]");
+ let cmdSelectAll = searchContextMenu.querySelector("[command=cmd_selectAll]");
+ let cmdCut = searchContextMenu.querySelector("[command=cmd_cut]");
+ let cmdCopy = searchContextMenu.querySelector("[command=cmd_copy]");
+ let cmdPaste = searchContextMenu.querySelector("[command=cmd_paste]");
+
+ info("Opening context menu");
+
+ emptyClipboard();
+
+ let onFocus = once(searchField, "focus");
+ searchField.focus();
+ yield onFocus;
+
+ let onContextMenuPopup = once(searchContextMenu, "popupshowing");
+ EventUtils.synthesizeMouse(searchField, 2, 2,
+ {type: "contextmenu", button: 2}, win);
+ yield onContextMenuPopup;
+
+ is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled");
+ is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled");
+ is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled");
+
+ // Cut/Copy items are enabled in context menu even if there
+ // is no selection. See also Bug 1303033
+ is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
+ is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
+
+ if (isWindows()) {
+ // emptyClipboard only works on Windows (666254), assert paste only for this OS.
+ is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled");
+ }
+
+ info("Closing context menu");
+ let onContextMenuHidden = once(searchContextMenu, "popuphidden");
+ searchContextMenu.hidePopup();
+ yield onContextMenuHidden;
+
+ info("Copy text in search field using the context menu");
+ searchField.value = TEST_INPUT;
+ searchField.select();
+ EventUtils.synthesizeMouse(searchField, 2, 2,
+ {type: "contextmenu", button: 2}, win);
+ yield onContextMenuPopup;
+ yield waitForClipboardPromise(() => cmdCopy.click(), TEST_INPUT);
+ searchContextMenu.hidePopup();
+ yield onContextMenuHidden;
+
+ info("Reopen context menu and check command properties");
+ EventUtils.synthesizeMouse(searchField, 2, 2,
+ {type: "contextmenu", button: 2}, win);
+ yield onContextMenuPopup;
+
+ is(cmdUndo.getAttribute("disabled"), "", "cmdUndo is enabled");
+ is(cmdDelete.getAttribute("disabled"), "", "cmdDelete is enabled");
+ is(cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled");
+ is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
+ is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
+ is(cmdPaste.getAttribute("disabled"), "", "cmdPaste is enabled");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_escape-keypress.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_escape-keypress.js
new file mode 100644
index 000000000..21848dce8
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_escape-keypress.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 rule view search filter escape keypress will clear the search
+// field.
+
+const SEARCH = "00F";
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ background-color: #00F;
+ }
+ .testclass {
+ width: 100%;
+ }
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+ yield testEscapeKeypress(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(rule.textProps[0].editor.container.classList
+ .contains("ruleview-highlight"),
+ "background-color text property is correctly highlighted.");
+}
+
+function* testEscapeKeypress(inspector, view) {
+ info("Pressing the escape key on search filter");
+
+ let doc = view.styleDocument;
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ let onRuleViewFiltered = inspector.once("ruleview-filtered");
+
+ searchField.focus();
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+ yield onRuleViewFiltered;
+
+ info("Check the search filter is cleared and no rules are highlighted");
+ is(view.element.children.length, 3, "Should have 3 rules.");
+ ok(!searchField.value, "Search filter is cleared");
+ ok(!doc.querySelectorAll(".ruleview-highlight").length,
+ "No rules are higlighted");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js b/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js
new file mode 100644
index 000000000..b3f4ef364
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js
@@ -0,0 +1,171 @@
+/* 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
+
+const osString = Services.appinfo.OS;
+
+const TEST_URI = `
+ <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">
+ <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">
+ 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>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+ yield checkCopySelection(view);
+ yield checkSelectAll(view);
+ yield checkCopyEditorValue(view);
+});
+
+function* checkCopySelection(view) {
+ info("Testing selection copy");
+
+ let contentDoc = view.styleDocument;
+ let win = view.styleWindow;
+ let prop = contentDoc.querySelector(".ruleview-property");
+ let values = contentDoc.querySelectorAll(".ruleview-propertyvaluecontainer");
+
+ let range = contentDoc.createRange();
+ range.setStart(prop, 0);
+ range.setEnd(values[4], 2);
+ win.getSelection().addRange(range);
+ info("Checking that _Copy() returns the correct clipboard value");
+
+ let expectedPattern = " margin: 10em;[\\r\\n]+" +
+ " font-size: 14pt;[\\r\\n]+" +
+ " font-family: helvetica, sans-serif;[\\r\\n]+" +
+ " color: #AAA;[\\r\\n]+" +
+ "}[\\r\\n]+" +
+ "html {[\\r\\n]+" +
+ " color: #000000;[\\r\\n]*";
+
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, prop);
+ let menuitemCopy = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy"));
+
+ ok(menuitemCopy.visible,
+ "Copy menu item is displayed as expected");
+
+ try {
+ yield waitForClipboardPromise(() => menuitemCopy.click(),
+ () => checkClipboardData(expectedPattern));
+ } catch (e) {
+ failedClipboard(expectedPattern);
+ }
+}
+
+function* checkSelectAll(view) {
+ info("Testing select-all copy");
+
+ let contentDoc = view.styleDocument;
+ let prop = contentDoc.querySelector(".ruleview-property");
+
+ info("Checking that _SelectAll() then copy returns the correct " +
+ "clipboard value");
+ view._contextmenu._onSelectAll();
+ let expectedPattern = "element {[\\r\\n]+" +
+ " margin: 10em;[\\r\\n]+" +
+ " font-size: 14pt;[\\r\\n]+" +
+ " font-family: helvetica, sans-serif;[\\r\\n]+" +
+ " color: #AAA;[\\r\\n]+" +
+ "}[\\r\\n]+" +
+ "html {[\\r\\n]+" +
+ " color: #000000;[\\r\\n]+" +
+ "}[\\r\\n]*";
+
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, prop);
+ let menuitemCopy = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy"));
+
+ ok(menuitemCopy.visible,
+ "Copy menu item is displayed as expected");
+
+ try {
+ yield waitForClipboardPromise(() => menuitemCopy.click(),
+ () => checkClipboardData(expectedPattern));
+ } catch (e) {
+ failedClipboard(expectedPattern);
+ }
+}
+
+function* checkCopyEditorValue(view) {
+ info("Testing CSS property editor value copy");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+
+ let editor = yield focusEditableField(view, propEditor.valueSpan);
+
+ info("Checking that copying a css property value editor returns the correct" +
+ " clipboard value");
+
+ let expectedPattern = "10em";
+
+ let allMenuItems = openStyleContextMenuAndGetAllItems(view, editor.input);
+ let menuitemCopy = allMenuItems.find(item => item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy"));
+
+ ok(menuitemCopy.visible,
+ "Copy menu item is displayed as expected");
+
+ try {
+ yield waitForClipboardPromise(() => menuitemCopy.click(),
+ () => checkClipboardData(expectedPattern));
+ } catch (e) {
+ 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/devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js
new file mode 100644
index 000000000..54e25c399
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js
@@ -0,0 +1,38 @@
+/* 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 hidden on page navigation.
+
+const TEST_URI = `
+ <style type="text/css">
+ body, p, td {
+ background: red;
+ }
+ </style>
+ Test the selector highlighter
+`;
+
+const TEST_URI_2 = "data:text/html,<html><body>test</body></html>";
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ let highlighters = view.highlighters;
+
+ info("Clicking on a selector icon");
+ let icon = getRuleViewSelectorHighlighterIcon(view, "body, p, td");
+
+ let onToggled = view.once("ruleview-selectorhighlighter-toggled");
+ EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow);
+ let isVisible = yield onToggled;
+
+ ok(highlighters.selectorHighlighterShown, "The selectorHighlighterShown is set.");
+ ok(view.selectorHighlighter, "The selectorhighlighter instance was created");
+ ok(isVisible, "The toggle event says the highlighter is visible");
+
+ yield navigateTo(inspector, TEST_URI_2);
+ ok(!highlighters.selectorHighlighterShown, "The selectorHighlighterShown is unset.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_01.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_01.js
new file mode 100644
index 000000000..4c8853e02
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_01.js
@@ -0,0 +1,35 @@
+/* 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 clicking on a selector
+// icon in the rule view.
+
+const TEST_URI = `
+ <style type="text/css">
+ body, p, td {
+ background: red;
+ }
+ </style>
+ Test the selector highlighter
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {view} = yield openRuleView();
+
+ ok(!view.selectorHighlighter,
+ "No selectorhighlighter exist in the rule-view");
+
+ info("Clicking on a selector icon");
+ let icon = getRuleViewSelectorHighlighterIcon(view, "body, p, td");
+
+ let onToggled = view.once("ruleview-selectorhighlighter-toggled");
+ EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow);
+ let isVisible = yield onToggled;
+
+ ok(view.selectorHighlighter, "The selectorhighlighter instance was created");
+ ok(isVisible, "The toggle event says the highlighter is visible");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_02.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_02.js
new file mode 100644
index 000000000..33f73e587
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_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";
+
+// Test that the selector highlighter is shown when clicking on a selector icon
+// 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 TEST_URI = `
+ <style type="text/css">
+ body {
+ background: red;
+ }
+ p {
+ color: white;
+ }
+ </style>
+ <p>Testing the selector highlighter</p>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = 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
+ view.selectorHighlighter = HighlighterFront;
+
+ let icon = getRuleViewSelectorHighlighterIcon(view, "body");
+
+ info("Checking that the HighlighterFront's show/hide methods are called");
+
+ info("Clicking once on the body selector highlighter icon");
+ yield clickSelectorIcon(icon, view);
+ ok(HighlighterFront.isShown, "The highlighter is shown");
+
+ info("Clicking once again on the body selector highlighter icon");
+ yield clickSelectorIcon(icon, view);
+ ok(!HighlighterFront.isShown, "The highlighter is hidden");
+
+ info("Checking that the right NodeFront reference and options are passed");
+ yield selectNode("p", inspector);
+ icon = getRuleViewSelectorHighlighterIcon(view, "p");
+
+ yield clickSelectorIcon(icon, view);
+ 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);
+ icon = getRuleViewSelectorHighlighterIcon(view, "body");
+ yield clickSelectorIcon(icon, view);
+ 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)");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_03.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_03.js
new file mode 100644
index 000000000..1ffbac012
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_03.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";
+
+// Test that the selector highlighter toggling mechanism works correctly.
+
+// Note that in this test, we mock the highlighter front, merely testing the
+// behavior of the style-inspector UI for now
+
+const TEST_URI = `
+ <style type="text/css">
+ div {text-decoration: underline;}
+ .node-1 {color: red;}
+ .node-2 {color: green;}
+ </style>
+ <div class="node-1">Node 1</div>
+ <div class="node-2">Node 2</div>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ // Mock the highlighter front.
+ let HighlighterFront = {
+ isShown: false,
+ show: function () {
+ this.isShown = true;
+ },
+ hide: function () {
+ this.isShown = false;
+ }
+ };
+
+ // Inject the mock highlighter in the rule-view
+ view.selectorHighlighter = HighlighterFront;
+
+ info("Select .node-1 and click on the .node-1 selector icon");
+ yield selectNode(".node-1", inspector);
+ let icon = getRuleViewSelectorHighlighterIcon(view, ".node-1");
+ yield clickSelectorIcon(icon, view);
+ ok(HighlighterFront.isShown, "The highlighter is shown");
+
+ info("With .node-1 still selected, click again on the .node-1 selector icon");
+ yield clickSelectorIcon(icon, view);
+ ok(!HighlighterFront.isShown, "The highlighter is now hidden");
+
+ info("With .node-1 still selected, click on the div selector icon");
+ icon = getRuleViewSelectorHighlighterIcon(view, "div");
+ yield clickSelectorIcon(icon, view);
+ ok(HighlighterFront.isShown, "The highlighter is shown again");
+
+ info("With .node-1 still selected, click again on the .node-1 selector icon");
+ icon = getRuleViewSelectorHighlighterIcon(view, ".node-1");
+ yield clickSelectorIcon(icon, view);
+ ok(HighlighterFront.isShown,
+ "The highlighter is shown again since the clicked selector was different");
+
+ info("Selecting .node-2");
+ yield selectNode(".node-2", inspector);
+ ok(HighlighterFront.isShown,
+ "The highlighter is still shown after selection");
+
+ info("With .node-2 selected, click on the div selector icon");
+ icon = getRuleViewSelectorHighlighterIcon(view, "div");
+ yield clickSelectorIcon(icon, view);
+ ok(HighlighterFront.isShown,
+ "The highlighter is shown still since the selected was different");
+
+ info("Switching back to .node-1 and clicking on the div selector");
+ yield selectNode(".node-1", inspector);
+ icon = getRuleViewSelectorHighlighterIcon(view, "div");
+ yield clickSelectorIcon(icon, view);
+ ok(!HighlighterFront.isShown,
+ "The highlighter is hidden now that the same selector was clicked");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_04.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_04.js
new file mode 100644
index 000000000..b770f8127
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_04.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 selector highlighter is shown when clicking on a selector icon
+// for the 'element {}' rule
+
+// Note that in this test, we mock the highlighter front, merely testing the
+// behavior of the style-inspector UI for now
+
+const TEST_URI = `
+<p>Testing the selector highlighter for the 'element {}' rule</p>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = 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
+ view.selectorHighlighter = HighlighterFront;
+
+ info("Checking that the right NodeFront reference and options are passed");
+ yield selectNode("p", inspector);
+ let icon = getRuleViewSelectorHighlighterIcon(view, "element");
+
+ yield clickSelectorIcon(icon, view);
+ is(HighlighterFront.nodeFront.tagName, "P",
+ "The right NodeFront is passed to the highlighter (1)");
+ is(HighlighterFront.options.selector, "body > p:nth-child(1)",
+ "The right selector option is passed to the highlighter (1)");
+ ok(HighlighterFront.isShown, "The toggle event says the highlighter is visible");
+
+ yield clickSelectorIcon(icon, view);
+ ok(!HighlighterFront.isShown, "The toggle event says the highlighter is not visible");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_selector_highlight.js b/devtools/client/inspector/rules/test/browser_rules_selector_highlight.js
new file mode 100644
index 000000000..91422d57a
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector_highlight.js
@@ -0,0 +1,144 @@
+/* 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 selector text is highlighted correctly according
+// to the components of the selector.
+
+const TEST_URI = [
+ "<style type='text/css'>",
+ " h1 {}",
+ " h1#testid {}",
+ " h1 + p {}",
+ " div[hidden=\"true\"] {}",
+ " div[title=\"test\"][checked=true] {}",
+ " p:empty {}",
+ " p:lang(en) {}",
+ " .testclass:active {}",
+ " .testclass:focus {}",
+ " .testclass:hover {}",
+ "</style>",
+ "<h1>Styled Node</h1>",
+ "<p>Paragraph</p>",
+ "<h1 id=\"testid\">Styled Node</h1>",
+ "<div hidden=\"true\"></div>",
+ "<div title=\"test\" checked=\"true\"></div>",
+ "<p></p>",
+ "<p lang=\"en\">Paragraph<p>",
+ "<div class=\"testclass\">Styled Node</div>"
+].join("\n");
+
+const SELECTOR_ATTRIBUTE = "ruleview-selector-attribute";
+const SELECTOR_ELEMENT = "ruleview-selector";
+const SELECTOR_PSEUDO_CLASS = "ruleview-selector-pseudo-class";
+const SELECTOR_PSEUDO_CLASS_LOCK = "ruleview-selector-pseudo-class-lock";
+
+const TEST_DATA = [
+ {
+ node: "h1",
+ expected: [
+ { value: "h1", class: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ node: "h1 + p",
+ expected: [
+ { value: "h1 + p", class: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ node: "h1#testid",
+ expected: [
+ { value: "h1#testid", class: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ node: "div[hidden='true']",
+ expected: [
+ { value: "div", class: SELECTOR_ELEMENT },
+ { value: "[hidden=\"true\"]", class: SELECTOR_ATTRIBUTE }
+ ]
+ },
+ {
+ node: "div[title=\"test\"][checked=\"true\"]",
+ expected: [
+ { value: "div", class: SELECTOR_ELEMENT },
+ { value: "[title=\"test\"]", class: SELECTOR_ATTRIBUTE },
+ { value: "[checked=\"true\"]", class: SELECTOR_ATTRIBUTE }
+ ]
+ },
+ {
+ node: "p:empty",
+ expected: [
+ { value: "p", class: SELECTOR_ELEMENT },
+ { value: ":empty", class: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ node: "p:lang(en)",
+ expected: [
+ { value: "p", class: SELECTOR_ELEMENT },
+ { value: ":lang(en)", class: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ node: ".testclass",
+ pseudoClass: ":active",
+ expected: [
+ { value: ".testclass", class: SELECTOR_ELEMENT },
+ { value: ":active", class: SELECTOR_PSEUDO_CLASS_LOCK }
+ ]
+ },
+ {
+ node: ".testclass",
+ pseudoClass: ":focus",
+ expected: [
+ { value: ".testclass", class: SELECTOR_ELEMENT },
+ { value: ":focus", class: SELECTOR_PSEUDO_CLASS_LOCK }
+ ]
+ },
+ {
+ node: ".testclass",
+ pseudoClass: ":hover",
+ expected: [
+ { value: ".testclass", class: SELECTOR_ELEMENT },
+ { value: ":hover", class: SELECTOR_PSEUDO_CLASS_LOCK }
+ ]
+ },
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ for (let {node, pseudoClass, expected} of TEST_DATA) {
+ yield selectNode(node, inspector);
+
+ if (pseudoClass) {
+ let onRefresh = inspector.once("rule-view-refreshed");
+ inspector.togglePseudoClass(pseudoClass);
+ yield onRefresh;
+ }
+
+ let selectorContainer =
+ getRuleViewRuleEditor(view, 1).selectorText.firstChild;
+
+ if (selectorContainer.children.length === expected.length) {
+ for (let i = 0; i < expected.length; i++) {
+ is(expected[i].value, selectorContainer.children[i].textContent,
+ "Got expected selector value: " + expected[i].value + " == " +
+ selectorContainer.children[i].textContent);
+ is(expected[i].class, selectorContainer.children[i].className,
+ "Got expected class name: " + expected[i].class + " == " +
+ selectorContainer.children[i].className);
+ }
+ } else {
+ for (let selector of selectorContainer.children) {
+ info("Actual selector components: { value: " + selector.textContent +
+ ", class: " + selector.className + " }\n");
+ }
+ }
+ }
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_strict-search-filter-computed-list_01.js b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter-computed-list_01.js
new file mode 100644
index 000000000..dea9fff32
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter-computed-list_01.js
@@ -0,0 +1,182 @@
+/* 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 strict search filter and clear button works properly
+// in the computed list
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ margin: 4px 0px 10px 44px;
+ }
+ .testclass {
+ background-color: red;
+ }
+ </style>
+ <h1 id="testid" class="testclass">Styled Node</h1>
+`;
+
+const TEST_DATA = [
+ {
+ desc: "Tests that the strict search filter works properly in the " +
+ "computed list for property names",
+ search: "`margin-left`",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: false,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: true
+ },
+ {
+ desc: "Tests that the strict search filter works properly in the " +
+ "computed list for property values",
+ search: "`0px`",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: false,
+ isMarginRightHighlighted: true,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: false
+ },
+ {
+ desc: "Tests that the strict search filter works properly in the " +
+ "computed list for parsed property names",
+ search: "`margin-left`:",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: false,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: true
+ },
+ {
+ desc: "Tests that the strict search filter works properly in the " +
+ "computed list for parsed property values",
+ search: ":`4px`",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: true,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: false
+ },
+ {
+ desc: "Tests that the strict search filter works properly in the " +
+ "computed list for property line input",
+ search: "`margin-top`:`4px`",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: true,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: false
+ },
+ {
+ desc: "Tests that the strict search filter works properly in the " +
+ "computed list for a parsed strict property name and non-strict " +
+ "property value",
+ search: "`margin-top`:4px",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: true,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: false
+ },
+ {
+ desc: "Tests that the strict search filter works properly in the " +
+ "computed list for a parsed strict property value and non-strict " +
+ "property name",
+ search: "i:`4px`",
+ isExpanderOpen: true,
+ isFilterOpen: true,
+ isMarginHighlighted: false,
+ isMarginTopHighlighted: true,
+ isMarginRightHighlighted: false,
+ isMarginBottomHighlighted: false,
+ isMarginLeftHighlighted: false
+ },
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ for (let data of TEST_DATA) {
+ info(data.desc);
+ yield setSearchFilter(view, data.search);
+ yield checkRules(view, data);
+ yield clearSearchAndCheckRules(view);
+ }
+}
+
+function* checkRules(view, data) {
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let textPropEditor = rule.textProps[0].editor;
+ let computed = textPropEditor.computed;
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ is(!!textPropEditor.expander.getAttribute("open"), data.isExpanderOpen,
+ "Got correct expander state.");
+ is(computed.hasAttribute("filter-open"), data.isFilterOpen,
+ "Got correct expanded state for margin computed list.");
+ is(textPropEditor.container.classList.contains("ruleview-highlight"),
+ data.isMarginHighlighted,
+ "Got correct highlight for margin text property.");
+
+ is(computed.children[0].classList.contains("ruleview-highlight"),
+ data.isMarginTopHighlighted,
+ "Got correct highlight for margin-top computed property.");
+ is(computed.children[1].classList.contains("ruleview-highlight"),
+ data.isMarginRightHighlighted,
+ "Got correct highlight for margin-right computed property.");
+ is(computed.children[2].classList.contains("ruleview-highlight"),
+ data.isMarginBottomHighlighted,
+ "Got correct highlight for margin-bottom computed property.");
+ is(computed.children[3].classList.contains("ruleview-highlight"),
+ data.isMarginLeftHighlighted,
+ "Got correct highlight for margin-left computed property.");
+}
+
+function* clearSearchAndCheckRules(view) {
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ let searchClearButton = view.searchClearButton;
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let textPropEditor = rule.textProps[0].editor;
+ let computed = textPropEditor.computed;
+
+ info("Clearing the search filter");
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
+ yield view.inspector.once("ruleview-filtered");
+
+ info("Check the search filter is cleared and no rules are highlighted");
+ is(view.element.children.length, 3, "Should have 3 rules.");
+ ok(!searchField.value, "Search filter is cleared");
+ ok(!view.styleDocument.querySelectorAll(".ruleview-highlight").length,
+ "No rules are higlighted");
+
+ ok(!textPropEditor.expander.getAttribute("open"), "Expander is closed.");
+ ok(!computed.hasAttribute("filter-open"),
+ "margin computed list is closed.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_01.js b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_01.js
new file mode 100644
index 000000000..50948e174
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_01.js
@@ -0,0 +1,130 @@
+/* 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 strict search filter works properly for property
+// names.
+
+const TEST_URI = `
+ <style type="text/css">
+ #testid {
+ width: 2%;
+ color: red;
+ }
+ .testclass {
+ width: 22%;
+ background-color: #00F;
+ }
+ </style>
+ <h1 id="testid" class="testclass">Styled Node</h1>
+`;
+
+const TEST_DATA = [
+ {
+ desc: "Tests that the strict search filter works properly for property " +
+ "names",
+ search: "`color`",
+ ruleCount: 2,
+ propertyIndex: 1
+ },
+ {
+ desc: "Tests that the strict search filter works properly for property " +
+ "values",
+ search: "`2%`",
+ ruleCount: 2,
+ propertyIndex: 0
+ },
+ {
+ desc: "Tests that the strict search filter works properly for parsed " +
+ "property names",
+ search: "`color`:",
+ ruleCount: 2,
+ propertyIndex: 1
+ },
+ {
+ desc: "Tests that the strict search filter works properly for parsed " +
+ "property values",
+ search: ":`2%`",
+ ruleCount: 2,
+ propertyIndex: 0
+ },
+ {
+ desc: "Tests that the strict search filter works properly for property " +
+ "line input",
+ search: "`width`:`2%`",
+ ruleCount: 2,
+ propertyIndex: 0
+ },
+ {
+ desc: "Tests that the search filter works properly for a parsed strict " +
+ "property name and non-strict property value.",
+ search: "`width`:2%",
+ ruleCount: 3,
+ propertyIndex: 0
+ },
+ {
+ desc: "Tests that the search filter works properly for a parsed strict " +
+ "property value and non-strict property name.",
+ search: "i:`2%`",
+ ruleCount: 2,
+ propertyIndex: 0
+ }
+];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ for (let data of TEST_DATA) {
+ info(data.desc);
+ yield setSearchFilter(view, data.search);
+ yield checkRules(view, data);
+ yield clearSearchAndCheckRules(view);
+ }
+}
+
+function* checkRules(view, data) {
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, data.ruleCount,
+ "Should have " + data.ruleCount + " rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(rule.textProps[data.propertyIndex].editor.container.classList
+ .contains("ruleview-highlight"),
+ "Text property is correctly highlighted.");
+
+ if (data.ruleCount > 2) {
+ rule = getRuleViewRuleEditor(view, 2).rule;
+ is(rule.selectorText, ".testclass", "Third rule is .testclass.");
+ ok(rule.textProps[data.propertyIndex].editor.container.classList
+ .contains("ruleview-highlight"),
+ "Text property is correctly highlighted.");
+ }
+}
+
+function* clearSearchAndCheckRules(view) {
+ let doc = view.styleDocument;
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ let searchClearButton = view.searchClearButton;
+
+ info("Clearing the search filter");
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
+ yield view.inspector.once("ruleview-filtered");
+
+ info("Check the search filter is cleared and no rules are highlighted");
+ is(view.element.children.length, 3, "Should have 3 rules.");
+ ok(!searchField.value, "Search filter is cleared.");
+ ok(!doc.querySelectorAll(".ruleview-highlight").length,
+ "No rules are higlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_02.js b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_02.js
new file mode 100644
index 000000000..0c76f0518
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_02.js
@@ -0,0 +1,34 @@
+/* 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 strict search filter works properly for stylesheet
+// source.
+
+const SEARCH = "`doc_urls_clickable.css:1`";
+const TEST_URI = URL_ROOT + "doc_urls_clickable.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".relative1", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let rule = getRuleViewRuleEditor(view, 1).rule;
+ let source = rule.textProps[0].editor.ruleEditor.source;
+
+ is(rule.selectorText, ".relative1", "Second rule is .relative1.");
+ ok(source.classList.contains("ruleview-highlight"),
+ "stylesheet source is correctly highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_03.js b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_03.js
new file mode 100644
index 000000000..0326b0e9c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_03.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 that the rule view strict search filter works properly for selector
+// values.
+
+const SEARCH = "`.testclass`";
+
+const TEST_URI = `
+ <style type="text/css">
+ .testclass1 {
+ background-color: #00F;
+ }
+ .testclass {
+ color: red;
+ }
+ </style>
+ <h1 id="testid" class="testclass testclass1">Styled Node</h1>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#testid", inspector);
+ yield testAddTextInFilter(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, view) {
+ yield setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 2, "Should have 2 rules.");
+ is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element",
+ "First rule is inline element.");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ is(ruleEditor.rule.selectorText, ".testclass", "Second rule is .testclass.");
+ ok(ruleEditor.selectorText.children[0].classList
+ .contains("ruleview-highlight"), ".testclass selector is highlighted.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js b/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js
new file mode 100644
index 000000000..927deb8ce
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js
@@ -0,0 +1,203 @@
+/* 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";
+
+// FIXME: 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 = 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>
+`);
+
+add_task(function* () {
+ yield addTab(DOCUMENT_URL);
+ let {toolbox, inspector, view, testActor} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ yield testInlineStyle(view);
+ yield testFirstInlineStyleSheet(view, toolbox, testActor);
+ yield testSecondInlineStyleSheet(view, toolbox, testActor);
+ yield testExternalStyleSheet(view, toolbox, testActor);
+ yield testDisabledStyleEditor(view, toolbox);
+});
+
+function* testInlineStyle(view) {
+ info("Testing inline style");
+
+ let onTab = waitForTab();
+ info("Clicking on the first link in the rule-view");
+ clickLinkByIndex(view, 0);
+
+ let tab = yield onTab;
+
+ let tabURI = tab.linkedBrowser.documentURI.spec;
+ ok(tabURI.startsWith("view-source:"), "View source tab is open");
+ info("Closing tab");
+ gBrowser.removeTab(tab);
+}
+
+function* testFirstInlineStyleSheet(view, toolbox, testActor) {
+ 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");
+
+ yield validateStyleEditorSheet(editor, 0, testActor);
+}
+
+function* testSecondInlineStyleSheet(view, toolbox, testActor) {
+ 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");
+ yield validateStyleEditorSheet(editor, 1, testActor);
+}
+
+function* testExternalStyleSheet(view, toolbox, testActor) {
+ 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");
+ yield validateStyleEditorSheet(editor, 2, testActor);
+}
+
+function* validateStyleEditorSheet(editor, expectedSheetIndex, testActor) {
+ info("validating style editor stylesheet");
+ is(editor.styleSheet.styleSheetIndex, expectedSheetIndex,
+ "loaded stylesheet index matches document stylesheet");
+
+ let href = editor.styleSheet.href || editor.styleSheet.nodeHref;
+
+ let expectedHref = yield testActor.eval(
+ `content.document.styleSheets[${expectedSheetIndex}].href ||
+ content.document.location.href`);
+
+ is(href, expectedHref, "loaded stylesheet href matches document stylesheet");
+}
+
+function* testDisabledStyleEditor(view, toolbox) {
+ info("Testing with the style editor disabled");
+
+ info("Switching to the inspector panel in the toolbox");
+ yield toolbox.selectTool("inspector");
+
+ info("Disabling the style editor");
+ Services.prefs.setBoolPref("devtools.styleeditor.enabled", false);
+ gDevTools.emit("tool-unregistered", "styleeditor");
+
+ info("Clicking on a link");
+ testUnselectableRuleViewLink(view, 1);
+ clickLinkByIndex(view, 1);
+
+ is(toolbox.currentToolId, "inspector", "The click should have no effect");
+
+ info("Enabling the style editor");
+ Services.prefs.setBoolPref("devtools.styleeditor.enabled", true);
+ gDevTools.emit("tool-registered", "styleeditor");
+
+ info("Clicking on a link");
+ let onStyleEditorSelected = toolbox.once("styleeditor-selected");
+ clickLinkByIndex(view, 1);
+ yield onStyleEditorSelected;
+ is(toolbox.currentToolId, "styleeditor", "Style Editor should be selected");
+
+ Services.prefs.clearUserPref("devtools.styleeditor.enabled");
+}
+
+function testRuleViewLinkLabel(view) {
+ let link = getRuleViewLinkByIndex(view, 2);
+ let labelElem = link.querySelector(".ruleview-rule-source-label");
+ let value = labelElem.textContent;
+ let tooltipText = labelElem.getAttribute("title");
+
+ 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 testUnselectableRuleViewLink(view, index) {
+ let link = getRuleViewLinkByIndex(view, index);
+ let unselectable = link.hasAttribute("unselectable");
+
+ ok(unselectable, "Rule view is unselectable");
+}
+
+function clickLinkByIndex(view, index) {
+ let link = getRuleViewLinkByIndex(view, index);
+ link.scrollIntoView();
+ link.click();
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_urls-clickable.js b/devtools/client/inspector/rules/test/browser_rules_urls-clickable.js
new file mode 100644
index 000000000..fb1211e3c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_urls-clickable.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";
+
+// Tests to make sure that URLs are clickable in the rule view
+
+const TEST_URI = URL_ROOT + "doc_urls_clickable.html";
+const TEST_IMAGE = URL_ROOT + "doc_test_image.png";
+const BASE_64_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAA" +
+ "FCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAA" +
+ "BJRU5ErkJggg==";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {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.styleDocument
+ .querySelector(".ruleview-propertyvaluecontainer a");
+ ok(relativeLink, "Link exists for relative1 node");
+ is(relativeLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ yield selectNode(relative2, inspector);
+ relativeLink = ruleView.styleDocument
+ .querySelector(".ruleview-propertyvaluecontainer a");
+ ok(relativeLink, "Link exists for relative2 node");
+ is(relativeLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ yield selectNode(absolute, inspector);
+ let absoluteLink = ruleView.styleDocument
+ .querySelector(".ruleview-propertyvaluecontainer a");
+ ok(absoluteLink, "Link exists for absolute node");
+ is(absoluteLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ yield selectNode(inline, inspector);
+ let inlineLink = ruleView.styleDocument
+ .querySelector(".ruleview-propertyvaluecontainer a");
+ ok(inlineLink, "Link exists for inline node");
+ is(inlineLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ yield selectNode(base64, inspector);
+ let base64Link = ruleView.styleDocument
+ .querySelector(".ruleview-propertyvaluecontainer a");
+ ok(base64Link, "Link exists for base64 node");
+ is(base64Link.getAttribute("href"), BASE_64_URL, "href matches");
+
+ yield selectNode(inlineresolved, inspector);
+ let inlineResolvedLink = ruleView.styleDocument
+ .querySelector(".ruleview-propertyvaluecontainer a");
+ ok(inlineResolvedLink, "Link exists for style tag node");
+ is(inlineResolvedLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ yield selectNode(noimage, inspector);
+ let noimageLink = ruleView.styleDocument
+ .querySelector(".ruleview-propertyvaluecontainer a");
+ ok(!noimageLink, "There is no link for the node with no background image");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_user-agent-styles-uneditable.js b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles-uneditable.js
new file mode 100644
index 000000000..e1bafff9b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles-uneditable.js
@@ -0,0 +1,58 @@
+/* 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
+
+const TEST_URI = `
+ <blockquote type=cite>
+ <pre _moz_quote=true>
+ inspect <a href='foo' style='color:orange'>user agent</a> styles
+ </pre>
+ </blockquote>
+`;
+
+var PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
+
+add_task(function* () {
+ info("Starting the test with the pref set to true before toolbox is opened");
+ Services.prefs.setBoolPref(PREF_UA_STYLES, true);
+
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {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");
+
+ let firstProp = rule.textProps.filter(p => !p.invisible)[0];
+
+ ok(!firstProp.editor.nameSpan._editable,
+ "nameSpan is not editable");
+ ok(!firstProp.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/devtools/client/inspector/rules/test/browser_rules_user-agent-styles.js b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles.js
new file mode 100644
index 000000000..6852e3c03
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles.js
@@ -0,0 +1,183 @@
+/* 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.
+
+var PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
+const { PrefObserver } = require("devtools/client/styleeditor/utils");
+
+const TEST_URI = URL_ROOT + "doc_author-sheet.html";
+
+const TEST_DATA = [
+ {
+ selector: "blockquote",
+ numUserRules: 1,
+ numUARules: 0
+ },
+ {
+ selector: "pre",
+ numUserRules: 1,
+ numUARules: 0
+ },
+ {
+ selector: "input[type=range]",
+ numUserRules: 1,
+ numUARules: 0
+ },
+ {
+ selector: "input[type=number]",
+ numUserRules: 1,
+ numUARules: 0
+ },
+ {
+ selector: "input[type=color]",
+ numUserRules: 1,
+ numUARules: 0
+ },
+ {
+ selector: "input[type=text]",
+ numUserRules: 1,
+ numUARules: 0
+ },
+ {
+ selector: "progress",
+ numUserRules: 1,
+ numUARules: 0
+ },
+ // Note that some tests below assume that the "a" selector is the
+ // last test in TEST_DATA.
+ {
+ selector: "a",
+ numUserRules: 3,
+ numUARules: 0
+ }
+];
+
+add_task(function* () {
+ requestLongerTimeout(4);
+
+ info("Starting the test with the pref set to true before toolbox is opened");
+ yield setUserAgentStylesPref(true);
+
+ yield addTab(TEST_URI);
+ let {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 = 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");
+
+ let userRules;
+ let uaRules;
+
+ for (let data of TEST_DATA) {
+ yield selectNode(data.selector, 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, data.numUserRules, "Correct number of user rules");
+ ok(uaRules.length > data.numUARules, "Has UA rules");
+ }
+
+ ok(userRules.some(rule => rule.matchedSelectors.length === 1),
+ "There is an inline style for element in user styles");
+
+ // These tests rely on the "a" selector being the last test in
+ // TEST_DATA.
+ ok(uaRules.some(rule => {
+ return rule.matchedSelectors.indexOf(":any-link") !== -1;
+ }), "There is a rule for :any-link");
+ ok(uaRules.some(rule => {
+ return rule.matchedSelectors.indexOf("*|*:link") !== -1;
+ }), "There is a rule for *|*:link");
+ ok(uaRules.some(rule => {
+ return rule.matchedSelectors.length === 1;
+ }), "Inline styles for ua styles");
+}
+
+function* userAgentStylesNotVisible(inspector, view) {
+ info("Making sure that user agent styles are not currently visible");
+
+ let userRules;
+ let uaRules;
+
+ for (let data of TEST_DATA) {
+ yield selectNode(data.selector, 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, data.numUserRules, "Correct number of user rules");
+ is(uaRules.length, data.numUARules, "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
+ }
+ );
+
+ // We may see multiple entries that map to a given rule; filter the
+ // duplicates here to match what the UI does.
+ let entryMap = new Map();
+ for (let entry of entries) {
+ entryMap.set(entry.rule, entry);
+ }
+ entries = [...entryMap.values()];
+
+ let elementStyle = view._elementStyle;
+ is(elementStyle.rules.length, entries.length,
+ "Should have correct number of rules (" + entries.length + ")");
+
+ entries = entries.sort((a, b) => {
+ return (a.pseudoElement || "z") > (b.pseudoElement || "z");
+ });
+
+ 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/devtools/client/inspector/rules/test/browser_rules_user-property-reset.js b/devtools/client/inspector/rules/test/browser_rules_user-property-reset.js
new file mode 100644
index 000000000..62b1d927c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_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
+
+const TEST_URI = `
+ <p id='id1' style='width:200px;'>element 1</p>
+ <p id='id2' style='width:100px;'>element 2</p>
+`;
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = 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, testActor);
+
+ 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.styleDocument.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(ruleView, 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 onStyleChanged = waitForStyleModification(inspector);
+ EventUtils.sendKey("return");
+ yield onBlur;
+ yield onStyleChanged;
+
+ info("Escaping out of the new property field that has been created after " +
+ "the value was edited");
+ let onNewFieldBlur = once(ruleView.styleDocument.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++;
+ }
+ return undefined;
+}
+
+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);
+}
diff --git a/devtools/client/inspector/rules/test/doc_author-sheet.html b/devtools/client/inspector/rules/test/doc_author-sheet.html
new file mode 100644
index 000000000..f8c2eadd5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_author-sheet.html
@@ -0,0 +1,39 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <title>authored sheet test</title>
+
+ <style>
+ pre a {
+ color: orange;
+ }
+ </style>
+
+ <script>
+ "use strict";
+ var gIOService = SpecialPowers.Cc["@mozilla.org/network/io-service;1"]
+ .getService(SpecialPowers.Ci.nsIIOService);
+
+ var style = "data:text/css,a { background-color: seagreen; }";
+ var uri = gIOService.newURI(style, null, null);
+ var windowUtils = SpecialPowers.wrap(window)
+ .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+ .getInterface(SpecialPowers.Ci.nsIDOMWindowUtils);
+ windowUtils.loadSheet(uri, windowUtils.AUTHOR_SHEET);
+ </script>
+
+</head>
+<body>
+ <input type=text placeholder=test></input>
+ <input type=color></input>
+ <input type=range></input>
+ <input type=number></input>
+ <progress></progress>
+ <blockquote type=cite>
+ <pre _moz_quote=true>
+ inspect <a href="foo">user agent</a> styles
+ </pre>
+ </blockquote>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_blob_stylesheet.html b/devtools/client/inspector/rules/test/doc_blob_stylesheet.html
new file mode 100644
index 000000000..c9973993b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_blob_stylesheet.html
@@ -0,0 +1,39 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+</html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Blob stylesheet sourcemap</title>
+</head>
+<body>
+<h1>Test</h1>
+<script>
+"use strict";
+
+var cssContent = `body {
+ background-color: black;
+}
+body > h1 {
+ color: white;
+}
+` +
+"/*# sourceMappingURL=data:application/json;base64,ewoidmVyc2lvbiI6IDMsCiJtYX" +
+"BwaW5ncyI6ICJBQUFBLElBQUs7RUFDSCxnQkFBZ0IsRUFBRSxLQUFLOztBQUN2QixTQUFPO0VBQ0" +
+"wsS0FBSyxFQUFFLEtBQUsiLAoic291cmNlcyI6IFsidGVzdC5zY3NzIl0sCiJzb3VyY2VzQ29udG" +
+"VudCI6IFsiYm9keSB7XG4gIGJhY2tncm91bmQtY29sb3I6IGJsYWNrO1xuICAmID4gaDEge1xuIC" +
+"AgIGNvbG9yOiB3aGl0ZTsgIFxuICB9XG59XG4iXSwKIm5hbWVzIjogW10sCiJmaWxlIjogInRlc3" +
+"QuY3NzIgp9Cg== */";
+var cssBlob = new Blob([cssContent], {type: "text/css"});
+var url = URL.createObjectURL(cssBlob);
+
+var head = document.querySelector("head");
+var link = document.createElement("link");
+link.rel = "stylesheet";
+link.type = "text/css";
+link.href = url;
+head.appendChild(link);
+</script>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet.html b/devtools/client/inspector/rules/test/doc_content_stylesheet.html
new file mode 100644
index 000000000..3ea65f606
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_content_stylesheet.html
@@ -0,0 +1,35 @@
+<html>
+<head>
+ <title>test</title>
+
+ <link href="./doc_content_stylesheet_linked.css" rel="stylesheet" type="text/css">
+
+ <script>
+ /* eslint no-unused-vars: [2, {"vars": "local"}] */
+ "use strict";
+ // Load script.css
+ function loadCSS() {
+ let 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/devtools/client/inspector/rules/test/doc_content_stylesheet_imported.css b/devtools/client/inspector/rules/test/doc_content_stylesheet_imported.css
new file mode 100644
index 000000000..ea1a3d986
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_content_stylesheet_imported.css
@@ -0,0 +1,5 @@
+@import url("./doc_content_stylesheet_imported2.css");
+
+#target {
+ text-decoration: underline;
+}
diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet_imported2.css b/devtools/client/inspector/rules/test/doc_content_stylesheet_imported2.css
new file mode 100644
index 000000000..77c73299e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_content_stylesheet_imported2.css
@@ -0,0 +1,3 @@
+#target {
+ text-decoration: underline;
+}
diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet_linked.css b/devtools/client/inspector/rules/test/doc_content_stylesheet_linked.css
new file mode 100644
index 000000000..712ba78fb
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_content_stylesheet_linked.css
@@ -0,0 +1,3 @@
+table {
+ border-collapse: collapse;
+}
diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet_script.css b/devtools/client/inspector/rules/test/doc_content_stylesheet_script.css
new file mode 100644
index 000000000..5aa5e2c6c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_content_stylesheet_script.css
@@ -0,0 +1,5 @@
+@import url("./doc_content_stylesheet_imported.css");
+
+table {
+ opacity: 1;
+}
diff --git a/devtools/client/inspector/rules/test/doc_copystyles.css b/devtools/client/inspector/rules/test/doc_copystyles.css
new file mode 100644
index 000000000..83f0c87b1
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_copystyles.css
@@ -0,0 +1,11 @@
+/* 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/. */
+
+html, body, #testid {
+ color: #F00;
+ background-color: #00F;
+ font-size: 12px;
+ border-color: #00F !important;
+ --var: "*/";
+}
diff --git a/devtools/client/inspector/rules/test/doc_copystyles.html b/devtools/client/inspector/rules/test/doc_copystyles.html
new file mode 100644
index 000000000..da1b4c0b3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_copystyles.html
@@ -0,0 +1,11 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+ <head>
+ <title>Test case for copying stylesheet in rule-view</title>
+ <link rel="stylesheet" type="text/css" href="doc_copystyles.css"/>
+ </head>
+ <body>
+ <div id='testid'>Styled Node</div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_cssom.html b/devtools/client/inspector/rules/test/doc_cssom.html
new file mode 100644
index 000000000..28de66d7d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_cssom.html
@@ -0,0 +1,22 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <title>CSSOM test</title>
+
+ <script>
+ "use strict";
+ window.onload = function () {
+ let x = document.styleSheets[0];
+ x.insertRule("div { color: seagreen; }", 1);
+ };
+ </script>
+
+ <style>
+ span { }
+ </style>
+</head>
+<body>
+ <div id="target"> the ocean </div>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_custom.html b/devtools/client/inspector/rules/test/doc_custom.html
new file mode 100644
index 000000000..09bf501d5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_custom.html
@@ -0,0 +1,33 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+ <head>
+ <style>
+ #testidSimple {
+ --background-color: blue;
+ }
+ .testclassSimple {
+ --background-color: green;
+ }
+
+ .testclassImportant {
+ --background-color: green !important;
+ }
+ #testidImportant {
+ --background-color: blue;
+ }
+
+ #testidDisable {
+ --background-color: blue;
+ }
+ .testclassDisable {
+ --background-color: green;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="testidSimple" class="testclassSimple">Styled Node</div>
+ <div id="testidImportant" class="testclassImportant">Styled Node</div>
+ <div id="testidDisable" class="testclassDisable">Styled Node</div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_filter.html b/devtools/client/inspector/rules/test/doc_filter.html
new file mode 100644
index 000000000..cb2df9feb
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_filter.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<!-- 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/. -->
+<html>
+<head>
+ <title>Bug 1055181 - CSS Filter Editor Widget</title>
+ <style>
+ body {
+ filter: blur(2px) contrast(2);
+ }
+ </style>
+</head>
diff --git a/devtools/client/inspector/rules/test/doc_frame_script.js b/devtools/client/inspector/rules/test/doc_frame_script.js
new file mode 100644
index 000000000..88da043f1
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_frame_script.js
@@ -0,0 +1,113 @@
+/* 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/ */
+/* globals addMessageListener, sendAsyncMessage */
+
+"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:msgName", data, true);
+// The response message should have the same name "Test:msgName"
+//
+// Some listeners do not send a response message back.
+
+var {utils: Cu} = Components;
+
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var defer = require("devtools/shared/defer");
+
+/**
+ * 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 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);
+});
+
+/**
+ * Wait the property value from the computed style for an element and
+ * compare it with the expected value
+ * @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
+ * - {String} expected: the expected value for property
+ */
+addMessageListener("Test:WaitForComputedStylePropertyValue", function (msg) {
+ let {selector, pseudo, name, expected} = msg.data;
+ let element = content.document.querySelector(selector);
+ waitForSuccess(() => {
+ let value = content.document.defaultView.getComputedStyle(element, pseudo)
+ .getPropertyValue(name);
+
+ return value === expected;
+ }).then(() => {
+ sendAsyncMessage("Test:WaitForComputedStylePropertyValue");
+ });
+});
+
+var dumpn = msg => dump(msg + "\n");
+
+/**
+ * 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.
+ * @return a promise that resolves when the function returned true or rejects
+ * if the timeout is reached
+ */
+function waitForSuccess(validatorFn) {
+ let def = defer();
+
+ function wait(fn) {
+ if (fn()) {
+ def.resolve();
+ } else {
+ setTimeout(() => wait(fn), 200);
+ }
+ }
+ wait(validatorFn);
+
+ return def.promise;
+}
diff --git a/devtools/client/inspector/rules/test/doc_inline_sourcemap.html b/devtools/client/inspector/rules/test/doc_inline_sourcemap.html
new file mode 100644
index 000000000..cb107d424
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_inline_sourcemap.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html>
+<head>
+ <title>CSS source maps in inline stylesheets</title>
+</head>
+<body>
+ <div>CSS source maps in inline stylesheets</div>
+ <style>
+div {
+ color: #ff0066; }
+
+span {
+ background-color: #EEE; }
+
+/*# sourceMappingURL=doc_sourcemaps.css.map */
+ </style>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_invalid_sourcemap.css b/devtools/client/inspector/rules/test/doc_invalid_sourcemap.css
new file mode 100644
index 000000000..ff96a6b54
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_invalid_sourcemap.css
@@ -0,0 +1,3 @@
+div { color: gold; }
+
+/*# sourceMappingURL=this-source-map-does-not-exist.css.map */ \ No newline at end of file
diff --git a/devtools/client/inspector/rules/test/doc_invalid_sourcemap.html b/devtools/client/inspector/rules/test/doc_invalid_sourcemap.html
new file mode 100644
index 000000000..2e6422bec
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_invalid_sourcemap.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Invalid source map</title>
+ <link rel="stylesheet" type="text/css" href="doc_invalid_sourcemap.css">
+</head>
+<body>
+ <div>invalid source map</div>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html b/devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html
new file mode 100644
index 000000000..8fce04584
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>keyframe line numbers test</title>
+ <style type="text/css">
+div {
+ animation-duration: 1s;
+ animation-iteration-count: infinite;
+ animation-direction: alternate;
+ animation-name: CC;
+}
+
+span {
+ animation-duration: 3s;
+ animation-iteration-count: infinite;
+ animation-direction: alternate;
+ animation-name: DD;
+}
+
+@keyframes CC {
+ from {
+ background: #ffffff;
+ }
+ to {
+ background: #f0c;
+ }
+}
+
+@keyframes DD {
+ from {
+ background: seagreen;
+ }
+ to {
+ background: chartreuse;
+ }
+}
+ </style>
+</head>
+<body>
+ <div id="outer">
+ <span id="inner">lizards</div>
+ </div>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_keyframeanimation.css b/devtools/client/inspector/rules/test/doc_keyframeanimation.css
new file mode 100644
index 000000000..64582ed35
--- /dev/null
+++ b/devtools/client/inspector/rules/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/devtools/client/inspector/rules/test/doc_keyframeanimation.html b/devtools/client/inspector/rules/test/doc_keyframeanimation.html
new file mode 100644
index 000000000..4e02c32f0
--- /dev/null
+++ b/devtools/client/inspector/rules/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/devtools/client/inspector/rules/test/doc_media_queries.html b/devtools/client/inspector/rules/test/doc_media_queries.html
new file mode 100644
index 000000000..1adb8bc7a
--- /dev/null
+++ b/devtools/client/inspector/rules/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/devtools/client/inspector/rules/test/doc_pseudoelement.html b/devtools/client/inspector/rules/test/doc_pseudoelement.html
new file mode 100644
index 000000000..6145d4bf1
--- /dev/null
+++ b/devtools/client/inspector/rules/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/devtools/client/inspector/rules/test/doc_ruleLineNumbers.html b/devtools/client/inspector/rules/test/doc_ruleLineNumbers.html
new file mode 100644
index 000000000..5a157f384
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_ruleLineNumbers.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>simple testcase</title>
+ <style type="text/css">
+ #testid {
+ background-color: seagreen;
+ }
+
+ body {
+ color: chartreuse;
+ }
+ </style>
+</head>
+<body>
+ <div id="testid">simple testcase</div>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_sourcemaps.css b/devtools/client/inspector/rules/test/doc_sourcemaps.css
new file mode 100644
index 000000000..a9b437a40
--- /dev/null
+++ b/devtools/client/inspector/rules/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/devtools/client/inspector/rules/test/doc_sourcemaps.css.map b/devtools/client/inspector/rules/test/doc_sourcemaps.css.map
new file mode 100644
index 000000000..0f7486fd9
--- /dev/null
+++ b/devtools/client/inspector/rules/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/devtools/client/inspector/rules/test/doc_sourcemaps.html b/devtools/client/inspector/rules/test/doc_sourcemaps.html
new file mode 100644
index 000000000..0014e55fe
--- /dev/null
+++ b/devtools/client/inspector/rules/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/devtools/client/inspector/rules/test/doc_sourcemaps.scss b/devtools/client/inspector/rules/test/doc_sourcemaps.scss
new file mode 100644
index 000000000..0ff6c471b
--- /dev/null
+++ b/devtools/client/inspector/rules/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/devtools/client/inspector/rules/test/doc_style_editor_link.css b/devtools/client/inspector/rules/test/doc_style_editor_link.css
new file mode 100644
index 000000000..e49e1f587
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_style_editor_link.css
@@ -0,0 +1,3 @@
+div {
+ opacity: 1;
+} \ No newline at end of file
diff --git a/devtools/client/inspector/rules/test/doc_test_image.png b/devtools/client/inspector/rules/test/doc_test_image.png
new file mode 100644
index 000000000..769c63634
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_test_image.png
Binary files differ
diff --git a/devtools/client/inspector/rules/test/doc_urls_clickable.css b/devtools/client/inspector/rules/test/doc_urls_clickable.css
new file mode 100644
index 000000000..04315b2c3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_urls_clickable.css
@@ -0,0 +1,9 @@
+.relative1 {
+ background-image: url(./doc_test_image.png);
+}
+.absolute {
+ background: url("http://example.com/browser/devtools/client/inspector/rules/test/doc_test_image.png");
+}
+.base64 {
+ background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==');
+}
diff --git a/devtools/client/inspector/rules/test/doc_urls_clickable.html b/devtools/client/inspector/rules/test/doc_urls_clickable.html
new file mode 100644
index 000000000..b0265a703
--- /dev/null
+++ b/devtools/client/inspector/rules/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/devtools/client/inspector/rules/test/head.js b/devtools/client/inspector/rules/test/head.js
new file mode 100644
index 000000000..5e5ede09b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/head.js
@@ -0,0 +1,840 @@
+/* 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/ */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../test/head.js */
+"use strict";
+
+// Import the inspector's head.js first (which itself imports shared-head.js).
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
+ this);
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.defaultColorUnit");
+});
+
+var {getInplaceEditorForSpan: inplaceEditor} =
+ require("devtools/client/shared/inplace-editor");
+
+const ROOT_TEST_DIR = getRootDirectory(gTestPath);
+const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
+
+const STYLE_INSPECTOR_L10N
+ = new LocalizationHelper("devtools/shared/locales/styleinspector.properties");
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.defaultColorUnit");
+});
+
+/**
+ * The rule-view tests rely on a frame-script to be injected in the content test
+ * page. So override the shared-head's addTab to load the frame script after the
+ * tab was added.
+ * FIXME: Refactor the rule-view tests to use the testActor instead of a frame
+ * script, so they can run on remote targets too.
+ */
+var _addTab = addTab;
+addTab = function (url) {
+ return _addTab(url).then(tab => {
+ info("Loading the helper frame script " + FRAME_SCRIPT_URL);
+ let browser = tab.linkedBrowser;
+ browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
+ return tab;
+ });
+};
+
+/**
+ * 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 = 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);
+ }
+
+ 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,
+ pseudo,
+ name: propName});
+}
+
+/**
+ * Get an element's inline style property value.
+ * @param {TestActor} testActor
+ * @param {String} selector
+ * The selector used to obtain the element.
+ * @param {String} name
+ * name of the property.
+ */
+function getStyle(testActor, selector, propName) {
+ return testActor.eval(`
+ content.document.querySelector("${selector}")
+ .style.getPropertyValue("${propName}");
+ `);
+}
+
+/**
+ * Send an async message to the frame script and wait until the requested
+ * computed style property has the expected value.
+ *
+ * @param {String} selector
+ * The selector used to obtain the element.
+ * @param {String} pseudo
+ * pseudo id to query, or null.
+ * @param {String} prop
+ * name of the property.
+ * @param {String} expected
+ * expected value of property
+ * @param {String} name
+ * the name used in test message
+ */
+function* waitForComputedStyleProperty(selector, pseudo, name, expected) {
+ return yield executeInContent("Test:WaitForComputedStylePropertyValue",
+ {selector,
+ pseudo,
+ expected,
+ name});
+}
+
+/**
+ * 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
+ */
+var focusEditableField = Task.async(function* (ruleView, 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);
+ yield onFocus;
+
+ info("Editable field gained focus, returning the input field now");
+ let onEdit = inplaceEditor(editable.ownerDocument.activeElement);
+
+ return onEdit;
+});
+
+/**
+ * When a tooltip is closed, this ends up "commiting" the value changed within
+ * the tooltip (e.g. the color in case of a colorpicker) which, in turn, ends up
+ * setting the value of the corresponding css property in the rule-view.
+ * Use this function to close the tooltip and make sure the test waits for the
+ * ruleview-changed event.
+ * @param {SwatchBasedEditorTooltip} editorTooltip
+ * @param {CSSRuleView} view
+ */
+function* hideTooltipAndWaitForRuleViewChanged(editorTooltip, view) {
+ let onModified = view.once("ruleview-changed");
+ let onHidden = editorTooltip.tooltip.once("hidden");
+ editorTooltip.hide();
+ yield onModified;
+ yield onHidden;
+}
+
+/**
+ * Polls a given generator function waiting for it to return true.
+ *
+ * @param {Function} validatorFn
+ * A validator generator 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.
+ * @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
+ */
+var waitForSuccess = Task.async(function* (validatorFn, desc = "untitled") {
+ let i = 0;
+ while (true) {
+ info("Checking: " + desc);
+ if (yield validatorFn()) {
+ ok(true, "Success: " + desc);
+ break;
+ }
+ i++;
+ if (i > 10) {
+ ok(false, "Failure: " + desc);
+ break;
+ }
+ yield new Promise(r => setTimeout(r, 200));
+ }
+});
+
+/**
+ * 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.styleDocument.querySelectorAll(".ruleview-rule")) {
+ let selector = r.querySelector(".ruleview-selectorcontainer, " +
+ ".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");
+}
+
+/**
+ * Get a reference to the selectorhighlighter icon 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 selectorhighlighter icon DOM element
+ */
+function getRuleViewSelectorHighlighterIcon(view, selectorText) {
+ let rule = getRuleViewRule(view, selectorText);
+ return rule.querySelector(".ruleview-selectorhighlighter");
+}
+
+/**
+ * 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.
+ * Note that this function assumes that the colorpicker popup is already open
+ * and it won't close it after having selected the new color.
+ *
+ * @param {RuleView} ruleView
+ * The related rule view instance
+ * @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:
+ * - {String} selector The selector to 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
+ */
+var simulateColorPickerChange = Task.async(function* (ruleView, colorPicker,
+ newRgba, expectedChange) {
+ let onComputedStyleChanged;
+ if (expectedChange) {
+ let {selector, name, value} = expectedChange;
+ onComputedStyleChanged = waitForComputedStyleProperty(selector, null, name, value);
+ }
+ let onRuleViewChanged = ruleView.once("ruleview-changed");
+ info("Getting the spectrum colorpicker object");
+ let spectrum = colorPicker.spectrum;
+ info("Setting the new color");
+ spectrum.rgb = newRgba;
+ info("Applying the change");
+ spectrum.updateUI();
+ spectrum.onChange();
+ info("Waiting for rule-view to update");
+ yield onRuleViewChanged;
+
+ if (expectedChange) {
+ info("Waiting for the style to be applied on the page");
+ yield onComputedStyleChanged;
+ }
+});
+
+/**
+ * Open the color picker popup for a given property in a given rule and
+ * simulate a color change. Optionally wait for a given element in the page to
+ * have its style changed as a result.
+ *
+ * @param {RuleView} view
+ * The related rule view instance
+ * @param {Number} ruleIndex
+ * Which rule to target in the rule view
+ * @param {Number} propIndex
+ * Which property to target in the rule
+ * @param {Array} newRgba
+ * The new color to be set [r, g, b, a]
+ * @param {Object} expectedChange
+ * Optional object that needs the following props:
+ * - {String} selector The selector to 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
+ */
+var openColorPickerAndSelectColor = Task.async(function* (view, ruleIndex,
+ propIndex, newRgba, expectedChange) {
+ let ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
+ let propEditor = ruleEditor.rule.textProps[propIndex].editor;
+ let swatch = propEditor.valueSpan.querySelector(".ruleview-colorswatch");
+ let cPicker = view.tooltips.colorPicker;
+
+ info("Opening the colorpicker by clicking the color swatch");
+ let onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ yield onColorPickerReady;
+
+ yield simulateColorPickerChange(view, cPicker, newRgba, expectedChange);
+
+ return {propEditor, swatch, cPicker};
+});
+
+/**
+ * Open the cubicbezier popup for a given property in a given rule and
+ * simulate a curve change. Optionally wait for a given element in the page to
+ * have its style changed as a result.
+ *
+ * @param {RuleView} view
+ * The related rule view instance
+ * @param {Number} ruleIndex
+ * Which rule to target in the rule view
+ * @param {Number} propIndex
+ * Which property to target in the rule
+ * @param {Array} coords
+ * The new coordinates to be used, e.g. [0.1, 2, 0.9, -1]
+ * @param {Object} expectedChange
+ * Optional object that needs the following props:
+ * - {String} selector The selector to 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
+ */
+var openCubicBezierAndChangeCoords = Task.async(function* (view, ruleIndex,
+ propIndex, coords, expectedChange) {
+ let ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
+ let propEditor = ruleEditor.rule.textProps[propIndex].editor;
+ let swatch = propEditor.valueSpan.querySelector(".ruleview-bezierswatch");
+ let bezierTooltip = view.tooltips.cubicBezier;
+
+ info("Opening the cubicBezier by clicking the swatch");
+ let onBezierWidgetReady = bezierTooltip.once("ready");
+ swatch.click();
+ yield onBezierWidgetReady;
+
+ let widget = yield bezierTooltip.widget;
+
+ info("Simulating a change of curve in the widget");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ widget.coordinates = coords;
+ yield onRuleViewChanged;
+
+ if (expectedChange) {
+ info("Waiting for the style to be applied on the page");
+ let {selector, name, value} = expectedChange;
+ yield waitForComputedStyleProperty(selector, null, name, value);
+ }
+
+ return {propEditor, swatch, bezierTooltip};
+});
+
+/**
+ * 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.styleDocument.querySelectorAll(".ruleview-rule-source");
+ return links[index];
+}
+
+/**
+ * Get rule-link text 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 {String} The string at this index
+ */
+function getRuleViewLinkTextByIndex(view, index) {
+ let link = getRuleViewLinkByIndex(view, index);
+ return link.querySelector(".ruleview-rule-source-label").textContent;
+}
+
+/**
+ * Simulate adding a new property in an existing rule in the rule-view.
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {Number} ruleIndex
+ * The index of the rule to use. Note that if ruleIndex is 0, you might
+ * want to also listen to markupmutation events in your test since
+ * that's going to change the style attribute of the selected node.
+ * @param {String} name
+ * The name for the new property
+ * @param {String} value
+ * The value for the new property
+ * @param {String} commitValueWith
+ * Which key should be used to commit the new value. VK_RETURN is used by
+ * default, but tests might want to use another key to test cancelling
+ * for exemple.
+ * @param {Boolean} blurNewProperty
+ * After the new value has been added, a new property would have been
+ * focused. This parameter is true by default, and that causes the new
+ * property to be blurred. Set to false if you don't want this.
+ * @return {TextProperty} The instance of the TextProperty that was added
+ */
+var addProperty = Task.async(function* (view, ruleIndex, name, value,
+ commitValueWith = "VK_RETURN",
+ blurNewProperty = true) {
+ info("Adding new property " + name + ":" + value + " to rule " + ruleIndex);
+
+ let ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
+ let editor = yield focusNewRuleViewProperty(ruleEditor);
+ let numOfProps = ruleEditor.rule.textProps.length;
+
+ info("Adding name " + name);
+ editor.input.value = name;
+ let onNameAdded = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onNameAdded;
+
+ // Focus has moved to the value inplace-editor automatically.
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ let textProps = ruleEditor.rule.textProps;
+ let textProp = textProps[textProps.length - 1];
+
+ is(ruleEditor.rule.textProps.length, numOfProps + 1,
+ "A new test property was added");
+ is(editor, inplaceEditor(textProp.editor.valueSpan),
+ "The inplace editor appeared for the value");
+
+ info("Adding value " + value);
+ // Setting the input value schedules a preview to be shown in 10ms which
+ // triggers a ruleview-changed event (see bug 1209295).
+ let onPreview = view.once("ruleview-changed");
+ editor.input.value = value;
+ view.throttle.flush();
+ yield onPreview;
+
+ let onValueAdded = view.once("ruleview-changed");
+ EventUtils.synthesizeKey(commitValueWith, {}, view.styleWindow);
+ yield onValueAdded;
+
+ if (blurNewProperty) {
+ view.styleDocument.activeElement.blur();
+ }
+
+ return textProp;
+});
+
+/**
+ * Simulate changing the value of a property in a rule in the rule-view.
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {TextProperty} textProp
+ * The instance of the TextProperty to be changed
+ * @param {String} value
+ * The new value to be used. If null is passed, then the value will be
+ * deleted
+ * @param {Boolean} blurNewProperty
+ * After the value has been changed, a new property would have been
+ * focused. This parameter is true by default, and that causes the new
+ * property to be blurred. Set to false if you don't want this.
+ */
+var setProperty = Task.async(function* (view, textProp, value,
+ blurNewProperty = true) {
+ yield focusEditableField(view, textProp.editor.valueSpan);
+
+ let onPreview = view.once("ruleview-changed");
+ if (value === null) {
+ EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow);
+ } else {
+ EventUtils.sendString(value, view.styleWindow);
+ }
+ view.throttle.flush();
+ yield onPreview;
+
+ let onValueDone = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onValueDone;
+
+ if (blurNewProperty) {
+ view.styleDocument.activeElement.blur();
+ }
+});
+
+/**
+ * Simulate removing a property from an existing rule in the rule-view.
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {TextProperty} textProp
+ * The instance of the TextProperty to be removed
+ * @param {Boolean} blurNewProperty
+ * After the property has been removed, a new property would have been
+ * focused. This parameter is true by default, and that causes the new
+ * property to be blurred. Set to false if you don't want this.
+ */
+var removeProperty = Task.async(function* (view, textProp,
+ blurNewProperty = true) {
+ yield focusEditableField(view, textProp.editor.nameSpan);
+
+ let onModifications = view.once("ruleview-changed");
+ info("Deleting the property name now");
+ EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow);
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ yield onModifications;
+
+ if (blurNewProperty) {
+ view.styleDocument.activeElement.blur();
+ }
+});
+
+/**
+ * Simulate clicking the enable/disable checkbox next to a property in a rule.
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {TextProperty} textProp
+ * The instance of the TextProperty to be enabled/disabled
+ */
+var togglePropStatus = Task.async(function* (view, textProp) {
+ let onRuleViewRefreshed = view.once("ruleview-changed");
+ textProp.editor.enable.click();
+ yield onRuleViewRefreshed;
+});
+
+/**
+ * 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
+ */
+var focusNewRuleViewProperty = Task.async(function* (ruleEditor) {
+ info("Clicking on a close ruleEditor brace to start editing a new property");
+
+ // Use bottom alignment to avoid scrolling out of the parent element area.
+ ruleEditor.closeBrace.scrollIntoView(false);
+ let editor = yield focusEditableField(ruleEditor.ruleView,
+ ruleEditor.closeBrace);
+
+ is(inplaceEditor(ruleEditor.newPropSpan), editor,
+ "Focused editor is the new property editor.");
+
+ 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
+ */
+var 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;
+});
+
+/**
+ * Set the search value for the rule-view filter styles search box.
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {String} searchValue
+ * The filter search value
+ * @return a promise that resolves when the rule-view is filtered for the
+ * search term
+ */
+var setSearchFilter = Task.async(function* (view, searchValue) {
+ info("Setting filter text to \"" + searchValue + "\"");
+ let win = view.styleWindow;
+ let searchField = view.searchField;
+ searchField.focus();
+ synthesizeKeys(searchValue, win);
+ yield view.inspector.once("ruleview-filtered");
+});
+
+/**
+ * Reload the current page and wait for the inspector to be initialized after
+ * the navigation
+ *
+ * @param {InspectorPanel} inspector
+ * The instance of InspectorPanel currently loaded in the toolbox
+ * @param {TestActor} testActor
+ * The current instance of the TestActor
+ */
+function* reloadPage(inspector, testActor) {
+ let onNewRoot = inspector.once("new-root");
+ yield testActor.reload();
+ yield onNewRoot;
+ yield inspector.markup._waitForChildren();
+}
+
+/**
+ * Create a new rule by clicking on the "add rule" button.
+ * This will leave the selector inplace-editor active.
+ *
+ * @param {InspectorPanel} inspector
+ * The instance of InspectorPanel currently loaded in the toolbox
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @return a promise that resolves after the rule has been added
+ */
+function* addNewRule(inspector, view) {
+ info("Adding the new rule using the button");
+ view.addRuleButton.click();
+
+ info("Waiting for rule view to change");
+ yield view.once("ruleview-changed");
+}
+
+/**
+ * Create a new rule by clicking on the "add rule" button, dismiss the editor field and
+ * verify that the selector is correct.
+ *
+ * @param {InspectorPanel} inspector
+ * The instance of InspectorPanel currently loaded in the toolbox
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {String} expectedSelector
+ * The value we expect the selector to have
+ * @param {Number} expectedIndex
+ * The index we expect the rule to have in the rule-view
+ * @return a promise that resolves after the rule has been added
+ */
+function* addNewRuleAndDismissEditor(inspector, view, expectedSelector, expectedIndex) {
+ yield addNewRule(inspector, view);
+
+ info("Getting the new rule at index " + expectedIndex);
+ let ruleEditor = getRuleViewRuleEditor(view, expectedIndex);
+ let editor = ruleEditor.selectorText.ownerDocument.activeElement;
+ is(editor.value, expectedSelector,
+ "The editor for the new selector has the correct value: " + expectedSelector);
+
+ info("Pressing escape to leave the editor");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+
+ is(ruleEditor.selectorText.textContent, expectedSelector,
+ "The new selector has the correct text: " + expectedSelector);
+}
+
+/**
+ * Simulate a sequence of non-character keys (return, escape, tab) and wait for
+ * a given element to receive the focus.
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {DOMNode} element
+ * The element that should be focused
+ * @param {Array} keys
+ * Array of non-character keys, the part that comes after "DOM_VK_" eg.
+ * "RETURN", "ESCAPE"
+ * @return a promise that resolves after the element received the focus
+ */
+function* sendKeysAndWaitForFocus(view, element, keys) {
+ let onFocus = once(element, "focus", true);
+ for (let key of keys) {
+ EventUtils.sendKey(key, view.styleWindow);
+ }
+ yield onFocus;
+}
+
+/**
+ * Open the style editor context menu and return all of it's items in a flat array
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @return An array of MenuItems
+ */
+function openStyleContextMenuAndGetAllItems(view, target) {
+ let menu = view._contextmenu._openMenu({target: target});
+
+ // Flatten all menu items into a single array to make searching through it easier
+ let allItems = [].concat.apply([], menu.items.map(function addItem(item) {
+ if (item.submenu) {
+ return addItem(item.submenu.items);
+ }
+ return item;
+ }));
+
+ return allItems;
+}
+
+/**
+ * Wait for a markupmutation event on the inspector that is for a style modification.
+ * @param {InspectorPanel} inspector
+ * @return {Promise}
+ */
+function waitForStyleModification(inspector) {
+ return new Promise(function (resolve) {
+ function checkForStyleModification(name, mutations) {
+ for (let mutation of mutations) {
+ if (mutation.type === "attributes" && mutation.attributeName === "style") {
+ inspector.off("markupmutation", checkForStyleModification);
+ resolve();
+ return;
+ }
+ }
+ }
+ inspector.on("markupmutation", checkForStyleModification);
+ });
+}
+
+/**
+ * Click on the selector icon
+ * @param {DOMNode} icon
+ * @param {CSSRuleView} view
+ */
+function* clickSelectorIcon(icon, view) {
+ let onToggled = view.once("ruleview-selectorhighlighter-toggled");
+ EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow);
+ yield onToggled;
+}
+
+/**
+ * Make sure window is properly focused before sending a key event.
+ * @param {Window} win
+ * @param {Event} key
+ */
+function focusAndSendKey(win, key) {
+ win.document.documentElement.focus();
+ EventUtils.sendKey(key, win);
+}
diff --git a/devtools/client/inspector/rules/views/moz.build b/devtools/client/inspector/rules/views/moz.build
new file mode 100644
index 000000000..ac0a24d76
--- /dev/null
+++ b/devtools/client/inspector/rules/views/moz.build
@@ -0,0 +1,8 @@
+# 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/.
+
+DevToolsModules(
+ 'rule-editor.js',
+ 'text-property-editor.js',
+)
diff --git a/devtools/client/inspector/rules/views/rule-editor.js b/devtools/client/inspector/rules/views/rule-editor.js
new file mode 100644
index 000000000..2587bf19c
--- /dev/null
+++ b/devtools/client/inspector/rules/views/rule-editor.js
@@ -0,0 +1,620 @@
+/* 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 {l10n} = require("devtools/shared/inspector/css-logic");
+const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
+const {PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils");
+const {Rule} = require("devtools/client/inspector/rules/models/rule");
+const {InplaceEditor, editableField, editableItem} =
+ require("devtools/client/shared/inplace-editor");
+const {TextPropertyEditor} =
+ require("devtools/client/inspector/rules/views/text-property-editor");
+const {
+ createChild,
+ blurOnMultipleProperties,
+ promiseWarn
+} = require("devtools/client/inspector/shared/utils");
+const {
+ parseDeclarations,
+ parsePseudoClassesAndAttributes,
+ SELECTOR_ATTRIBUTE,
+ SELECTOR_ELEMENT,
+ SELECTOR_PSEUDO_CLASS
+} = require("devtools/shared/css/parsing-utils");
+const promise = require("promise");
+const Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {Task} = require("devtools/shared/task");
+
+const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties";
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
+
+/**
+ * RuleEditor is responsible for the following:
+ * Owns a Rule object and creates a list of TextPropertyEditors
+ * for its TextProperties.
+ * Manages creation of new text properties.
+ *
+ * One step of a RuleEditor's instantiation is figuring out what's the original
+ * source link to the parent stylesheet (in case of source maps). This step is
+ * asynchronous and is triggered as soon as the RuleEditor is instantiated (see
+ * updateSourceLink). If you need to know when the RuleEditor is done with this,
+ * you need to listen to the source-link-updated event.
+ *
+ * @param {CssRuleView} ruleView
+ * The CssRuleView containg the document holding this rule editor.
+ * @param {Rule} rule
+ * The Rule object we're editing.
+ */
+function RuleEditor(ruleView, rule) {
+ EventEmitter.decorate(this);
+
+ this.ruleView = ruleView;
+ this.doc = this.ruleView.styleDocument;
+ this.toolbox = this.ruleView.inspector.toolbox;
+ this.rule = rule;
+
+ this.isEditable = !rule.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._locationChanged = this._locationChanged.bind(this);
+ this.updateSourceLink = this.updateSourceLink.bind(this);
+
+ this.rule.domRule.on("location-changed", this._locationChanged);
+ this.toolbox.on("tool-registered", this.updateSourceLink);
+ this.toolbox.on("tool-unregistered", this.updateSourceLink);
+
+ this._create();
+}
+
+RuleEditor.prototype = {
+ destroy: function () {
+ this.rule.domRule.off("location-changed");
+ this.toolbox.off("tool-registered", this.updateSourceLink);
+ this.toolbox.off("tool-unregistered", this.updateSourceLink);
+ },
+
+ get isSelectorEditable() {
+ let trait = this.isEditable &&
+ this.ruleView.inspector.target.client.traits.selectorEditable &&
+ this.rule.domRule.type !== ELEMENT_STYLE &&
+ this.rule.domRule.type !== CSSRule.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.createElement("div");
+ this.element.className = "ruleview-rule theme-separator";
+ this.element.setAttribute("uneditable", !this.isEditable);
+ this.element.setAttribute("unmatched", this.rule.isUnmatched);
+ 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.
+ this.source = createChild(this.element, "div", {
+ class: "ruleview-rule-source theme-link"
+ });
+ this.source.addEventListener("click", function () {
+ if (this.source.hasAttribute("unselectable")) {
+ return;
+ }
+ let rule = this.rule.domRule;
+ this.ruleView.emit("ruleview-linked-clicked", rule);
+ }.bind(this));
+ let sourceLabel = this.doc.createElement("span");
+ sourceLabel.classList.add("ruleview-rule-source-label");
+ this.source.appendChild(sourceLabel);
+
+ this.updateSourceLink();
+
+ let code = createChild(this.element, "div", {
+ class: "ruleview-code"
+ });
+
+ let header = createChild(code, "div", {});
+
+ this.selectorText = createChild(header, "span", {
+ class: "ruleview-selectorcontainer theme-fg-color3",
+ tabindex: this.isSelectorEditable ? "0" : "-1",
+ });
+
+ if (this.isSelectorEditable) {
+ this.selectorText.addEventListener("click", event => {
+ // Clicks within the selector shouldn't propagate any further.
+ event.stopPropagation();
+ }, false);
+
+ editableField({
+ element: this.selectorText,
+ done: this._onSelectorDone,
+ cssProperties: this.rule.cssProperties,
+ contextMenu: this.ruleView.inspector.onTextBoxContextMenu
+ });
+ }
+
+ if (this.rule.domRule.type !== CSSRule.KEYFRAME_RULE) {
+ let selector = this.rule.domRule.selectors
+ ? this.rule.domRule.selectors.join(", ")
+ : this.ruleView.inspector.selectionCssSelector;
+
+ let selectorHighlighter = createChild(header, "span", {
+ class: "ruleview-selectorhighlighter" +
+ (this.ruleView.highlighters.selectorHighlighterShown === selector ?
+ " highlighted" : ""),
+ title: l10n("rule.selectorHighlighter.tooltip")
+ });
+ selectorHighlighter.addEventListener("click", () => {
+ this.ruleView.toggleSelectorHighlighter(selectorHighlighter, selector);
+ });
+ }
+
+ 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: "}"
+ });
+
+ if (this.isEditable) {
+ // A newProperty editor should only be created when no editor was
+ // previously displayed. Since the editors are cleared on blur,
+ // check this.ruleview.isEditing on mousedown
+ this._ruleViewIsEditing = false;
+
+ code.addEventListener("mousedown", () => {
+ this._ruleViewIsEditing = this.ruleView.isEditing;
+ });
+
+ code.addEventListener("click", () => {
+ let selection = this.doc.defaultView.getSelection();
+ if (selection.isCollapsed && !this._ruleViewIsEditing) {
+ this.newProperty();
+ }
+ // Cleanup the _ruleViewIsEditing flag
+ this._ruleViewIsEditing = false;
+ }, false);
+
+ this.element.addEventListener("mousedown", () => {
+ this.doc.defaultView.focus();
+ }, false);
+
+ // Create a property editor when the close brace is clicked.
+ editableItem({ element: this.closeBrace }, () => {
+ this.newProperty();
+ });
+ }
+ },
+
+ /**
+ * Event handler called when a property changes on the
+ * StyleRuleActor.
+ */
+ _locationChanged: function () {
+ this.updateSourceLink();
+ },
+
+ updateSourceLink: function () {
+ let sourceLabel = this.element.querySelector(".ruleview-rule-source-label");
+ let title = this.rule.title;
+ let sourceHref = (this.rule.sheet && this.rule.sheet.href) ?
+ this.rule.sheet.href : title;
+ let sourceLine = this.rule.ruleLine > 0 ? ":" + this.rule.ruleLine : "";
+
+ sourceLabel.setAttribute("title", sourceHref + sourceLine);
+
+ if (this.toolbox.isToolRegistered("styleeditor")) {
+ this.source.removeAttribute("unselectable");
+ } else {
+ this.source.setAttribute("unselectable", true);
+ }
+
+ if (this.rule.isSystem) {
+ let uaLabel = STYLE_INSPECTOR_L10N.getStr("rule.userAgentStyles");
+ sourceLabel.textContent = uaLabel + " " + 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") {
+ this.source.setAttribute("unselectable", "true");
+ sourceLabel.textContent = uaLabel;
+ sourceLabel.removeAttribute("title");
+ }
+ } else {
+ sourceLabel.textContent = title;
+ if (this.rule.ruleLine === -1 && this.rule.domRule.parentStyleSheet) {
+ this.source.setAttribute("unselectable", "true");
+ }
+ }
+
+ let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
+ if (showOrig && !this.rule.isSystem &&
+ this.rule.domRule.type !== ELEMENT_STYLE) {
+ // Only get the original source link if the right pref is set, if the rule
+ // isn't a system rule and if it isn't an inline rule.
+ this.rule.getOriginalSourceStrings().then((strings) => {
+ sourceLabel.textContent = strings.short;
+ sourceLabel.setAttribute("title", strings.full);
+ }, e => console.error(e)).then(() => {
+ this.emit("source-link-updated");
+ });
+ } else {
+ // If we're not getting the original source link, then we can emit the
+ // event immediately (but still asynchronously to give consumers a chance
+ // to register it after having instantiated the RuleEditor).
+ promise.resolve().then(() => {
+ this.emit("source-link-updated");
+ });
+ }
+ },
+
+ /**
+ * 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 === CSSRule.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 containerClass =
+ (this.rule.matchedSelectors.indexOf(selector) > -1) ?
+ "ruleview-selector-matched" : "ruleview-selector-unmatched";
+ let selectorContainer = createChild(this.selectorText, "span", {
+ class: containerClass
+ });
+
+ let parsedSelector = parsePseudoClassesAndAttributes(selector);
+
+ for (let selectorText of parsedSelector) {
+ let selectorClass = "";
+
+ switch (selectorText.type) {
+ case SELECTOR_ATTRIBUTE:
+ selectorClass = "ruleview-selector-attribute";
+ break;
+ case SELECTOR_ELEMENT:
+ selectorClass = "ruleview-selector";
+ break;
+ case SELECTOR_PSEUDO_CLASS:
+ selectorClass = [":active", ":focus", ":hover"].some(
+ pseudo => selectorText.value === pseudo) ?
+ "ruleview-selector-pseudo-class-lock" :
+ "ruleview-selector-pseudo-class";
+ break;
+ default:
+ break;
+ }
+
+ createChild(selectorContainer, "span", {
+ textContent: selectorText.value,
+ class: selectorClass
+ });
+ }
+ });
+ }
+
+ for (let prop of this.rule.textProps) {
+ if (!prop.editor && !prop.invisible) {
+ let editor = new TextPropertyEditor(this, prop);
+ this.propertyList.appendChild(editor.element);
+ }
+ }
+ },
+
+ /**
+ * Programatically add a new property to the rule.
+ *
+ * @param {String} name
+ * Property name.
+ * @param {String} value
+ * Property value.
+ * @param {String} priority
+ * Property priority.
+ * @param {Boolean} enabled
+ * True if the property should be enabled.
+ * @param {TextProperty} siblingProp
+ * Optional, property next to which the new property will be added.
+ * @return {TextProperty}
+ * The new property
+ */
+ addProperty: function (name, value, priority, enabled, siblingProp) {
+ let prop = this.rule.createProperty(name, value, priority, enabled,
+ siblingProp);
+ 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 siblingProp.
+ // 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} properties
+ * Array of properties, which are objects with this signature:
+ * {
+ * name: {string},
+ * value: {string},
+ * priority: {string}
+ * }
+ * @param {TextProperty} siblingProp
+ * Optional, the property next to which all new props should be added.
+ */
+ addProperties: function (properties, siblingProp) {
+ if (!properties || !properties.length) {
+ return;
+ }
+
+ let lastProp = siblingProp;
+ for (let p of properties) {
+ let isCommented = Boolean(p.commentOffsets);
+ let enabled = !isCommented;
+ lastProp = this.addProperty(p.name, p.value, p.priority, enabled,
+ 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,
+ cssProperties: this.rule.cssProperties,
+ contextMenu: this.ruleView.inspector.onTextBoxContextMenu
+ });
+
+ // Auto-close the input if multiple rules get pasted into new property.
+ this.editor.input.addEventListener("paste",
+ blurOnMultipleProperties(this.rule.cssProperties), false);
+ },
+
+ /**
+ * Called when the new property input has been dismissed.
+ *
+ * @param {String} value
+ * The value in the editor.
+ * @param {Boolean} commit
+ * True if the value should be committed.
+ */
+ _onNewProperty: function (value, commit) {
+ if (!value || !commit) {
+ 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(this.rule.cssProperties.isKnown, value, true)
+ .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} value
+ * The value contained in the editor.
+ * @param {Boolean} commit
+ * True if the change should be applied.
+ * @param {Number} direction
+ * The move focus direction number.
+ */
+ _onSelectorDone: Task.async(function* (value, commit, direction) {
+ if (!commit || this.isEditing || value === "" ||
+ value === this.rule.selectorText) {
+ return;
+ }
+
+ let ruleView = this.ruleView;
+ let elementStyle = ruleView._elementStyle;
+ let element = elementStyle.element;
+ let supportsUnmatchedRules =
+ this.rule.domRule.supportsModifySelectorUnmatched;
+
+ this.isEditing = true;
+
+ try {
+ let response = yield this.rule.domRule.modifySelector(element, value);
+
+ if (!supportsUnmatchedRules) {
+ this.isEditing = false;
+
+ if (response) {
+ this.ruleView.refreshPanel();
+ }
+ return;
+ }
+
+ // We recompute the list of applied styles, because editing a
+ // selector might cause this rule's position to change.
+ let applied = yield elementStyle.pageStyle.getApplied(element, {
+ inherited: true,
+ matchedSelectors: true,
+ filter: elementStyle.showUserAgentStyles ? "ua" : undefined
+ });
+
+ this.isEditing = false;
+
+ let {ruleProps, isMatching} = response;
+ if (!ruleProps) {
+ // Notify for changes, even when nothing changes,
+ // just to allow tests being able to track end of this request.
+ ruleView.emit("ruleview-invalid-selector");
+ return;
+ }
+
+ ruleProps.isUnmatched = !isMatching;
+ let newRule = new Rule(elementStyle, ruleProps);
+ let editor = new RuleEditor(ruleView, newRule);
+ let rules = elementStyle.rules;
+
+ let newRuleIndex = applied.findIndex((r) => r.rule == ruleProps.rule);
+ let oldIndex = rules.indexOf(this.rule);
+
+ // If the selector no longer matches, then we leave the rule in
+ // the same relative position.
+ if (newRuleIndex === -1) {
+ newRuleIndex = oldIndex;
+ }
+
+ // Remove the old rule and insert the new rule.
+ rules.splice(oldIndex, 1);
+ rules.splice(newRuleIndex, 0, newRule);
+ elementStyle._changed();
+ elementStyle.markOverriddenAll();
+
+ // We install the new editor in place of the old -- you might
+ // think we would replicate the list-modification logic above,
+ // but that is complicated due to the way the UI installs
+ // pseudo-element rules and the like.
+ this.element.parentNode.replaceChild(editor.element, this.element);
+
+ // Remove highlight for modified selector
+ if (ruleView.highlighters.selectorHighlighterShown) {
+ ruleView.toggleSelectorHighlighter(ruleView.lastSelectorIcon,
+ ruleView.highlighters.selectorHighlighterShown);
+ }
+
+ editor._moveSelectorFocus(direction);
+ } catch (err) {
+ this.isEditing = false;
+ promiseWarn(err);
+ }
+ }),
+
+ /**
+ * Handle moving the focus change after a tab or return keypress in the
+ * selector inplace editor.
+ *
+ * @param {Number} direction
+ * The move focus direction number.
+ */
+ _moveSelectorFocus: function (direction) {
+ if (!direction || direction === Services.focus.MOVEFOCUS_BACKWARD) {
+ return;
+ }
+
+ if (this.rule.textProps.length > 0) {
+ this.rule.textProps[0].editor.nameSpan.click();
+ } else {
+ this.propertyList.click();
+ }
+ }
+};
+
+exports.RuleEditor = RuleEditor;
diff --git a/devtools/client/inspector/rules/views/text-property-editor.js b/devtools/client/inspector/rules/views/text-property-editor.js
new file mode 100644
index 000000000..d3015f931
--- /dev/null
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -0,0 +1,880 @@
+/* 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 {l10n} = require("devtools/shared/inspector/css-logic");
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+const {InplaceEditor, editableField} =
+ require("devtools/client/shared/inplace-editor");
+const {
+ createChild,
+ appendText,
+ advanceValidate,
+ blurOnMultipleProperties
+} = require("devtools/client/inspector/shared/utils");
+const {
+ parseDeclarations,
+ parseSingleValue,
+} = require("devtools/shared/css/parsing-utils");
+const Services = require("Services");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const SHARED_SWATCH_CLASS = "ruleview-swatch";
+const COLOR_SWATCH_CLASS = "ruleview-colorswatch";
+const BEZIER_SWATCH_CLASS = "ruleview-bezierswatch";
+const FILTER_SWATCH_CLASS = "ruleview-filterswatch";
+const ANGLE_SWATCH_CLASS = "ruleview-angleswatch";
+
+/*
+ * An actionable element is an element which on click triggers a specific action
+ * (e.g. shows a color tooltip, opens a link, …).
+ */
+const ACTIONABLE_ELEMENTS_SELECTORS = [
+ `.${COLOR_SWATCH_CLASS}`,
+ `.${BEZIER_SWATCH_CLASS}`,
+ `.${FILTER_SWATCH_CLASS}`,
+ `.${ANGLE_SWATCH_CLASS}`,
+ "a"
+];
+
+/**
+ * TextPropertyEditor is responsible for the following:
+ * Owns a TextProperty object.
+ * Manages changes to the TextProperty.
+ * Can be expanded to display computed properties.
+ * Can mark a property disabled or enabled.
+ *
+ * @param {RuleEditor} ruleEditor
+ * The rule editor that owns this TextPropertyEditor.
+ * @param {TextProperty} property
+ * The text property to edit.
+ */
+function TextPropertyEditor(ruleEditor, property) {
+ this.ruleEditor = ruleEditor;
+ this.ruleView = this.ruleEditor.ruleView;
+ this.doc = this.ruleEditor.doc;
+ this.popup = this.ruleView.popup;
+ this.prop = property;
+ this.prop.editor = this;
+ this.browserWindow = this.doc.defaultView.top;
+ this._populatedComputed = false;
+ this._hasPendingClick = false;
+ this._clickedElementOptions = null;
+
+ const toolbox = this.ruleView.inspector.toolbox;
+ this.cssProperties = getCssProperties(toolbox);
+
+ 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._onSwatchCommit = this._onSwatchCommit.bind(this);
+ this._onSwatchPreview = this._onSwatchPreview.bind(this);
+ this._onSwatchRevert = this._onSwatchRevert.bind(this);
+ this._onValidate = this.ruleView.throttle(this._previewValue, 10, this);
+ this.update = this.update.bind(this);
+ this.updatePropertyState = this.updatePropertyState.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.ruleView.tooltips.isEditing) || this.popup.isOpen;
+ },
+
+ /**
+ * Get the rule to the current text property
+ */
+ get rule() {
+ return this.prop.rule;
+ },
+
+ /**
+ * Create the property editor's DOM.
+ */
+ _create: function () {
+ this.element = this.doc.createElementNS(HTML_NS, "li");
+ this.element.classList.add("ruleview-property");
+ this.element._textPropertyEditor = this;
+
+ this.container = createChild(this.element, "div", {
+ class: "ruleview-propertycontainer"
+ });
+
+ // The enable checkbox will disable or enable the rule.
+ this.enable = createChild(this.container, "div", {
+ class: "ruleview-enableproperty theme-checkbox",
+ tabindex: "-1"
+ });
+
+ // Click to expand the computed properties of the text property.
+ this.expander = createChild(this.container, "span", {
+ class: "ruleview-expander theme-twisty"
+ });
+ this.expander.addEventListener("click", this._onExpandClicked, true);
+
+ this.nameContainer = createChild(this.container, "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.
+ this.valueContainer = createChild(this.container, "span", {
+ class: "ruleview-propertyvaluecontainer"
+ });
+
+ // 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(this.valueContainer, "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.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(this.valueContainer, ";");
+
+ this.warning = createChild(this.container, "div", {
+ class: "ruleview-warning",
+ hidden: "",
+ title: l10n("rule.warning.title"),
+ });
+
+ // Filter button that filters for the current property name and is
+ // displayed when the property is overridden by another rule.
+ this.filterProperty = createChild(this.container, "div", {
+ class: "ruleview-overridden-rule-filter",
+ hidden: "",
+ title: l10n("rule.filterProperty.title"),
+ });
+
+ this.filterProperty.addEventListener("click", event => {
+ this.ruleEditor.ruleView.setFilterStyles("`" + this.prop.name + "`");
+ event.stopPropagation();
+ }, false);
+
+ // 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", (event) => {
+ // Clicks within the name shouldn't propagate any further.
+ event.stopPropagation();
+
+ // Forward clicks on nameContainer to the editable nameSpan
+ if (event.target === this.nameContainer) {
+ this.nameSpan.click();
+ }
+ }, false);
+
+ editableField({
+ start: this._onStartEditing,
+ element: this.nameSpan,
+ done: this._onNameDone,
+ destroy: this.updatePropertyState,
+ advanceChars: ":",
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
+ popup: this.popup,
+ cssProperties: this.cssProperties,
+ contextMenu: this.ruleView.inspector.onTextBoxContextMenu
+ });
+
+ // Auto blur name field on multiple CSS rules get pasted in.
+ this.nameContainer.addEventListener("paste",
+ blurOnMultipleProperties(this.cssProperties), false);
+
+ this.valueContainer.addEventListener("click", (event) => {
+ // Clicks within the value shouldn't propagate any further.
+ event.stopPropagation();
+
+ // Forward clicks on valueContainer to the editable valueSpan
+ if (event.target === this.valueContainer) {
+ this.valueSpan.click();
+ }
+ }, false);
+
+ // The mousedown event could trigger a blur event on nameContainer, which
+ // will trigger a call to the update function. The update function clears
+ // valueSpan's markup. Thus the regular click event does not bubble up, and
+ // listener's callbacks are not called.
+ // So we need to remember where the user clicks in order to re-trigger the click
+ // after the valueSpan's markup is re-populated. We only need to track this for
+ // valueSpan's child elements, because direct click on valueSpan will always
+ // trigger a click event.
+ this.valueSpan.addEventListener("mousedown", (event) => {
+ let clickedEl = event.target;
+ if (clickedEl === this.valueSpan) {
+ return;
+ }
+ this._hasPendingClick = true;
+
+ let matchedSelector = ACTIONABLE_ELEMENTS_SELECTORS.find(
+ (selector) => clickedEl.matches(selector));
+ if (matchedSelector) {
+ let similarElements = [...this.valueSpan.querySelectorAll(matchedSelector)];
+ this._clickedElementOptions = {
+ selector: matchedSelector,
+ index: similarElements.indexOf(clickedEl)
+ };
+ }
+ }, false);
+
+ this.valueSpan.addEventListener("mouseup", (event) => {
+ this._clickedElementOptions = null;
+ this._hasPendingClick = false;
+ }, 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: advanceValidate,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
+ property: this.prop,
+ popup: this.popup,
+ multiline: true,
+ maxWidth: () => this.container.getBoundingClientRect().width,
+ cssProperties: this.cssProperties,
+ contextMenu: this.ruleView.inspector.onTextBoxContextMenu
+ });
+ }
+ },
+
+ /**
+ * Get the path from which to resolve requests for this
+ * rule's stylesheet.
+ *
+ * @return {String} the stylesheet's href.
+ */
+ get sheetHref() {
+ let domRule = this.rule.domRule;
+ if (domRule) {
+ return domRule.href || domRule.nodeHref;
+ }
+ return undefined;
+ },
+
+ /**
+ * Populate the span based on changes to the TextProperty.
+ */
+ update: function () {
+ if (this.ruleView.isDestroyed) {
+ return;
+ }
+
+ this.updatePropertyState();
+
+ 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.rule.elementStyle.store;
+ let val = store.userProperties.getProperty(this.rule.style, name,
+ this.prop.value);
+ if (this.prop.priority) {
+ val += " !" + this.prop.priority;
+ }
+
+ let propDirty = store.userProperties.contains(this.rule.style, name);
+
+ if (propDirty) {
+ this.element.setAttribute("dirty", "");
+ } else {
+ this.element.removeAttribute("dirty");
+ }
+
+ let outputParser = this.ruleView._outputParser;
+ let parserOptions = {
+ angleClass: "ruleview-angle",
+ angleSwatchClass: SHARED_SWATCH_CLASS + " " + ANGLE_SWATCH_CLASS,
+ bezierClass: "ruleview-bezier",
+ bezierSwatchClass: SHARED_SWATCH_CLASS + " " + BEZIER_SWATCH_CLASS,
+ colorClass: "ruleview-color",
+ colorSwatchClass: SHARED_SWATCH_CLASS + " " + COLOR_SWATCH_CLASS,
+ filterClass: "ruleview-filter",
+ filterSwatchClass: SHARED_SWATCH_CLASS + " " + FILTER_SWATCH_CLASS,
+ gridClass: "ruleview-grid",
+ defaultColorType: !propDirty,
+ urlClass: "theme-link",
+ baseURI: this.sheetHref
+ };
+ let frag = outputParser.parseCssProperty(name, val, parserOptions);
+ this.valueSpan.innerHTML = "";
+ this.valueSpan.appendChild(frag);
+
+ this.ruleView.emit("property-value-updated", this.valueSpan);
+
+ // Attach the color picker tooltip to the color swatches
+ this._colorSwatchSpans =
+ this.valueSpan.querySelectorAll("." + COLOR_SWATCH_CLASS);
+ if (this.ruleEditor.isEditable) {
+ for (let span of this._colorSwatchSpans) {
+ // Adding this swatch to the list of swatches our colorpicker
+ // knows about
+ this.ruleView.tooltips.colorPicker.addSwatch(span, {
+ onShow: this._onStartEditing,
+ onPreview: this._onSwatchPreview,
+ onCommit: this._onSwatchCommit,
+ onRevert: this._onSwatchRevert
+ });
+ span.on("unit-change", this._onSwatchCommit);
+ let title = l10n("rule.colorSwatch.tooltip");
+ span.setAttribute("title", title);
+ }
+ }
+
+ // Attach the cubic-bezier tooltip to the bezier swatches
+ this._bezierSwatchSpans =
+ this.valueSpan.querySelectorAll("." + BEZIER_SWATCH_CLASS);
+ if (this.ruleEditor.isEditable) {
+ for (let span of this._bezierSwatchSpans) {
+ // Adding this swatch to the list of swatches our colorpicker
+ // knows about
+ this.ruleView.tooltips.cubicBezier.addSwatch(span, {
+ onShow: this._onStartEditing,
+ onPreview: this._onSwatchPreview,
+ onCommit: this._onSwatchCommit,
+ onRevert: this._onSwatchRevert
+ });
+ let title = l10n("rule.bezierSwatch.tooltip");
+ span.setAttribute("title", title);
+ }
+ }
+
+ // Attach the filter editor tooltip to the filter swatch
+ let span = this.valueSpan.querySelector("." + FILTER_SWATCH_CLASS);
+ if (this.ruleEditor.isEditable) {
+ if (span) {
+ parserOptions.filterSwatch = true;
+
+ this.ruleView.tooltips.filterEditor.addSwatch(span, {
+ onShow: this._onStartEditing,
+ onPreview: this._onSwatchPreview,
+ onCommit: this._onSwatchCommit,
+ onRevert: this._onSwatchRevert
+ }, outputParser, parserOptions);
+ let title = l10n("rule.filterSwatch.tooltip");
+ span.setAttribute("title", title);
+ }
+ }
+
+ this.angleSwatchSpans =
+ this.valueSpan.querySelectorAll("." + ANGLE_SWATCH_CLASS);
+ if (this.ruleEditor.isEditable) {
+ for (let angleSpan of this.angleSwatchSpans) {
+ angleSpan.on("unit-change", this._onSwatchCommit);
+ let title = l10n("rule.angleSwatch.tooltip");
+ angleSpan.setAttribute("title", title);
+ }
+ }
+
+ let gridToggle = this.valueSpan.querySelector(".ruleview-grid");
+ if (gridToggle) {
+ gridToggle.setAttribute("title", l10n("rule.gridToggle.tooltip"));
+ if (this.ruleView.highlighters.gridHighlighterShown ===
+ this.ruleView.inspector.selection.nodeFront) {
+ gridToggle.classList.add("active");
+ }
+ }
+
+ // Now that we have updated the property's value, we might have a pending
+ // click on the value container. If we do, we have to trigger a click event
+ // on the right element.
+ if (this._hasPendingClick) {
+ this._hasPendingClick = false;
+ let elToClick;
+
+ if (this._clickedElementOptions !== null) {
+ let {selector, index} = this._clickedElementOptions;
+ elToClick = this.valueSpan.querySelectorAll(selector)[index];
+
+ this._clickedElementOptions = null;
+ }
+
+ if (!elToClick) {
+ elToClick = this.valueSpan;
+ }
+ elToClick.click();
+ }
+
+ // Populate the computed styles.
+ this._updateComputed();
+
+ // Update the rule property highlight.
+ this.ruleView._updatePropertyHighlight(this);
+ },
+
+ _onStartEditing: function () {
+ this.element.classList.remove("ruleview-overridden");
+ this.filterProperty.hidden = true;
+ this.enable.style.visibility = "hidden";
+ },
+
+ /**
+ * Update the visibility of the enable checkbox, the warning indicator and
+ * the filter property, as well as the overriden state of the property.
+ */
+ updatePropertyState: function () {
+ 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();
+ this.filterProperty.hidden = this.editing ||
+ !this.isValid() ||
+ !this.prop.overridden ||
+ this.ruleEditor.rule.isUnmatched;
+
+ if (!this.editing &&
+ (this.prop.overridden || !this.prop.enabled ||
+ !this.prop.isKnownProperty())) {
+ this.element.classList.add("ruleview-overridden");
+ } else {
+ this.element.classList.remove("ruleview-overridden");
+ }
+ },
+
+ /**
+ * Update the indicator for computed styles. The computed styles themselves
+ * are populated on demand, when they become visible.
+ */
+ _updateComputed: function () {
+ this.computed.innerHTML = "";
+
+ let showExpander = this.prop.computed.some(c => c.name !== this.prop.name);
+ this.expander.style.visibility = showExpander ? "visible" : "hidden";
+
+ this._populatedComputed = false;
+ if (this.expander.hasAttribute("open")) {
+ this._populateComputed();
+ }
+ },
+
+ /**
+ * Populate the list of computed styles.
+ */
+ _populateComputed: function () {
+ if (this._populatedComputed) {
+ return;
+ }
+ this._populatedComputed = true;
+
+ for (let computed of this.prop.computed) {
+ // Don't bother to duplicate information already
+ // shown in the text property.
+ if (computed.name === this.prop.name) {
+ continue;
+ }
+
+ 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.ruleView._outputParser;
+ let frag = outputParser.parseCssProperty(
+ computed.name, computed.value, {
+ colorSwatchClass: "ruleview-swatch ruleview-colorswatch",
+ urlClass: "theme-link",
+ baseURI: this.sheetHref
+ }
+ );
+
+ // Store the computed property value that was parsed for output
+ computed.parsedValue = frag.textContent;
+
+ createChild(li, "span", {
+ class: "ruleview-propertyvalue theme-fg-color1",
+ child: frag
+ });
+
+ appendText(li, ";");
+
+ // Store the computed style element for easy access when highlighting
+ // styles
+ computed.element = li;
+ }
+ },
+
+ /**
+ * Handles clicks on the disabled property.
+ */
+ _onEnableClicked: function (event) {
+ let checked = this.enable.hasAttribute("checked");
+ if (checked) {
+ this.enable.removeAttribute("checked");
+ } else {
+ this.enable.setAttribute("checked", "");
+ }
+ this.prop.setEnabled(!checked);
+ event.stopPropagation();
+ },
+
+ /**
+ * Handles clicks on the computed property expander. If the computed list is
+ * open due to user expanding or style filtering, collapse the computed list
+ * and close the expander. Otherwise, add user-open attribute which is used to
+ * expand the computed list and tracks whether or not the computed list is
+ * expanded by manually by the user.
+ */
+ _onExpandClicked: function (event) {
+ if (this.computed.hasAttribute("filter-open") ||
+ this.computed.hasAttribute("user-open")) {
+ this.expander.removeAttribute("open");
+ this.computed.removeAttribute("filter-open");
+ this.computed.removeAttribute("user-open");
+ } else {
+ this.expander.setAttribute("open", "true");
+ this.computed.setAttribute("user-open", "");
+ this._populateComputed();
+ }
+
+ event.stopPropagation();
+ },
+
+ /**
+ * Expands the computed list when a computed property is matched by the style
+ * filtering. The filter-open attribute is used to track whether or not the
+ * computed list was toggled opened by the filter.
+ */
+ expandForFilter: function () {
+ if (!this.computed.hasAttribute("user-open")) {
+ this.expander.setAttribute("open", "true");
+ this.computed.setAttribute("filter-open", "");
+ this._populateComputed();
+ }
+ },
+
+ /**
+ * Collapses the computed list that was expanded by style filtering.
+ */
+ collapseForFilter: function () {
+ this.computed.removeAttribute("filter-open");
+
+ if (!this.computed.hasAttribute("user-open")) {
+ this.expander.removeAttribute("open");
+ }
+ },
+
+ /**
+ * Called when the property name's inplace editor is closed.
+ * Ignores the change if the user pressed escape, otherwise
+ * commits it.
+ *
+ * @param {String} value
+ * The value contained in the editor.
+ * @param {Boolean} commit
+ * True if the change should be applied.
+ * @param {Number} direction
+ * The move focus direction number.
+ */
+ _onNameDone: function (value, commit, direction) {
+ let isNameUnchanged = (!commit && !this.ruleEditor.isEditing) ||
+ this.committed.name === value;
+ if (this.prop.value && isNameUnchanged) {
+ return;
+ }
+
+ // Remove a property if the name is empty
+ if (!value.trim()) {
+ this.remove(direction);
+ return;
+ }
+
+ // Remove a property if the property value is empty and the property
+ // value is not about to be focused
+ if (!this.prop.value &&
+ direction !== Services.focus.MOVEFOCUS_FORWARD) {
+ this.remove(direction);
+ return;
+ }
+
+ // 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(this.cssProperties.isKnown, value);
+
+ if (properties.length) {
+ this.prop.setName(properties[0].name);
+ this.committed.name = this.prop.name;
+
+ if (!this.prop.enabled) {
+ this.prop.setEnabled(true);
+ }
+
+ 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 or previous available property given the focus
+ * direction.
+ *
+ * @param {Number} direction
+ * The move focus direction number.
+ */
+ remove: function (direction) {
+ if (this._colorSwatchSpans && this._colorSwatchSpans.length) {
+ for (let span of this._colorSwatchSpans) {
+ this.ruleView.tooltips.colorPicker.removeSwatch(span);
+ span.off("unit-change", this._onSwatchCommit);
+ }
+ }
+
+ if (this.angleSwatchSpans && this.angleSwatchSpans.length) {
+ for (let span of this.angleSwatchSpans) {
+ span.off("unit-change", this._onSwatchCommit);
+ }
+ }
+
+ this.element.parentNode.removeChild(this.element);
+ this.ruleEditor.rule.editClosestTextProperty(this.prop, direction);
+ 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} value
+ * The value contained in the editor.
+ * @param {Boolean} commit
+ * True if the change should be applied.
+ * @param {Number} direction
+ * The move focus direction number.
+ */
+ _onValueDone: function (value = "", commit, direction) {
+ let parsedProperties = this._getValueAndExtraProperties(value);
+ let val = parseSingleValue(this.cssProperties.isKnown,
+ parsedProperties.firstValue);
+ let isValueUnchanged = (!commit && !this.ruleEditor.isEditing) ||
+ !parsedProperties.propertiesToAdd.length &&
+ this.committed.value === val.value &&
+ this.committed.priority === val.priority;
+ // If the value is not empty and unchanged, revert the property back to
+ // its original value and enabled or disabled state
+ if (value.trim() && isValueUnchanged) {
+ this.ruleEditor.rule.previewPropertyValue(this.prop, val.value,
+ val.priority);
+ this.rule.setPropertyEnabled(this.prop, this.prop.enabled);
+ return;
+ }
+
+ if (this.isDisplayGrid()) {
+ this.ruleView.highlighters._hideGridHighlighter();
+ }
+
+ // First, set this property value (common case, only modified a property)
+ this.prop.setValue(val.value, val.priority);
+
+ if (!this.prop.enabled) {
+ this.prop.setEnabled(true);
+ }
+
+ this.committed.value = this.prop.value;
+ this.committed.priority = this.prop.priority;
+
+ // If needed, add any new properties after this.prop.
+ this.ruleEditor.addProperties(parsedProperties.propertiesToAdd, this.prop);
+
+ // If the input value is empty and the focus is moving forward to the next
+ // editable field, 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 (!value.trim() && direction !== Services.focus.MOVEFOCUS_BACKWARD) {
+ setTimeout(() => {
+ if (!this.editing) {
+ this.remove(direction);
+ }
+ }, 0);
+ }
+ },
+
+ /**
+ * Called when the swatch editor wants to commit a value change.
+ */
+ _onSwatchCommit: function () {
+ this._onValueDone(this.valueSpan.textContent, true);
+ this.update();
+ },
+
+ /**
+ * Called when the swatch editor wants to preview a value change.
+ */
+ _onSwatchPreview: function () {
+ this._previewValue(this.valueSpan.textContent);
+ },
+
+ /**
+ * Called when the swatch editor closes from an ESC. Revert to the original
+ * value of this property before editing.
+ */
+ _onSwatchRevert: function () {
+ this._previewValue(this.prop.value, true);
+ this.update();
+ },
+
+ /**
+ * 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} value
+ * 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 (value) {
+ // 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 = value;
+ let propertiesToAdd = [];
+
+ let properties = parseDeclarations(this.cssProperties.isKnown, value);
+
+ // 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);
+ } else if (properties[0].name && properties[0].value) {
+ // In some cases, the value could be a property:value pair
+ // itself. Join them as one value string and append
+ // potentially following properties
+ firstValue = properties[0].name + ": " + properties[0].value;
+ propertiesToAdd = properties.slice(1);
+ }
+ }
+
+ return {
+ propertiesToAdd: propertiesToAdd,
+ firstValue: firstValue
+ };
+ },
+
+ /**
+ * Live preview this property, without committing changes.
+ *
+ * @param {String} value
+ * The value to set the current property to.
+ * @param {Boolean} reverting
+ * True if we're reverting the previously previewed value
+ */
+ _previewValue: function (value, reverting = false) {
+ // Since function call is throttled, we need to make sure we are still
+ // editing, and any selector modifications have been completed
+ if (!reverting && (!this.editing || this.ruleEditor.isEditing)) {
+ return;
+ }
+
+ let val = parseSingleValue(this.cssProperties.isKnown, value);
+ 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 {Boolean} true if the property value is valid, false otherwise.
+ */
+ isValid: function () {
+ return this.prop.isValid();
+ },
+
+ /**
+ * Returns true if the property is a `display: grid` declaration.
+ *
+ * @return {Boolean} true if the property is a `display: grid` declaration.
+ */
+ isDisplayGrid: function () {
+ return this.prop.name === "display" && this.prop.value === "grid";
+ }
+};
+
+exports.TextPropertyEditor = TextPropertyEditor;