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/constants.js19
-rw-r--r--devtools/client/inspector/rules/models/class-list.js271
-rw-r--r--devtools/client/inspector/rules/models/element-style.js818
-rw-r--r--devtools/client/inspector/rules/models/moz.build13
-rw-r--r--devtools/client/inspector/rules/models/rule.js831
-rw-r--r--devtools/client/inspector/rules/models/text-property.js400
-rw-r--r--devtools/client/inspector/rules/models/user-properties.js85
-rw-r--r--devtools/client/inspector/rules/moz.build25
-rw-r--r--devtools/client/inspector/rules/rules.js2199
-rw-r--r--devtools/client/inspector/rules/test/browser_part1.ini172
-rw-r--r--devtools/client/inspector/rules/test/browser_part2.ini223
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js63
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-cancel_01.js54
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-cancel_02.js45
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-cancel_03.js51
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-commented.js50
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-invalid-identifier.js33
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property-svg.js24
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property_01.js31
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-property_02.js78
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js31
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-and-remove-style-node.js43
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-button-state.js50
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-csp.js40
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-edit-selector.js54
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-iframes.js53
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-namespace-elements.js40
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-pseudo-class.js64
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-then-property-edit-selector.js82
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js44
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_add-rule.js46
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_authored.js73
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_authored_color.js78
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_authored_override.js56
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js26
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_class_panel_add.js108
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_class_panel_autocomplete.js266
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_class_panel_content.js124
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_class_panel_edit.js56
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_class_panel_invalid_nodes.js51
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_class_panel_mutation.js75
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_class_panel_state_preserved.js41
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_class_panel_toggle.js53
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorUnit.js70
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation.js159
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation_bfcache.js103
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation_meta.js82
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation_rdm.js90
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_01.js73
-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-or-keyboard-activation.js72
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-commit-on-ENTER.js76
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-contrast-ratio.js232
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-edit-gradient.js82
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-element-without-quads.js77
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-element-picker.js31
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-on-tooltip.js54
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-multiple-changes.js110
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-release-outside-frame.js79
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-revert-on-ESC.js67
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-swatch-displayed.js101
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-works-with-css-vars.js76
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_colorpicker-wrap-focus.js76
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js148
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js128
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-new-property_01.js112
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js137
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-new-property_03.js49
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-new-property_04.js77
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js140
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_completion-on-empty.js60
-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_completion-shortcut.js70
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_computed-lists_01.js60
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_computed-lists_02.js86
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_computed-lists_03.js42
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_conditional_import.js117
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_container-queries.js308
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_content_01.js72
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_content_02.js76
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_copy_styles.js359
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_css-compatibility-add-rename-rule.js125
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_css-compatibility-check-add-fix.js134
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_css-compatibility-learn-more-link.js66
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_css-compatibility-toggle-rules.js150
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_css-compatibility-tooltip-telemetry.js58
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cssom.js32
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cubicbezier-appears-on-swatch-click.js79
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cubicbezier-commit-on-ENTER.js82
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cubicbezier-revert-on-ESC.js67
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_custom.js89
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cycle-angle.js122
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_cycle-color.js225
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js53
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js56
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-click.js60
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js101
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js108
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js820
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-order.js130
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-remove_01.js66
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-remove_02.js66
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-remove_03.js84
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property-remove_04.js45
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_01.js160
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_02.js143
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_03.js49
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_04.js83
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_05.js72
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_06.js68
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_07.js79
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_08.js62
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_09.js80
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-property_10.js51
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js96
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector-click.js84
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector-commit.js136
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_01.js66
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_02.js92
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_03.js55
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js70
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_05.js82
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_06.js83
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_07.js67
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_08.js78
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_09.js120
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_10.js67
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_11.js72
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-selector_12.js41
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-size-property-dragging.js481
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js119
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.js80
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.js86
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_04.js76
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-variable-add.js40
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-variable-remove.js42
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_edit-variable.js47
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_editable-field-focus_01.js111
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_editable-field-focus_02.js120
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_eyedropper.js160
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_filtereditor-appears-on-swatch-click.js69
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_filtereditor-commit-on-ENTER.js51
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_filtereditor-revert-on-ESC.js74
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-on-mutation.js55
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-on-navigate.js46
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-on-reload.js57
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-restored-after-reload.js69
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_flexbox-toggle-telemetry.js53
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_01.js89
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_01b.js89
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_02.js114
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_03.js133
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_04.js89
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_font-family-parsing.js60
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-mutation.js46
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.js42
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-reload.js55
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-highlighter-restored-after-reload.js83
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-template-areas.js178
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-toggle-telemetry.js42
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js81
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-toggle_01b.js81
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js95
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js137
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-toggle_04.js81
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_grid-toggle_05.js139
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_gridline-names-are-shown-correctly.js146
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_gridline-names-autocomplete.js205
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_guessIndentation.js46
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_highlight-element-rule.js49
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_highlight-property.js94
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_highlight-used-fonts.js125
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_imported_stylesheet_edit.js46
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inactive_css_display-justify.js47
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inactive_css_flexbox.js161
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inactive_css_grid.js267
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inactive_css_inline.js75
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inactive_css_split-condition.js31
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inactive_css_visited.js67
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inactive_css_xul.js43
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inherited-properties_01.js53
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inherited-properties_02.js35
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inherited-properties_03.js49
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inherited-properties_04.js30
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inline-source-map.js25
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_inline-style-order.js86
-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.js32
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_keybindings.js48
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_keyframeLineNumbers.js24
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_keyframes-rule-shadowdom.js76
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js121
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_keyframes-rule_02.js95
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_large_base64_background_image.js72
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_layer.js107
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_lineNumbers.js31
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_linear-easing-swatch.js487
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_livepreview.js76
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_01.js57
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_02.js48
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_03.js33
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_04.js34
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_05.js30
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_06.js65
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_07.js93
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mark_overridden_08.js51
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_mathml-element.js62
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_media-queries.js35
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_media-queries_reload.js67
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple-properties-duplicates.js117
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple-properties-priority.js72
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_01.js99
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js107
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple_properties_01.js84
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_multiple_properties_02.js80
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_nested_at_rules.js85
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_non_ascii.js37
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_original-source-link.js93
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_original-source-link2.js89
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_preview-tooltips-sizes.js106
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_print_media_simulation.js96
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js395
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js54
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_pseudo-visited.js31
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_pseudo-visited_in_media-query.js23
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_pseudo-visited_with_style-attribute.js26
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_pseudo_lock_options.js159
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_refresh-no-flicker.js47
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_01.js71
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.js43
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_01.js180
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_02.js118
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_03.js55
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_04.js75
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_expander.js113
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-media-queries-layers.js195
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter-overridden-property.js89
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_01.js98
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_02.js36
-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.js82
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_05.js37
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_06.js29
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_07.js66
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_08.js57
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_09.js79
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_10.js96
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js99
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_search-filter_escape-keypress.js71
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js210
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter-iframe-picker.js60
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js35
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter_01.js32
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter_02.js48
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter_03.js64
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter_04.js35
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter_05.js46
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector-highlighter_order.js54
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_selector_highlight.js152
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_shadowdom_slot_rules.js102
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_shapes-toggle_01.js82
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_shapes-toggle_02.js61
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_shapes-toggle_03.js128
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_shapes-toggle_04.js48
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_shapes-toggle_05.js43
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_shapes-toggle_06.js104
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_shapes-toggle_07.js99
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_shapes-toggle_basic-shapes-default.js102
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_shorthand-overridden-lists.js85
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_shorthand-overridden-lists_01.js35
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_strict-search-filter-computed-list_01.js208
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_strict-search-filter_01.js150
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_strict-search-filter_02.js38
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_strict-search-filter_03.js50
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_style-editor-link.js251
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_update_mask_image_cors.js60
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_url-click-opens-new-tab.js35
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_urls-clickable.js77
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_user-agent-styles-uneditable.js62
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_user-agent-styles.js216
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_user-property-reset.js117
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_variables-in-pseudo-element_01.js45
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_variables-in-pseudo-element_02.js44
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_variables_01.js72
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_variables_02.js321
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_variables_03-case-sensitive.js32
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_variables_04-valid-chars.js58
-rw-r--r--devtools/client/inspector/rules/test/doc_author-sheet.html34
-rw-r--r--devtools/client/inspector/rules/test/doc_blob_stylesheet.html39
-rw-r--r--devtools/client/inspector/rules/test/doc_class_panel_autocomplete.html41
-rw-r--r--devtools/client/inspector/rules/test/doc_class_panel_autocomplete_stylesheet.css20
-rw-r--r--devtools/client/inspector/rules/test/doc_conditional_import.css3
-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.html25
-rw-r--r--devtools/client/inspector/rules/test/doc_custom.html33
-rw-r--r--devtools/client/inspector/rules/test/doc_edit_imported_selector.html13
-rw-r--r--devtools/client/inspector/rules/test/doc_filter.html13
-rw-r--r--devtools/client/inspector/rules/test/doc_grid_area_gridline_names.html59
-rw-r--r--devtools/client/inspector/rules/test/doc_grid_names.html17
-rw-r--r--devtools/client/inspector/rules/test/doc_imported_anonymous_layer.css4
-rw-r--r--devtools/client/inspector/rules/test/doc_imported_named_layer.css12
-rw-r--r--devtools/client/inspector/rules/test/doc_imported_no_layer.css3
-rw-r--r--devtools/client/inspector/rules/test/doc_inactive_css_xul.xhtml15
-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.html42
-rw-r--r--devtools/client/inspector/rules/test/doc_print_media_simulation.html27
-rw-r--r--devtools/client/inspector/rules/test/doc_pseudoelement.html149
-rw-r--r--devtools/client/inspector/rules/test/doc_ruleLineNumbers.html19
-rw-r--r--devtools/client/inspector/rules/test/doc_rules_imported_stylesheet_edit.html4
-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_sourcemaps2.css5
-rw-r--r--devtools/client/inspector/rules/test/doc_sourcemaps2.css^headers^1
-rw-r--r--devtools/client/inspector/rules/test/doc_sourcemaps2.html11
-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/doc_variables_1.html23
-rw-r--r--devtools/client/inspector/rules/test/doc_variables_2.html45
-rw-r--r--devtools/client/inspector/rules/test/doc_variables_3.html16
-rw-r--r--devtools/client/inspector/rules/test/doc_variables_4.html23
-rw-r--r--devtools/client/inspector/rules/test/doc_visited.html27
-rw-r--r--devtools/client/inspector/rules/test/doc_visited_in_media_query.html18
-rw-r--r--devtools/client/inspector/rules/test/doc_visited_with_style_attribute.html9
-rw-r--r--devtools/client/inspector/rules/test/head.js1284
-rw-r--r--devtools/client/inspector/rules/test/sjs_imported_stylesheet_edit.sjs53
-rw-r--r--devtools/client/inspector/rules/test/square_svg.sjs13
-rw-r--r--devtools/client/inspector/rules/types.js165
-rw-r--r--devtools/client/inspector/rules/utils/l10n.js15
-rw-r--r--devtools/client/inspector/rules/utils/moz.build10
-rw-r--r--devtools/client/inspector/rules/utils/utils.js357
-rw-r--r--devtools/client/inspector/rules/views/class-list-previewer.js310
-rw-r--r--devtools/client/inspector/rules/views/moz.build9
-rw-r--r--devtools/client/inspector/rules/views/rule-editor.js842
-rw-r--r--devtools/client/inspector/rules/views/text-property-editor.js1598
349 files changed, 35428 insertions, 0 deletions
diff --git a/devtools/client/inspector/rules/constants.js b/devtools/client/inspector/rules/constants.js
new file mode 100644
index 0000000000..7414014cb2
--- /dev/null
+++ b/devtools/client/inspector/rules/constants.js
@@ -0,0 +1,19 @@
+/* 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";
+
+// Compatibility tooltip message id shared between the
+// models/text-property.js and the tooltip tests
+exports.COMPATIBILITY_TOOLTIP_MESSAGE = {
+ default: "css-compatibility-default-message",
+ deprecated: "css-compatibility-deprecated-message",
+ "deprecated-experimental":
+ "css-compatibility-deprecated-experimental-message",
+ "deprecated-experimental-supported":
+ "css-compatibility-deprecated-experimental-supported-message",
+ "deprecated-supported": "css-compatibility-deprecated-supported-message",
+ experimental: "css-compatibility-experimental-message",
+ "experimental-supported": "css-compatibility-experimental-supported-message",
+};
diff --git a/devtools/client/inspector/rules/models/class-list.js b/devtools/client/inspector/rules/models/class-list.js
new file mode 100644
index 0000000000..9173977382
--- /dev/null
+++ b/devtools/client/inspector/rules/models/class-list.js
@@ -0,0 +1,271 @@
+/* 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 EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+// This serves as a local cache for the classes applied to each of the node we care about
+// here.
+// The map is indexed by NodeFront. Any time a new node is selected in the inspector, an
+// entry is added here, indexed by the corresponding NodeFront.
+// The value for each entry is an array of each of the class this node has. Items of this
+// array are objects like: { name, isApplied } where the name is the class itself, and
+// isApplied is a Boolean indicating if the class is applied on the node or not.
+const CLASSES = new WeakMap();
+
+/**
+ * Manages the list classes per DOM elements we care about.
+ * The actual list is stored in the CLASSES const, indexed by NodeFront objects.
+ * The responsibility of this class is to be the source of truth for anyone who wants to
+ * know which classes a given NodeFront has, and which of these are enabled and which are
+ * disabled.
+ * It also reacts to DOM mutations so the list of classes is up to date with what is in
+ * the DOM.
+ * It can also be used to enable/disable a given class, or add classes.
+ *
+ * @param {Inspector} inspector
+ * The current inspector instance.
+ */
+class ClassList {
+ constructor(inspector) {
+ EventEmitter.decorate(this);
+
+ this.inspector = inspector;
+
+ this.onMutations = this.onMutations.bind(this);
+ this.inspector.on("markupmutation", this.onMutations);
+
+ this.classListProxyNode = this.inspector.panelDoc.createElement("div");
+ this.previewClasses = [];
+ this.unresolvedStateChanges = [];
+ }
+
+ destroy() {
+ this.inspector.off("markupmutation", this.onMutations);
+ this.inspector = null;
+ this.classListProxyNode = null;
+ }
+
+ /**
+ * The current node selection (which only returns if the node is an ELEMENT_NODE type
+ * since that's the only type this model can work with.)
+ */
+ get currentNode() {
+ if (
+ this.inspector.selection.isElementNode() &&
+ !this.inspector.selection.isPseudoElementNode()
+ ) {
+ return this.inspector.selection.nodeFront;
+ }
+ return null;
+ }
+
+ /**
+ * The class states for the current node selection. See the documentation of the CLASSES
+ * constant.
+ */
+ get currentClasses() {
+ if (!this.currentNode) {
+ return [];
+ }
+
+ if (!CLASSES.has(this.currentNode)) {
+ // Use the proxy node to get a clean list of classes.
+ this.classListProxyNode.className = this.currentNode.className;
+ const nodeClasses = [...new Set([...this.classListProxyNode.classList])]
+ .filter(
+ className =>
+ !this.previewClasses.some(
+ previewClass =>
+ previewClass.className === className &&
+ !previewClass.wasAppliedOnNode
+ )
+ )
+ .map(name => {
+ return { name, isApplied: true };
+ });
+
+ CLASSES.set(this.currentNode, nodeClasses);
+ }
+
+ return CLASSES.get(this.currentNode);
+ }
+
+ /**
+ * Same as currentClasses, but returns it in the form of a className string, where only
+ * enabled classes are added.
+ */
+ get currentClassesPreview() {
+ const currentClasses = this.currentClasses
+ .filter(({ isApplied }) => isApplied)
+ .map(({ name }) => name);
+ const previewClasses = this.previewClasses
+ .filter(previewClass => !currentClasses.includes(previewClass.className))
+ .filter(item => item !== "")
+ .map(({ className }) => className);
+
+ return currentClasses.concat(previewClasses).join(" ").trim();
+ }
+
+ /**
+ * Set the state for a given class on the current node.
+ *
+ * @param {String} name
+ * The class which state should be changed.
+ * @param {Boolean} isApplied
+ * True if the class should be enabled, false otherwise.
+ * @return {Promise} Resolves when the change has been made in the DOM.
+ */
+ setClassState(name, isApplied) {
+ // Do the change in our local model.
+ const nodeClasses = this.currentClasses;
+ nodeClasses.find(({ name: cName }) => cName === name).isApplied = isApplied;
+
+ return this.applyClassState();
+ }
+
+ /**
+ * Add several classes to the current node at once.
+ *
+ * @param {String} classNameString
+ * The string that contains all classes.
+ * @return {Promise} Resolves when the change has been made in the DOM.
+ */
+ addClassName(classNameString) {
+ this.classListProxyNode.className = classNameString;
+ this.eraseClassPreview();
+ return Promise.all(
+ [...new Set([...this.classListProxyNode.classList])].map(name => {
+ return this.addClass(name);
+ })
+ );
+ }
+
+ /**
+ * Add a class to the current node at once.
+ *
+ * @param {String} name
+ * The class to be added.
+ * @return {Promise} Resolves when the change has been made in the DOM.
+ */
+ addClass(name) {
+ // Avoid adding the same class again.
+ if (this.currentClasses.some(({ name: cName }) => cName === name)) {
+ return Promise.resolve();
+ }
+
+ // Change the local model, so we retain the state of the existing classes.
+ this.currentClasses.push({ name, isApplied: true });
+
+ return this.applyClassState();
+ }
+
+ /**
+ * Used internally by other functions like addClass or setClassState. Actually applies
+ * the class change to the DOM.
+ *
+ * @return {Promise} Resolves when the change has been made in the DOM.
+ */
+ applyClassState() {
+ // If there is no valid inspector selection, bail out silently. No need to report an
+ // error here.
+ if (!this.currentNode) {
+ return Promise.resolve();
+ }
+
+ // Remember which node & className we applied until their mutation event is received, so we
+ // can filter out dom mutations that are caused by us in onMutations, even in situations when
+ // a new change is applied before that the event of the previous one has been received yet
+ this.unresolvedStateChanges.push({
+ node: this.currentNode,
+ className: this.currentClassesPreview,
+ });
+
+ // Apply the change to the node.
+ const mod = this.currentNode.startModifyingAttributes();
+ mod.setAttribute("class", this.currentClassesPreview);
+ return mod.apply();
+ }
+
+ onMutations(mutations) {
+ for (const { type, target, attributeName } of mutations) {
+ // Only care if this mutation is for the class attribute.
+ if (type !== "attributes" || attributeName !== "class") {
+ continue;
+ }
+
+ const isMutationForOurChange = this.unresolvedStateChanges.some(
+ previousStateChange =>
+ previousStateChange.node === target &&
+ previousStateChange.className === target.className
+ );
+
+ if (!isMutationForOurChange) {
+ CLASSES.delete(target);
+ if (target === this.currentNode) {
+ this.emit("current-node-class-changed");
+ }
+ } else {
+ this.removeResolvedStateChanged(target, target.className);
+ }
+ }
+ }
+
+ /**
+ * Get the available classNames in the document where the current selected node lives:
+ * - the one already used on elements of the document
+ * - the one defined in Stylesheets of the document
+ *
+ * @param {String} filter: A string the classNames should start with (an insensitive
+ * case matching will be done).
+ * @returns {Promise<Array<String>>} A promise that resolves with an array of strings
+ * matching the passed filter.
+ */
+ getClassNames(filter) {
+ return this.currentNode.inspectorFront.pageStyle.getAttributesInOwnerDocument(
+ filter,
+ "class",
+ this.currentNode
+ );
+ }
+
+ previewClass(inputClasses) {
+ if (
+ this.previewClasses
+ .map(previewClass => previewClass.className)
+ .join(" ") !== inputClasses
+ ) {
+ this.previewClasses = [];
+ inputClasses.split(" ").forEach(className => {
+ this.previewClasses.push({
+ className,
+ wasAppliedOnNode: this.isClassAlreadyApplied(className),
+ });
+ });
+ this.applyClassState();
+ }
+ }
+
+ eraseClassPreview() {
+ this.previewClass("");
+ }
+
+ removeResolvedStateChanged(currentNode, currentClassesPreview) {
+ this.unresolvedStateChanges.splice(
+ 0,
+ this.unresolvedStateChanges.findIndex(
+ previousState =>
+ previousState.node === currentNode &&
+ previousState.className === currentClassesPreview
+ ) + 1
+ );
+ }
+
+ isClassAlreadyApplied(className) {
+ return this.currentClasses.some(({ name }) => name === className);
+ }
+}
+
+module.exports = ClassList;
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 0000000000..0de235fe96
--- /dev/null
+++ b/devtools/client/inspector/rules/models/element-style.js
@@ -0,0 +1,818 @@
+/* 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 Rule = require("resource://devtools/client/inspector/rules/models/rule.js");
+const UserProperties = require("resource://devtools/client/inspector/rules/models/user-properties.js");
+const {
+ style: { ELEMENT_STYLE },
+} = require("resource://devtools/shared/constants.js");
+
+loader.lazyRequireGetter(
+ this,
+ "promiseWarn",
+ "resource://devtools/client/inspector/shared/utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ ["parseDeclarations", "parseNamedDeclarations", "parseSingleValue"],
+ "resource://devtools/shared/css/parsing-utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "isCssVariable",
+ "resource://devtools/client/fronts/css-properties.js",
+ true
+);
+
+const PREF_INACTIVE_CSS_ENABLED = "devtools.inspector.inactive.css.enabled";
+
+/**
+ * ElementStyle is responsible for the following:
+ * Keeps track of which properties are overridden.
+ * Maintains a list of Rule objects for a given element.
+ */
+class ElementStyle {
+ /**
+ * @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?
+ */
+ constructor(element, ruleView, store, pageStyle, showUserAgentStyles) {
+ this.element = element;
+ this.ruleView = ruleView;
+ this.store = store || {};
+ this.pageStyle = pageStyle;
+ this.pseudoElements = [];
+ this.showUserAgentStyles = showUserAgentStyles;
+ this.rules = [];
+ this.cssProperties = this.ruleView.cssProperties;
+ this.variablesMap = new Map();
+
+ // 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();
+ }
+ }
+
+ get unusedCssEnabled() {
+ if (!this._unusedCssEnabled) {
+ this._unusedCssEnabled = Services.prefs.getBoolPref(
+ PREF_INACTIVE_CSS_ENABLED,
+ false
+ );
+ }
+ return this._unusedCssEnabled;
+ }
+
+ destroy() {
+ if (this.destroyed) {
+ return;
+ }
+
+ this.destroyed = true;
+ this.pseudoElements = [];
+
+ for (const rule of this.rules) {
+ if (rule.editor) {
+ rule.editor.destroy();
+ }
+
+ rule.destroy();
+ }
+ }
+
+ /**
+ * Called by the Rule object when it has been changed through the
+ * setProperty* methods.
+ */
+ _changed() {
+ 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() {
+ const populated = this.pageStyle
+ .getApplied(this.element, {
+ inherited: true,
+ matchedSelectors: true,
+ filter: this.showUserAgentStyles ? "ua" : undefined,
+ })
+ .then(entries => {
+ if (this.destroyed || this.populated !== populated) {
+ return Promise.resolve(undefined);
+ }
+
+ // Store the current list of rules (if any) during the population
+ // process. They will be reused if possible.
+ const existingRules = this.rules;
+
+ this.rules = [];
+
+ for (const entry of entries) {
+ this._maybeAddRule(entry, existingRules);
+ }
+
+ // Store a list of all pseudo-element types found in the matching rules.
+ this.pseudoElements = this.rules
+ .filter(r => r.pseudoElement)
+ .map(r => r.pseudoElement);
+
+ // Mark overridden computed styles.
+ this.onRuleUpdated();
+
+ this._sortRulesForPseudoElement();
+
+ // We're done with the previous list of rules.
+ for (const r of existingRules) {
+ if (r?.editor) {
+ r.editor.destroy();
+ }
+
+ r.destroy();
+ }
+
+ return undefined;
+ })
+ .catch(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;
+ }
+
+ /**
+ * Returns the Rule object of the given rule id.
+ *
+ * @param {String|null} id
+ * The id of the Rule object.
+ * @return {Rule|undefined} of the given rule id or undefined if it cannot be found.
+ */
+ getRule(id) {
+ return id
+ ? this.rules.find(rule => rule.domRule.actorID === id)
+ : undefined;
+ }
+
+ /**
+ * Get the font families in use by the element.
+ *
+ * Returns a promise that will be resolved to a list of CSS family
+ * names. The list might have duplicates.
+ */
+ getUsedFontFamilies() {
+ return new Promise((resolve, reject) => {
+ this.ruleView.styleWindow.requestIdleCallback(async () => {
+ if (this.element.isDestroyed()) {
+ resolve([]);
+ return;
+ }
+ try {
+ const fonts = await this.pageStyle.getUsedFontFaces(this.element, {
+ includePreviews: false,
+ });
+ resolve(fonts.map(font => font.CSSFamilyName));
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ }
+
+ /**
+ * Put pseudo elements in front of others.
+ */
+ _sortRulesForPseudoElement() {
+ this.rules = this.rules.sort((a, b) => {
+ return (a.pseudoElement || "z") > (b.pseudoElement || "z");
+ });
+ }
+
+ /**
+ * Add a rule if it's one we care about. Filters out duplicates and
+ * inherited styles with no inherited properties.
+ *
+ * @param {Object} 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(options, existingRules) {
+ // If we've already included this domRule (for example, when a
+ // common selector is inherited), ignore it.
+ if (
+ options.system ||
+ (options.rule && this.rules.some(rule => rule.domRule === options.rule))
+ ) {
+ return false;
+ }
+
+ let rule = null;
+
+ // If we're refreshing and the rule previously existed, reuse the
+ // Rule object.
+ if (existingRules) {
+ const 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 updateDeclarations with all supported pseudo elements
+ */
+ onRuleUpdated() {
+ this.updateDeclarations();
+
+ // Update declarations for matching rules for pseudo-elements.
+ for (const pseudo of this.pseudoElements) {
+ this.updateDeclarations(pseudo);
+ }
+ }
+
+ /**
+ * Go over all CSS rules matching the selected element and mark the CSS declarations
+ * (aka TextProperty instances) with an `overridden` Boolean flag if an earlier or
+ * higher priority declaration overrides it. Rules are already ordered by specificity.
+ *
+ * If a pseudo-element type is passed (ex: ::before, ::first-line, etc),
+ * restrict the operation only to declarations in rules matching that pseudo-element.
+ *
+ * At the end, update the declaration's view (TextPropertyEditor instance) so it relects
+ * the latest state. Use this opportunity to also trigger checks for the "inactive"
+ * state of the declaration (whether it has effect or not).
+ *
+ * @param {String} pseudo
+ * Optional pseudo-element for which to restrict marking CSS declarations as
+ * overridden.
+ */
+ updateDeclarations(pseudo = "") {
+ // Gather all text properties applicable to the selected element or pseudo-element.
+ const textProps = this._getDeclarations(pseudo);
+ // Gather all the computed properties applied by those text properties.
+ let computedProps = [];
+ for (const textProp of textProps) {
+ computedProps = computedProps.concat(textProp.computed);
+ }
+
+ // CSS Variables inherits from the normal element in case of pseudo element.
+ const variables = new Map(pseudo ? this.variablesMap.get("") : null);
+
+ // 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.
+ const taken = {};
+ for (const computedProp of computedProps) {
+ const 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" &&
+ // For !important only consider rules applying to the same parent node.
+ computedProp.textProp.rule.inherited == earlier.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;
+
+ if (isCssVariable(computedProp.name)) {
+ variables.set(computedProp.name, computedProp.value);
+ }
+ }
+ }
+
+ // Find the CSS variables that have been updated.
+ const previousVariablesMap = new Map(this.variablesMap.get(pseudo));
+ const changedVariableNamesSet = new Set(
+ [...variables.keys(), ...previousVariablesMap.keys()].filter(
+ k => variables.get(k) !== previousVariablesMap.get(k)
+ )
+ );
+
+ this.variablesMap.set(pseudo, variables);
+
+ // For each TextProperty, mark it overridden if all of its computed
+ // properties are marked overridden. Update the text property's associated
+ // editor, if any. This will clear the _overriddenDirty state on all
+ // computed properties. For each editor we also show or hide the inactive
+ // CSS icon as needed.
+ for (const textProp of textProps) {
+ // _updatePropertyOverridden will return true if the
+ // overridden state has changed for the text property.
+ // _hasUpdatedCSSVariable will return true if the declaration contains any
+ // of the updated CSS variable names.
+ if (
+ this._updatePropertyOverridden(textProp) ||
+ this._hasUpdatedCSSVariable(textProp, changedVariableNamesSet)
+ ) {
+ textProp.updateEditor();
+ }
+
+ // For each editor show or hide the inactive CSS icon as needed.
+ if (textProp.editor && this.unusedCssEnabled) {
+ textProp.editor.updatePropertyState();
+ }
+ }
+ }
+
+ /**
+ * Returns true if the given declaration's property value contains a CSS variable
+ * matching any of the updated CSS variable names.
+ *
+ * @param {TextProperty} declaration
+ * A TextProperty of a rule.
+ * @param {Set<>String} variableNamesSet
+ * A Set of CSS variable names that have been updated.
+ */
+ _hasUpdatedCSSVariable(declaration, variableNamesSet) {
+ for (const variableName of variableNamesSet) {
+ if (declaration.hasCSSVariable(variableName)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Helper for |this.updateDeclarations()| to mark CSS declarations as overridden.
+ *
+ * Returns an array of CSS declarations (aka TextProperty instances) from all rules
+ * applicable to the selected element ordered from more- to less-specific.
+ *
+ * If a pseudo-element type is given, restrict the result only to declarations
+ * applicable to that pseudo-element.
+ *
+ * NOTE: this method skips CSS declarations in @keyframes rules because a number of
+ * criteria such as time and animation delay need to be checked in order to determine
+ * if the property is overridden at runtime.
+ *
+ * @param {String} pseudo
+ * Optional pseudo-element for which to restrict marking CSS declarations as
+ * overridden. If omitted, only declarations for regular style rules are
+ * returned (no pseudo-element style rules).
+ *
+ * @return {Array}
+ * Array of TextProperty instances.
+ */
+ _getDeclarations(pseudo = "") {
+ const textProps = [];
+
+ for (const rule of this.rules) {
+ // Skip @keyframes rules
+ if (rule.keyframes) {
+ continue;
+ }
+
+ // Style rules must be considered only when they have selectors that match the node.
+ // When renaming a selector, the unmatched rule lingers in the Rule view, but it no
+ // longer matches the node. This strict check avoids accidentally causing
+ // declarations to be overridden in the remaining matching rules.
+ const isStyleRule =
+ rule.pseudoElement === "" && !!rule.matchedSelectors.length;
+
+ // Style rules for pseudo-elements must always be considered, regardless if their
+ // selector matches the node. As a convenience, declarations in rules for
+ // pseudo-elements show up in a separate Pseudo-elements accordion when selecting
+ // the host node (instead of the pseudo-element node directly, which is sometimes
+ // impossible, for example with ::selection or ::first-line).
+ // Loosening the strict check on matched selectors ensures these declarations
+ // participate in the algorithm below to mark them as overridden.
+ const isPseudoElementRule =
+ rule.pseudoElement !== "" && rule.pseudoElement === pseudo;
+
+ const isElementStyle = rule.domRule.type === ELEMENT_STYLE;
+
+ const filterCondition =
+ pseudo === "" ? isStyleRule || isElementStyle : isPseudoElementRule;
+
+ // Collect all relevant CSS declarations (aka TextProperty instances).
+ if (filterCondition) {
+ for (const textProp of rule.textProps.slice(0).reverse()) {
+ if (textProp.enabled) {
+ textProps.push(textProp);
+ }
+ }
+ }
+ }
+
+ return textProps;
+ }
+
+ /**
+ * Adds a new declaration to the rule.
+ *
+ * @param {String} ruleId
+ * The id of the Rule to be modified.
+ * @param {String} value
+ * The new declaration value.
+ */
+ addNewDeclaration(ruleId, value) {
+ const rule = this.getRule(ruleId);
+ if (!rule) {
+ return;
+ }
+
+ const declarationsToAdd = parseNamedDeclarations(
+ this.cssProperties.isKnown,
+ value,
+ true
+ );
+ if (!declarationsToAdd.length) {
+ return;
+ }
+
+ this._addMultipleDeclarations(rule, declarationsToAdd);
+ }
+
+ /**
+ * Adds a new rule. The rules view is updated from a "stylesheet-updated" event
+ * emitted the PageStyleActor as a result of the rule being inserted into the
+ * the stylesheet.
+ */
+ async addNewRule() {
+ await this.pageStyle.addNewRule(
+ this.element,
+ this.element.pseudoClassLocks
+ );
+ }
+
+ /**
+ * Given the id of the rule and the new declaration name, modifies the existing
+ * declaration name to the new given value.
+ *
+ * @param {String} ruleId
+ * The Rule id of the given CSS declaration.
+ * @param {String} declarationId
+ * The TextProperty id for the CSS declaration.
+ * @param {String} name
+ * The new declaration name.
+ */
+ async modifyDeclarationName(ruleId, declarationId, name) {
+ const rule = this.getRule(ruleId);
+ if (!rule) {
+ return;
+ }
+
+ const declaration = rule.getDeclaration(declarationId);
+ if (!declaration || declaration.name === name) {
+ return;
+ }
+
+ // Adding multiple rules inside of name field overwrites the current
+ // property with the first, then adds any more onto the property list.
+ const declarations = parseDeclarations(this.cssProperties.isKnown, name);
+ if (!declarations.length) {
+ return;
+ }
+
+ await declaration.setName(declarations[0].name);
+
+ if (!declaration.enabled) {
+ await declaration.setEnabled(true);
+ }
+ }
+
+ /**
+ * Helper function to addNewDeclaration() and modifyDeclarationValue() for
+ * adding multiple declarations to a rule.
+ *
+ * @param {Rule} rule
+ * The Rule object to write new declarations to.
+ * @param {Array<Object>} declarationsToAdd
+ * An array of object containg the parsed declaration data to be added.
+ * @param {TextProperty|null} siblingDeclaration
+ * Optional declaration next to which the new declaration will be added.
+ */
+ _addMultipleDeclarations(rule, declarationsToAdd, siblingDeclaration = null) {
+ for (const { commentOffsets, name, value, priority } of declarationsToAdd) {
+ const isCommented = Boolean(commentOffsets);
+ const enabled = !isCommented;
+ siblingDeclaration = rule.createProperty(
+ name,
+ value,
+ priority,
+ enabled,
+ siblingDeclaration
+ );
+ }
+ }
+
+ /**
+ * Parse a value string and break it into pieces, starting with the
+ * first value, and into an array of additional declarations (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"
+ * declarationsToAdd: An array with additional declarations, following the
+ * parseDeclarations format of { name, value, priority }
+ */
+ _getValueAndExtraProperties(value) {
+ // The inplace editor will prevent manual typing of multiple declarations,
+ // but we need to deal with the case during a paste event.
+ // Adding multiple declarations inside of value editor sets value with the
+ // first, then adds any more onto the declaration list (below this declarations).
+ let firstValue = value;
+ let declarationsToAdd = [];
+
+ const declarations = parseDeclarations(this.cssProperties.isKnown, value);
+
+ // Check to see if the input string can be parsed as multiple declarations
+ if (declarations.length) {
+ // Get the first property value (if any), and any remaining
+ // declarations (if any)
+ if (!declarations[0].name && declarations[0].value) {
+ firstValue = declarations[0].value;
+ declarationsToAdd = declarations.slice(1);
+ } else if (declarations[0].name && declarations[0].value) {
+ // In some cases, the value could be a property:value pair
+ // itself. Join them as one value string and append
+ // potentially following declarations
+ firstValue = declarations[0].name + ": " + declarations[0].value;
+ declarationsToAdd = declarations.slice(1);
+ }
+ }
+
+ return {
+ declarationsToAdd,
+ firstValue,
+ };
+ }
+
+ /**
+ * Given the id of the rule and the new declaration value, modifies the existing
+ * declaration value to the new given value.
+ *
+ * @param {String} ruleId
+ * The Rule id of the given CSS declaration.
+ * @param {String} declarationId
+ * The TextProperty id for the CSS declaration.
+ * @param {String} value
+ * The new declaration value.
+ */
+ async modifyDeclarationValue(ruleId, declarationId, value) {
+ const rule = this.getRule(ruleId);
+ if (!rule) {
+ return;
+ }
+
+ const declaration = rule.getDeclaration(declarationId);
+ if (!declaration) {
+ return;
+ }
+
+ const { declarationsToAdd, firstValue } =
+ this._getValueAndExtraProperties(value);
+ const parsedValue = parseSingleValue(
+ this.cssProperties.isKnown,
+ firstValue
+ );
+
+ if (
+ !declarationsToAdd.length &&
+ declaration.value === parsedValue.value &&
+ declaration.priority === parsedValue.priority
+ ) {
+ return;
+ }
+
+ // First, set this declaration value (common case, only modified a property)
+ await declaration.setValue(parsedValue.value, parsedValue.priority);
+
+ if (!declaration.enabled) {
+ await declaration.setEnabled(true);
+ }
+
+ this._addMultipleDeclarations(rule, declarationsToAdd, declaration);
+ }
+
+ /**
+ * Modifies the existing rule's selector to the new given value.
+ *
+ * @param {String} ruleId
+ * The id of the Rule to be modified.
+ * @param {String} selector
+ * The new selector value.
+ */
+ async modifySelector(ruleId, selector) {
+ try {
+ const rule = this.getRule(ruleId);
+ if (!rule) {
+ return;
+ }
+
+ const response = await rule.domRule.modifySelector(
+ this.element,
+ selector
+ );
+ const { ruleProps, isMatching } = response;
+
+ if (!ruleProps) {
+ // Notify for changes, even when nothing changes, just to allow tests
+ // being able to track end of this request.
+ this.ruleView.emit("ruleview-invalid-selector");
+ return;
+ }
+
+ const newRule = new Rule(this, {
+ ...ruleProps,
+ isUnmatched: !isMatching,
+ });
+
+ // Recompute the list of applied styles because editing a
+ // selector might cause this rule's position to change.
+ const appliedStyles = await this.pageStyle.getApplied(this.element, {
+ inherited: true,
+ matchedSelectors: true,
+ filter: this.showUserAgentStyles ? "ua" : undefined,
+ });
+ const newIndex = appliedStyles.findIndex(r => r.rule == ruleProps.rule);
+ const oldIndex = this.rules.indexOf(rule);
+
+ // Remove the old rule and insert the new rule according to where it appears
+ // in the list of applied styles.
+ this.rules.splice(oldIndex, 1);
+ // If the selector no longer matches, then we leave the rule in
+ // the same relative position.
+ this.rules.splice(newIndex === -1 ? oldIndex : newIndex, 0, newRule);
+
+ // Recompute, mark and update the UI for any properties that are
+ // overridden or contain inactive CSS according to the new list of rules.
+ this.onRuleUpdated();
+
+ // In order to keep the new rule in place of the old in the rules view, we need
+ // to remove the rule again if the rule was inserted to its new index according
+ // to the list of applied styles.
+ // Note: 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.
+ if (newIndex !== -1) {
+ this.rules.splice(newIndex, 1);
+ this.rules.splice(oldIndex, 0, newRule);
+ }
+ this._changed();
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ /**
+ * Toggles the enabled state of the given CSS declaration.
+ *
+ * @param {String} ruleId
+ * The Rule id of the given CSS declaration.
+ * @param {String} declarationId
+ * The TextProperty id for the CSS declaration.
+ */
+ toggleDeclaration(ruleId, declarationId) {
+ const rule = this.getRule(ruleId);
+ if (!rule) {
+ return;
+ }
+
+ const declaration = rule.getDeclaration(declarationId);
+ if (!declaration) {
+ return;
+ }
+
+ declaration.setEnabled(!declaration.enabled);
+ }
+
+ /**
+ * 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(prop) {
+ let overridden = true;
+ let dirty = false;
+
+ for (const 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;
+ }
+
+ /**
+ * Returns the current value of a CSS variable; or null if the
+ * variable is not defined.
+ *
+ * @param {String} name
+ * The name of the variable.
+ * @param {String} pseudo
+ * The pseudo-element name of the rule.
+ * @return {String} the variable's value or null if the variable is
+ * not defined.
+ */
+ getVariable(name, pseudo = "") {
+ const variables = this.variablesMap.get(pseudo);
+ return variables ? variables.get(name) : null;
+ }
+}
+
+module.exports = ElementStyle;
diff --git a/devtools/client/inspector/rules/models/moz.build b/devtools/client/inspector/rules/models/moz.build
new file mode 100644
index 0000000000..7a5561e213
--- /dev/null
+++ b/devtools/client/inspector/rules/models/moz.build
@@ -0,0 +1,13 @@
+# -*- 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(
+ "class-list.js",
+ "element-style.js",
+ "rule.js",
+ "text-property.js",
+ "user-properties.js",
+)
diff --git a/devtools/client/inspector/rules/models/rule.js b/devtools/client/inspector/rules/models/rule.js
new file mode 100644
index 0000000000..626ef883d6
--- /dev/null
+++ b/devtools/client/inspector/rules/models/rule.js
@@ -0,0 +1,831 @@
+/* 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 {
+ style: { ELEMENT_STYLE },
+} = require("resource://devtools/shared/constants.js");
+const CssLogic = require("resource://devtools/shared/inspector/css-logic.js");
+const TextProperty = require("resource://devtools/client/inspector/rules/models/text-property.js");
+
+loader.lazyRequireGetter(
+ this,
+ "promiseWarn",
+ "resource://devtools/client/inspector/shared/utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "parseNamedDeclarations",
+ "resource://devtools/shared/css/parsing-utils.js",
+ true
+);
+
+const STYLE_INSPECTOR_PROPERTIES =
+ "devtools/shared/locales/styleinspector.properties";
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+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.
+ */
+class Rule {
+ /**
+ * @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.
+ */
+ constructor(elementStyle, options) {
+ this.elementStyle = elementStyle;
+ this.domRule = options.rule;
+ this.compatibilityIssues = null;
+ 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.cssProperties = this.elementStyle.ruleView.cssProperties;
+ this.inspector = this.elementStyle.ruleView.inspector;
+ this.store = this.elementStyle.ruleView.store;
+
+ // 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());
+
+ this.getUniqueSelector = this.getUniqueSelector.bind(this);
+ this.onStyleRuleFrontUpdated = this.onStyleRuleFrontUpdated.bind(this);
+
+ this.domRule.on("rule-updated", this.onStyleRuleFrontUpdated);
+ }
+
+ destroy() {
+ if (this._unsubscribeSourceMap) {
+ this._unsubscribeSourceMap();
+ }
+
+ this.domRule.off("rule-updated", this.onStyleRuleFrontUpdated);
+ this.compatibilityIssues = null;
+ this.destroyed = true;
+ }
+
+ get declarations() {
+ return this.textProps;
+ }
+
+ get inheritance() {
+ if (!this.inherited) {
+ return null;
+ }
+
+ return {
+ inherited: this.inherited,
+ inheritedSource: this.inheritedSource,
+ };
+ }
+
+ get selector() {
+ return {
+ getUniqueSelector: this.getUniqueSelector,
+ matchedSelectors: this.matchedSelectors,
+ selectors: this.domRule.selectors,
+ selectorText: this.keyframes ? this.domRule.keyText : this.selectorText,
+ };
+ }
+
+ get sourceMapURLService() {
+ return this.inspector.toolbox.sourceMapURLService;
+ }
+
+ get title() {
+ let title = CssLogic.shortSource(this.sheet);
+ if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) {
+ title += ":" + this.ruleLine;
+ }
+
+ return title;
+ }
+
+ 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 keyframesRule() {
+ if (!this.keyframes) {
+ return null;
+ }
+
+ return {
+ id: this.keyframes.actorID,
+ keyframesName: 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 : -1;
+ }
+
+ /**
+ * The rule's column within a stylesheet
+ */
+ get ruleColumn() {
+ return this.domRule ? this.domRule.column : null;
+ }
+
+ /**
+ * Get the declaration block issues from the compatibility actor
+ * @returns A promise that resolves with an array of objects in following form:
+ * {
+ * // Type of compatibility issue
+ * type: <string>,
+ * // The CSS declaration that has compatibility issues
+ * property: <string>,
+ * // Alias to the given CSS property
+ * alias: <Array>,
+ * // Link to MDN documentation for the particular CSS rule
+ * url: <string>,
+ * deprecated: <boolean>,
+ * experimental: <boolean>,
+ * // An array of all the browsers that don't support the given CSS rule
+ * unsupportedBrowsers: <Array>,
+ * }
+ */
+ async getCompatibilityIssues() {
+ if (!this.compatibilityIssues) {
+ this.compatibilityIssues =
+ this.inspector.commands.inspectorCommand.getCSSDeclarationBlockIssues(
+ this.domRule.declarations
+ );
+ }
+
+ return this.compatibilityIssues;
+ }
+
+ /**
+ * Returns the TextProperty with the given id or undefined if it cannot be found.
+ *
+ * @param {String|null} id
+ * A TextProperty id.
+ * @return {TextProperty|undefined} with the given id in the current Rule or undefined
+ * if it cannot be found.
+ */
+ getDeclaration(id) {
+ return id ? this.textProps.find(textProp => textProp.id === id) : undefined;
+ }
+
+ /**
+ * Returns an unique selector for the CSS rule.
+ */
+ async getUniqueSelector() {
+ let selector = "";
+
+ if (this.domRule.selectors) {
+ // This is a style rule with a selector.
+ selector = this.domRule.selectors.join(", ");
+ } else if (this.inherited) {
+ // This is an inline style from an inherited rule. Need to resolve the unique
+ // selector from the node which rule this is inherited from.
+ selector = await this.inherited.getUniqueSelector();
+ } else {
+ // This is an inline style from the current node.
+ selector = await this.inspector.selection.nodeFront.getUniqueSelector();
+ }
+
+ return selector;
+ }
+
+ /**
+ * Returns true if the rule matches the creation options
+ * specified.
+ *
+ * @param {Object} options
+ * Creation options. See the Rule constructor for documentation.
+ */
+ matches(options) {
+ return this.domRule === 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(name, value, priority, enabled, siblingProp) {
+ const 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(modifications) {
+ this.elementStyle.onRuleUpdated();
+
+ const disabledProps = [];
+
+ for (const 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.
+ const disabled = this.elementStyle.store.disabled;
+ if (disabledProps.length) {
+ disabled.set(this.domRule, disabledProps);
+ } else {
+ disabled.delete(this.domRule);
+ }
+
+ return modifications.apply().then(() => {
+ const 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 (const cssProp of parseNamedDeclarations(
+ this.cssProperties.isKnown,
+ this.domRule.authoredText
+ )) {
+ cssProps[cssProp.name] = cssProp;
+ }
+
+ for (const 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(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 (const index in modifications.changedDeclarations) {
+ const newValue = modifications.changedDeclarations[index];
+ this.textProps[index].updateValue(newValue);
+ }
+ // Recompute and redisplay the computed properties.
+ for (const 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(modifier) {
+ // If there is already a pending modification, we have to wait
+ // until it settles before applying the next modification.
+ const resultPromise = Promise.resolve(this._applyingModifications)
+ .then(() => {
+ const modifications = this.domRule.startModifyingProperties(
+ this.cssProperties
+ );
+ modifier(modifications);
+ if (this.domRule.canSetRuleText) {
+ return this._applyPropertiesAuthored(modifications);
+ }
+ return this._applyPropertiesNoAuthored(modifications);
+ })
+ .then(() => {
+ this.elementStyle.onRuleUpdated();
+
+ 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").
+ * @return {Promise}
+ */
+ setPropertyName(property, name) {
+ if (name === property.name) {
+ return Promise.resolve();
+ }
+
+ const oldName = property.name;
+ property.name = name;
+ const index = this.textProps.indexOf(property);
+ return 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).
+ * @return {Promise}
+ */
+ setPropertyValue(property, value, priority) {
+ if (value === property.value && priority === property.priority) {
+ return Promise.resolve();
+ }
+
+ property.value = value;
+ property.priority = priority;
+
+ const index = this.textProps.indexOf(property);
+ return 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).
+ **@return {Promise}
+ */
+ previewPropertyValue(property, value, priority) {
+ this.elementStyle.ruleView.emitForTests("start-preview-property-value");
+ const modifications = this.domRule.startModifyingProperties(
+ this.cssProperties
+ );
+ modifications.setProperty(
+ this.textProps.indexOf(property),
+ property.name,
+ value,
+ priority
+ );
+ return 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(property, value) {
+ if (property.enabled === !!value) {
+ return;
+ }
+ property.enabled = !!value;
+ const 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(property) {
+ const 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);
+ });
+ }
+
+ /**
+ * Event handler for "rule-updated" event fired by StyleRuleActor.
+ *
+ * @param {StyleRuleFront} front
+ */
+ onStyleRuleFrontUpdated(front) {
+ // Overwritting this reference is not required, but it's here to avoid confusion.
+ // Whenever an actor is passed over the protocol, either as a return value or as
+ // payload on an event, the `form` of its corresponding front will be automatically
+ // updated. No action required.
+ // Even if this `domRule` reference here is not explicitly updated, lookups of
+ // `this.domRule.declarations` will point to the latest state of declarations set
+ // on the actor. Everything on `StyleRuleForm.form` will point to the latest state.
+ this.domRule = front;
+ }
+
+ /**
+ * Get the list of TextProperties from the style. Needs
+ * to parse the style's authoredText.
+ */
+ _getTextProperties() {
+ const textProps = [];
+ const store = this.elementStyle.store;
+
+ for (const prop of this.domRule.declarations) {
+ const name = prop.name;
+ // 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.
+ const invisible = this.inherited && !this.cssProperties.isInherited(name);
+ const value = store.userProperties.getProperty(
+ this.domRule,
+ name,
+ prop.value
+ );
+ const 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() {
+ const store = this.elementStyle.store;
+
+ // Include properties from the disabled property store, if any.
+ const disabledProps = store.disabled.get(this.domRule);
+ if (!disabledProps) {
+ return [];
+ }
+
+ const textProps = [];
+
+ for (const prop of disabledProps) {
+ const value = store.userProperties.getProperty(
+ this.domRule,
+ prop.name,
+ prop.value
+ );
+ const 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(options) {
+ this.matchedSelectors = options.matchedSelectors || [];
+ const newTextProps = this._getTextProperties();
+
+ // The element style rule behaves differently on refresh. We basically need to update
+ // it to reflect the new text properties exactly. The order might have changed, some
+ // properties might have been removed, etc. And we don't need to mark anything as
+ // disabled here. The element style rule should always reflect the content of the
+ // style attribute.
+ if (this.domRule.type === ELEMENT_STYLE) {
+ this.textProps = newTextProps;
+
+ if (this.editor) {
+ this.editor.populate(true);
+ }
+
+ return;
+ }
+
+ // 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.
+ const brandNewProps = [];
+ for (const newProp of newTextProps) {
+ if (!this._updateTextProperty(newProp)) {
+ brandNewProps.push(newProp);
+ }
+ }
+
+ // Refresh editors and disabled state for all the properties that
+ // were updated.
+ for (const 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(newProp) {
+ const match = { rank: 0, prop: null };
+
+ for (const 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(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() {
+ const selectorText = this.selectorText;
+ let cssText = "";
+ const terminator = Services.appinfo.OS === "WINNT" ? "\r\n" : "\n";
+
+ for (const 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() {
+ for (const prop of this.textProps) {
+ if (!prop.invisible) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
+module.exports = 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 0000000000..d7568f74f3
--- /dev/null
+++ b/devtools/client/inspector/rules/models/text-property.js
@@ -0,0 +1,400 @@
+/* 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 { generateUUID } = require("resource://devtools/shared/generate-uuid.js");
+const {
+ COMPATIBILITY_TOOLTIP_MESSAGE,
+} = require("resource://devtools/client/inspector/rules/constants.js");
+
+loader.lazyRequireGetter(
+ this,
+ "escapeCSSComment",
+ "resource://devtools/shared/css/parsing-utils.js",
+ true
+);
+
+loader.lazyRequireGetter(
+ this,
+ "getCSSVariables",
+ "resource://devtools/client/inspector/rules/utils/utils.js",
+ true
+);
+
+/**
+ * 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.
+ */
+class TextProperty {
+ /**
+ * @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. In an inherited rule, only show
+ * the inherited declarations. The other declarations are considered
+ * invisible and 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.
+ */
+ constructor(rule, name, value, priority, enabled = true, invisible = false) {
+ this.id = name + "_" + generateUUID().toString();
+ this.rule = rule;
+ this.name = name;
+ this.value = value;
+ this.priority = priority;
+ this.enabled = !!enabled;
+ this.invisible = invisible;
+ this.elementStyle = this.rule.elementStyle;
+ this.cssProperties = this.elementStyle.ruleView.cssProperties;
+ this.panelDoc = this.elementStyle.ruleView.inspector.panelDoc;
+ this.userProperties = this.elementStyle.store.userProperties;
+ // Names of CSS variables used in the value of this declaration.
+ this.usedVariables = new Set();
+
+ this.updateComputed();
+ this.updateUsedVariables();
+ }
+
+ get computedProperties() {
+ return this.computed
+ .filter(computed => computed.name !== this.name)
+ .map(computed => {
+ return {
+ isOverridden: computed.overridden,
+ name: computed.name,
+ priority: computed.priority,
+ value: computed.value,
+ };
+ });
+ }
+
+ /**
+ * Returns whether or not the declaration's name is known.
+ *
+ * @return {Boolean} true if the declaration name is known, false otherwise.
+ */
+ get isKnownProperty() {
+ return this.cssProperties.isKnown(this.name);
+ }
+
+ /**
+ * Returns whether or not the declaration is changed by the user.
+ *
+ * @return {Boolean} true if the declaration is changed by the user, false
+ * otherwise.
+ */
+ get isPropertyChanged() {
+ return this.userProperties.contains(this.rule.domRule, this.name);
+ }
+
+ /**
+ * Update the editor associated with this text property,
+ * if any.
+ */
+ updateEditor() {
+ // When the editor updates, reset the saved
+ // compatibility issues list as any updates
+ // may alter the compatibility status of declarations
+ this.rule.compatibilityIssues = null;
+ if (this.editor) {
+ this.editor.update();
+ }
+ }
+
+ /**
+ * Update the list of computed properties for this text property.
+ */
+ updateComputed() {
+ 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.
+ const dummyElement = this.elementStyle.ruleView.dummyElement;
+ const 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 === "".
+ const subProps = this.cssProperties.getSubproperties(this.name);
+
+ for (const prop of subProps) {
+ this.computed.push({
+ textProp: this,
+ name: prop,
+ value: dummyStyle.getPropertyValue(prop),
+ priority: dummyStyle.getPropertyPriority(prop),
+ });
+ }
+ }
+
+ /**
+ * Extract all CSS variable names used in this declaration's value into a Set for
+ * easy querying. Call this method any time the declaration's value changes.
+ */
+ updateUsedVariables() {
+ this.usedVariables.clear();
+
+ for (const variable of getCSSVariables(this.value)) {
+ this.usedVariables.add(variable);
+ }
+ }
+
+ /**
+ * Set all the values from another TextProperty instance into
+ * this TextProperty instance.
+ *
+ * @param {TextProperty} prop
+ * The other TextProperty instance.
+ */
+ set(prop) {
+ let changed = false;
+ for (const item of ["name", "value", "priority", "enabled"]) {
+ if (this[item] !== prop[item]) {
+ this[item] = prop[item];
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ this.updateUsedVariables();
+ this.updateEditor();
+ }
+ }
+
+ setValue(value, priority, force = false) {
+ if (value !== this.value || force) {
+ this.userProperties.setProperty(this.rule.domRule, this.name, value);
+ }
+ return this.rule.setPropertyValue(this, value, priority).then(() => {
+ this.updateUsedVariables();
+ this.updateEditor();
+ });
+ }
+
+ /**
+ * Called when the property's value has been updated externally, and
+ * the property and editor should update to reflect that value.
+ *
+ * @param {String} value
+ * Property value
+ */
+ updateValue(value) {
+ if (value !== this.value) {
+ this.value = value;
+ this.updateUsedVariables();
+ this.updateEditor();
+ }
+ }
+
+ async setName(name) {
+ if (name !== this.name) {
+ this.userProperties.setProperty(this.rule.domRule, name, this.value);
+ }
+
+ await this.rule.setPropertyName(this, name);
+ this.updateEditor();
+ }
+
+ setEnabled(value) {
+ this.rule.setPropertyEnabled(this, value);
+ this.updateEditor();
+ }
+
+ remove() {
+ this.rule.removeProperty(this);
+ }
+
+ /**
+ * Return a string representation of the rule property.
+ */
+ stringifyProperty() {
+ // Get the displayed property value
+ let declaration = this.name + ": " + this.value;
+
+ if (this.priority) {
+ declaration += " !" + this.priority;
+ }
+
+ declaration += ";";
+
+ // Comment out property declarations that are not enabled
+ if (!this.enabled) {
+ declaration = "/* " + escapeCSSComment(declaration) + " */";
+ }
+
+ return declaration;
+ }
+
+ /**
+ * Validate this property. Does it make sense for this value to be assigned
+ * to this property name?
+ *
+ * @return {Boolean} true if the whole CSS declaration is valid, false otherwise.
+ */
+ isValid() {
+ const 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;
+ }
+
+ isUsed() {
+ const selfIndex = this.rule.textProps.indexOf(this);
+ const declarations = this.rule.domRule.declarations;
+
+ // StyleRuleActor's declarations may have a isUsed flag (if the server is the right
+ // version). Just return true if the information is missing.
+ if (
+ !declarations ||
+ !declarations[selfIndex] ||
+ !declarations[selfIndex].isUsed
+ ) {
+ return { used: true };
+ }
+
+ return declarations[selfIndex].isUsed;
+ }
+
+ /**
+ * Get compatibility issue linked with the textProp.
+ *
+ * @returns A JSON objects with compatibility information in following form:
+ * {
+ * // A boolean to denote the compatibility status
+ * isCompatible: <boolean>,
+ * // The CSS declaration that has compatibility issues
+ * property: <string>,
+ * // The un-aliased root CSS declaration for the given property
+ * rootProperty: <string>,
+ * // The l10n message id for the tooltip message
+ * msgId: <string>,
+ * // Link to MDN documentation for the rootProperty
+ * url: <string>,
+ * // An array of all the browsers that don't support the given CSS rule
+ * unsupportedBrowsers: <Array>,
+ * }
+ */
+ async isCompatible() {
+ // This is a workaround for Bug 1648339
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1648339
+ // that makes the tooltip icon inconsistent with the
+ // position of the rule it is associated with. Once solved,
+ // the compatibility data can be directly accessed from the
+ // declaration and this logic can be used to set isCompatible
+ // property directly to domRule in StyleRuleActor's form() method.
+ if (!this.enabled) {
+ return { isCompatible: true };
+ }
+
+ const compatibilityIssues = await this.rule.getCompatibilityIssues();
+ if (!compatibilityIssues.length) {
+ return { isCompatible: true };
+ }
+
+ const property = this.name;
+ const indexOfProperty = compatibilityIssues.findIndex(
+ issue => issue.property === property || issue.aliases?.includes(property)
+ );
+
+ if (indexOfProperty < 0) {
+ return { isCompatible: true };
+ }
+
+ const {
+ property: rootProperty,
+ deprecated,
+ experimental,
+ specUrl,
+ url,
+ unsupportedBrowsers,
+ } = compatibilityIssues[indexOfProperty];
+
+ let msgId = COMPATIBILITY_TOOLTIP_MESSAGE.default;
+ if (deprecated && experimental && !unsupportedBrowsers.length) {
+ msgId =
+ COMPATIBILITY_TOOLTIP_MESSAGE["deprecated-experimental-supported"];
+ } else if (deprecated && experimental) {
+ msgId = COMPATIBILITY_TOOLTIP_MESSAGE["deprecated-experimental"];
+ } else if (deprecated && !unsupportedBrowsers.length) {
+ msgId = COMPATIBILITY_TOOLTIP_MESSAGE["deprecated-supported"];
+ } else if (deprecated) {
+ msgId = COMPATIBILITY_TOOLTIP_MESSAGE.deprecated;
+ } else if (experimental && !unsupportedBrowsers.length) {
+ msgId = COMPATIBILITY_TOOLTIP_MESSAGE["experimental-supported"];
+ } else if (experimental) {
+ msgId = COMPATIBILITY_TOOLTIP_MESSAGE.experimental;
+ }
+
+ return {
+ isCompatible: false,
+ property,
+ rootProperty,
+ msgId,
+ specUrl,
+ url,
+ unsupportedBrowsers,
+ };
+ }
+
+ /**
+ * Validate the name of this property.
+ *
+ * @return {Boolean} true if the property name is valid, false otherwise.
+ */
+ isNameValid() {
+ const 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].isNameValid;
+ }
+
+ /**
+ * Returns true if the property value is a CSS variables and contains the given variable
+ * name, and false otherwise.
+ *
+ * @param {String}
+ * CSS variable name (e.g. "--color")
+ * @return {Boolean}
+ */
+ hasCSSVariable(name) {
+ return this.usedVariables.has(name);
+ }
+}
+
+module.exports = TextProperty;
diff --git a/devtools/client/inspector/rules/models/user-properties.js b/devtools/client/inspector/rules/models/user-properties.js
new file mode 100644
index 0000000000..381b800e59
--- /dev/null
+++ b/devtools/client/inspector/rules/models/user-properties.js
@@ -0,0 +1,85 @@
+/* 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";
+
+/**
+ * Store of CSSStyleDeclarations mapped to properties that have been changed by
+ * the user.
+ */
+class UserProperties {
+ constructor() {
+ this.map = new Map();
+ }
+
+ /**
+ * 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(style, name, value) {
+ const key = this.getKey(style);
+ const 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} name
+ * The name of the property to set.
+ * @param {String} userValue
+ * The value of the property to set.
+ */
+ setProperty(style, name, userValue) {
+ const key = this.getKey(style, name);
+ const entry = this.map.get(key, null);
+
+ if (entry) {
+ entry[name] = userValue;
+ } else {
+ const props = {};
+ props[name] = 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(style, name) {
+ const key = this.getKey(style, name);
+ const entry = this.map.get(key, null);
+ return !!entry && name in entry;
+ }
+
+ getKey(style, name) {
+ return style.actorID + ":" + name;
+ }
+
+ clear() {
+ this.map.clear();
+ }
+}
+
+module.exports = UserProperties;
diff --git a/devtools/client/inspector/rules/moz.build b/devtools/client/inspector/rules/moz.build
new file mode 100644
index 0000000000..0cc40dab81
--- /dev/null
+++ b/devtools/client/inspector/rules/moz.build
@@ -0,0 +1,25 @@
+# -*- 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",
+ "utils",
+ "views",
+]
+
+DevToolsModules(
+ "constants.js",
+ "rules.js",
+ "types.js",
+)
+
+BROWSER_CHROME_MANIFESTS += [
+ "test/browser_part1.ini",
+ "test/browser_part2.ini",
+]
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "Inspector: Rules")
diff --git a/devtools/client/inspector/rules/rules.js b/devtools/client/inspector/rules/rules.js
new file mode 100644
index 0000000000..5ddef719d5
--- /dev/null
+++ b/devtools/client/inspector/rules/rules.js
@@ -0,0 +1,2199 @@
+/* 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 flags = require("resource://devtools/shared/flags.js");
+const { l10n } = require("resource://devtools/shared/inspector/css-logic.js");
+const {
+ style: { ELEMENT_STYLE },
+} = require("resource://devtools/shared/constants.js");
+const {
+ PSEUDO_CLASSES,
+} = require("resource://devtools/shared/css/constants.js");
+const OutputParser = require("resource://devtools/client/shared/output-parser.js");
+const { PrefObserver } = require("resource://devtools/client/shared/prefs.js");
+const ElementStyle = require("resource://devtools/client/inspector/rules/models/element-style.js");
+const RuleEditor = require("resource://devtools/client/inspector/rules/views/rule-editor.js");
+const TooltipsOverlay = require("resource://devtools/client/inspector/shared/tooltips-overlay.js");
+const {
+ createChild,
+ promiseWarn,
+} = require("resource://devtools/client/inspector/shared/utils.js");
+const { debounce } = require("resource://devtools/shared/debounce.js");
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+const DOUBLESPACE = " ";
+
+loader.lazyRequireGetter(
+ this,
+ ["flashElementOn", "flashElementOff"],
+ "resource://devtools/client/inspector/markup/utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "ClassListPreviewer",
+ "resource://devtools/client/inspector/rules/views/class-list-previewer.js"
+);
+loader.lazyRequireGetter(
+ this,
+ ["getNodeInfo", "getNodeCompatibilityInfo", "getRuleFromNode"],
+ "resource://devtools/client/inspector/rules/utils/utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "StyleInspectorMenu",
+ "resource://devtools/client/inspector/shared/style-inspector-menu.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "AutocompletePopup",
+ "resource://devtools/client/shared/autocomplete-popup.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "KeyShortcuts",
+ "resource://devtools/client/shared/key-shortcuts.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "clipboardHelper",
+ "resource://devtools/shared/platform/clipboard.js"
+);
+
+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_DRAGGABLE = "devtools.inspector.draggable_properties";
+const FILTER_CHANGED_TIMEOUT = 150;
+// Removes the flash-out class from an element after 1 second.
+const PROPERTY_FLASHING_DURATION = 1000;
+
+// 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.
+ */
+function CssRuleView(inspector, document, store) {
+ EventEmitter.decorate(this);
+
+ this.inspector = inspector;
+ this.cssProperties = inspector.cssProperties;
+ this.styleDocument = document;
+ this.styleWindow = this.styleDocument.defaultView;
+ this.store = store || {};
+
+ // Allow tests to override debouncing behavior, as this can cause intermittents.
+ this.debounce = debounce;
+
+ // Variable used to stop the propagation of mouse events to children
+ // when we are updating a value by dragging the mouse and we then release it
+ this.childHasDragged = false;
+
+ 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);
+ this._onToggleClassPanel = this._onToggleClassPanel.bind(this);
+ this._onToggleLightColorSchemeSimulation =
+ this._onToggleLightColorSchemeSimulation.bind(this);
+ this._onToggleDarkColorSchemeSimulation =
+ this._onToggleDarkColorSchemeSimulation.bind(this);
+ this._onTogglePrintSimulation = this._onTogglePrintSimulation.bind(this);
+ this.highlightElementRule = this.highlightElementRule.bind(this);
+ this.highlightProperty = this.highlightProperty.bind(this);
+ this.refreshPanel = this.refreshPanel.bind(this);
+
+ const doc = this.styleDocument;
+ // Delegate bulk handling of events happening within the DOM tree of the Rules view
+ // to this.handleEvent(). Listening on the capture phase of the event bubbling to be
+ // able to stop event propagation on a case-by-case basis and prevent event target
+ // ancestor nodes from handling them.
+ this.styleDocument.addEventListener("click", this, { capture: true });
+ 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.classPanel = doc.getElementById("ruleview-class-panel");
+ this.classToggle = doc.getElementById("class-panel-toggle");
+ this.colorSchemeLightSimulationButton = doc.getElementById(
+ "color-scheme-simulation-light-toggle"
+ );
+ this.colorSchemeDarkSimulationButton = doc.getElementById(
+ "color-scheme-simulation-dark-toggle"
+ );
+ this.printSimulationButton = doc.getElementById("print-simulation-toggle");
+
+ this._initSimulationFeatures();
+
+ this.searchClearButton.hidden = true;
+
+ this.onHighlighterShown = data =>
+ this.handleHighlighterEvent("highlighter-shown", data);
+ this.onHighlighterHidden = data =>
+ this.handleHighlighterEvent("highlighter-hidden", data);
+ this.inspector.highlighters.on("highlighter-shown", this.onHighlighterShown);
+ this.inspector.highlighters.on(
+ "highlighter-hidden",
+ this.onHighlighterHidden
+ );
+
+ this.shortcuts = new KeyShortcuts({ window: this.styleWindow });
+ this._onShortcut = this._onShortcut.bind(this);
+ this.shortcuts.on("Escape", event => this._onShortcut("Escape", event));
+ this.shortcuts.on("Return", event => this._onShortcut("Return", event));
+ this.shortcuts.on("Space", event => this._onShortcut("Space", event));
+ this.shortcuts.on("CmdOrCtrl+F", event =>
+ this._onShortcut("CmdOrCtrl+F", event)
+ );
+ 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.searchClearButton.addEventListener("click", this._onClearSearch);
+ this.pseudoClassToggle.addEventListener(
+ "click",
+ this._onTogglePseudoClassPanel
+ );
+ this.classToggle.addEventListener("click", this._onToggleClassPanel);
+ // The "change" event bubbles up from checkbox inputs nested within the panel container.
+ this.pseudoClassPanel.addEventListener("change", this._onTogglePseudoClass);
+
+ if (flags.testing) {
+ // In tests, we start listening immediately to avoid having to simulate a mousemove.
+ this.highlighters.addToView(this);
+ } else {
+ this.element.addEventListener(
+ "mousemove",
+ () => {
+ this.highlighters.addToView(this);
+ },
+ { once: true }
+ );
+ }
+
+ this._handlePrefChange = this._handlePrefChange.bind(this);
+ this._handleUAStylePrefChange = this._handleUAStylePrefChange.bind(this);
+ this._handleDefaultColorUnitPrefChange =
+ this._handleDefaultColorUnitPrefChange.bind(this);
+ this._handleDraggablePrefChange = this._handleDraggablePrefChange.bind(this);
+
+ this._prefObserver = new PrefObserver("devtools.");
+ this._prefObserver.on(PREF_UA_STYLES, this._handleUAStylePrefChange);
+ this._prefObserver.on(
+ PREF_DEFAULT_COLOR_UNIT,
+ this._handleDefaultColorUnitPrefChange
+ );
+ this._prefObserver.on(PREF_DRAGGABLE, this._handleDraggablePrefChange);
+ // Initialize value of this.draggablePropertiesEnabled
+ this._handleDraggablePrefChange();
+
+ this.pseudoClassCheckboxes = this._createPseudoClassCheckboxes();
+ this.showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES);
+
+ // Add the tooltips and highlighters to the view
+ this.tooltips = new TooltipsOverlay(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 popup() {
+ if (!this._popup) {
+ // The popup will be attached to the toolbox document.
+ this._popup = new AutocompletePopup(this.inspector.toolbox.doc, {
+ autoSelect: true,
+ });
+ }
+
+ return this._popup;
+ },
+
+ get classListPreviewer() {
+ if (!this._classListPreviewer) {
+ this._classListPreviewer = new ClassListPreviewer(
+ this.inspector,
+ this.classPanel
+ );
+ }
+
+ return this._classListPreviewer;
+ },
+
+ get contextMenu() {
+ if (!this._contextMenu) {
+ this._contextMenu = new StyleInspectorMenu(this, { isRuleView: true });
+ }
+
+ return this._contextMenu;
+ },
+
+ // Get the dummy elemenet.
+ get dummyElement() {
+ return this._dummyElement;
+ },
+
+ // Get the highlighters overlay from the Inspector.
+ get highlighters() {
+ if (!this._highlighters) {
+ // highlighters is a lazy getter in the inspector.
+ this._highlighters = this.inspector.highlighters;
+ }
+
+ return this._highlighters;
+ },
+
+ // Get the filter search value.
+ get searchValue() {
+ return this.searchField.value.toLowerCase();
+ },
+
+ get rules() {
+ return this._elementStyle ? this._elementStyle.rules : [];
+ },
+
+ get currentTarget() {
+ return this.inspector.toolbox.target;
+ },
+
+ /**
+ * Highlight/unhighlight all the nodes that match a given selector
+ * 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 {String} selector
+ * Elements matching this selector will be highlighted on the page.
+ */
+ async toggleSelectorHighlighter(selector) {
+ if (this.isSelectorHighlighted(selector)) {
+ await this.inspector.highlighters.hideHighlighterType(
+ this.inspector.highlighters.TYPES.SELECTOR
+ );
+ } else {
+ await this.inspector.highlighters.showHighlighterTypeForNode(
+ this.inspector.highlighters.TYPES.SELECTOR,
+ this.inspector.selection.nodeFront,
+ {
+ hideInfoBar: true,
+ hideGuides: true,
+ selector,
+ }
+ );
+ }
+ },
+
+ isPanelVisible() {
+ if (this.inspector.is3PaneModeEnabled) {
+ return true;
+ }
+ return (
+ this.inspector.toolbox &&
+ this.inspector.sidebar &&
+ this.inspector.toolbox.currentToolId === "inspector" &&
+ this.inspector.sidebar.getCurrentTabID() == "ruleview"
+ );
+ },
+
+ /**
+ * Check whether a SelectorHighlighter is active for the given selector text.
+ *
+ * @param {String} selector
+ * @return {Boolean}
+ */
+ isSelectorHighlighted(selector) {
+ const options = this.inspector.highlighters.getOptionsForActiveHighlighter(
+ this.inspector.highlighters.TYPES.SELECTOR
+ );
+
+ return options?.selector === selector;
+ },
+
+ /**
+ * Delegate handler for events happening within the DOM tree of the Rules view.
+ * Itself delegates to specific handlers by event type.
+ *
+ * Use this instead of attaching specific event handlers when:
+ * - there are many elements with the same event handler (eases memory pressure)
+ * - you want to avoid having to remove event handlers manually
+ * - elements are added/removed from the DOM tree arbitrarily over time
+ *
+ * @param {MouseEvent|UIEvent} event
+ */
+ handleEvent(event) {
+ if (this.childHasDragged) {
+ this.childHasDragged = false;
+ event.stopPropagation();
+ return;
+ }
+ switch (event.type) {
+ case "click":
+ this.handleClickEvent(event);
+ break;
+ default:
+ }
+ },
+
+ /**
+ * Delegate handler for click events happening within the DOM tree of the Rules view.
+ * Stop propagation of click event wrapping a CSS rule or CSS declaration to avoid
+ * triggering the prompt to add a new CSS declaration or to edit the existing one.
+ *
+ * @param {MouseEvent} event
+ */
+ async handleClickEvent(event) {
+ const target = event.target;
+
+ // Handle click on the icon next to a CSS selector.
+ if (target.classList.contains("js-toggle-selector-highlighter")) {
+ event.stopPropagation();
+ let selector = target.dataset.selector;
+ // dataset.selector will be empty for inline styles (inherited or not)
+ // Rules associated with a regular selector should have this data-attirbute
+ // set in devtools/client/inspector/rules/views/rule-editor.js
+ if (selector === "") {
+ try {
+ const rule = getRuleFromNode(target, this._elementStyle);
+ if (rule.inherited) {
+ // This is an inline style from an inherited rule. Need to resolve the
+ // unique selector from the node which this rule is inherited from.
+ selector = await rule.inherited.getUniqueSelector();
+ } else {
+ // This is an inline style from the current node.
+ selector =
+ await this.inspector.selection.nodeFront.getUniqueSelector();
+ }
+
+ // Now that the selector was computed, we can store it in
+ // dataset.selector for subsequent usage.
+ target.dataset.selector = selector;
+ } finally {
+ // Could not resolve a unique selector for the inline style.
+ }
+ }
+
+ this.toggleSelectorHighlighter(selector);
+ }
+
+ // Handle click on swatches next to flex and inline-flex CSS properties
+ if (target.classList.contains("js-toggle-flexbox-highlighter")) {
+ event.stopPropagation();
+ this.inspector.highlighters.toggleFlexboxHighlighter(
+ this.inspector.selection.nodeFront,
+ "rule"
+ );
+ }
+
+ // Handle click on swatches next to grid CSS properties
+ if (target.classList.contains("js-toggle-grid-highlighter")) {
+ event.stopPropagation();
+ this.inspector.highlighters.toggleGridHighlighter(
+ this.inspector.selection.nodeFront,
+ "rule"
+ );
+ }
+ },
+
+ /**
+ * Delegate handler for highlighter events.
+ *
+ * This is the place to observe for highlighter events, check the highlighter type and
+ * event name, then react to specific events, for example by modifying the DOM.
+ *
+ * @param {String} eventName
+ * Highlighter event name. One of: "highlighter-hidden", "highlighter-shown"
+ * @param {Object} data
+ * Object with data associated with the highlighter event.
+ */
+ handleHighlighterEvent(eventName, data) {
+ switch (data.type) {
+ // Toggle the "highlighted" class on selector icons in the Rules view when
+ // the SelectorHighlighter is shown/hidden for a certain CSS selector.
+ case this.inspector.highlighters.TYPES.SELECTOR:
+ {
+ const selector = data?.options?.selector;
+ if (!selector) {
+ return;
+ }
+
+ const query = `.js-toggle-selector-highlighter[data-selector='${selector}']`;
+ for (const node of this.styleDocument.querySelectorAll(query)) {
+ node.classList.toggle(
+ "highlighted",
+ eventName == "highlighter-shown"
+ );
+ }
+ }
+ break;
+
+ // Toggle the "active" class on swatches next to flex and inline-flex CSS properties
+ // when the FlexboxHighlighter is shown/hidden for the currently selected node.
+ case this.inspector.highlighters.TYPES.FLEXBOX:
+ {
+ const query = ".js-toggle-flexbox-highlighter";
+ for (const node of this.styleDocument.querySelectorAll(query)) {
+ node.classList.toggle("active", eventName == "highlighter-shown");
+ }
+ }
+ break;
+
+ // Toggle the "active" class on swatches next to grid CSS properties
+ // when the GridHighlighter is shown/hidden for the currently selected node.
+ case this.inspector.highlighters.TYPES.GRID:
+ {
+ const query = ".js-toggle-grid-highlighter";
+ for (const node of this.styleDocument.querySelectorAll(query)) {
+ // From the Layout panel, we can toggle grid highlighters for nodes which are
+ // not currently selected. The Rules view shows `display: grid` declarations
+ // only for the selected node. Avoid mistakenly marking them as "active".
+ if (data.nodeFront === this.inspector.selection.nodeFront) {
+ node.classList.toggle("active", eventName == "highlighter-shown");
+ }
+
+ // When the max limit of grid highlighters is reached (default 3),
+ // mark inactive grid swatches as disabled.
+ node.toggleAttribute(
+ "disabled",
+ !this.inspector.highlighters.canGridHighlighterToggle(
+ this.inspector.selection.nodeFront
+ )
+ );
+ }
+ }
+ break;
+ }
+ },
+
+ /**
+ * Enables the print and color scheme simulation only for local and remote tab debugging.
+ */
+ async _initSimulationFeatures() {
+ if (!this.inspector.commands.descriptorFront.isTabDescriptor) {
+ return;
+ }
+ this.colorSchemeLightSimulationButton.removeAttribute("hidden");
+ this.colorSchemeDarkSimulationButton.removeAttribute("hidden");
+ this.printSimulationButton.removeAttribute("hidden");
+ this.printSimulationButton.addEventListener(
+ "click",
+ this._onTogglePrintSimulation
+ );
+ this.colorSchemeLightSimulationButton.addEventListener(
+ "click",
+ this._onToggleLightColorSchemeSimulation
+ );
+ this.colorSchemeDarkSimulationButton.addEventListener(
+ "click",
+ this._onToggleDarkColorSchemeSimulation
+ );
+ },
+
+ /**
+ * Get the type of a given node in the rule-view
+ *
+ * @param {DOMNode} node
+ * The node which we want information about
+ * @return {Object|null} containing the following props:
+ * - type {String} One of the VIEW_NODE_XXX_TYPE const in
+ * client/inspector/shared/node-types.
+ * - rule {Rule} The Rule object.
+ * - value {Object} Depends on the type of the node.
+ * Otherwise, returns null if the node isn't anything we care about.
+ */
+ getNodeInfo(node) {
+ return getNodeInfo(node, this._elementStyle);
+ },
+
+ /**
+ * Get the node's compatibility issues
+ *
+ * @param {DOMNode} node
+ * The node which we want information about
+ * @return {Object|null} containing the following props:
+ * - type {String} Compatibility issue type.
+ * - property {string} The incompatible rule
+ * - alias {Array} The browser specific alias of rule
+ * - url {string} Link to MDN documentation
+ * - deprecated {bool} True if the rule is deprecated
+ * - experimental {bool} True if rule is experimental
+ * - unsupportedBrowsers {Array} Array of unsupported browser
+ * Otherwise, returns null if the node has cross-browser compatible CSS
+ */
+ async getNodeCompatibilityInfo(node) {
+ const compatibilityInfo = await getNodeCompatibilityInfo(
+ node,
+ this._elementStyle
+ );
+
+ return compatibilityInfo;
+ },
+
+ /**
+ * Context menu handler.
+ */
+ _onContextMenu(event) {
+ if (
+ event.originalTarget.closest("input[type=text]") ||
+ event.originalTarget.closest("input:not([type])") ||
+ event.originalTarget.closest("textarea")
+ ) {
+ return;
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+
+ this.contextMenu.show(event);
+ },
+
+ /**
+ * Callback for copy event. Copy the selected text.
+ *
+ * @param {Event} event
+ * copy event object.
+ */
+ _onCopy(event) {
+ if (event) {
+ this.copySelection(event.target);
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ },
+
+ /**
+ * 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(target) {
+ try {
+ let text = "";
+
+ const nodeName = target?.nodeName;
+ const targetType = target?.type;
+
+ if (
+ // The target can be the enable/disable rule checkbox here (See Bug 1680893).
+ (nodeName === "input" && targetType !== "checkbox") ||
+ nodeName == "textarea"
+ ) {
+ const start = Math.min(target.selectionStart, target.selectionEnd);
+ const end = Math.max(target.selectionStart, target.selectionEnd);
+ const 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");
+
+ // Replace 4 space indentation with 2 Spaces.
+ text = text.replace(/\ {4}/g, DOUBLESPACE);
+ }
+
+ clipboardHelper.copyString(text);
+ } catch (e) {
+ console.error(e);
+ }
+ },
+
+ /**
+ * Add a new rule to the current element.
+ */
+ _onAddRule() {
+ const elementStyle = this._elementStyle;
+ const element = elementStyle.element;
+ const pseudoClasses = element.pseudoClassLocks;
+
+ // 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.
+ const eventPromise = this.once("ruleview-refreshed");
+ const newRulePromise = this.pageStyle.addNewRule(element, pseudoClasses);
+ Promise.all([eventPromise, newRulePromise]).then(values => {
+ const options = values[1];
+ // Be sure the reference the correct |rules| here.
+ for (const rule of this._elementStyle.rules) {
+ if (options.rule === rule.domRule) {
+ rule.editor.selectorText.click();
+ elementStyle._changed();
+ break;
+ }
+ }
+ });
+ },
+
+ /**
+ * Disables add rule button when needed
+ */
+ refreshAddRuleButtonState() {
+ const shouldBeDisabled =
+ !this._viewedElement ||
+ !this.inspector.selection.isElementNode() ||
+ this.inspector.selection.isAnonymousNode();
+ this.addRuleButton.disabled = shouldBeDisabled;
+ },
+
+ /**
+ * 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
+ );
+ },
+
+ _handleUAStylePrefChange() {
+ this.showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES);
+ this._handlePrefChange(PREF_UA_STYLES);
+ },
+
+ _handleDefaultColorUnitPrefChange() {
+ this._handlePrefChange(PREF_DEFAULT_COLOR_UNIT);
+ },
+
+ _handleDraggablePrefChange() {
+ this.draggablePropertiesEnabled = Services.prefs.getBoolPref(
+ PREF_DRAGGABLE,
+ false
+ );
+ // This event is consumed by text-property-editor instances in order to
+ // update their draggable behavior. Preferences observer are costly, so
+ // we are forwarding the preference update via the EventEmitter.
+ this.emit("draggable-preference-updated");
+ },
+
+ _handlePrefChange(pref) {
+ // Reselect the currently selected element
+ const refreshOnPrefs = [PREF_UA_STYLES, PREF_DEFAULT_COLOR_UNIT];
+ if (refreshOnPrefs.indexOf(pref) > -1) {
+ this.selectElement(this._viewedElement, true);
+ }
+ },
+
+ /**
+ * Set the filter style search value.
+ * @param {String} value
+ * The search value.
+ */
+ setFilterStyles(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() {
+ if (this._filterChangedTimeout) {
+ clearTimeout(this._filterChangedTimeout);
+ }
+
+ const filterTimeout = this.searchValue.length ? FILTER_CHANGED_TIMEOUT : 0;
+ this.searchClearButton.hidden = this.searchValue.length === 0;
+
+ this._filterChangedTimeout = setTimeout(() => {
+ 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).
+ const 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() {
+ if (this.searchField.value) {
+ this.setFilterStyles("");
+ return true;
+ }
+
+ return false;
+ },
+
+ destroy() {
+ this.isDestroyed = true;
+ this.clear();
+
+ this._dummyElement = null;
+ // off handlers must have the same reference as their on handlers
+ this._prefObserver.off(PREF_UA_STYLES, this._handleUAStylePrefChange);
+ this._prefObserver.off(
+ PREF_DEFAULT_COLOR_UNIT,
+ this._handleDefaultColorUnitPrefChange
+ );
+ this._prefObserver.off(PREF_DRAGGABLE, this._handleDraggablePrefChange);
+ this._prefObserver.destroy();
+
+ this._outputParser = null;
+
+ if (this._classListPreviewer) {
+ this._classListPreviewer.destroy();
+ this._classListPreviewer = null;
+ }
+
+ if (this._contextMenu) {
+ this._contextMenu.destroy();
+ this._contextMenu = null;
+ }
+
+ if (this._highlighters) {
+ this._highlighters.removeFromView(this);
+ this._highlighters = null;
+ }
+
+ // Clean-up for simulations.
+ this.colorSchemeLightSimulationButton.removeEventListener(
+ "click",
+ this._onToggleLightColorSchemeSimulation
+ );
+ this.colorSchemeDarkSimulationButton.removeEventListener(
+ "click",
+ this._onToggleDarkColorSchemeSimulation
+ );
+ this.printSimulationButton.removeEventListener(
+ "click",
+ this._onTogglePrintSimulation
+ );
+
+ this.colorSchemeLightSimulationButton = null;
+ this.colorSchemeDarkSimulationButton = null;
+ this.printSimulationButton = null;
+
+ this.tooltips.destroy();
+
+ // Remove bound listeners
+ this.shortcuts.destroy();
+ this.styleDocument.removeEventListener("click", this, { capture: true });
+ 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.searchClearButton.removeEventListener("click", this._onClearSearch);
+ this.pseudoClassPanel.removeEventListener(
+ "change",
+ this._onTogglePseudoClass
+ );
+ this.pseudoClassToggle.removeEventListener(
+ "click",
+ this._onTogglePseudoClassPanel
+ );
+ this.classToggle.removeEventListener("click", this._onToggleClassPanel);
+ this.inspector.highlighters.off(
+ "highlighter-shown",
+ this.onHighlighterShown
+ );
+ this.inspector.highlighters.off(
+ "highlighter-hidden",
+ this.onHighlighterHidden
+ );
+
+ this.searchField = null;
+ this.searchClearButton = null;
+ this.pseudoClassPanel = null;
+ this.pseudoClassToggle = null;
+ this.pseudoClassCheckboxes = null;
+ this.classPanel = null;
+ this.classToggle = null;
+
+ this.inspector = null;
+ this.styleDocument = null;
+ this.styleWindow = null;
+
+ if (this.element.parentNode) {
+ this.element.remove();
+ }
+
+ if (this._elementStyle) {
+ this._elementStyle.destroy();
+ }
+
+ if (this._popup) {
+ this._popup.destroy();
+ this._popup = null;
+ }
+ },
+
+ /**
+ * 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() {
+ this.element.classList.add("non-interactive");
+ },
+
+ /**
+ * Mark the view as no longer selecting an element, re-enabling interaction.
+ */
+ _stopSelectingElement() {
+ 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(element, allowRefresh = false) {
+ const refresh = this._viewedElement === element;
+ if (refresh && !allowRefresh) {
+ return Promise.resolve(undefined);
+ }
+
+ if (this._popup && 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();
+ if (this.pageStyle) {
+ this.pageStyle.off("stylesheet-updated", this.refreshPanel);
+ this.pageStyle = null;
+ }
+ return Promise.resolve(undefined);
+ }
+
+ this.pageStyle = element.inspectorFront.pageStyle;
+ this.pageStyle.on("stylesheet-updated", this.refreshPanel);
+
+ // 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.
+ const dummyElementPromise = Promise.resolve(this.styleDocument)
+ .then(document => {
+ // ::before and ::after do not have a namespaceURI
+ const namespaceURI =
+ this.element.namespaceURI || document.documentElement.namespaceURI;
+ this._dummyElement = document.createElementNS(
+ namespaceURI,
+ this.element.tagName
+ );
+ })
+ .catch(promiseWarn);
+
+ const 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();
+ };
+ }
+ })
+ .catch(e => {
+ if (this._elementStyle === elementStyle) {
+ this._stopSelectingElement();
+ this._clearRules();
+ }
+ console.error(e);
+ });
+ },
+
+ /**
+ * Update the rules for the currently highlighted element.
+ */
+ refreshPanel() {
+ // Ignore refreshes when the panel is hidden, or during editing or when no element is selected.
+ if (!this.isPanelVisible() || this.isEditing || !this._elementStyle) {
+ return Promise.resolve(undefined);
+ }
+
+ // Repopulate the element style once the current modifications are done.
+ const promises = [];
+ for (const 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() {
+ this.pseudoClassCheckboxes.forEach(checkbox => {
+ checkbox.checked = false;
+ checkbox.disabled = false;
+ });
+ },
+
+ /**
+ * For each item in PSEUDO_CLASSES, create a checkbox input element for toggling a
+ * pseudo-class on the selected element and append it to the pseudo-class panel.
+ *
+ * Returns an array with the checkbox input elements for pseudo-classes.
+ *
+ * @return {Array}
+ */
+ _createPseudoClassCheckboxes() {
+ const doc = this.styleDocument;
+ const fragment = doc.createDocumentFragment();
+
+ for (const pseudo of PSEUDO_CLASSES) {
+ const label = doc.createElement("label");
+ const checkbox = doc.createElement("input");
+ checkbox.setAttribute("tabindex", "-1");
+ checkbox.setAttribute("type", "checkbox");
+ checkbox.setAttribute("value", pseudo);
+
+ label.append(checkbox, pseudo);
+ fragment.append(label);
+ }
+
+ this.pseudoClassPanel.append(fragment);
+ return Array.from(
+ this.pseudoClassPanel.querySelectorAll("input[type=checkbox]")
+ );
+ },
+
+ /**
+ * Update the pseudo class options for the currently highlighted element.
+ */
+ refreshPseudoClassPanel() {
+ if (!this._elementStyle || !this.inspector.selection.isElementNode()) {
+ this.pseudoClassCheckboxes.forEach(checkbox => {
+ checkbox.disabled = true;
+ });
+ return;
+ }
+
+ const pseudoClassLocks = this._elementStyle.element.pseudoClassLocks;
+ this.pseudoClassCheckboxes.forEach(checkbox => {
+ checkbox.disabled = false;
+ checkbox.checked = pseudoClassLocks.includes(checkbox.value);
+ });
+ },
+
+ _populate() {
+ const elementStyle = this._elementStyle;
+ return this._elementStyle
+ .populate()
+ .then(() => {
+ if (this._elementStyle !== elementStyle || this.isDestroyed) {
+ return null;
+ }
+
+ this._clearRules();
+ const onEditorsReady = this._createEditors();
+ this.refreshPseudoClassPanel();
+
+ // Notify anyone that cares that we refreshed.
+ return onEditorsReady.then(() => {
+ this.emit("ruleview-refreshed");
+ }, console.error);
+ })
+ .catch(promiseWarn);
+ },
+
+ /**
+ * Show the user that the rule view has no node selected.
+ */
+ _showEmpty() {
+ if (this.styleDocument.getElementById("ruleview-no-results")) {
+ return;
+ }
+
+ createChild(this.element, "div", {
+ id: "ruleview-no-results",
+ class: "devtools-sidepanel-no-result",
+ textContent: l10n("rule.empty"),
+ });
+ },
+
+ /**
+ * Clear the rules.
+ */
+ _clearRules() {
+ this.element.innerHTML = "";
+ },
+
+ /**
+ * Clear the rule view.
+ */
+ clear(clearDom = true) {
+ if (clearDom) {
+ this._clearRules();
+ }
+ this._viewedElement = null;
+
+ if (this._elementStyle) {
+ this._elementStyle.destroy();
+ this._elementStyle = null;
+ }
+
+ if (this.pageStyle) {
+ this.pageStyle.off("stylesheet-updated", this.refreshPanel);
+ this.pageStyle = null;
+ }
+ },
+
+ /**
+ * Called when the user has made changes to the ElementStyle.
+ * Emits an event that clients can listen to.
+ */
+ _changed() {
+ 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(label, isPseudo = false) {
+ const header = this.styleDocument.createElementNS(HTML_NS, "div");
+ header.className = this._getRuleViewHeaderClassName(true);
+ header.setAttribute("role", "heading");
+ header.textContent = label;
+
+ const twisty = this.styleDocument.createElementNS(HTML_NS, "span");
+ twisty.className = "ruleview-expander theme-twisty";
+ twisty.setAttribute("open", "true");
+ twisty.setAttribute("role", "button");
+ twisty.setAttribute("aria-label", l10n("rule.twistyCollapse.label"));
+
+ header.insertBefore(twisty, header.firstChild);
+ this.element.appendChild(header);
+
+ const container = this.styleDocument.createElementNS(HTML_NS, "div");
+ container.classList.add("ruleview-expandable-container");
+ container.hidden = false;
+ this.element.appendChild(container);
+
+ header.addEventListener("click", () => {
+ this._toggleContainerVisibility(
+ twisty,
+ container,
+ isPseudo,
+ !this.showPseudoElements
+ );
+ });
+
+ if (isPseudo) {
+ container.id = "pseudo-elements-container";
+ twisty.id = "pseudo-elements-header-twisty";
+ 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(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");
+ twisty.setAttribute("aria-label", l10n("rule.twistyExpand.label"));
+ } else {
+ twisty.setAttribute("open", "true");
+ twisty.setAttribute("aria-label", l10n("rule.twistyCollapse.label"));
+ }
+ },
+
+ _getRuleViewHeaderClassName(isPseudo) {
+ const baseClassName = "ruleview-header";
+ return isPseudo
+ ? baseClassName + " ruleview-expandable-header"
+ : baseClassName;
+ },
+
+ /**
+ * Creates editor UI for each of the rules in _elementStyle.
+ */
+ // eslint-disable-next-line complexity
+ _createEditors() {
+ // 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();
+ }
+
+ const editorReadyPromises = [];
+ for (const 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;
+ const div = this.styleDocument.createElementNS(HTML_NS, "div");
+ div.className = this._getRuleViewHeaderClassName();
+ div.setAttribute("role", "heading");
+ div.textContent = this.selectedElementLabel;
+ this.element.appendChild(div);
+ }
+
+ const inheritedSource = rule.inherited;
+ if (inheritedSource && inheritedSource !== lastInheritedSource) {
+ const div = this.styleDocument.createElementNS(HTML_NS, "div");
+ div.className = this._getRuleViewHeaderClassName();
+ div.setAttribute("role", "heading");
+ div.setAttribute("aria-level", "3");
+ div.textContent = rule.inheritedSource;
+ lastInheritedSource = inheritedSource;
+ this.element.appendChild(div);
+ }
+
+ if (!seenPseudoElement && rule.pseudoElement) {
+ seenPseudoElement = true;
+ container = this.createExpandableContainer(
+ this.pseudoElementLabel,
+ true
+ );
+ }
+
+ const keyframes = rule.keyframes;
+ if (keyframes && keyframes !== lastKeyframes) {
+ lastKeyframes = keyframes;
+ container = this.createExpandableContainer(rule.keyframesName);
+ }
+
+ rule.editor.element.setAttribute("role", "article");
+ if (container && (rule.pseudoElement || keyframes)) {
+ container.appendChild(rule.editor.element);
+ } else {
+ this.element.appendChild(rule.editor.element);
+ }
+ }
+
+ const searchBox = this.searchField.parentNode;
+ searchBox.classList.toggle(
+ "devtools-searchbox-no-match",
+ this.searchValue && !seenSearchTerm
+ );
+
+ 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(rule) {
+ const isRuleSelectorHighlighted = this._highlightRuleSelector(rule);
+ const isStyleSheetHighlighted = this._highlightStyleSheet(rule);
+ const isAncestorRulesHighlighted = this._highlightAncestorRules(rule);
+ let isHighlighted =
+ isRuleSelectorHighlighted ||
+ isStyleSheetHighlighted ||
+ isAncestorRulesHighlighted;
+
+ // Highlight search matches in the rule properties
+ for (const 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(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 (const selectorNode of selectorNodes) {
+ const 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 ancestor rules data (@media / @layer) that matches the filter search
+ * value and returns a boolean indicating whether or not element was highlighted.
+ *
+ * @return {Boolean} true if the element was highlighted, false otherwise.
+ */
+ _highlightAncestorRules(rule) {
+ const element = rule.editor.ancestorDataEl;
+ if (!element) {
+ return false;
+ }
+
+ let isHighlighted = false;
+ for (let i = 0; i < element.childNodes.length; i++) {
+ const child = element.childNodes[i];
+ const dataText = child.innerText.toLowerCase();
+ const matches = this.searchData.strictSearchValue
+ ? dataText === this.searchData.strictSearchValue
+ : dataText.includes(this.searchValue);
+ if (matches) {
+ isHighlighted = true;
+ child.classList.add("ruleview-highlight");
+ }
+ }
+
+ return isHighlighted;
+ },
+
+ /**
+ * 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(rule) {
+ const styleSheetSource = rule.title.toLowerCase();
+ const 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(editor) {
+ const isPropertyHighlighted = this._highlightRuleProperty(editor);
+ const 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(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(editor) {
+ // Get the actual property value displayed in the rule view
+ const propertyName = editor.prop.name.toLowerCase();
+ const 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(editor) {
+ let isComputedHighlighted = false;
+
+ // Highlight search matches in the computed list of properties
+ editor._populateComputed();
+ for (const computed of editor.prop.computed) {
+ if (computed.element) {
+ // Get the actual property value displayed in the computed list
+ const computedName = computed.name.toLowerCase();
+ const 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(element, propertyName, propertyValue) {
+ const {
+ 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.
+ const hasNameAndValue =
+ searchPropertyMatch && searchPropertyName && searchPropertyValue;
+ const 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(element) {
+ for (const el of element.querySelectorAll(".ruleview-highlight")) {
+ el.classList.remove("ruleview-highlight");
+ }
+
+ for (const 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() {
+ if (this.pseudoClassPanel.hidden) {
+ this.showPseudoClassPanel();
+ } else {
+ this.hidePseudoClassPanel();
+ }
+ },
+
+ showPseudoClassPanel() {
+ this.hideClassPanel();
+
+ this.pseudoClassToggle.classList.add("checked");
+ this.pseudoClassCheckboxes.forEach(checkbox => {
+ checkbox.setAttribute("tabindex", "0");
+ });
+ this.pseudoClassPanel.hidden = false;
+ },
+
+ hidePseudoClassPanel() {
+ this.pseudoClassToggle.classList.remove("checked");
+ this.pseudoClassCheckboxes.forEach(checkbox => {
+ checkbox.setAttribute("tabindex", "-1");
+ });
+ this.pseudoClassPanel.hidden = true;
+ },
+
+ /**
+ * Called when a pseudo class checkbox is clicked and toggles
+ * the pseudo class for the current selected element.
+ */
+ _onTogglePseudoClass(event) {
+ const target = event.target;
+ this.inspector.togglePseudoClass(target.value);
+ },
+
+ /**
+ * Called when the class panel button is clicked and toggles the display of the class
+ * panel.
+ */
+ _onToggleClassPanel() {
+ if (this.classPanel.hidden) {
+ this.showClassPanel();
+ } else {
+ this.hideClassPanel();
+ }
+ },
+
+ showClassPanel() {
+ this.hidePseudoClassPanel();
+
+ this.classToggle.classList.add("checked");
+ this.classPanel.hidden = false;
+
+ this.classListPreviewer.focusAddClassField();
+ },
+
+ hideClassPanel() {
+ this.classToggle.classList.remove("checked");
+ this.classPanel.hidden = true;
+ },
+
+ /**
+ * Handle the keypress event in the rule view.
+ */
+ _onShortcut(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();
+ }
+ },
+
+ async _onToggleLightColorSchemeSimulation() {
+ const shouldSimulateLightScheme =
+ this.colorSchemeLightSimulationButton.classList.toggle("checked");
+
+ const darkColorSchemeEnabled =
+ this.colorSchemeDarkSimulationButton.classList.contains("checked");
+ if (shouldSimulateLightScheme && darkColorSchemeEnabled) {
+ this.colorSchemeDarkSimulationButton.classList.toggle("checked");
+ }
+
+ await this.inspector.commands.targetConfigurationCommand.updateConfiguration(
+ {
+ colorSchemeSimulation: shouldSimulateLightScheme ? "light" : null,
+ }
+ );
+ // Refresh the current element's rules in the panel.
+ this.refreshPanel();
+ },
+
+ async _onToggleDarkColorSchemeSimulation() {
+ const shouldSimulateDarkScheme =
+ this.colorSchemeDarkSimulationButton.classList.toggle("checked");
+
+ const lightColorSchemeEnabled =
+ this.colorSchemeLightSimulationButton.classList.contains("checked");
+ if (shouldSimulateDarkScheme && lightColorSchemeEnabled) {
+ this.colorSchemeLightSimulationButton.classList.toggle("checked");
+ }
+
+ await this.inspector.commands.targetConfigurationCommand.updateConfiguration(
+ {
+ colorSchemeSimulation: shouldSimulateDarkScheme ? "dark" : null,
+ }
+ );
+ // Refresh the current element's rules in the panel.
+ this.refreshPanel();
+ },
+
+ async _onTogglePrintSimulation() {
+ const enabled = this.printSimulationButton.classList.toggle("checked");
+ await this.inspector.commands.targetConfigurationCommand.updateConfiguration(
+ {
+ printSimulationEnabled: enabled,
+ }
+ );
+ // Refresh the current element's rules in the panel.
+ this.refreshPanel();
+ },
+
+ /**
+ * Temporarily flash the given element.
+ *
+ * @param {Element} element
+ * The element.
+ */
+ _flashElement(element) {
+ flashElementOn(element, {
+ backgroundClass: "theme-bg-contrast",
+ });
+
+ if (this._flashMutationTimer) {
+ clearTimeout(this._removeFlashOutTimer);
+ this._flashMutationTimer = null;
+ }
+
+ this._flashMutationTimer = setTimeout(() => {
+ flashElementOff(element, {
+ backgroundClass: "theme-bg-contrast",
+ });
+
+ // Emit "scrolled-to-property" for use by tests.
+ this.emit("scrolled-to-element");
+ }, PROPERTY_FLASHING_DURATION);
+ },
+
+ /**
+ * Scrolls to the top of either the rule or declaration. The view will try to scroll to
+ * the rule if both can fit in the viewport. If not, then scroll to the declaration.
+ *
+ * @param {Element} rule
+ * The rule to scroll to.
+ * @param {Element|null} declaration
+ * Optional. The declaration to scroll to.
+ * @param {String} scrollBehavior
+ * Optional. The transition animation when scrolling. If prefers-reduced-motion
+ * system pref is set, then the scroll behavior will be overridden to "auto".
+ */
+ _scrollToElement(rule, declaration, scrollBehavior = "smooth") {
+ let elementToScrollTo = rule;
+
+ if (declaration) {
+ const { offsetTop, offsetHeight } = declaration;
+ // Get the distance between both the rule and declaration. If the distance is
+ // greater than the height of the rule view, then only scroll to the declaration.
+ const distance = offsetTop + offsetHeight - rule.offsetTop;
+
+ if (this.element.parentNode.offsetHeight <= distance) {
+ elementToScrollTo = declaration;
+ }
+ }
+
+ // Ensure that smooth scrolling is disabled when the user prefers reduced motion.
+ const win = elementToScrollTo.ownerGlobal;
+ const reducedMotion = win.matchMedia("(prefers-reduced-motion)").matches;
+ scrollBehavior = reducedMotion ? "auto" : scrollBehavior;
+
+ elementToScrollTo.scrollIntoView({ behavior: scrollBehavior });
+ },
+
+ /**
+ * Toggles the visibility of the pseudo element rule's container.
+ */
+ _togglePseudoElementRuleContainer() {
+ const container = this.styleDocument.getElementById(
+ "pseudo-elements-container"
+ );
+ const twisty = this.styleDocument.getElementById(
+ "pseudo-elements-header-twisty"
+ );
+ this._toggleContainerVisibility(twisty, container, true, true);
+ },
+
+ /**
+ * Finds the rule with the matching actorID and highlights it.
+ *
+ * @param {String} ruleId
+ * The actorID of the rule.
+ */
+ highlightElementRule(ruleId) {
+ let scrollBehavior = "smooth";
+
+ const rule = this.rules.find(r => r.domRule.actorID === ruleId);
+
+ if (!rule) {
+ return;
+ }
+
+ if (rule.domRule.actorID === ruleId) {
+ // If using 2-Pane mode, then switch to the Rules tab first.
+ if (!this.inspector.is3PaneModeEnabled) {
+ this.inspector.sidebar.select("ruleview");
+ }
+
+ if (rule.pseudoElement.length && !this.showPseudoElements) {
+ scrollBehavior = "auto";
+ this._togglePseudoElementRuleContainer();
+ }
+
+ const {
+ editor: { element },
+ } = rule;
+
+ // Scroll to the top of the rule and highlight it.
+ this._scrollToElement(element, null, scrollBehavior);
+ this._flashElement(element);
+ }
+ },
+
+ /**
+ * Finds the specified TextProperty name in the rule view. If found, scroll to and
+ * flash the TextProperty.
+ *
+ * @param {String} name
+ * The property name to scroll to and highlight.
+ * @return {Boolean} true if the TextProperty name is found, and false otherwise.
+ */
+ highlightProperty(name) {
+ for (const rule of this.rules) {
+ for (const textProp of rule.textProps) {
+ if (textProp.overridden || textProp.invisible || !textProp.enabled) {
+ continue;
+ }
+
+ const {
+ editor: { selectorText },
+ } = rule;
+ let scrollBehavior = "smooth";
+
+ // First, search for a matching authored property.
+ if (textProp.name === name) {
+ // If using 2-Pane mode, then switch to the Rules tab first.
+ if (!this.inspector.is3PaneModeEnabled) {
+ this.inspector.sidebar.select("ruleview");
+ }
+
+ // If the property is being applied by a pseudo element rule, expand the pseudo
+ // element list container.
+ if (rule.pseudoElement.length && !this.showPseudoElements) {
+ // Set the scroll behavior to "auto" to avoid timing issues between toggling
+ // the pseudo element container and scrolling smoothly to the rule.
+ scrollBehavior = "auto";
+ this._togglePseudoElementRuleContainer();
+ }
+
+ // Scroll to the top of the property's rule so that both the property and its
+ // rule are visible.
+ this._scrollToElement(
+ selectorText,
+ textProp.editor.element,
+ scrollBehavior
+ );
+ this._flashElement(textProp.editor.element);
+
+ return true;
+ }
+
+ // If there is no matching property, then look in computed properties.
+ for (const computed of textProp.computed) {
+ if (computed.overridden) {
+ continue;
+ }
+
+ if (computed.name === name) {
+ if (!this.inspector.is3PaneModeEnabled) {
+ this.inspector.sidebar.select("ruleview");
+ }
+
+ if (
+ textProp.rule.pseudoElement.length &&
+ !this.showPseudoElements
+ ) {
+ scrollBehavior = "auto";
+ this._togglePseudoElementRuleContainer();
+ }
+
+ // Expand the computed list.
+ textProp.editor.expandForFilter();
+
+ this._scrollToElement(
+ selectorText,
+ computed.element,
+ scrollBehavior
+ );
+ this._flashElement(computed.element);
+
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ },
+};
+
+function RuleViewTool(inspector, window) {
+ this.inspector = inspector;
+ this.document = window.document;
+
+ this.view = new CssRuleView(this.inspector, this.document);
+
+ this._onResourceAvailable = this._onResourceAvailable.bind(this);
+ this.refresh = this.refresh.bind(this);
+ this.onDetachedFront = this.onDetachedFront.bind(this);
+ this.onPanelSelected = this.onPanelSelected.bind(this);
+ this.onDetachedFront = this.onDetachedFront.bind(this);
+ this.onSelected = this.onSelected.bind(this);
+ this.onViewRefreshed = this.onViewRefreshed.bind(this);
+
+ this.view.on("ruleview-refreshed", this.onViewRefreshed);
+ this.inspector.selection.on("detached-front", this.onDetachedFront);
+ this.inspector.selection.on("new-node-front", this.onSelected);
+ this.inspector.selection.on("pseudoclass", this.refresh);
+ this.inspector.ruleViewSideBar.on("ruleview-selected", this.onPanelSelected);
+ this.inspector.sidebar.on("ruleview-selected", this.onPanelSelected);
+ this.inspector.styleChangeTracker.on("style-changed", this.refresh);
+
+ this.inspector.commands.resourceCommand.watchResources(
+ [this.inspector.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
+ {
+ onAvailable: this._onResourceAvailable,
+ ignoreExistingResources: true,
+ }
+ );
+
+ // At the moment `readyPromise` is only consumed in tests (see `openRuleView`) to be
+ // notified when the ruleview was first populated to match the initial selected node.
+ this.readyPromise = this.onSelected();
+}
+
+RuleViewTool.prototype = {
+ isPanelVisible() {
+ if (!this.view) {
+ return false;
+ }
+ return this.view.isPanelVisible();
+ },
+
+ onDetachedFront() {
+ this.onSelected(false);
+ },
+
+ onSelected(selectElement = true) {
+ // 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 null;
+ }
+
+ const isInactive =
+ !this.isPanelVisible() && this.inspector.selection.nodeFront;
+ if (isInactive) {
+ return null;
+ }
+
+ if (
+ !this.inspector.selection.isConnected() ||
+ !this.inspector.selection.isElementNode()
+ ) {
+ return this.view.selectElement(null);
+ }
+
+ if (!selectElement) {
+ return null;
+ }
+
+ const done = this.inspector.updating("rule-view");
+ return this.view
+ .selectElement(this.inspector.selection.nodeFront)
+ .then(done, done);
+ },
+
+ refresh() {
+ if (this.isPanelVisible()) {
+ this.view.refreshPanel();
+ }
+ },
+
+ _onResourceAvailable(resources) {
+ for (const resource of resources) {
+ if (
+ resource.resourceType ===
+ this.inspector.commands.resourceCommand.TYPES.DOCUMENT_EVENT &&
+ resource.name === "will-navigate" &&
+ resource.targetFront.isTopLevel
+ ) {
+ this.clearUserProperties();
+ }
+ }
+ },
+
+ clearUserProperties() {
+ if (this.view && this.view.store && this.view.store.userProperties) {
+ this.view.store.userProperties.clear();
+ }
+ },
+
+ onPanelSelected() {
+ if (this.inspector.selection.nodeFront === this.view._viewedElement) {
+ this.refresh();
+ } else {
+ this.onSelected();
+ }
+ },
+
+ onViewRefreshed() {
+ this.inspector.emit("rule-view-refreshed");
+ },
+
+ destroy() {
+ this.inspector.styleChangeTracker.off("style-changed", this.refresh);
+ this.inspector.selection.off("detached-front", this.onDetachedFront);
+ this.inspector.selection.off("pseudoclass", this.refresh);
+ this.inspector.selection.off("new-node-front", this.onSelected);
+ this.inspector.currentTarget.off("navigate", this.clearUserProperties);
+ this.inspector.sidebar.off("ruleview-selected", this.onPanelSelected);
+
+ this.inspector.commands.resourceCommand.unwatchResources(
+ [this.inspector.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
+ {
+ onAvailable: this._onResourceAvailable,
+ }
+ );
+
+ this.view.off("ruleview-refreshed", this.onViewRefreshed);
+
+ this.view.destroy();
+
+ this.view = this.document = this.inspector = this.readyPromise = null;
+ },
+};
+
+exports.CssRuleView = CssRuleView;
+exports.RuleViewTool = RuleViewTool;
diff --git a/devtools/client/inspector/rules/test/browser_part1.ini b/devtools/client/inspector/rules/test/browser_part1.ini
new file mode 100644
index 0000000000..b31e7765a0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_part1.ini
@@ -0,0 +1,172 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ doc_blob_stylesheet.html
+ doc_copystyles.css
+ doc_copystyles.html
+ doc_class_panel_autocomplete_stylesheet.css
+ doc_class_panel_autocomplete.html
+ doc_conditional_import.css
+ doc_cssom.html
+ doc_custom.html
+ doc_edit_imported_selector.html
+ doc_imported_named_layer.css
+ doc_imported_no_layer.css
+ doc_test_image.png
+ doc_variables_4.html
+ head.js
+ !/devtools/client/inspector/test/head.js
+ !/devtools/client/inspector/test/shared-head.js
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+ !/devtools/client/shared/test/highlighter-test-actor.js
+
+[browser_rules_add-property-and-reselect.js]
+skip-if = http3 # Bug 1829298
+[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]
+skip-if =
+ verify && debug && os == "win"
+[browser_rules_add-property_01.js]
+[browser_rules_add-property_02.js]
+[browser_rules_add-property-invalid-identifier.js]
+[browser_rules_add-property-svg.js]
+[browser_rules_add-rule-and-property.js]
+[browser_rules_add-rule-and-remove-style-node.js]
+[browser_rules_add-rule-button-state.js]
+[browser_rules_add-rule-csp.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]
+skip-if =
+ os == "linux" && os_version == '18.04' && !debug # Bug 1559315
+ apple_catalina # Bug 1713158
+ win10_2004 # Bug 1559315, 1723573
+ win11_2009 # Bug 1797751
+[browser_rules_authored_override.js]
+[browser_rules_blob_stylesheet.js]
+[browser_rules_class_panel_add.js]
+[browser_rules_class_panel_autocomplete.js]
+[browser_rules_class_panel_content.js]
+[browser_rules_class_panel_edit.js]
+[browser_rules_class_panel_invalid_nodes.js]
+[browser_rules_class_panel_mutation.js]
+[browser_rules_class_panel_state_preserved.js]
+[browser_rules_class_panel_toggle.js]
+[browser_rules_color_scheme_simulation_bfcache.js]
+[browser_rules_color_scheme_simulation_meta.js]
+[browser_rules_color_scheme_simulation_rdm.js]
+skip-if =
+ win10_2004 # Bug 1723573
+[browser_rules_color_scheme_simulation.js]
+skip-if =
+ os == "win" && !debug # Bug 1703465
+[browser_rules_colorpicker-and-image-tooltip_01.js]
+[browser_rules_colorpicker-and-image-tooltip_02.js]
+[browser_rules_colorpicker-appears-on-swatch-click-or-keyboard-activation.js]
+[browser_rules_colorpicker-commit-on-ENTER.js]
+[browser_rules_colorpicker-contrast-ratio.js]
+[browser_rules_colorpicker-edit-gradient.js]
+[browser_rules_colorpicker-element-without-quads.js]
+[browser_rules_colorpicker-hides-element-picker.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_colorpicker-works-with-css-vars.js]
+[browser_rules_colorpicker-wrap-focus.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]
+skip-if =
+ verify && !debug && os == "win"
+[browser_rules_completion-new-property_03.js]
+[browser_rules_completion-new-property_04.js]
+[browser_rules_completion-new-property_multiline.js]
+[browser_rules_completion-on-empty.js]
+[browser_rules_completion-shortcut.js]
+[browser_rules_computed-lists_01.js]
+[browser_rules_computed-lists_02.js]
+[browser_rules_computed-lists_03.js]
+[browser_rules_completion-popup-hidden-after-navigation.js]
+[browser_rules_conditional_import.js]
+[browser_rules_container-queries.js]
+[browser_rules_content_01.js]
+[browser_rules_content_02.js]
+[browser_rules_variables-in-pseudo-element_01.js]
+[browser_rules_variables-in-pseudo-element_02.js]
+[browser_rules_variables_01.js]
+[browser_rules_variables_02.js]
+skip-if = debug # Bug 1250058 - Docshell leak on debug
+[browser_rules_variables_03-case-sensitive.js]
+[browser_rules_variables_04-valid-chars.js]
+[browser_rules_copy_styles.js]
+[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]
+skip-if =
+ verify && debug && os == "win"
+[browser_rules_edit-property-remove_02.js]
+[browser_rules_edit-property-remove_03.js]
+[browser_rules_edit-property-remove_04.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-property_10.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-selector_12.js]
+[browser_rules_edit-size-property-dragging.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_edit-variable.js]
+[browser_rules_edit-variable-add.js]
+[browser_rules_edit-variable-remove.js]
+[browser_rules_editable-field-focus_01.js]
+[browser_rules_editable-field-focus_02.js]
+[browser_rules_eyedropper.js]
diff --git a/devtools/client/inspector/rules/test/browser_part2.ini b/devtools/client/inspector/rules/test/browser_part2.ini
new file mode 100644
index 0000000000..3d245f2e2c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_part2.ini
@@ -0,0 +1,223 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ doc_author-sheet.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_filter.html
+ doc_grid_names.html
+ doc_grid_area_gridline_names.html
+ 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_print_media_simulation.html
+ doc_pseudoelement.html
+ doc_ruleLineNumbers.html
+ doc_rules_imported_stylesheet_edit.html
+ doc_sourcemaps.css
+ doc_sourcemaps.css.map
+ doc_sourcemaps.html
+ doc_sourcemaps.scss
+ doc_sourcemaps2.css
+ doc_sourcemaps2.css^headers^
+ doc_sourcemaps2.html
+ doc_style_editor_link.css
+ doc_test_image.png
+ doc_urls_clickable.css
+ doc_urls_clickable.html
+ doc_variables_1.html
+ doc_variables_2.html
+ doc_variables_3.html
+ doc_visited.html
+ doc_visited_in_media_query.html
+ doc_visited_with_style_attribute.html
+ doc_imported_anonymous_layer.css
+ doc_imported_named_layer.css
+ doc_imported_no_layer.css
+ doc_inactive_css_xul.xhtml
+ head.js
+ sjs_imported_stylesheet_edit.sjs
+ square_svg.sjs
+ !/devtools/client/inspector/test/head.js
+ !/devtools/client/inspector/test/shared-head.js
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+ !/devtools/client/shared/test/highlighter-test-actor.js
+ !/devtools/client/webconsole/test/browser/shared-head.js
+
+[browser_rules_css-compatibility-add-rename-rule.js]
+skip-if =
+ os == "linux" #bug 1657807
+ os == "win" #bug 1657807
+[browser_rules_css-compatibility-check-add-fix.js]
+[browser_rules_css-compatibility-learn-more-link.js]
+[browser_rules_css-compatibility-toggle-rules.js]
+[browser_rules_css-compatibility-tooltip-telemetry.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_flexbox-highlighter-on-mutation.js]
+[browser_rules_flexbox-highlighter-on-navigate.js]
+[browser_rules_flexbox-highlighter-on-reload.js]
+[browser_rules_flexbox-highlighter-restored-after-reload.js]
+[browser_rules_flexbox-toggle-telemetry.js]
+[browser_rules_flexbox-toggle_01.js]
+[browser_rules_flexbox-toggle_01b.js]
+[browser_rules_flexbox-toggle_02.js]
+[browser_rules_flexbox-toggle_03.js]
+[browser_rules_flexbox-toggle_04.js]
+[browser_rules_font-family-parsing.js]
+[browser_rules_grid-highlighter-on-mutation.js]
+[browser_rules_grid-highlighter-on-navigate.js]
+[browser_rules_grid-highlighter-on-reload.js]
+[browser_rules_grid-highlighter-restored-after-reload.js]
+[browser_rules_grid-template-areas.js]
+[browser_rules_grid-toggle-telemetry.js]
+[browser_rules_grid-toggle_01.js]
+[browser_rules_grid-toggle_01b.js]
+[browser_rules_grid-toggle_02.js]
+[browser_rules_grid-toggle_03.js]
+[browser_rules_grid-toggle_04.js]
+[browser_rules_grid-toggle_05.js]
+[browser_rules_gridline-names-autocomplete.js]
+skip-if =
+ win10_2004 # Bug 1723573
+ os == "mac" && !debug # Bug 1675592; high frequency with/out fission
+[browser_rules_gridline-names-are-shown-correctly.js]
+skip-if = os == "linux" # focusEditableField times out consistently on linux.
+[browser_rules_guessIndentation.js]
+[browser_rules_highlight-element-rule.js]
+[browser_rules_highlight-property.js]
+[browser_rules_highlight-used-fonts.js]
+[browser_rules_imported_stylesheet_edit.js]
+skip-if = http3 # Bug 1829298
+[browser_rules_inactive_css_display-justify.js]
+[browser_rules_inactive_css_flexbox.js]
+[browser_rules_inactive_css_grid.js]
+[browser_rules_inactive_css_inline.js]
+[browser_rules_inactive_css_split-condition.js]
+[browser_rules_inactive_css_visited.js]
+[browser_rules_inactive_css_xul.js]
+[browser_rules_inherited-properties_01.js]
+[browser_rules_inherited-properties_02.js]
+[browser_rules_inherited-properties_03.js]
+[browser_rules_inherited-properties_04.js]
+[browser_rules_inline-source-map.js]
+[browser_rules_inline-style-order.js]
+[browser_rules_invalid.js]
+[browser_rules_invalid-source-map.js]
+[browser_rules_keybindings.js]
+[browser_rules_keyframes-rule-shadowdom.js]
+[browser_rules_keyframes-rule_01.js]
+[browser_rules_keyframes-rule_02.js]
+[browser_rules_keyframeLineNumbers.js]
+[browser_rules_large_base64_background_image.js]
+[browser_rules_layer.js]
+[browser_rules_linear-easing-swatch.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_mark_overridden_08.js]
+[browser_rules_mathml-element.js]
+disabled = bug 1231085 # This should be rewritten now that MathMLElement.style is available.
+[browser_rules_media-queries_reload.js]
+skip-if =
+ ccov && os == "win" # Bug 1516686
+[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_nested_at_rules.js]
+[browser_rules_non_ascii.js]
+[browser_rules_original-source-link.js]
+skip-if =
+ ccov #Bug 1432176
+ http3 # Bug 1829298
+[browser_rules_original-source-link2.js]
+skip-if =
+ ccov # Bug 1432176
+ http3 # Bug 1829298
+[browser_rules_preview-tooltips-sizes.js]
+[browser_rules_print_media_simulation.js]
+[browser_rules_pseudo-element_01.js]
+[browser_rules_pseudo-element_02.js]
+[browser_rules_pseudo-visited.js]
+[browser_rules_pseudo-visited_in_media-query.js]
+[browser_rules_pseudo-visited_with_style-attribute.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-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-media-queries-layers.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]
+[browser_rules_search-filter_escape-keypress.js]
+[browser_rules_select-and-copy-styles.js]
+[browser_rules_selector-highlighter-iframe-picker.js]
+[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-highlighter_05.js]
+[browser_rules_selector-highlighter_order.js]
+[browser_rules_selector_highlight.js]
+[browser_rules_shadowdom_slot_rules.js]
+[browser_rules_shapes-toggle_01.js]
+[browser_rules_shapes-toggle_02.js]
+[browser_rules_shapes-toggle_03.js]
+[browser_rules_shapes-toggle_04.js]
+[browser_rules_shapes-toggle_05.js]
+[browser_rules_shapes-toggle_06.js]
+[browser_rules_shapes-toggle_07.js]
+fail-if = a11y_checks # bug 1687723 ruleview-shapeswatch is not accessible
+[browser_rules_shapes-toggle_basic-shapes-default.js]
+[browser_rules_shorthand-overridden-lists.js]
+[browser_rules_shorthand-overridden-lists_01.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_update_mask_image_cors.js]
+[browser_rules_url-click-opens-new-tab.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]
+skip-if =
+ os == "win" && debug # bug 1758768, frequent leaks on win debug
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 0000000000..d71af090cf
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js
@@ -0,0 +1,63 @@
+/* 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(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+ await selectNode("#target", inspector);
+
+ info("Setting a font-weight property on all rules");
+ await setPropertyOnAllRules(view, inspector);
+
+ info("Reselecting the element");
+ await selectNode("body", inspector);
+ await selectNode("#target", inspector);
+
+ checkPropertyOnAllRules(view);
+});
+
+async function setPropertyOnAllRules(view, inspector) {
+ // Set the inline style rule first independently because it needs to wait for specific
+ // events and the DOM mutation that it causes refreshes the rules view, so we need to
+ // get the list of rules again later.
+ info("Adding font-weight:bold in the inline style rule");
+ const inlineStyleRuleEditor = view._elementStyle.rules[0].editor;
+
+ const onMutation = inspector.once("markupmutation");
+ const onRuleViewRefreshed = view.once("ruleview-refreshed");
+
+ inlineStyleRuleEditor.addProperty("font-weight", "bold", "", true);
+
+ await Promise.all([onMutation, onRuleViewRefreshed]);
+
+ // Now set the other rules after having retrieved the list.
+ const allRules = view._elementStyle.rules;
+
+ for (let i = 1; i < allRules.length; i++) {
+ info(`Adding font-weight:bold in rule ${i}`);
+ const rule = allRules[i];
+ const ruleEditor = rule.editor;
+
+ const onRuleViewChanged = view.once("ruleview-changed");
+
+ ruleEditor.addProperty("font-weight", "bold", "", true);
+
+ await onRuleViewChanged;
+ }
+}
+
+function checkPropertyOnAllRules(view) {
+ for (const rule of view._elementStyle.rules) {
+ const lastProperty = rule.textProps[rule.textProps.length - 1];
+
+ is(lastProperty.name, "font-weight", "Last property name is font-weight");
+ is(lastProperty.value, "bold", "Last property 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 0000000000..9131da6423
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_01.js
@@ -0,0 +1,54 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ const elementRuleEditor = getRuleViewRuleEditor(view, 0);
+ const editor = await focusNewRuleViewProperty(elementRuleEditor);
+ is(
+ inplaceEditor(elementRuleEditor.newPropSpan),
+ editor,
+ "The new property editor got focused"
+ );
+
+ info("Escape the new property editor");
+ const onBlur = once(editor.input, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ await 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 0000000000..e47023e8f3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_02.js
@@ -0,0 +1,45 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ info("Test creating a new property and escaping");
+ await addProperty(view, 1, "color", "red", {
+ commitValueWith: "VK_ESCAPE",
+ blurNewProperty: false,
+ });
+
+ is(
+ view.styleDocument.activeElement,
+ view.styleDocument.body,
+ "Correct element has focus"
+ );
+
+ const 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 0000000000..ebaa27ae4c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_03.js
@@ -0,0 +1,51 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ // Add a property to the element's style declaration, add some text,
+ // then press escape.
+
+ const elementRuleEditor = getRuleViewRuleEditor(view, 1);
+ const editor = await focusNewRuleViewProperty(elementRuleEditor);
+
+ is(
+ inplaceEditor(elementRuleEditor.newPropSpan),
+ editor,
+ "Next focused editor should be the new property editor."
+ );
+
+ EventUtils.sendString("background", view.styleWindow);
+
+ const onBlur = once(editor.input, "blur");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await 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 0000000000..eebade7d70
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-commented.js
@@ -0,0 +1,50 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testCreateNewSetOfCommentedAndUncommentedProperties(view);
+});
+
+async function testCreateNewSetOfCommentedAndUncommentedProperties(view) {
+ info("Test creating a new set of commented and uncommented properties");
+
+ info("Focusing a new property name in the rule-view");
+ const ruleEditor = getRuleViewRuleEditor(view, 0);
+ const editor = await 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"
+ );
+ const 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");
+ const onModifications = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ await onModifications;
+
+ const 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-invalid-identifier.js b/devtools/client/inspector/rules/test/browser_rules_add-property-invalid-identifier.js
new file mode 100644
index 0000000000..e4a4b0fba1
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-invalid-identifier.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test adding properties that are invalid identifiers.
+
+const TEST_URI = "<div id='testid'>Styled Node</div>";
+const TEST_DATA = [
+ { name: "1", value: "100" },
+ { name: "-1", value: "100" },
+ { name: "1a", value: "100" },
+ { name: "-11a", value: "100" },
+];
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ for (const { name, value } of TEST_DATA) {
+ info(`Test creating a new property ${name}: ${value}`);
+ const declaration = await addProperty(view, 0, name, value);
+
+ is(declaration.name, name, "Property name should have been changed.");
+ is(declaration.value, value, "Property value should have been changed.");
+ is(
+ declaration.editor.isValid(),
+ false,
+ "The declaration should be invalid."
+ );
+ }
+});
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 0000000000..f286d6f427
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property-svg.js
@@ -0,0 +1,24 @@
+/* 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://devtools/skin/images/alert.svg";
+var TEST_SELECTOR = "path";
+
+add_task(async function () {
+ await addTab(TEST_URL);
+ const { inspector, view } = await openRuleView();
+ await selectNode(TEST_SELECTOR, inspector);
+
+ info("Test creating a new property");
+ await addProperty(view, 0, "fill", "red");
+
+ is(
+ await 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 0000000000..34c6a2066d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property_01.js
@@ -0,0 +1,31 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ info("Test creating a new property");
+ const textProp = await 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 0000000000..fbe38c92ef
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-property_02.js
@@ -0,0 +1,78 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ info("Focus the new property name field");
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = await focusNewRuleViewProperty(ruleEditor);
+ const 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");
+ const onNameAdded = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+
+ await 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."
+ );
+ const textProp = ruleEditor.rule.textProps[1];
+ is(
+ editor,
+ inplaceEditor(textProp.editor.valueSpan),
+ "Should be editing the value span now."
+ );
+
+ info("Entering the property value");
+ const onValueAdded = view.once("ruleview-changed");
+ editor.input.value = "purple";
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ await 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 0000000000..ddb99464e8
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js
@@ -0,0 +1,31 @@
+/* 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(async function () {
+ await addTab(
+ "data:text/html;charset=utf-8,<div id='testid'>Styled Node</div>"
+ );
+ const { inspector, view } = await openRuleView();
+
+ info("Selecting the test node");
+ await selectNode("#testid", inspector);
+
+ info("Adding a new rule for this node and blurring the new selector field");
+ await addNewRuleAndDismissEditor(inspector, view, "#testid", 1);
+
+ info("Adding a new property for this rule");
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ const onRuleViewChanged = view.once("ruleview-changed");
+ ruleEditor.addProperty("font-weight", "bold", "", true);
+ await onRuleViewChanged;
+
+ const textProps = ruleEditor.rule.textProps;
+ const 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-and-remove-style-node.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-and-remove-style-node.js
new file mode 100644
index 0000000000..1505f70c32
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-and-remove-style-node.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that bug 1736412 is fixed
+// We press "add new rule", then we remove the style node
+// We then try to press "add new rule again"
+
+const TEST_URI = '<div id="testid">Test Node</div>';
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("#testid", inspector);
+ await addNewRule(inspector, view);
+ await testNewRule(view, 1);
+ await testRemoveStyleNode();
+ await addNewRule(inspector, view);
+ await testNewRule(view, 1);
+});
+
+function testNewRule(view) {
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+ const 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("KEY_Escape");
+}
+
+async function testRemoveStyleNode() {
+ info("Removing the style node from the dom");
+ const nbStyleSheets = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => {
+ content.document.styleSheets[0].ownerNode.remove();
+ return content.document.styleSheets.length;
+ }
+ );
+ is(nbStyleSheets, 0, "Style node has been removed");
+}
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 0000000000..578f50c5a8
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-button-state.js
@@ -0,0 +1,50 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await testDisabledButton(inspector, view);
+});
+
+async function testDisabledButton(inspector, view) {
+ const node = "#testid";
+
+ info("Selecting a real element");
+ await selectNode(node, inspector);
+ ok(!view.addRuleButton.disabled, "Add rule button should be enabled");
+
+ info("Select a null element");
+ await view.selectElement(null);
+ ok(view.addRuleButton.disabled, "Add rule button should be disabled");
+
+ info("Selecting a real element");
+ await selectNode(node, inspector);
+ ok(!view.addRuleButton.disabled, "Add rule button should be enabled");
+
+ info("Selecting a pseudo element");
+ const pseudo = await getNodeFront("#pseudo", inspector);
+ const children = await inspector.walker.children(pseudo);
+ const before = children.nodes[0];
+ await selectNode(before, inspector);
+ ok(view.addRuleButton.disabled, "Add rule button should be disabled");
+
+ info("Selecting a real element");
+ await 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-csp.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-csp.js
new file mode 100644
index 0000000000..85d91a621d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-csp.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = `
+<!doctype html>
+<html>
+ <head>
+ <meta http-equiv="Content-Security-Policy" content="style-src 'none'">
+ </head>
+ <body>
+ <div id="testid"></div>
+ </body>
+</html>
+`;
+
+// Tests adding a new rule works on a page with CSP style-src none.
+add_task(async function () {
+ await addTab(`data:text/html;charset=utf-8,${encodeURIComponent(TEST_URI)}`);
+ const { inspector, view } = await openRuleView();
+
+ info("Selecting the test node");
+ await selectNode("#testid", inspector);
+
+ info("Adding a new rule for this node and blurring the new selector field");
+ await addNewRuleAndDismissEditor(inspector, view, "#testid", 1);
+
+ info("Adding a new property for this rule");
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ const onRuleViewChanged = view.once("ruleview-changed");
+ ruleEditor.addProperty("color", "red", "", true);
+ await onRuleViewChanged;
+
+ const textProps = ruleEditor.rule.textProps;
+ const prop = textProps[textProps.length - 1];
+ is(prop.name, "color", "The last property name is color");
+ is(prop.value, "red", "The last property value is red");
+});
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 0000000000..c5323983fc
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-edit-selector.js
@@ -0,0 +1,54 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ await addNewRule(inspector, view);
+ await testEditSelector(view, "span");
+
+ info("Selecting the modified element with the new rule");
+ await selectNode("span", inspector);
+ await checkModifiedElement(view, "span");
+});
+
+async function testEditSelector(view, name) {
+ info("Test editing existing selector field");
+ const idRuleEditor = getRuleViewRuleEditor(view, 1);
+ const editor = idRuleEditor.selectorText.ownerDocument.activeElement;
+
+ info("Entering a new selector name and committing");
+ editor.value = name;
+
+ info("Waiting for rule view to update");
+ const onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await 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 0000000000..5a46978bb7
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-iframes.js
@@ -0,0 +1,53 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+ await addNewRuleAndDismissEditor(inspector, view, "div", 1);
+ await addNewProperty(view, 1, "color", "red");
+
+ await selectNodeInFrames(["#frame1", "div"], inspector);
+ await addNewRuleAndDismissEditor(inspector, view, "div", 1);
+ await addNewProperty(view, 1, "color", "blue");
+
+ await selectNodeInFrames(["#frame2", "div"], inspector);
+ await addNewRuleAndDismissEditor(inspector, view, "div", 1);
+ await 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.
+ */
+async function addNewProperty(view, index, name, value) {
+ const idRuleEditor = getRuleViewRuleEditor(view, index);
+ info(`Adding new property "${name}: ${value};"`);
+
+ const onRuleViewChanged = view.once("ruleview-changed");
+ idRuleEditor.addProperty(name, value, "", true);
+ await onRuleViewChanged;
+
+ const textProps = idRuleEditor.rule.textProps;
+ const 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 0000000000..ef3a958875
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-namespace-elements.js
@@ -0,0 +1,40 @@
+/* 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(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+
+ for (const data of TEST_DATA) {
+ const { node, expected } = data;
+ await selectNode(node, inspector);
+ await 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 0000000000..2963da9034
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-pseudo-class.js
@@ -0,0 +1,64 @@
+/* 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"],
+ [":focus-within"],
+ [":hover", ":focus-within"],
+ [":hover", ":active", ":focus-within"],
+];
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#element", inspector);
+
+ for (const data of TEST_DATA) {
+ await runTestData(inspector, view, data);
+ }
+});
+
+async function runTestData(inspector, view, pseudoClasses) {
+ await setPseudoLocks(inspector, view, pseudoClasses);
+
+ const expected = EXPECTED_SELECTOR + pseudoClasses.join("");
+ await addNewRuleAndDismissEditor(inspector, view, expected, 1);
+
+ await resetPseudoLocks(inspector, view);
+}
+
+async function setPseudoLocks(inspector, view, pseudoClasses) {
+ if (!pseudoClasses.length) {
+ return;
+ }
+
+ for (const pseudoClass of pseudoClasses) {
+ const checkbox = getPseudoClassCheckbox(view, pseudoClass);
+ if (checkbox) {
+ checkbox.click();
+ }
+ await inspector.once("rule-view-refreshed");
+ }
+}
+
+async function resetPseudoLocks(inspector, view) {
+ for (const checkbox of view.pseudoClassCheckboxes) {
+ if (checkbox.checked) {
+ checkbox.click();
+ await 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 0000000000..e9647f8351
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-then-property-edit-selector.js
@@ -0,0 +1,82 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ await addNewRuleAndDismissEditor(inspector, view, "#testid", 1);
+
+ info("Adding a new property to the new rule");
+ await testAddingProperty(view, 1);
+
+ info("Editing existing selector field");
+ await testEditSelector(view, "span");
+
+ info("Selecting the modified element");
+ await selectNode("span", inspector);
+
+ info("Check new rule and property exist in the modified element");
+ await checkModifiedElement(view, "span", 1);
+});
+
+function testAddingProperty(view, index) {
+ const ruleEditor = getRuleViewRuleEditor(view, index);
+ ruleEditor.addProperty("font-weight", "bold", "", true);
+ const textProps = ruleEditor.rule.textProps;
+ const 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");
+}
+
+async function testEditSelector(view, name) {
+ const idRuleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ const editor = await 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");
+ const onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await 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.");
+
+ const idRuleEditor = getRuleViewRuleEditor(view, index);
+ const textProps = idRuleEditor.rule.textProps;
+ const 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 0000000000..b3b23a6ae9
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js
@@ -0,0 +1,44 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("#testid", inspector);
+ await addNewRuleFromContextMenu(inspector, view);
+ await testNewRule(view);
+});
+
+async function addNewRuleFromContextMenu(inspector, view) {
+ info("Waiting for context menu to be shown");
+
+ const allMenuItems = openStyleContextMenuAndGetAllItems(view, view.element);
+ const 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");
+ const onRuleViewChanged = view.once("ruleview-changed");
+ menuitemAddRule.click();
+ await onRuleViewChanged;
+}
+
+function testNewRule(view) {
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+ const 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("KEY_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 0000000000..19315e33e2
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule.js
@@ -0,0 +1,46 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ for (const data of TEST_DATA) {
+ const { node, expected } = data;
+ await selectNode(node, inspector);
+ await 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 0000000000..406e03d69c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_authored.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for as-authored styles.
+
+async function createTestContent(style) {
+ const html = `<style type="text/css">
+ ${style}
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>`;
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(html));
+
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ return view;
+}
+
+add_task(async function () {
+ const view = await createTestContent(
+ "#testid {" +
+ // Invalid property.
+ " something: random;" +
+ // Invalid value.
+ " color: orang;" +
+ // Override.
+ " background-color: blue;" +
+ " background-color: #f06;" +
+ "} "
+ );
+
+ const elementStyle = view._elementStyle;
+
+ const expected = [
+ { name: "something", overridden: true, isNameValid: false, isValid: false },
+ { name: "color", overridden: true, isNameValid: true, isValid: false },
+ {
+ name: "background-color",
+ overridden: true,
+ isNameValid: true,
+ isValid: true,
+ },
+ {
+ name: "background-color",
+ overridden: false,
+ isNameValid: true,
+ isValid: true,
+ },
+ ];
+
+ const rule = elementStyle.rules[1];
+
+ for (let i = 0; i < expected.length; ++i) {
+ const prop = rule.textProps[i];
+ is(prop.name, expected[i].name, "Check name for prop " + i);
+ is(
+ prop.overridden,
+ expected[i].overridden,
+ "Check overridden for prop " + i
+ );
+ is(
+ prop.isNameValid(),
+ expected[i].isNameValid,
+ "Check if property name is valid for prop " + i
+ );
+ is(
+ prop.isValid(),
+ expected[i].isValid,
+ "Check if whole declaration is valid 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 0000000000..ea163d509d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_authored_color.js
@@ -0,0 +1,78 @@
+/* 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: "#f06", result: "#0f0" },
+ {
+ name: "rgb",
+ id: "test2",
+ color: "rgb(0,128,250)",
+ result: "rgb(0, 255, 0)",
+ },
+ // Test case preservation.
+ { name: "hex", id: "test3", color: "#F06", result: "#0F0" },
+];
+
+add_task(async function () {
+ Services.prefs.setCharPref("devtools.defaultColorUnit", "authored");
+
+ let html = "";
+ for (const { color, id } of colors) {
+ html += `<div id="${id}" style="color: ${color}">Styled Node</div>`;
+ }
+
+ const tab = await addTab(
+ "data:text/html;charset=utf-8," + encodeURIComponent(html)
+ );
+
+ const { inspector, view } = await openRuleView();
+
+ for (const color of colors) {
+ const selector = "#" + color.id;
+ await selectNode(selector, inspector);
+
+ const swatch = getRuleViewProperty(
+ view,
+ "element",
+ "color"
+ ).valueSpan.querySelector(".ruleview-colorswatch");
+ const cPicker = view.tooltips.getTooltip("colorPicker");
+ const onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ await onColorPickerReady;
+
+ await simulateColorPickerChange(view, cPicker, [0, 255, 0, 1], {
+ selector,
+ name: "color",
+ value: "rgb(0, 255, 0)",
+ });
+
+ const spectrum = cPicker.spectrum;
+ const onHidden = cPicker.tooltip.once("hidden");
+ // Validating the color change ends up updating the rule view twice
+ const onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2);
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+ await onHidden;
+ await onRuleViewChanged;
+
+ is(
+ getRuleViewPropertyValue(view, "element", "color"),
+ color.result,
+ "changing the color preserved the unit for " + color.name
+ );
+ }
+
+ await gDevTools.closeToolboxForTab(tab);
+ 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 0000000000..d8067d5dc2
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_authored_override.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for as-authored styles.
+
+async function createTestContent(style) {
+ const html = `<style type="text/css">
+ ${style}
+ </style>
+ <div id="testid" class="testclass">Styled Node</div>`;
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(html));
+
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ return view;
+}
+
+add_task(async function () {
+ const gradientText1 = "(orange, blue);";
+ const gradientText2 = "(pink, teal);";
+
+ const view = await createTestContent(
+ "#testid {" +
+ " background-image: linear-gradient" +
+ gradientText1 +
+ " background-image: -ms-linear-gradient" +
+ gradientText2 +
+ " background-image: linear-gradient" +
+ gradientText2 +
+ "} "
+ );
+
+ const elementStyle = view._elementStyle;
+ const rule = elementStyle.rules[1];
+
+ // Initially the last property should be active.
+ for (let i = 0; i < 3; ++i) {
+ const prop = rule.textProps[i];
+ is(prop.name, "background-image", "check the property name");
+ is(prop.overridden, i !== 2, "check overridden for " + i);
+ }
+
+ await togglePropStatus(view, rule.textProps[2]);
+
+ // Now the first property should be active.
+ for (let i = 0; i < 3; ++i) {
+ const 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 0000000000..4042695eaa
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js
@@ -0,0 +1,26 @@
+/* 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(async function () {
+ await addTab(TEST_URL);
+ const { inspector, view } = await openRuleView();
+
+ await 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_class_panel_add.js b/devtools/client/inspector/rules/test/browser_rules_class_panel_add.js
new file mode 100644
index 0000000000..dfba3a63bf
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_class_panel_add.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that classes can be added in the class panel
+
+// This array contains the list of test cases. Each test case contains these properties:
+// - {String} textEntered The text to be entered in the field
+// - {Boolean} expectNoMutation Set to true if we shouldn't wait for a DOM mutation
+// - {Array} expectedClasses The expected list of classes to be applied to the DOM and to
+// be found in the class panel
+const TEST_ARRAY = [
+ {
+ textEntered: "",
+ expectNoMutation: true,
+ expectedClasses: [],
+ },
+ {
+ textEntered: "class",
+ expectedClasses: ["class"],
+ },
+ {
+ textEntered: "class",
+ expectNoMutation: true,
+ expectedClasses: ["class"],
+ },
+ {
+ textEntered: "a a a a a a a a a a",
+ expectedClasses: ["class", "a"],
+ },
+ {
+ textEntered: "class2 class3",
+ expectedClasses: ["class", "a", "class2", "class3"],
+ },
+ {
+ textEntered: " ",
+ expectNoMutation: true,
+ expectedClasses: ["class", "a", "class2", "class3"],
+ },
+ {
+ textEntered: " class4",
+ expectedClasses: ["class", "a", "class2", "class3", "class4"],
+ },
+ {
+ textEntered: " \t class5 \t \t\t ",
+ expectedClasses: ["class", "a", "class2", "class3", "class4", "class5"],
+ },
+];
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8,");
+ const { inspector, view } = await openRuleView();
+
+ info("Open the class panel");
+ view.showClassPanel();
+
+ const textField = inspector.panelDoc.querySelector(
+ "#ruleview-class-panel .add-class"
+ );
+ ok(textField, "The input field exists in the class panel");
+
+ textField.focus();
+
+ let onMutation;
+ for (const { textEntered, expectNoMutation, expectedClasses } of TEST_ARRAY) {
+ if (!expectNoMutation) {
+ onMutation = inspector.once("markupmutation");
+ }
+
+ info(`Enter the test string in the field: ${textEntered}`);
+ for (const key of textEntered.split("")) {
+ const onPreviewMutation = inspector.once("markupmutation");
+ EventUtils.synthesizeKey(key, {}, view.styleWindow);
+ await onPreviewMutation;
+ }
+
+ info("Submit the change and wait for the textfield to become empty");
+ const onEmpty = waitForFieldToBeEmpty(textField);
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+
+ if (!expectNoMutation) {
+ info("Wait for the DOM to change");
+ await onMutation;
+ }
+
+ await onEmpty;
+
+ info("Check the state of the DOM node");
+ const className = await getContentPageElementAttribute("body", "class");
+ const expectedClassName = expectedClasses.length
+ ? expectedClasses.join(" ")
+ : null;
+ is(className, expectedClassName, "The DOM node has the right className");
+
+ info("Check the content of the class panel");
+ checkClassPanelContent(
+ view,
+ expectedClasses.map(name => {
+ return { name, state: true };
+ })
+ );
+ }
+});
+
+function waitForFieldToBeEmpty(textField) {
+ return waitForSuccess(() => !textField.value);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_class_panel_autocomplete.js b/devtools/client/inspector/rules/test/browser_rules_class_panel_autocomplete.js
new file mode 100644
index 0000000000..9cd7993244
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_class_panel_autocomplete.js
@@ -0,0 +1,266 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the autocomplete for the class panel input behaves as expected. The test also
+// checks that we're using the cache to retrieve the data when we can do so, and that the
+// cache gets cleared, and we're getting data from the server, when there's mutation on
+// the page.
+
+const TEST_URI = `${URL_ROOT}doc_class_panel_autocomplete.html`;
+
+add_task(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+ const { addEl: textInput } = view.classListPreviewer;
+ await selectNode("#auto-div-id-3", inspector);
+
+ info("Open the class panel");
+ view.showClassPanel();
+
+ textInput.focus();
+
+ info("Type a letter and check that the popup has the expected items");
+ const allClasses = [
+ "auto-body-class-1",
+ "auto-body-class-2",
+ "auto-bold",
+ "auto-cssom-primary-color",
+ "auto-div-class-1",
+ "auto-div-class-2",
+ "auto-html-class-1",
+ "auto-html-class-2",
+ "auto-inline-class-1",
+ "auto-inline-class-2",
+ "auto-inline-class-3",
+ "auto-inline-class-4",
+ "auto-inline-class-5",
+ "auto-stylesheet-class-1",
+ "auto-stylesheet-class-2",
+ "auto-stylesheet-class-3",
+ "auto-stylesheet-class-4",
+ "auto-stylesheet-class-5",
+ ];
+
+ const { autocompletePopup } = view.classListPreviewer;
+ let onPopupOpened = autocompletePopup.once("popup-opened");
+ EventUtils.synthesizeKey("a", {}, view.styleWindow);
+ await waitForClassApplied("auto-body-class-1", "#auto-div-id-3");
+ await onPopupOpened;
+ await checkAutocompleteItems(
+ autocompletePopup,
+ allClasses,
+ "The autocomplete popup has all the classes used in the DOM and in stylesheets"
+ );
+
+ info(
+ "Test that typing more letters filters the autocomplete popup and uses the cache mechanism"
+ );
+ EventUtils.sendString("uto-b", view.styleWindow);
+ await waitForClassApplied("auto-body-class-1", "#auto-div-id-3");
+
+ await checkAutocompleteItems(
+ autocompletePopup,
+ allClasses.filter(cls => cls.startsWith("auto-b")),
+ "The autocomplete popup was filtered with the content of the input"
+ );
+ ok(true, "The results were retrieved from the cache mechanism");
+
+ info("Test that autocomplete shows up-to-date results");
+ // Modify the content page and assert that the new class is displayed in the
+ // autocomplete if the user types a new letter.
+ const onNewMutation = inspector.inspectorFront.walker.once("new-mutations");
+ await ContentTask.spawn(gBrowser.selectedBrowser, null, async function () {
+ content.document.body.classList.add("auto-body-added-by-script");
+ });
+ await onNewMutation;
+ await waitForClassApplied("auto-body-added-by-script", "body");
+
+ // close & reopen the autocomplete so it picks up the added to another element while autocomplete was opened
+ let onPopupClosed = autocompletePopup.once("popup-closed");
+ EventUtils.synthesizeKey("KEY_Escape", {}, view.styleWindow);
+ await onPopupClosed;
+
+ // input is now auto-body
+ onPopupOpened = autocompletePopup.once("popup-opened");
+ EventUtils.sendString("ody", view.styleWindow);
+ await onPopupOpened;
+ await checkAutocompleteItems(
+ autocompletePopup,
+ [
+ ...allClasses.filter(cls => cls.startsWith("auto-body")),
+ "auto-body-added-by-script",
+ ].sort(),
+ "The autocomplete popup was filtered with the content of the input"
+ );
+
+ info(
+ "Test that typing a letter that won't match any of the item closes the popup"
+ );
+ // input is now auto-bodyy
+ onPopupClosed = autocompletePopup.once("popup-closed");
+ EventUtils.synthesizeKey("y", {}, view.styleWindow);
+ await waitForClassApplied("auto-bodyy", "#auto-div-id-3");
+ await onPopupClosed;
+ ok(true, "The popup was closed as expected");
+ await checkAutocompleteItems(autocompletePopup, [], "The popup was cleared");
+
+ info("Clear the input and try to autocomplete again");
+ textInput.select();
+ EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow);
+ // Wait a bit so the debounced function can be executed
+ await wait(200);
+
+ onPopupOpened = autocompletePopup.once("popup-opened");
+ EventUtils.synthesizeKey("a", {}, view.styleWindow);
+ await onPopupOpened;
+
+ await checkAutocompleteItems(
+ autocompletePopup,
+ [...allClasses, "auto-body-added-by-script"].sort(),
+ "The autocomplete popup was updated with the new class added to the DOM"
+ );
+
+ info("Test keyboard shortcut when the popup is displayed");
+ // Escape to hide
+ onPopupClosed = autocompletePopup.once("popup-closed");
+ EventUtils.synthesizeKey("KEY_Escape", {}, view.styleWindow);
+ await onPopupClosed;
+ ok(true, "The popup was closed when hitting escape");
+
+ // Ctrl + space to show again
+ onPopupOpened = autocompletePopup.once("popup-opened");
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, view.styleWindow);
+ await onPopupOpened;
+ ok(true, "Popup was opened again with Ctrl+Space");
+ await checkAutocompleteItems(
+ autocompletePopup,
+ [...allClasses, "auto-body-added-by-script"].sort()
+ );
+
+ // Arrow left to hide
+ onPopupClosed = autocompletePopup.once("popup-closed");
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, view.styleWindow);
+ await onPopupClosed;
+ ok(true, "The popup was closed as when hitting ArrowLeft");
+
+ // Arrow right and Ctrl + space to show again, and Arrow Right to accept
+ onPopupOpened = autocompletePopup.once("popup-opened");
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, view.styleWindow);
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, view.styleWindow);
+ await onPopupOpened;
+
+ onPopupClosed = autocompletePopup.once("popup-closed");
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, view.styleWindow);
+ await waitForClassApplied("auto-body-added-by-script", "#auto-div-id-3");
+ await onPopupClosed;
+ is(
+ textInput.value,
+ "auto-body-added-by-script",
+ "ArrowRight puts the selected item in the input and closes the popup"
+ );
+
+ // Backspace to show the list again
+ onPopupOpened = autocompletePopup.once("popup-opened");
+ EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow);
+ await waitForClassApplied("auto-body-added-by-script", "#auto-div-id-3");
+ await onPopupOpened;
+ is(
+ textInput.value,
+ "auto-body-added-by-scrip",
+ "ArrowRight puts the selected item in the input and closes the popup"
+ );
+ await checkAutocompleteItems(
+ autocompletePopup,
+ ["auto-body-added-by-script"],
+ "The autocomplete does show the matching items after hitting backspace"
+ );
+
+ // Enter to accept
+ onPopupClosed = autocompletePopup.once("popup-closed");
+ EventUtils.synthesizeKey("KEY_Enter", {}, view.styleWindow);
+ await waitForClassRemoved("auto-body-added-by-scrip");
+ await onPopupClosed;
+ is(
+ textInput.value,
+ "auto-body-added-by-script",
+ "Enter puts the selected item in the input and closes the popup"
+ );
+
+ // Backspace to show again
+ onPopupOpened = autocompletePopup.once("popup-opened");
+ EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow);
+ await waitForClassApplied("auto-body-added-by-script", "#auto-div-id-3");
+ await onPopupOpened;
+ is(
+ textInput.value,
+ "auto-body-added-by-scrip",
+ "ArrowRight puts the selected item in the input and closes the popup"
+ );
+ await checkAutocompleteItems(
+ autocompletePopup,
+ ["auto-body-added-by-script"],
+ "The autocomplete does show the matching items after hitting backspace"
+ );
+
+ // Tab to accept
+ onPopupClosed = autocompletePopup.once("popup-closed");
+ EventUtils.synthesizeKey("KEY_Tab", {}, view.styleWindow);
+ await onPopupClosed;
+ is(
+ textInput.value,
+ "auto-body-added-by-script",
+ "Tab puts the selected item in the input and closes the popup"
+ );
+ await waitForClassRemoved("auto-body-added-by-scrip");
+});
+
+async function checkAutocompleteItems(
+ autocompletePopup,
+ expectedItems,
+ assertionMessage
+) {
+ await waitForSuccess(
+ () =>
+ getAutocompleteItems(autocompletePopup).length === expectedItems.length
+ );
+ const items = getAutocompleteItems(autocompletePopup);
+ const formatList = list => `\n${list.join("\n")}\n`;
+ is(formatList(items), formatList(expectedItems), assertionMessage);
+}
+
+function getAutocompleteItems(autocompletePopup) {
+ return Array.from(autocompletePopup._panel.querySelectorAll("li")).map(
+ el => el.textContent
+ );
+}
+
+async function waitForClassApplied(cls, selector) {
+ info("Wait for class to be applied: " + cls);
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [cls, selector],
+ async (_cls, _selector) => {
+ return ContentTaskUtils.waitForCondition(() =>
+ content.document.querySelector(_selector).classList.contains(_cls)
+ );
+ }
+ );
+ // Wait for debounced functions to be executed
+ await wait(200);
+}
+
+async function waitForClassRemoved(cls) {
+ info("Wait for class to be removed: " + cls);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [cls], async _cls => {
+ return ContentTaskUtils.waitForCondition(
+ () =>
+ !content.document
+ .querySelector("#auto-div-id-3")
+ .classList.contains(_cls)
+ );
+ });
+ // Wait for debounced functions to be executed
+ await wait(200);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_class_panel_content.js b/devtools/client/inspector/rules/test/browser_rules_class_panel_content.js
new file mode 100644
index 0000000000..52d490b1d0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_class_panel_content.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that class panel shows the right content when selecting various nodes.
+
+// This array contains the list of test cases. Each test case contains these properties:
+// - {String} inputClassName The className on a node
+// - {Array} expectedClasses The expected list of classes in the class panel
+const TEST_ARRAY = [
+ {
+ inputClassName: "",
+ expectedClasses: [],
+ },
+ {
+ inputClassName: " a a a a a a a a a",
+ expectedClasses: ["a"],
+ },
+ {
+ inputClassName: "c1 c2 c3 c4 c5",
+ expectedClasses: ["c1", "c2", "c3", "c4", "c5"],
+ },
+ {
+ inputClassName: "a a b b c c a a b b c c",
+ expectedClasses: ["a", "b", "c"],
+ },
+ {
+ inputClassName:
+ "ajdhfkasjhdkjashdkjghaskdgkauhkbdhvliashdlghaslidghasldgliashdglhasli",
+ expectedClasses: [
+ "ajdhfkasjhdkjashdkjghaskdgkauhkbdhvliashdlghaslidghasldgliashdglhasli",
+ ],
+ },
+ {
+ inputClassName:
+ "c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 " +
+ "c10 c11 c12 c13 c14 c15 c16 c17 c18 c19 " +
+ "c20 c21 c22 c23 c24 c25 c26 c27 c28 c29 " +
+ "c30 c31 c32 c33 c34 c35 c36 c37 c38 c39 " +
+ "c40 c41 c42 c43 c44 c45 c46 c47 c48 c49",
+ expectedClasses: [
+ "c0",
+ "c1",
+ "c2",
+ "c3",
+ "c4",
+ "c5",
+ "c6",
+ "c7",
+ "c8",
+ "c9",
+ "c10",
+ "c11",
+ "c12",
+ "c13",
+ "c14",
+ "c15",
+ "c16",
+ "c17",
+ "c18",
+ "c19",
+ "c20",
+ "c21",
+ "c22",
+ "c23",
+ "c24",
+ "c25",
+ "c26",
+ "c27",
+ "c28",
+ "c29",
+ "c30",
+ "c31",
+ "c32",
+ "c33",
+ "c34",
+ "c35",
+ "c36",
+ "c37",
+ "c38",
+ "c39",
+ "c40",
+ "c41",
+ "c42",
+ "c43",
+ "c44",
+ "c45",
+ "c46",
+ "c47",
+ "c48",
+ "c49",
+ ],
+ },
+ {
+ inputClassName: " \n \n class1 \t class2 \t\tclass3\t",
+ expectedClasses: ["class1", "class2", "class3"],
+ },
+];
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8,<div>");
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("div", inspector);
+
+ info("Open the class panel");
+ view.showClassPanel();
+
+ for (const { inputClassName, expectedClasses } of TEST_ARRAY) {
+ info(`Apply the '${inputClassName}' className to the node`);
+ const onMutation = inspector.once("markupmutation");
+ await setContentPageElementAttribute("div", "class", inputClassName);
+ await onMutation;
+
+ info("Check the content of the class panel");
+ checkClassPanelContent(
+ view,
+ expectedClasses.map(name => {
+ return { name, state: true };
+ })
+ );
+ }
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_class_panel_edit.js b/devtools/client/inspector/rules/test/browser_rules_class_panel_edit.js
new file mode 100644
index 0000000000..f43a5fd487
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_class_panel_edit.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that classes can be toggled in the class panel
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8,<body class='class1 class2'>");
+ const { view } = await openRuleView();
+
+ info("Open the class panel");
+ view.showClassPanel();
+
+ info(
+ "Click on class1 and check that the checkbox is unchecked and the DOM is updated"
+ );
+ await toggleClassPanelCheckBox(view, "class1");
+ checkClassPanelContent(view, [
+ { name: "class1", state: false },
+ { name: "class2", state: true },
+ ]);
+ let newClassName = await getContentPageElementAttribute("body", "class");
+ is(newClassName, "class2", "The class attribute has been updated in the DOM");
+
+ info("Click on class2 and check the same thing");
+ await toggleClassPanelCheckBox(view, "class2");
+ checkClassPanelContent(view, [
+ { name: "class1", state: false },
+ { name: "class2", state: false },
+ ]);
+ newClassName = await getContentPageElementAttribute("body", "class");
+ is(newClassName, "", "The class attribute has been updated in the DOM");
+
+ info("Click on class2 and checks that the class is added again");
+ await toggleClassPanelCheckBox(view, "class2");
+ checkClassPanelContent(view, [
+ { name: "class1", state: false },
+ { name: "class2", state: true },
+ ]);
+ newClassName = await getContentPageElementAttribute("body", "class");
+ is(newClassName, "class2", "The class attribute has been updated in the DOM");
+
+ info("And finally, click on class1 again and checks it is added again");
+ await toggleClassPanelCheckBox(view, "class1");
+ checkClassPanelContent(view, [
+ { name: "class1", state: true },
+ { name: "class2", state: true },
+ ]);
+ newClassName = await getContentPageElementAttribute("body", "class");
+ is(
+ newClassName,
+ "class1 class2",
+ "The class attribute has been updated in the DOM"
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_class_panel_invalid_nodes.js b/devtools/client/inspector/rules/test/browser_rules_class_panel_invalid_nodes.js
new file mode 100644
index 0000000000..24a0dfbcfc
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_class_panel_invalid_nodes.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the class panel shows a message when invalid nodes are selected.
+// text nodes, pseudo-elements, DOCTYPE, comment nodes.
+
+add_task(async function () {
+ await addTab(`data:text/html;charset=utf-8,
+ <body>
+ <style>div::after {content: "test";}</style>
+ <!-- comment -->
+ Some text
+ <div></div>
+ </body>`);
+
+ info("Open the class panel");
+ const { inspector, view } = await openRuleView();
+ view.showClassPanel();
+
+ info("Selecting the DOCTYPE node");
+ const { nodes } = await inspector.walker.children(inspector.walker.rootNode);
+ await selectNode(nodes[0], inspector);
+ checkMessageIsDisplayed(view);
+
+ info("Selecting the comment node");
+ const styleNode = await getNodeFront("style", inspector);
+ const commentNode = await inspector.walker.nextSibling(styleNode);
+ await selectNode(commentNode, inspector);
+ checkMessageIsDisplayed(view);
+
+ info("Selecting the text node");
+ const textNode = await inspector.walker.nextSibling(commentNode);
+ await selectNode(textNode, inspector);
+ checkMessageIsDisplayed(view);
+
+ info("Selecting the ::after pseudo-element");
+ const divNode = await getNodeFront("div", inspector);
+ const pseudoElement = (await inspector.walker.children(divNode)).nodes[0];
+ await selectNode(pseudoElement, inspector);
+ checkMessageIsDisplayed(view);
+});
+
+function checkMessageIsDisplayed(view) {
+ ok(
+ view.classListPreviewer.classesEl.querySelector(".no-classes"),
+ "The message is displayed"
+ );
+ checkClassPanelContent(view, []);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_class_panel_mutation.js b/devtools/client/inspector/rules/test/browser_rules_class_panel_mutation.js
new file mode 100644
index 0000000000..4d3340b2ea
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_class_panel_mutation.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that class panel updates on markup mutations
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8,<div class='c1 c2'>");
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("div", inspector);
+
+ info("Open the class panel");
+ view.showClassPanel();
+
+ info("Trigger an unrelated mutation on the div (id attribute change)");
+ let onMutation = view.inspector.once("markupmutation");
+ await setContentPageElementAttribute("div", "id", "test-id");
+ await onMutation;
+
+ info("Check that the panel still contains the right classes");
+ checkClassPanelContent(view, [
+ { name: "c1", state: true },
+ { name: "c2", state: true },
+ ]);
+
+ info("Trigger a class mutation on a different, unknown, node");
+ onMutation = view.inspector.once("markupmutation");
+ await setContentPageElementAttribute("body", "class", "test-class");
+ await onMutation;
+
+ info("Check that the panel still contains the right classes");
+ checkClassPanelContent(view, [
+ { name: "c1", state: true },
+ { name: "c2", state: true },
+ ]);
+
+ info("Trigger a class mutation on the current node");
+ onMutation = view.inspector.once("markupmutation");
+ await setContentPageElementAttribute("div", "class", "c3 c4");
+ await onMutation;
+
+ info("Check that the panel now contains the new classes");
+ checkClassPanelContent(view, [
+ { name: "c3", state: true },
+ { name: "c4", state: true },
+ ]);
+
+ info("Change the state of one of the new classes");
+ await toggleClassPanelCheckBox(view, "c4");
+ checkClassPanelContent(view, [
+ { name: "c3", state: true },
+ { name: "c4", state: false },
+ ]);
+
+ info("Select another node");
+ await selectNode("body", inspector);
+
+ info("Trigger a class mutation on the div");
+ onMutation = view.inspector.once("markupmutation");
+ await setContentPageElementAttribute("div", "class", "c5 c6 c7");
+ await onMutation;
+
+ info(
+ "Go back to the previous node and check the content of the class panel." +
+ "Even if hidden, it should have refreshed when we changed the DOM"
+ );
+ await selectNode("div", inspector);
+ checkClassPanelContent(view, [
+ { name: "c5", state: true },
+ { name: "c6", state: true },
+ { name: "c7", state: true },
+ ]);
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_class_panel_state_preserved.js b/devtools/client/inspector/rules/test/browser_rules_class_panel_state_preserved.js
new file mode 100644
index 0000000000..c4c20e3976
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_class_panel_state_preserved.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that class states are preserved when switching to other nodes
+
+add_task(async function () {
+ await addTab(
+ "data:text/html;charset=utf-8,<body class='class1 class2 class3'><div>"
+ );
+ const { inspector, view } = await openRuleView();
+
+ info("Open the class panel");
+ view.showClassPanel();
+
+ info("With the <body> selected, uncheck class2 and class3 in the panel");
+ await toggleClassPanelCheckBox(view, "class2");
+ await toggleClassPanelCheckBox(view, "class3");
+
+ info("Now select the <div> so the panel gets refreshed");
+ await selectNode("div", inspector);
+ is(
+ view.classPanel.querySelectorAll("[type=checkbox]").length,
+ 0,
+ "The panel content doesn't contain any checkboxes anymore"
+ );
+
+ info("Select the <body> again");
+ await selectNode("body", inspector);
+ const checkBoxes = view.classPanel.querySelectorAll("[type=checkbox]");
+
+ is(checkBoxes[0].dataset.name, "class1", "The first checkbox is class1");
+ is(checkBoxes[0].checked, true, "The first checkbox is still checked");
+
+ is(checkBoxes[1].dataset.name, "class2", "The second checkbox is class2");
+ is(checkBoxes[1].checked, false, "The second checkbox is still unchecked");
+
+ is(checkBoxes[2].dataset.name, "class3", "The third checkbox is class3");
+ is(checkBoxes[2].checked, false, "The third checkbox is still unchecked");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_class_panel_toggle.js b/devtools/client/inspector/rules/test/browser_rules_class_panel_toggle.js
new file mode 100644
index 0000000000..42b411481f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_class_panel_toggle.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the class panel can be toggled.
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8,<body class='class1 class2'>");
+ const { inspector, view } = await openRuleView();
+
+ info("Check that the toggle button exists");
+ const button = inspector.panelDoc.querySelector("#class-panel-toggle");
+ ok(button, "The class panel toggle button exists");
+ is(view.classToggle, button, "The rule-view refers to the right element");
+
+ info("Check that the panel exists and is hidden by default");
+ const panel = inspector.panelDoc.querySelector("#ruleview-class-panel");
+ ok(panel, "The class panel exists");
+ is(view.classPanel, panel, "The rule-view refers to the right element");
+ ok(panel.hasAttribute("hidden"), "The panel is hidden");
+
+ info("Click on the button to show the panel");
+ button.click();
+ ok(!panel.hasAttribute("hidden"), "The panel is shown");
+ ok(button.classList.contains("checked"), "The button is checked");
+
+ info("Click again to hide the panel");
+ button.click();
+ ok(panel.hasAttribute("hidden"), "The panel is hidden");
+ ok(!button.classList.contains("checked"), "The button is unchecked");
+
+ info("Open the pseudo-class panel first, then the class panel");
+ view.pseudoClassToggle.click();
+ ok(
+ !view.pseudoClassPanel.hasAttribute("hidden"),
+ "The pseudo-class panel is shown"
+ );
+ button.click();
+ ok(!panel.hasAttribute("hidden"), "The panel is shown");
+ ok(
+ view.pseudoClassPanel.hasAttribute("hidden"),
+ "The pseudo-class panel is hidden"
+ );
+
+ info("Click again on the pseudo-class button");
+ view.pseudoClassToggle.click();
+ ok(panel.hasAttribute("hidden"), "The panel is hidden");
+ ok(
+ !view.pseudoClassPanel.hasAttribute("hidden"),
+ "The pseudo-class panel is shown"
+ );
+});
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 0000000000..d7d92f8a8f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorUnit.js
@@ -0,0 +1,70 @@
+/* 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(async function () {
+ const TESTS = [
+ { name: "hex", result: "#0f0" },
+ { name: "rgb", result: "rgb(0, 255, 0)" },
+ ];
+
+ for (const { name, result } of TESTS) {
+ info("starting test for " + name);
+ Services.prefs.setCharPref("devtools.defaultColorUnit", name);
+
+ const tab = await addTab(
+ "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)
+ );
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("#testid", inspector);
+ await basicTest(view, name, result);
+
+ await gDevTools.closeToolboxForTab(tab);
+ gBrowser.removeCurrentTab();
+ }
+});
+
+async function basicTest(view, name, result) {
+ const cPicker = view.tooltips.getTooltip("colorPicker");
+ const swatch = getRuleViewProperty(
+ view,
+ "#testid",
+ "color"
+ ).valueSpan.querySelector(".ruleview-colorswatch");
+ const onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ await onColorPickerReady;
+
+ await simulateColorPickerChange(view, cPicker, [0, 255, 0, 1], {
+ selector: "#testid",
+ name: "color",
+ value: "rgb(0, 255, 0)",
+ });
+
+ const spectrum = cPicker.spectrum;
+ const onHidden = cPicker.tooltip.once("hidden");
+ // Validating the color change ends up updating the rule view twice
+ const onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2);
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+ await onHidden;
+ await onRuleViewChanged;
+
+ is(
+ getRuleViewPropertyValue(view, "#testid", "color"),
+ result,
+ "changing the color used the " + name + " unit"
+ );
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation.js b/devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation.js
new file mode 100644
index 0000000000..172acebcaf
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation.js
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test color scheme simulation.
+const TEST_URI = URL_ROOT_SSL + "doc_media_queries.html";
+
+add_task(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view, toolbox } = await openRuleView();
+
+ info("Check that the color scheme simulation buttons exist");
+ const lightButton = inspector.panelDoc.querySelector(
+ "#color-scheme-simulation-light-toggle"
+ );
+ const darkButton = inspector.panelDoc.querySelector(
+ "#color-scheme-simulation-dark-toggle"
+ );
+ ok(lightButton, "The light color scheme simulation button exists");
+ ok(darkButton, "The dark color scheme simulation button exists");
+
+ is(
+ isButtonChecked(lightButton),
+ false,
+ "At first, the light button isn't checked"
+ );
+ is(
+ isButtonChecked(darkButton),
+ false,
+ "At first, the dark button isn't checked"
+ );
+
+ // Define functions checking if the rule view display the expected property.
+ const divHasDefaultStyling = async () =>
+ (await getPropertiesForRuleIndex(view, 1)).has("background-color:yellow");
+ const divHasDarkSchemeStyling = async () =>
+ (await getPropertiesForRuleIndex(view, 1)).has("background-color:darkblue");
+ const iframeElHasDefaultStyling = async () =>
+ (await getPropertiesForRuleIndex(view, 1)).has("background:cyan");
+ const iframeHasDarkSchemeStyling = async () =>
+ (await getPropertiesForRuleIndex(view, 1)).has("background:darkred");
+
+ info(
+ "Select the div that will change according to conditions in prefered color scheme"
+ );
+ await selectNode("div", inspector);
+ ok(
+ await divHasDefaultStyling(),
+ "The rule view shows the expected initial rule"
+ );
+
+ info("Click on the dark button");
+ darkButton.click();
+ await waitFor(() => isButtonChecked(darkButton));
+ ok(true, "The dark button is checked");
+ is(
+ isButtonChecked(lightButton),
+ false,
+ "the light button state didn't change when enabling dark mode"
+ );
+
+ await waitFor(() => divHasDarkSchemeStyling());
+ is(
+ getRuleViewAncestorRulesDataTextByIndex(view, 1),
+ "@media (prefers-color-scheme: dark)",
+ "The rules view was updated with the rule from the dark scheme media query"
+ );
+
+ info("Select the node from the remote iframe");
+ await selectNodeInFrames(["iframe", "html"], inspector);
+
+ ok(
+ await iframeHasDarkSchemeStyling(),
+ "The simulation is also applied on the remote iframe"
+ );
+ is(
+ getRuleViewAncestorRulesDataTextByIndex(view, 1),
+ "@media (prefers-color-scheme: dark)",
+ "The prefers-color-scheme media query is displayed"
+ );
+
+ info("Select the top level div again");
+ await selectNode("div", inspector);
+
+ info("Click the light button simulate light mode");
+ lightButton.click();
+ await waitFor(() => isButtonChecked(lightButton));
+ ok(true, "The button has the expected light state");
+ // TODO: Actually simulate light mode. This might require to set the OS-level preference
+ // to dark as the default state might consume the rule from the like scheme media query.
+
+ is(
+ isButtonChecked(darkButton),
+ false,
+ "the dark button was unchecked when enabling light mode"
+ );
+
+ await waitFor(() => divHasDefaultStyling());
+
+ info("Click the light button to disable simulation");
+ lightButton.click();
+ await waitFor(() => !isButtonChecked(lightButton));
+ ok(true, "The button isn't checked anymore");
+ await waitFor(() => divHasDefaultStyling());
+ ok(true, "We're not simulating color-scheme anymore");
+
+ info("Select the node from the remote iframe again");
+ await selectNodeInFrames(["iframe", "html"], inspector);
+ await waitFor(() => iframeElHasDefaultStyling());
+ ok(true, "The simulation stopped on the remote iframe as well");
+
+ info("Check that reloading keep the selected simulation");
+ await selectNode("div", inspector);
+ darkButton.click();
+ await waitFor(() => divHasDarkSchemeStyling());
+
+ await navigateTo(TEST_URI);
+ await selectNode("div", inspector);
+ await waitFor(() => view.element.children[1]);
+ ok(
+ await divHasDarkSchemeStyling(),
+ "dark mode is still simulated after reloading the page"
+ );
+ is(
+ getRuleViewAncestorRulesDataTextByIndex(view, 1),
+ "@media (prefers-color-scheme: dark)",
+ "The prefers-color-scheme media query is displayed on the rule after reloading"
+ );
+
+ await selectNodeInFrames(["iframe", "html"], inspector);
+ await waitFor(() => iframeHasDarkSchemeStyling());
+ ok(true, "simulation is still applied to the iframe after reloading");
+ is(
+ getRuleViewAncestorRulesDataTextByIndex(view, 1),
+ "@media (prefers-color-scheme: dark)",
+ "The prefers-color-scheme media query is still displayed on the rule for the element in iframe after reloading"
+ );
+
+ info("Check that closing DevTools reset the simulation");
+ await toolbox.destroy();
+ const matchesPrefersDarkColorSchemeMedia = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => {
+ const { matches } = content.matchMedia("(prefers-color-scheme: dark)");
+ return matches;
+ }
+ );
+ is(
+ matchesPrefersDarkColorSchemeMedia,
+ false,
+ "color scheme simulation is disabled after closing DevTools"
+ );
+});
+
+function isButtonChecked(el) {
+ return el.classList.contains("checked");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation_bfcache.js b/devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation_bfcache.js
new file mode 100644
index 0000000000..c4995a3cb3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation_bfcache.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test color scheme simulation.
+const TEST_URI = URL_ROOT_SSL + "doc_media_queries.html";
+
+add_task(async function testBfCacheNavigationWithDevTools() {
+ await addTab(TEST_URI);
+ const { inspector, toolbox } = await openRuleView();
+
+ is(await isSimulationEnabled(), false, "color scheme simulation is disabled");
+
+ const darkButton = inspector.panelDoc.querySelector(
+ "#color-scheme-simulation-dark-toggle"
+ );
+ ok(darkButton, "The dark color scheme simulation button exists");
+
+ info("Click on the dark button");
+ darkButton.click();
+ await waitFor(async () => isSimulationEnabled());
+ is(await isSimulationEnabled(), true, "color scheme simulation is enabled");
+
+ info("Navigate to a different URL and disable the color simulation");
+ await navigateTo(TEST_URI + "?someparameter");
+ darkButton.click();
+ await waitFor(async () => !(await isSimulationEnabled()));
+ is(await isSimulationEnabled(), false, "color scheme simulation is disabled");
+
+ info(
+ "Perform a bfcache navigation and check that the simulation is still disabled"
+ );
+ const waitForDevToolsReload = await watchForDevToolsReload(
+ gBrowser.selectedBrowser
+ );
+ gBrowser.goBack();
+ await waitForDevToolsReload();
+ is(await isSimulationEnabled(), false, "color scheme simulation is disabled");
+
+ await toolbox.destroy();
+});
+
+add_task(async function testBfCacheNavigationAfterClosingDevTools() {
+ await addTab(TEST_URI);
+ const { inspector, toolbox } = await openRuleView();
+
+ is(await isSimulationEnabled(), false, "color scheme simulation is disabled");
+
+ const darkButton = inspector.panelDoc.querySelector(
+ "#color-scheme-simulation-dark-toggle"
+ );
+ ok(darkButton, "The dark color scheme simulation button exists");
+
+ info("Click on the dark button");
+ darkButton.click();
+ await waitFor(async () => isSimulationEnabled());
+ is(await isSimulationEnabled(), true, "color scheme simulation is enabled");
+
+ // Wait for the iframe target to be processed before destroying the toolbox,
+ // to avoid unhandled promise rejections.
+ // The iframe URL starts with https://example.org/document-builder.sjs
+ let onIframeProcessed;
+
+ // Do not wait for the additional target in the noeft-nofis flavor.
+ const isNoEFTNoFis = !isFissionEnabled() && !isEveryFrameTargetEnabled();
+ if (!isNoEFTNoFis) {
+ const iframeURL = "https://example.org/document-builder.sjs";
+ onIframeProcessed = waitForTargetProcessed(toolbox.commands, targetFront =>
+ targetFront.url.startsWith(iframeURL)
+ );
+ }
+
+ info("Navigate to a different URL");
+ await navigateTo(TEST_URI + "?someparameter");
+
+ info("Wait for the iframe target to be processed by target-command");
+ await onIframeProcessed;
+
+ info("Close DevTools to disable the simulation");
+ await toolbox.destroy();
+ await waitFor(async () => !(await isSimulationEnabled()));
+ is(await isSimulationEnabled(), false, "color scheme simulation is disabled");
+
+ info(
+ "Perform a bfcache navigation and check that the simulation is still disabled"
+ );
+ const awaitPageShow = BrowserTestUtils.waitForContentEvent(
+ gBrowser.selectedBrowser,
+ "pageshow"
+ );
+ gBrowser.goBack();
+ await awaitPageShow;
+
+ is(await isSimulationEnabled(), false, "color scheme simulation is disabled");
+});
+
+function isSimulationEnabled() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const { matches } = content.matchMedia("(prefers-color-scheme: dark)");
+ return matches;
+ });
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation_meta.js b/devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation_meta.js
new file mode 100644
index 0000000000..ba8163f1c0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation_meta.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test page should follow the overall color scheme.
+// Default colors / background colors should change depending on the scheme.
+const TEST_URI = `https://example.com/document-builder.sjs?html=
+ <!DOCTYPE html>
+ <html lang=en>
+ <meta charset=utf-8>
+ <meta name=color-scheme content="dark light">
+ Hello!
+`;
+add_task(async function () {
+ await addTab(TEST_URI);
+ const { inspector } = await openRuleView();
+
+ // Retrieve light and dark scheme buttons.
+ const lightButton = inspector.panelDoc.querySelector(
+ "#color-scheme-simulation-light-toggle"
+ );
+ const darkButton = inspector.panelDoc.querySelector(
+ "#color-scheme-simulation-dark-toggle"
+ );
+
+ // Read the color scheme to know if we should click on the light or dark button
+ // to trigger a change.
+ info("Retrieve the default color scheme");
+ let isDarkScheme = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => {
+ return content.matchMedia("(prefers-color-scheme: dark)").matches;
+ }
+ );
+
+ // Clicks on the simulation button which triggers a color-scheme change.
+ // If current scheme is light, click on dark and vice-versa.
+ function toggleScheme() {
+ info(`Switch color scheme to ${isDarkScheme ? "light" : "dark"} mode`);
+ isDarkScheme ? lightButton.click() : darkButton.click();
+ isDarkScheme = !isDarkScheme;
+ }
+
+ info("Retrieve the initial (text) color of the page");
+ const initialColor = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => {
+ return content.getComputedStyle(content.document.body).color;
+ }
+ );
+
+ toggleScheme();
+
+ info("Wait until the color of the page changes");
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [initialColor],
+ _initialColor => {
+ return ContentTaskUtils.waitForCondition(() => {
+ const newColor = content.getComputedStyle(content.document.body).color;
+ return newColor !== _initialColor;
+ });
+ }
+ );
+
+ toggleScheme();
+
+ info("Wait until the color of the page goes back to the initial value");
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [initialColor],
+ _initialColor => {
+ return ContentTaskUtils.waitForCondition(() => {
+ const newColor = content.getComputedStyle(content.document.body).color;
+ return newColor === _initialColor;
+ });
+ }
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation_rdm.js b/devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation_rdm.js
new file mode 100644
index 0000000000..5045b9c675
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation_rdm.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test color scheme simulation when RDM is toggled
+const TEST_URI = URL_ROOT + "doc_media_queries.html";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+
+ info("Check that the color scheme simulation buttons exist");
+ const darkButton = inspector.panelDoc.querySelector(
+ "#color-scheme-simulation-dark-toggle"
+ );
+
+ // Define functions checking if the rule view display the expected property.
+ const divHasDefaultStyling = async () =>
+ (await getPropertiesForRuleIndex(view, 1)).has("background-color:yellow");
+ const divHasDarkSchemeStyling = async () =>
+ (await getPropertiesForRuleIndex(view, 1)).has("background-color:darkblue");
+
+ info(
+ "Select the div that will change according to conditions in prefered color scheme"
+ );
+ await selectNode("div", inspector);
+ ok(
+ await divHasDefaultStyling(),
+ "The rule view shows the expected initial rule"
+ );
+
+ info("Open responsive design mode");
+ await openRDM(tab);
+
+ info("Click on the dark button");
+ darkButton.click();
+ await waitFor(() => isButtonChecked(darkButton));
+ ok(true, "The dark button is checked");
+
+ await waitFor(() => divHasDarkSchemeStyling());
+ ok(
+ true,
+ "The rules view was updated with the rule view from the dark scheme media query"
+ );
+
+ info("Close responsive design mode");
+ await closeRDM(tab);
+
+ info("Wait for a bit before checking dark mode is still enabled");
+ await wait(1000);
+ ok(isButtonChecked(darkButton), "button is still checked");
+ ok(
+ await divHasDarkSchemeStyling(),
+ "dark mode color-scheme simulation is still enabled"
+ );
+
+ info("Click the button to disable simulation");
+ darkButton.click();
+ await waitFor(() => !isButtonChecked(darkButton));
+ ok(true, "The button isn't checked anymore");
+ await waitFor(() => divHasDefaultStyling());
+ ok(true, "We're not simulating color-scheme anymore");
+
+ info("Check that enabling dark-mode simulation before RDM does work as well");
+ darkButton.click();
+ await waitFor(() => isButtonChecked(darkButton));
+ await waitFor(() => divHasDarkSchemeStyling());
+ ok(
+ true,
+ "The rules view was updated with the rule view from the dark scheme media query"
+ );
+
+ info("Open responsive design mode again");
+ await openRDM(tab);
+
+ info("Click the button to disable simulation while RDM is still opened");
+ darkButton.click();
+ await waitFor(() => !isButtonChecked(darkButton));
+ ok(true, "The button isn't checked anymore");
+ await waitFor(() => divHasDefaultStyling());
+ ok(true, "We're not simulating color-scheme anymore");
+
+ info("Close responsive design mode");
+ await closeRDM(tab);
+});
+
+function isButtonChecked(el) {
+ return el.classList.contains("checked");
+}
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 0000000000..3a2c553f13
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_01.js
@@ -0,0 +1,73 @@
+/* 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://branding/content/icon64.png"), linear-gradient(white, #F06 400px);
+ }
+ </style>
+ Testing the color picker tooltip!
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { view } = await openRuleView();
+
+ // Bug 1767679 - Use { wait: true } to avoid frequent intermittents on linux.
+ const property = await getRuleViewProperty(view, "body", "background", {
+ wait: true,
+ });
+
+ const value = property.valueSpan;
+ const swatch = value.querySelectorAll(".ruleview-colorswatch")[0];
+ const url = value.querySelector(".theme-link");
+ await testImageTooltipAfterColorChange(swatch, url, view);
+});
+
+async function testImageTooltipAfterColorChange(swatch, url, ruleView) {
+ info("First, verify that the image preview tooltip works");
+ let previewTooltip = await assertShowPreviewTooltip(ruleView, url);
+ await assertTooltipHiddenOnMouseOut(previewTooltip, url);
+
+ info("Open the color picker tooltip and change the color");
+ const picker = ruleView.tooltips.getTooltip("colorPicker");
+ const onColorPickerReady = picker.once("ready");
+ swatch.click();
+ await onColorPickerReady;
+
+ await simulateColorPickerChange(ruleView, picker, [0, 0, 0, 1], {
+ selector: "body",
+ name: "background-image",
+ value:
+ 'url("chrome://branding/content/icon64.png"), linear-gradient(rgb(0, 0, 0), rgb(255, 0, 102) 400px)',
+ });
+
+ const spectrum = picker.spectrum;
+ const onHidden = picker.tooltip.once("hidden");
+
+ // On "RETURN", `ruleview-changed` is triggered when the SwatchBasedEditorTooltip calls
+ // its `commit` method, and then another event is emitted when the editor is hidden.
+ const onModifications = waitForNEvents(ruleView, "ruleview-changed", 2);
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+ await onHidden;
+ await 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");
+ previewTooltip = await assertShowPreviewTooltip(ruleView, url);
+
+ await assertTooltipHiddenOnMouseOut(previewTooltip, url);
+}
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 0000000000..3262349274
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.js
@@ -0,0 +1,66 @@
+/* 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://branding/content/icon64.png")
+ no-repeat center center;
+ }
+ </style>
+ Testing the color picker tooltip!
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { view } = await openRuleView();
+ await testColorChangeIsntRevertedWhenOtherTooltipIsShown(view);
+});
+
+async function testColorChangeIsntRevertedWhenOtherTooltipIsShown(ruleView) {
+ let swatch = getRuleViewProperty(
+ ruleView,
+ "body",
+ "background"
+ ).valueSpan.querySelector(".ruleview-colorswatch");
+
+ info("Open the color picker tooltip and change the color");
+ const picker = ruleView.tooltips.getTooltip("colorPicker");
+ const onColorPickerReady = picker.once("ready");
+ swatch.click();
+ await onColorPickerReady;
+
+ await simulateColorPickerChange(ruleView, picker, [0, 0, 0, 1], {
+ selector: "body",
+ name: "background-color",
+ value: "rgb(0, 0, 0)",
+ });
+
+ const spectrum = picker.spectrum;
+
+ const onModifications = waitForNEvents(ruleView, "ruleview-changed", 2);
+ const onHidden = picker.tooltip.once("hidden");
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+ await onHidden;
+ await onModifications;
+
+ info("Open the image preview tooltip");
+ const value = getRuleViewProperty(ruleView, "body", "background").valueSpan;
+ const url = value.querySelector(".theme-link");
+ const previewTooltip = await assertShowPreviewTooltip(ruleView, url);
+
+ 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");
+
+ await assertTooltipHiddenOnMouseOut(previewTooltip, url);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click-or-keyboard-activation.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click-or-keyboard-activation.js
new file mode 100644
index 0000000000..0a44f4813f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click-or-keyboard-activation.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that color pickers appear when clicking or using keyboard on color swatches.
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ color: red;
+ background-color: #ededed;
+ background-image: url(chrome://branding/content/icon64.png);
+ border: 2em solid rgba(120, 120, 120, .5);
+ }
+ </style>
+ Testing the color picker tooltip!
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { view } = await openRuleView();
+
+ const propertiesToTest = ["color", "background-color", "border"];
+
+ for (const property of propertiesToTest) {
+ info(`Test that the colorpicker appears on swatch click for ${property}`);
+ await testColorPickerAppearsOnColorSwatchActivation(view, property);
+
+ info(
+ `Test that swatch is focusable and colorpicker can be activated with a keyboard for ${property}`
+ );
+ await testColorPickerAppearsOnColorSwatchActivation(view, property, true);
+ }
+});
+
+async function testColorPickerAppearsOnColorSwatchActivation(
+ view,
+ property,
+ withKeyboard = false
+) {
+ const value = getRuleViewProperty(view, "body", property).valueSpan;
+ const swatch = value.querySelector(".ruleview-colorswatch");
+
+ const cPicker = view.tooltips.getTooltip("colorPicker");
+ ok(cPicker, "The rule-view has an expected colorPicker widget");
+
+ const cPickerPanel = cPicker.tooltip.panel;
+ ok(cPickerPanel, "The XUL panel for the color picker exists");
+
+ const onColorPickerReady = cPicker.once("ready");
+ if (withKeyboard) {
+ // Focus on the property value span
+ const doc = value.ownerDocument;
+ value.focus();
+
+ // Tab to focus on the color swatch
+ EventUtils.sendKey("Tab");
+ is(doc.activeElement, swatch, "Swatch successfully receives focus.");
+
+ // Press enter on the swatch to simulate click and open color picker
+ EventUtils.sendKey("Return");
+ } else {
+ swatch.click();
+ }
+ await onColorPickerReady;
+
+ info("The color picker was displayed");
+ ok(!inplaceEditor(swatch.parentNode), "The inplace editor wasn't displayed");
+
+ await 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 0000000000..5649d050d2
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-commit-on-ENTER.js
@@ -0,0 +1,76 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { view } = await openRuleView();
+
+ const swatch = getRuleViewProperty(
+ view,
+ "body",
+ "border"
+ ).valueSpan.querySelector(".ruleview-colorswatch");
+ await testPressingEnterCommitsChanges(swatch, view);
+});
+
+async function testPressingEnterCommitsChanges(swatch, ruleView) {
+ const cPicker = ruleView.tooltips.getTooltip("colorPicker");
+
+ const onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ await onColorPickerReady;
+
+ await simulateColorPickerChange(ruleView, cPicker, [0, 255, 0, 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"
+ );
+
+ const onModified = ruleView.once("ruleview-changed");
+ const spectrum = cPicker.spectrum;
+ const onHidden = cPicker.tooltip.once("hidden");
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+ await onHidden;
+ await onModified;
+
+ is(
+ await 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-contrast-ratio.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-contrast-ratio.js
new file mode 100644
index 0000000000..a0280e7106
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-contrast-ratio.js
@@ -0,0 +1,232 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests color pickers displays expected contrast ratio information.
+
+const TEST_URI = `
+ <style type="text/css">
+ :root {
+ --title-color: #000;
+ }
+
+ body {
+ color: #eee;
+ background-color: #eee;
+ }
+
+ h1 {
+ color: var(--title-color);
+ }
+
+ div {
+ color: var(--title-color);
+ /* Try to to have consistent results over different platforms:
+ - using hardstop-ish gradient so the min and max contrast are computed against white and black background
+ - having min-content width will make sure we get the gradient only cover the text, and not the whole screen width
+ */
+ background-image: linear-gradient(to right, black, white);
+ width: min-content;
+ font-size: 100px;
+ }
+
+ section {
+ color: color-mix(in srgb, blue, var(--title-color) 50%);
+ }
+ </style>
+ <h1>Testing the color picker contrast ratio data</h1>
+ <div>————</div>
+ <section>mixed colors</section>
+`;
+
+add_task(async function () {
+ await pushPref("layout.css.color-mix.enabled", true);
+
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await checkColorPickerConstrastData({
+ view,
+ label: "Displays contrast information on color property",
+ ruleViewPropertyEl: getRuleViewProperty(view, "body", "color"),
+ expectVisibleContrast: true,
+ expectedContrastValueResult: "FAIL",
+ expectedContrastValueTitle:
+ "Does not meet WCAG standards for accessible text. Calculated against background: rgba(238,238,238,1)",
+ expectedContrastValueScore: "1.00",
+ });
+
+ await checkColorPickerConstrastData({
+ view,
+ label: "Does not display contrast information on background-color property",
+ ruleViewPropertyEl: getRuleViewProperty(view, "body", "background-color"),
+ expectVisibleContrast: false,
+ });
+
+ await selectNode("h1", inspector);
+ await checkColorPickerConstrastData({
+ view,
+ label: "Displays contrast information on color from CSS variable",
+ ruleViewPropertyEl: getRuleViewProperty(view, "h1", "color"),
+ expectVisibleContrast: true,
+ expectedContrastValueResult: "AAA",
+ expectedContrastValueTitle:
+ "Meets WCAG AAA standards for accessible text. Calculated against background: rgba(238,238,238,1)",
+ expectedContrastValueScore: "18.10",
+ });
+
+ await selectNode("div", inspector);
+ await checkColorPickerConstrastData({
+ view,
+ label:
+ "Displays range contrast information on color against gradient background",
+ ruleViewPropertyEl: getRuleViewProperty(view, "div", "color"),
+ expectVisibleContrast: true,
+ expectContrastRange: true,
+ expectedMinContrastValueResult: "FAIL",
+ expectedMinContrastValueTitle:
+ "Does not meet WCAG standards for accessible text. Calculated against background: rgba(0,0,0,1)",
+ expectedMinContrastValueScore: "1.00",
+ expectedMaxContrastValueResult: "AAA",
+ expectedMaxContrastValueTitle:
+ "Meets WCAG AAA standards for accessible text. Calculated against background: rgba(255,255,255,1)",
+ expectedMaxContrastValueScore: "19.77",
+ });
+
+ await selectNode("section", inspector);
+ await checkColorPickerConstrastData({
+ view,
+ label:
+ "Does not displays contrast information on color within color-mix function (#1)",
+ ruleViewPropertyEl: getRuleViewProperty(view, "section", "color"),
+ swatchIndex: 0,
+ expectVisibleContrast: false,
+ });
+ await checkColorPickerConstrastData({
+ view,
+ label:
+ "Does not displays contrast information on color within color-mix function (#2)",
+ ruleViewPropertyEl: getRuleViewProperty(view, "section", "color"),
+ swatchIndex: 1,
+ expectVisibleContrast: false,
+ });
+});
+
+async function checkColorPickerConstrastData({
+ view,
+ ruleViewPropertyEl,
+ label,
+ swatchIndex = 0,
+ expectVisibleContrast,
+ expectedContrastValueResult,
+ expectedContrastValueTitle,
+ expectedContrastValueScore,
+ expectContrastRange = false,
+ expectedMinContrastValueResult,
+ expectedMinContrastValueTitle,
+ expectedMinContrastValueScore,
+ expectedMaxContrastValueResult,
+ expectedMaxContrastValueTitle,
+ expectedMaxContrastValueScore,
+}) {
+ info(`Checking color picker: "${label}"`);
+ const cPicker = view.tooltips.getTooltip("colorPicker");
+ ok(cPicker, "The rule-view has an expected colorPicker widget");
+ const cPickerPanel = cPicker.tooltip.panel;
+ ok(cPickerPanel, "The XUL panel for the color picker exists");
+
+ const colorSwatch = ruleViewPropertyEl.valueSpan.querySelectorAll(
+ ".ruleview-colorswatch"
+ )[swatchIndex];
+
+ const onColorPickerReady = cPicker.once("ready");
+ colorSwatch.click();
+ await onColorPickerReady;
+ ok(true, "The color picker was displayed");
+
+ const contrastEl = cPickerPanel.querySelector(".spectrum-color-contrast");
+
+ if (!expectVisibleContrast) {
+ ok(
+ !contrastEl.classList.contains("visible"),
+ "Contrast information is not displayed, as expected"
+ );
+ await hideTooltipAndWaitForRuleViewChanged(cPicker, view);
+ return;
+ }
+
+ ok(
+ contrastEl.classList.contains("visible"),
+ "Contrast information is displayed"
+ );
+ is(
+ contrastEl.classList.contains("range"),
+ expectContrastRange,
+ `Contrast information ${
+ expectContrastRange ? "has" : "does not have"
+ } a result range`
+ );
+
+ if (expectContrastRange) {
+ const minContrastValueEl = contrastEl.querySelector(
+ ".contrast-ratio-range .contrast-ratio-min .accessibility-contrast-value"
+ );
+ ok(
+ minContrastValueEl.classList.contains(expectedMinContrastValueResult),
+ `min contrast value has expected "${expectedMinContrastValueResult}" class`
+ );
+ // Scores vary from platform to platform, disable for now.
+ // This should be re-enabled as part of Bug 1780736
+ // is(
+ // minContrastValueEl.title,
+ // expectedMinContrastValueTitle,
+ // "min contrast value has expected title"
+ // );
+ // is(
+ // minContrastValueEl.textContent,
+ // expectedMinContrastValueScore,
+ // "min contrast value shows expected score"
+ // );
+
+ const maxContrastValueEl = contrastEl.querySelector(
+ ".contrast-ratio-range .contrast-ratio-max .accessibility-contrast-value"
+ );
+ ok(
+ maxContrastValueEl.classList.contains(expectedMaxContrastValueResult),
+ `max contrast value has expected "${expectedMaxContrastValueResult}" class`
+ );
+ // Scores vary from platform to platform, disable for now.
+ // This should be re-enabled as part of Bug 1780736
+ // is(
+ // maxContrastValueEl.title,
+ // expectedMaxContrastValueTitle,
+ // "max contrast value has expected title"
+ // );
+ // is(
+ // maxContrastValueEl.textContent,
+ // expectedMaxContrastValueScore,
+ // "max contrast value shows expected score"
+ // );
+ } else {
+ const contrastValueEl = contrastEl.querySelector(
+ ".accessibility-contrast-value"
+ );
+ ok(
+ contrastValueEl.classList.contains(expectedContrastValueResult),
+ `contrast value has expected "${expectedContrastValueResult}" class`
+ );
+ is(
+ contrastValueEl.title,
+ expectedContrastValueTitle,
+ "contrast value has expected title"
+ );
+ is(
+ contrastValueEl.textContent,
+ expectedContrastValueScore,
+ "contrast value shows expected score"
+ );
+ }
+
+ await hideTooltipAndWaitForRuleViewChanged(cPicker, view);
+}
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 0000000000..c5eef9d670
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-edit-gradient.js
@@ -0,0 +1,82 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { view } = await 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");
+ await testPickingNewColor(view);
+});
+
+function testColorParsing(view) {
+ const ruleEl = getRuleViewProperty(view, "body", "background-image");
+ ok(ruleEl, "The background-image gradient declaration was found");
+
+ const swatchEls = ruleEl.valueSpan.querySelectorAll(".ruleview-colorswatch");
+ ok(swatchEls, "The color swatch elements were found");
+ is(swatchEls.length, 3, "There are 3 color swatches");
+
+ const colorEls = ruleEl.valueSpan.querySelectorAll(".ruleview-color");
+ ok(colorEls, "The color elements were found");
+ is(colorEls.length, 3, "There are 3 color values");
+
+ const colors = ["#f06", "#333", "#000"];
+ for (let i = 0; i < colors.length; i++) {
+ is(colorEls[i].textContent, colors[i], "The right color value was found");
+ }
+}
+
+async function testPickingNewColor(view) {
+ // Grab the first color swatch and color in the gradient
+ const ruleEl = getRuleViewProperty(view, "body", "background-image");
+ const swatchEl = ruleEl.valueSpan.querySelector(".ruleview-colorswatch");
+ const colorEl = ruleEl.valueSpan.querySelector(".ruleview-color");
+
+ info("Get the color picker tooltip and clicking on the swatch to show it");
+ const cPicker = view.tooltips.getTooltip("colorPicker");
+ const onColorPickerReady = cPicker.once("ready");
+ swatchEl.click();
+ await onColorPickerReady;
+
+ const 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%)",
+ };
+ await 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(
+ await 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"
+ );
+
+ await hideTooltipAndWaitForRuleViewChanged(cPicker, view);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-element-without-quads.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-element-without-quads.js
new file mode 100644
index 0000000000..f8012ab56b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-element-without-quads.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = `
+ <style type="text/css">
+ button {
+ color: tomato;
+ background-color: green;
+ }
+ </style>
+ <!-- The space as text context for the button is mandatory here, so that the
+ firstChild of the button is an whitespace text node -->
+ <button id="button-with-no-quads"> </button>
+`;
+
+/**
+ * Check that we can still open the color picker on elements for which getQuads
+ * returns an empty array, which is used to compute the background-color
+ * contrast.
+ */
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ const hasEmptyQuads = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => {
+ const button = content.document.querySelector("button");
+ const quads = button.firstChild.getBoxQuads({
+ box: "content",
+ relativeTo: content.document,
+ createFramesForSuppressedWhitespace: false,
+ });
+ return quads.length === 0;
+ }
+ );
+ ok(hasEmptyQuads, "The test element has empty quads");
+
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("button", inspector);
+
+ const ruleEl = getRuleViewProperty(view, "button", "color");
+ ok(ruleEl, "The color declaration was found");
+
+ const swatchEl = ruleEl.valueSpan.querySelector(".ruleview-colorswatch");
+ const colorEl = ruleEl.valueSpan.querySelector(".ruleview-color");
+ is(colorEl.textContent, "tomato", "The right color value was found");
+
+ info("Open the color picker");
+ const cPicker = view.tooltips.getTooltip("colorPicker");
+ const onColorPickerReady = cPicker.once("ready");
+ swatchEl.click();
+ await onColorPickerReady;
+
+ info("Check that the background color of the button was correctly detected");
+ const contrastEl = cPicker.tooltip.container.querySelector(
+ ".contrast-value-and-swatch.contrast-ratio-single"
+ );
+ ok(
+ contrastEl.style.cssText.includes(
+ "--accessibility-contrast-bg: rgba(0,128,0,1)"
+ ),
+ "The background color contains the expected value"
+ );
+
+ info("Check that the color picker can be used");
+ await simulateColorPickerChange(view, cPicker, [255, 0, 0, 1], {
+ selector: "button",
+ name: "color",
+ value: "rgb(255, 0, 0)",
+ });
+
+ await hideTooltipAndWaitForRuleViewChanged(cPicker, view);
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-element-picker.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-element-picker.js
new file mode 100644
index 0000000000..53442989c4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-element-picker.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that on selecting colorpicker eyedropper stops picker
+// if the picker is already selected.
+
+const TEST_URI = `<style>body{background:red}</style>`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ const { toolbox, view } = await openRuleView();
+ const pickerStopped = toolbox.nodePicker.once("picker-stopped");
+
+ await startPicker(toolbox);
+
+ info("Get the background property from the rule-view");
+ const property = getRuleViewProperty(view, "body", "background");
+ const swatch = property.valueSpan.querySelector(".ruleview-colorswatch");
+ ok(swatch, "Color swatch is displayed for the background property");
+
+ info("Open the eyedropper from the colorpicker tooltip");
+ await openEyedropper(view, swatch);
+
+ info("Waiting for the picker-stopped event to be fired");
+ await pickerStopped;
+
+ ok(true, "picker-stopped event fired after eyedropper was clicked");
+});
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 0000000000..e1d290c005
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-on-tooltip.js
@@ -0,0 +1,54 @@
+/* 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://branding/content/icon64.png);
+ border: 2em solid rgba(120, 120, 120, .5);
+ }
+ </style>
+ Testing the color picker tooltip!
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { view } = await openRuleView();
+
+ const swatch = getRuleViewProperty(
+ view,
+ "body",
+ "color"
+ ).valueSpan.querySelector(".ruleview-colorswatch");
+
+ const bgImageSpan = getRuleViewProperty(
+ view,
+ "body",
+ "background-image"
+ ).valueSpan;
+ const uriSpan = bgImageSpan.querySelector(".theme-link");
+
+ const colorPicker = view.tooltips.getTooltip("colorPicker");
+ info("Showing the color picker tooltip by clicking on the color swatch");
+ const onColorPickerReady = colorPicker.once("ready");
+ swatch.click();
+ await onColorPickerReady;
+
+ info("Now showing the image preview tooltip to hide the color picker");
+ const onHidden = colorPicker.tooltip.once("hidden");
+ // Hiding the color picker refreshes the value.
+ const onRuleViewChanged = view.once("ruleview-changed");
+ const previewTooltip = await assertShowPreviewTooltip(view, uriSpan);
+ await onHidden;
+ await onRuleViewChanged;
+
+ await assertTooltipHiddenOnMouseOut(previewTooltip, uriSpan);
+
+ 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 0000000000..f641745fd2
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-multiple-changes.js
@@ -0,0 +1,110 @@
+/* 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://branding/content/icon64.png")
+ no-repeat center center;
+ }
+ p {
+ color: blue;
+ }
+ </style>
+ <p>Testing the color picker tooltip!</p>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await testSimpleMultipleColorChanges(inspector, view);
+ await testComplexMultipleColorChanges(inspector, view);
+});
+
+async function testSimpleMultipleColorChanges(inspector, ruleView) {
+ await selectNode("p", inspector);
+
+ info("Getting the <p> tag's color property");
+ const swatch = getRuleViewProperty(
+ ruleView,
+ "p",
+ "color"
+ ).valueSpan.querySelector(".ruleview-colorswatch");
+
+ info("Opening the color picker");
+ const picker = ruleView.tooltips.getTooltip("colorPicker");
+ const onColorPickerReady = picker.once("ready");
+ swatch.click();
+ await onColorPickerReady;
+
+ info("Changing the color several times");
+ const 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 (const { rgba, computed } of colors) {
+ await simulateColorPickerChange(ruleView, picker, rgba, {
+ selector: "p",
+ name: "color",
+ value: computed,
+ });
+ }
+
+ is(
+ await getComputedStyleProperty("p", null, "color"),
+ "rgb(200, 200, 200)",
+ "The color of the P tag is correct"
+ );
+}
+
+async function testComplexMultipleColorChanges(inspector, ruleView) {
+ await selectNode("body", inspector);
+
+ info("Getting the <body> tag's color property");
+ const swatch = getRuleViewProperty(
+ ruleView,
+ "body",
+ "background"
+ ).valueSpan.querySelector(".ruleview-colorswatch");
+
+ info("Opening the color picker");
+ const picker = ruleView.tooltips.getTooltip("colorPicker");
+ const onColorPickerReady = picker.once("ready");
+ swatch.click();
+ await onColorPickerReady;
+
+ info("Changing the color several times");
+ const 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 (const { rgba, computed } of colors) {
+ await simulateColorPickerChange(ruleView, picker, rgba, {
+ selector: "body",
+ name: "background-color",
+ value: computed,
+ });
+ }
+
+ info("Closing the color picker");
+ await hideTooltipAndWaitForRuleViewChanged(picker, ruleView);
+
+ is(
+ await 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 0000000000..f992e93094
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-release-outside-frame.js
@@ -0,0 +1,79 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { view } = await openRuleView();
+
+ const cSwatch = getRuleViewProperty(
+ view,
+ "element",
+ "color"
+ ).valueSpan.querySelector(".ruleview-colorswatch");
+
+ const picker = await openColorPickerForSwatch(cSwatch, view);
+ const spectrum = picker.spectrum;
+ const change = spectrum.once("changed");
+
+ info("Pressing mouse down over color picker.");
+ const onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeMouseAtCenter(
+ spectrum.dragger,
+ {
+ type: "mousedown",
+ },
+ spectrum.dragger.ownerDocument.defaultView
+ );
+ await onRuleViewChanged;
+
+ const value = await 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", 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
+ );
+});
+
+async function openColorPickerForSwatch(swatch, view) {
+ const cPicker = view.tooltips.getTooltip("colorPicker");
+ ok(cPicker, "The rule-view has an expected colorPicker widget");
+
+ const cPickerPanel = cPicker.tooltip.panel;
+ ok(cPickerPanel, "The XUL panel for the color picker exists");
+
+ const onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ await 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 0000000000..47a39d0518
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-revert-on-ESC.js
@@ -0,0 +1,67 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { view } = await openRuleView();
+ await testPressingEscapeRevertsChanges(view);
+});
+
+async function testPressingEscapeRevertsChanges(view) {
+ const { swatch, propEditor, cPicker } = await 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"
+ );
+
+ const spectrum = cPicker.spectrum;
+
+ info("Pressing ESCAPE to close the tooltip");
+ const onHidden = cPicker.tooltip.once("hidden");
+ const onModifications = view.once("ruleview-changed");
+ EventUtils.sendKey("ESCAPE", spectrum.element.ownerDocument.defaultView);
+ await onHidden;
+ await onModifications;
+
+ await waitForComputedStyleProperty(
+ "body",
+ null,
+ "background-color",
+ "rgb(237, 237, 237)"
+ );
+ is(
+ propEditor.valueSpan.textContent,
+ "#EDEDED",
+ "Got expected property value."
+ );
+}
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 0000000000..74b483172d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-swatch-displayed.js
@@ -0,0 +1,101 @@
+/* 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://branding/content/icon64.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)
+ ),
+ radial-gradient(
+ red,
+ blue
+ ),
+ conic-gradient(
+ from 90deg at 0 0,
+ lemonchiffon,
+ peachpuff
+ ),
+ repeating-linear-gradient(blue, pink),
+ repeating-radial-gradient(limegreen, bisque),
+ repeating-conic-gradient(chocolate, olive);
+ box-shadow: inset 0 0 2px 20px red, inset 0 0 2px 40px blue;
+ filter: drop-shadow(2px 2px 2px salmon);
+ text-shadow: 2px 2px color-mix(
+ in srgb,
+ color-mix(in srgb, tomato 30%, #FA8072),
+ hsla(5, 5%, 5%, 0.25) 5%
+ );
+ }
+ </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: 26 },
+ { selector: "*", propertyName: "box-shadow", nb: 2 },
+ { selector: "*", propertyName: "filter", nb: 1 },
+ { selector: "*", propertyName: "text-shadow", nb: 3 },
+];
+
+add_task(async function () {
+ await pushPref("layout.css.color-mix.enabled", true);
+
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { view } = await openRuleView();
+
+ for (const { selector, propertyName, nb } of TESTS) {
+ info(
+ "Looking for color swatches in property " +
+ propertyName +
+ " in selector " +
+ selector
+ );
+
+ const prop = (
+ await getRuleViewProperty(view, selector, propertyName, { wait: true })
+ ).valueSpan;
+ const 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_colorpicker-works-with-css-vars.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-works-with-css-vars.js
new file mode 100644
index 0000000000..6e28426b20
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-works-with-css-vars.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests color pickers work with CSS variables.
+
+const TEST_URI = `
+ <style type="text/css">
+ :root {
+ --main-bg-color: coral;
+ }
+ body {
+ color: red;
+ background-color: var(--main-bg-color);
+ border: 1px solid var(--main-bg-color);
+ }
+ </style>
+ Testing the color picker tooltip with CSS variables!
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { view } = await openRuleView();
+
+ const propertiesToTest = ["color", "background-color", "border"];
+
+ for (const property of propertiesToTest) {
+ info(`Test that the colorpicker appears on swatch click for ${property}`);
+ await testColorPickerAppearsOnColorSwatchActivation(view, property);
+
+ info(
+ `Test that swatch is focusable and colorpicker can be activated with a keyboard for ${property}`
+ );
+ await testColorPickerAppearsOnColorSwatchActivation(view, property, true);
+ }
+});
+
+async function testColorPickerAppearsOnColorSwatchActivation(
+ view,
+ property,
+ withKeyboard = false
+) {
+ const value = (
+ await getRuleViewProperty(view, "body", property, { wait: true })
+ ).valueSpan;
+ const swatch = value.querySelector(".ruleview-colorswatch");
+
+ const cPicker = view.tooltips.getTooltip("colorPicker");
+ ok(cPicker, "The rule-view has an expected colorPicker widget");
+
+ const cPickerPanel = cPicker.tooltip.panel;
+ ok(cPickerPanel, "The XUL panel for the color picker exists");
+
+ const onColorPickerReady = cPicker.once("ready");
+ if (withKeyboard) {
+ // Focus on the property value span
+ const doc = value.ownerDocument;
+ value.focus();
+
+ // Tab to focus on the color swatch
+ EventUtils.sendKey("Tab");
+ is(doc.activeElement, swatch, "Swatch successfully receives focus.");
+
+ // Press enter on the swatch to simulate click and open color picker
+ EventUtils.sendKey("Return");
+ } else {
+ swatch.click();
+ }
+ await onColorPickerReady;
+
+ info("The color picker was displayed");
+ ok(!inplaceEditor(swatch.parentNode), "The inplace editor wasn't displayed");
+
+ await hideTooltipAndWaitForRuleViewChanged(cPicker, view);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-wrap-focus.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-wrap-focus.js
new file mode 100644
index 0000000000..36ef6f15cb
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-wrap-focus.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that focus stays inside color picker on TAB and Shift + TAB
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ color: red;
+ background-color: #ededed;
+ background-image: url(chrome://branding/content/icon64.png);
+ border: 2em solid rgba(120, 120, 120, .5);
+ }
+ </style>
+ Testing the color picker tooltip!
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { view } = await openRuleView();
+
+ info("Focus on the property value span");
+ getRuleViewProperty(view, "body", "color").valueSpan.focus();
+
+ const cPicker = view.tooltips.getTooltip("colorPicker");
+ const onColorPickerReady = cPicker.once("ready");
+
+ info(
+ "Tab to focus on the color swatch and press enter to simulate a click event"
+ );
+ EventUtils.sendKey("Tab");
+ EventUtils.sendKey("Return");
+
+ await onColorPickerReady;
+ const doc = cPicker.spectrum.element.ownerDocument;
+ ok(
+ doc.activeElement.classList.contains("spectrum-color"),
+ "Focus is initially on the spectrum dragger when color picker is shown."
+ );
+
+ info("Test that tabbing should move focus to the next focusable elements.");
+ testFocusOnTab(doc, "devtools-button");
+ testFocusOnTab(doc, "spectrum-hue-input");
+ testFocusOnTab(doc, "spectrum-alpha-input");
+ testFocusOnTab(doc, "learn-more");
+
+ info(
+ "Test that tabbing on the last element wraps focus to the first element."
+ );
+ testFocusOnTab(doc, "spectrum-color");
+
+ info(
+ "Test that shift tabbing on the first element wraps focus to the last element."
+ );
+ testFocusOnTab(doc, "learn-more", true);
+
+ info(
+ "Test that shift tabbing should move focus to the previous focusable elements."
+ );
+ testFocusOnTab(doc, "spectrum-alpha-input", true);
+ testFocusOnTab(doc, "spectrum-hue-input", true);
+ testFocusOnTab(doc, "devtools-button", true);
+ testFocusOnTab(doc, "spectrum-color", true);
+
+ await hideTooltipAndWaitForRuleViewChanged(cPicker, view);
+});
+
+function testFocusOnTab(doc, expectedClass, shiftKey = false) {
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey });
+ ok(
+ doc.activeElement.classList.contains(expectedClass),
+ "Focus is on the correct element."
+ );
+}
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 0000000000..d9333fe40c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js
@@ -0,0 +1,148 @@
+/* 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", "d", OPEN, SELECTED],
+ ["VK_DOWN", "direction", OPEN, SELECTED],
+ ["VK_DOWN", "display", OPEN, SELECTED],
+ ["VK_UP", "direction", OPEN, SELECTED],
+ ["VK_UP", "d", 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { toolbox, inspector, view } = await openRuleView();
+
+ info("Test autocompletion after 1st page load");
+ await runAutocompletionTest(toolbox, inspector, view);
+
+ info("Test autocompletion after page navigation");
+ await reloadBrowser();
+ await runAutocompletionTest(toolbox, inspector, view);
+});
+
+async function runAutocompletionTest(toolbox, inspector, view) {
+ info("Selecting the test node");
+ await selectNode("h1", inspector);
+
+ info("Focusing the css property editable field");
+ const propertyName = view.styleDocument.querySelectorAll(
+ ".ruleview-propertyname"
+ )[0];
+ const editor = await focusEditableField(view, propertyName);
+
+ info("Starting to test for css property completion");
+ for (let i = 0; i < testData.length; i++) {
+ if (!testData[i].length) {
+ continue;
+ }
+ await testCompletion(testData[i], editor, view);
+ }
+}
+
+async 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)/gi.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.
+ const popupEvent = open ? "popup-opened" : "popup-closed";
+ const onPopupEvent =
+ editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null;
+
+ info("Synthesizing key " + key);
+ EventUtils.synthesizeKey(key, {}, view.styleWindow);
+
+ // Flush the debounce for the preview text.
+ view.debounce.flush();
+
+ await onSuggest;
+ await 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 0000000000..0d0546bc8d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js
@@ -0,0 +1,128 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { toolbox, inspector, view } = await openRuleView();
+
+ info("Test autocompletion after 1st page load");
+ await runAutocompletionTest(toolbox, inspector, view);
+
+ info("Test autocompletion after page navigation");
+ await reloadBrowser();
+ await runAutocompletionTest(toolbox, inspector, view);
+});
+
+async function runAutocompletionTest(toolbox, inspector, view) {
+ info("Selecting the test node");
+ await selectNode("h1", inspector);
+
+ const prop = getTextProperty(view, 0, { color: "red" });
+
+ info("Focusing the css property editable value");
+ let editor = await 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);
+ await testCompletion(testData[i], editor, view);
+ }
+}
+
+async 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.
+ const popupEvent = open ? "popup-opened" : "popup-closed";
+ const onPopupEvent =
+ editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null;
+
+ EventUtils.synthesizeKey(key, modifiers, view.styleWindow);
+
+ // Flush the debounce for the preview text.
+ view.debounce.flush();
+
+ await onDone;
+ await 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 0000000000..139c825db0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_01.js
@@ -0,0 +1,112 @@
+/* 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", "d", OPEN, SELECTED],
+ ["VK_DOWN", "direction", OPEN, SELECTED],
+ ["VK_DOWN", "display", OPEN, SELECTED],
+ ["VK_UP", "direction", OPEN, SELECTED],
+ ["VK_UP", "d", 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { toolbox, inspector, view } = await openRuleView();
+
+ info("Test autocompletion after 1st page load");
+ await runAutocompletionTest(toolbox, inspector, view);
+
+ info("Test autocompletion after page navigation");
+ await reloadBrowser();
+ await runAutocompletionTest(toolbox, inspector, view);
+});
+
+async function runAutocompletionTest(toolbox, inspector, view) {
+ info("Selecting the test node");
+ await selectNode("h1", inspector);
+
+ info("Focusing the css property editable field");
+ const ruleEditor = getRuleViewRuleEditor(view, 0);
+ const editor = await focusNewRuleViewProperty(ruleEditor);
+
+ info("Starting to test for css property completion");
+ for (let i = 0; i < testData.length; i++) {
+ if (!testData[i].length) {
+ continue;
+ }
+ await testCompletion(testData[i], editor, view);
+ }
+}
+
+async 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)/gi.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.
+ const popupEvent = open ? "popup-opened" : "popup-closed";
+ const onPopupEvent =
+ editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null;
+
+ info("Synthesizing key " + key);
+ EventUtils.synthesizeKey(key, {}, view.styleWindow);
+
+ await onSuggest;
+ await 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 0000000000..60df90e410
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js
@@ -0,0 +1,137 @@
+/* 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", {}, "revert", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "revert-layer", 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { toolbox, inspector, view } = await openRuleView();
+
+ info("Test autocompletion after 1st page load");
+ await runAutocompletionTest(toolbox, inspector, view);
+
+ info("Test autocompletion after page navigation");
+ await reloadBrowser();
+ await runAutocompletionTest(toolbox, inspector, view);
+});
+
+async function runAutocompletionTest(toolbox, inspector, view) {
+ info("Selecting the test node");
+ await selectNode("h1", inspector);
+
+ info("Focusing a new css property editable property");
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = await 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);
+ await testCompletion(testData[i], editor, view);
+ }
+}
+
+async 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.
+ const popupEvent = open ? "popup-opened" : "popup-closed";
+ const 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 debounce for the preview text.
+ view.debounce.flush();
+
+ await onDone;
+ await 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 0000000000..4f5577a21e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_03.js
@@ -0,0 +1,49 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { toolbox, inspector, view } = await openRuleView();
+
+ info("Test autocompletion for background-color");
+ await runAutocompletionTest(toolbox, inspector, view);
+});
+
+async function runAutocompletionTest(toolbox, inspector, view) {
+ info("Selecting the test node");
+ await selectNode("h1", inspector);
+
+ info("Focusing the new property editable field");
+ const ruleEditor = getRuleViewRuleEditor(view, 0);
+ const editor = await focusNewRuleViewProperty(ruleEditor);
+
+ info('Sending "background" to the editable field');
+ for (const key of "background") {
+ const onSuggest = editor.once("after-suggest");
+ EventUtils.synthesizeKey(key, {}, view.styleWindow);
+ await onSuggest;
+ }
+
+ const itemIndex = 4;
+
+ const bgcItem = editor.popup.getItemAtIndex(itemIndex);
+ is(
+ bgcItem.label,
+ "background-color",
+ "check the expected completion element"
+ );
+
+ editor.popup.selectedIndex = itemIndex;
+
+ const 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 0000000000..9002f89822
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_04.js
@@ -0,0 +1,77 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ info("Selecting the test node");
+ await selectNode("h1", inspector);
+
+ info("Focusing the new property editable field");
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+ let editor = await focusNewRuleViewProperty(ruleEditor);
+
+ info('Sending "background" to the editable field.');
+ for (const key of "background") {
+ const onSuggest = editor.once("after-suggest");
+ EventUtils.synthesizeKey(key, {}, view.styleWindow);
+ await onSuggest;
+ }
+
+ const itemIndex = 4;
+ const bgcItem = editor.popup.getItemAtIndex(itemIndex);
+ is(
+ bgcItem.label,
+ "background-color",
+ "Check the expected completion element is background-color."
+ );
+ editor.popup.selectItemAtIndex(itemIndex);
+
+ info("Select the background-color suggestion with a mouse click.");
+ const onSuggest = editor.once("after-suggest");
+ const node = editor.popup.elements.get(bgcItem);
+ EventUtils.synthesizeMouseAtCenter(node, {}, editor.popup._window);
+
+ await 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);
+
+ await onModifications;
+
+ // Getting the new value editor after focus
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ const 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");
+ onModifications = view.once("ruleview-changed");
+
+ EventUtils.sendString("#F00");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+
+ await 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 0000000000..a899b03c7a
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js
@@ -0,0 +1,140 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ info("Selecting the test node");
+ await selectNode("h1", inspector);
+
+ info("Focusing the property editable field");
+ const prop = getTextProperty(view, 1, { background: LONG_CSS_VALUE });
+
+ // Calculate offsets to click in the middle of the first box quad.
+ const rect = prop.editor.valueSpan.getBoundingClientRect();
+ const firstQuadBounds = prop.editor.valueSpan.getBoxQuads()[0].getBounds();
+ // 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.
+ const x = firstQuadBounds.left - rect.left + firstQuadBounds.width / 2;
+ // The first quad top edge is aligned with the bounding top edge, no
+ // translation needed here.
+ const y = firstQuadBounds.height / 2;
+
+ info("Focusing the css property editable value");
+ const editor = await 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 (const key of ", re") {
+ await 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.");
+
+ await synthesizeKeyForAutocomplete("VK_DOWN", editor, view.styleWindow);
+ is(editor.popup.selectedIndex, 1, "Using DOWN cycles autocomplete values.");
+ await synthesizeKeyForAutocomplete("VK_DOWN", editor, view.styleWindow);
+ is(editor.popup.selectedIndex, 2, "Using DOWN cycles autocomplete values.");
+ await 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");
+ const onSuggest = editor.once("after-suggest");
+
+ const node = editor.popup._list.childNodes[editor.popup.selectedIndex];
+ EventUtils.synthesizeMouseAtCenter(node, {}, editor.popup._window);
+
+ view.debounce.flush();
+ await onSuggest;
+ await 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);
+ await 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.
+ */
+async function synthesizeKeyForAutocomplete(key, editor, win) {
+ const onSuggest = editor.once("after-suggest");
+ EventUtils.synthesizeKey(key, {}, win);
+ await onSuggest;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-on-empty.js b/devtools/client/inspector/rules/test/browser_rules_completion-on-empty.js
new file mode 100644
index 0000000000..9a3348783f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-on-empty.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the suggest completion popup behavior of CSS property field.
+
+const TEST_URI = "<h1 style='color: lime'>Header</h1>";
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ info("Selecting the test node");
+ await selectNode("h1", inspector);
+
+ const prop = getTextProperty(view, 0, { color: "lime" });
+
+ info("Test with css property value field");
+ await testCompletion(view, prop.editor.valueSpan, true);
+
+ info("Test with css property name field");
+ await testCompletion(view, prop.editor.nameSpan, false);
+});
+
+async function testCompletion(view, target, isExpectedOpenPopup) {
+ const editor = await focusEditableField(view, target);
+
+ info(
+ "Check the suggest completion popup visibility after clearing the field"
+ );
+
+ const onChanged = view.once("ruleview-changed");
+ const popupEvent = isExpectedOpenPopup ? "popup-opened" : "popup-closed";
+ const onPopupEvent =
+ editor.popup.isOpen === isExpectedOpenPopup
+ ? Promise.resolve()
+ : once(view.popup, popupEvent);
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, view.styleWindow);
+
+ // Flush the debounce to update the preview text.
+ view.debounce.flush();
+
+ await Promise.all([onChanged, onPopupEvent]);
+ is(
+ editor.popup.isOpen,
+ isExpectedOpenPopup,
+ "The popup visibility is correct"
+ );
+
+ if (editor.popup.isOpen) {
+ info("Close the suggest completion popup");
+ const closingEvents = [
+ view.once("ruleview-changed"),
+ once(view.popup, "popup-closed"),
+ ];
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ await Promise.all(closingEvents);
+ }
+}
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 0000000000..6f22ab0d17
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-popup-hidden-after-navigation.js
@@ -0,0 +1,41 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ info("Test autocompletion popup is hidden after page navigation");
+
+ info("Selecting the test node");
+ await selectNode("h1", inspector);
+
+ info("Focusing the css property editable field");
+ const propertyName = view.styleDocument.querySelectorAll(
+ ".ruleview-propertyname"
+ )[0];
+ const editor = await focusEditableField(view, propertyName);
+
+ info("Pressing key VK_DOWN");
+ const onSuggest = once(editor.input, "keypress");
+ const onPopupOpened = once(editor.popup, "popup-opened");
+
+ EventUtils.synthesizeKey("VK_DOWN", {}, view.styleWindow);
+
+ info("Waiting for autocomplete popup to be displayed");
+ await onSuggest;
+ await onPopupOpened;
+
+ ok(view.popup && view.popup.isOpen, "Popup should be opened");
+
+ info("Reloading the page");
+ await reloadBrowser();
+
+ ok(!(view.popup && view.popup.isOpen), "Popup should be closed");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-shortcut.js b/devtools/client/inspector/rules/test/browser_rules_completion-shortcut.js
new file mode 100644
index 0000000000..2d81a770ff
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-shortcut.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the shortcut key for the suggest completion popup.
+
+const TEST_URI = "<h1 style='colo: lim'>Header</h1>";
+const TEST_SHORTCUTS = [
+ {
+ key: " ",
+ modifiers: { ctrlKey: true },
+ },
+ {
+ key: "VK_DOWN",
+ modifiers: {},
+ },
+];
+
+add_task(async function () {
+ for (const shortcut of TEST_SHORTCUTS) {
+ info(
+ "Start to test for the shortcut " +
+ `key: "${shortcut.key}" modifiers: ${Object.keys(shortcut.modifiers)}`
+ );
+
+ const tab = await addTab(
+ "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)
+ );
+ const { inspector, view } = await openRuleView();
+
+ info("Selecting the test node");
+ await selectNode("h1", inspector);
+
+ const prop = getTextProperty(view, 0, { colo: "lim" });
+
+ info("Test with css property name field");
+ const nameEditor = await focusEditableField(view, prop.editor.nameSpan);
+ await testCompletion(shortcut, view, nameEditor, "color");
+
+ info("Test with css property value field");
+ const valueEditor = inplaceEditor(view.styleDocument.activeElement);
+ await testCompletion(shortcut, view, valueEditor, "lime");
+
+ await removeTab(tab);
+ }
+});
+
+async function testCompletion(shortcut, view, editor, expectedValue) {
+ const spanEl = editor.elt;
+
+ info("Move cursor to the end");
+ EventUtils.synthesizeKey("VK_RIGHT", {}, view.styleWindow);
+ await waitUntil(
+ () => editor.input.selectionStart === editor.input.selectionEnd
+ );
+
+ info("Check whether the popup opens after sending the shortcut key");
+ const onPopupOpened = once(view.popup, "popup-opened");
+ EventUtils.synthesizeKey(shortcut.key, shortcut.modifiers, view.styleWindow);
+ await onPopupOpened;
+ ok(view.popup.isOpen, "The popup opened correctly");
+
+ info("Commit the suggestion");
+ const onChanged = view.once("ruleview-changed");
+ const onPopupClosed = once(view.popup, "popup-closed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ await Promise.all([onChanged, onPopupClosed]);
+ is(spanEl.textContent, expectedValue, "The value is set correctly");
+}
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 0000000000..af7c03220a
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_computed-lists_01.js
@@ -0,0 +1,60 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testExpandersShown(inspector, view);
+});
+
+function testExpandersShown(inspector, view) {
+ const 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.display,
+ "inline-block",
+ "margin expander is displayed."
+ );
+ is(
+ rule.textProps[1].editor.expander.style.display,
+ "none",
+ "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 0000000000..3d59baea68
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_computed-lists_02.js
@@ -0,0 +1,86 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testComputedList(inspector, view);
+});
+
+function testComputedList(inspector, view) {
+ const prop = getTextProperty(view, 1, { margin: "0px 1px 2px 3px" });
+ const propEditor = prop.editor;
+ const 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");
+
+ const computed = propEditor.prop.computed;
+ const computedDom = propEditor.computed;
+ const 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) => {
+ const 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_computed-lists_03.js b/devtools/client/inspector/rules/test/browser_rules_computed-lists_03.js
new file mode 100644
index 0000000000..f002a77c8b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_computed-lists_03.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule view does not show expanders for property values that contain
+// variables.
+// This should, in theory, be able to work, but the complexity outlined in
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1535315#c2 made us hide the expander
+// instead. Also, this is what Chrome does too.
+
+var TEST_URI = `
+ <style type="text/css">
+ #testid {
+ --primary-color: black;
+ background: var(--primary-color, red);
+ }
+ </style>
+ <h1 id="testid">Styled Node</h1>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+
+ is(
+ rule.textProps[0].name,
+ "--primary-color",
+ "The first property is the variable"
+ );
+ is(rule.textProps[1].name, "background", "The second property is background");
+
+ info("Check that the expander is hidden for the background property");
+ is(
+ rule.textProps[1].editor.expander.style.display,
+ "none",
+ "Expander is hidden"
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_conditional_import.js b/devtools/client/inspector/rules/test/browser_rules_conditional_import.js
new file mode 100644
index 0000000000..86494cf676
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_conditional_import.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the rule-view content displays @import conditions.
+
+const TEST_URI = `
+ <style type="text/css">
+ @import url(${URL_ROOT_COM_SSL}doc_conditional_import.css) screen and (width > 10px);
+ @import url(${URL_ROOT_COM_SSL}doc_imported_named_layer.css) layer(importedLayer) (height > 42px);
+ @import url(${URL_ROOT_COM_SSL}doc_conditional_import.css) supports(display: flex);
+ @import url(${URL_ROOT_COM_SSL}doc_conditional_import.css) supports(display: flex) screen and (width > 10px);
+ @import url(${URL_ROOT_COM_SSL}doc_imported_named_layer.css) layer(importedLayerTwo) supports(display: flex) screen and (width > 10px);
+ @import url(${URL_ROOT_COM_SSL}doc_imported_no_layer.css);
+ </style>
+ <h1>Hello @import!</h1>
+`;
+
+add_task(async function () {
+ // Enable the pref for @import supports()
+ await pushPref("layout.css.import-supports.enabled", true);
+
+ await addTab(
+ "https://example.com/document-builder.sjs?html=" +
+ encodeURIComponent(TEST_URI)
+ );
+ const { inspector, view } = await openRuleView();
+ await selectNode("h1", inspector);
+ const expectedRules = [
+ { selector: "element", ancestorRulesData: null },
+ {
+ // Checking that we don't show @import for rules from imported stylesheet with no conditions
+ selector: `h1, [test-hint="imported-no-layer--no-rule-layer"]`,
+ ancestorRulesData: null,
+ },
+ {
+ selector: `h1, [test-hint="imported-conditional"]`,
+ ancestorRulesData: [
+ "@import supports(display: flex) screen and (width > 10px)",
+ ],
+ },
+ {
+ selector: `h1, [test-hint="imported-conditional"]`,
+ ancestorRulesData: ["@import supports(display: flex)"],
+ },
+ {
+ selector: `h1, [test-hint="imported-conditional"]`,
+ ancestorRulesData: ["@import screen and (width > 10px)"],
+ },
+ {
+ selector: `h1, [test-hint="imported-named-layer--no-rule-layer"]`,
+ ancestorRulesData: [
+ "@import supports(display: flex) screen and (width > 10px)",
+ "@layer importedLayerTwo",
+ "@media screen",
+ ],
+ },
+ {
+ selector: `h1, [test-hint="imported-named-layer--named-layer"]`,
+ ancestorRulesData: [
+ "@import supports(display: flex) screen and (width > 10px)",
+ "@layer importedLayerTwo",
+ "@media screen",
+ "@layer in-imported-stylesheet",
+ ],
+ },
+ {
+ selector: `h1, [test-hint="imported-named-layer--no-rule-layer"]`,
+ ancestorRulesData: [
+ "@import (height > 42px)",
+ "@layer importedLayer",
+ "@media screen",
+ ],
+ },
+ {
+ selector: `h1, [test-hint="imported-named-layer--named-layer"]`,
+ ancestorRulesData: [
+ "@import (height > 42px)",
+ "@layer importedLayer",
+ "@media screen",
+ "@layer in-imported-stylesheet",
+ ],
+ },
+ ];
+
+ const rulesInView = Array.from(view.element.children);
+ is(
+ rulesInView.length,
+ expectedRules.length,
+ "All expected rules are displayed"
+ );
+
+ for (let i = 0; i < expectedRules.length; i++) {
+ const expectedRule = expectedRules[i];
+ info(`Checking rule #${i}: ${expectedRule.selector}`);
+
+ const selector = rulesInView[i].querySelector(
+ ".ruleview-selectorcontainer"
+ ).innerText;
+ is(selector, expectedRule.selector, `Expected selector for ${selector}`);
+
+ if (expectedRule.ancestorRulesData == null) {
+ is(
+ getRuleViewAncestorRulesDataElementByIndex(view, i),
+ null,
+ `No ancestor rules data displayed for ${selector}`
+ );
+ } else {
+ is(
+ getRuleViewAncestorRulesDataTextByIndex(view, i),
+ expectedRule.ancestorRulesData.join("\n"),
+ `Expected ancestor rules data displayed for ${selector}`
+ );
+ }
+ }
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_container-queries.js b/devtools/client/inspector/rules/test/browser_rules_container-queries.js
new file mode 100644
index 0000000000..63a13dbe47
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_container-queries.js
@@ -0,0 +1,308 @@
+/* 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 when the page defines container queries.
+const TEST_URI = `
+ <!DOCTYPE html>
+ <style type="text/css">
+ body {
+ container: mycontainer / size;
+ }
+
+ section {
+ container: mycontainer / inline-size;
+ }
+
+ @container (width > 0px) {
+ h1, [test-hint="nocontainername"]{
+ outline-color: chartreuse;
+ }
+ }
+
+ @container unknowncontainer (min-width: 2vw) {
+ h1, [test-hint="unknowncontainer"] {
+ border-color: salmon;
+ }
+ }
+
+ @container mycontainer (1px < width < 10000px) {
+ h1, [test-hint="container"] {
+ color: tomato;
+ }
+
+ section, [test-hint="container-duplicate-name--body"] {
+ color: gold;
+ }
+
+ div, [test-hint="container-duplicate-name--section"] {
+ color: salmon;
+ }
+ }
+ </style>
+ <body id=myBody class="a-container test">
+ <h1>Hello @container!</h1>
+ <section>
+ <div>
+ <h2>You rock</h2>
+ </div>
+ </section>
+ </body>
+`;
+
+add_task(async function () {
+ await pushPref("layout.css.container-queries.enabled", true);
+
+ await addTab(
+ "https://example.com/document-builder.sjs?html=" +
+ encodeURIComponent(TEST_URI)
+ );
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("h1", inspector);
+ assertContainerQueryData(view, [
+ { selector: "element", ancestorRulesData: null },
+ {
+ selector: `h1, [test-hint="container"]`,
+ ancestorRulesData: ["@container mycontainer (1px < width < 10000px)"],
+ },
+ {
+ selector: `h1, [test-hint="nocontainername"]`,
+ ancestorRulesData: ["@container (width > 0px)"],
+ },
+ ]);
+
+ info("Check that the query container tooltip works as expected");
+ // Retrieve query containers sizes
+ const { bodyInlineSize, bodyBlockSize, sectionInlineSize } =
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const body = content.document.body;
+ const section = content.document.querySelector("section");
+ return {
+ bodyInlineSize: content.getComputedStyle(body).inlineSize,
+ bodyBlockSize: content.getComputedStyle(body).blockSize,
+ sectionInlineSize: content.getComputedStyle(section).inlineSize,
+ };
+ });
+
+ await assertQueryContainerTooltip({
+ inspector,
+ view,
+ ruleIndex: 1,
+ expectedHeaderText: "<body#myBody.a-container.test>",
+ expectedBodyText: [
+ "container-type: size",
+ `inline-size: ${bodyInlineSize}`,
+ `block-size: ${bodyBlockSize}`,
+ ],
+ });
+
+ info("Check that the 'jump to container' button works as expected");
+ await assertJumpToContainerButton(inspector, view, 1, "body");
+
+ info("Check that inherited rules display container query data as expected");
+ await selectNode("h2", inspector);
+
+ assertContainerQueryData(view, [
+ { selector: "element", ancestorRulesData: null },
+ {
+ selector: `div, [test-hint="container-duplicate-name--section"]`,
+ ancestorRulesData: ["@container mycontainer (1px < width < 10000px)"],
+ },
+ {
+ selector: `section, [test-hint="container-duplicate-name--body"]`,
+ ancestorRulesData: ["@container mycontainer (1px < width < 10000px)"],
+ },
+ ]);
+
+ info(
+ "Check that the query container tooltip works as expected for inherited rules as well"
+ );
+ await assertQueryContainerTooltip({
+ inspector,
+ view,
+ ruleIndex: 1,
+ expectedHeaderText: "<section>",
+ expectedBodyText: [
+ "container-type: inline-size",
+ `inline-size: ${sectionInlineSize}`,
+ ],
+ });
+ await assertQueryContainerTooltip({
+ inspector,
+ view,
+ ruleIndex: 2,
+ expectedHeaderText: "<body#myBody.a-container.test>",
+ expectedBodyText: [
+ "container-type: size",
+ `inline-size: ${bodyInlineSize}`,
+ `block-size: ${bodyBlockSize}`,
+ ],
+ });
+
+ info(
+ "Check that the 'jump to container' button works as expected for inherited rules"
+ );
+ await assertJumpToContainerButton(inspector, view, 1, "section");
+
+ await selectNode("h2", inspector);
+ await assertJumpToContainerButton(inspector, view, 2, "body");
+});
+
+function assertContainerQueryData(view, expectedRules) {
+ const rulesInView = Array.from(
+ view.element.querySelectorAll(".ruleview-rule")
+ );
+
+ is(
+ rulesInView.length,
+ expectedRules.length,
+ "All expected rules are displayed"
+ );
+
+ for (let i = 0; i < expectedRules.length; i++) {
+ const expectedRule = expectedRules[i];
+ info(`Checking rule #${i}: ${expectedRule.selector}`);
+
+ const selector = rulesInView[i].querySelector(
+ ".ruleview-selectorcontainer"
+ ).innerText;
+ is(selector, expectedRule.selector, `Expected selector for ${selector}`);
+
+ const ancestorDataEl = getRuleViewAncestorRulesDataElementByIndex(view, i);
+
+ if (expectedRule.ancestorRulesData == null) {
+ is(
+ ancestorDataEl,
+ null,
+ `No ancestor rules data displayed for ${selector}`
+ );
+ } else {
+ is(
+ ancestorDataEl?.innerText,
+ expectedRule.ancestorRulesData.join("\n"),
+ `Expected ancestor rules data displayed for ${selector}`
+ );
+ ok(
+ ancestorDataEl.querySelector(".container-query .open-inspector") !==
+ null,
+ "An icon is displayed to select the container in the markup view"
+ );
+ }
+ }
+}
+
+async function assertJumpToContainerButton(
+ inspector,
+ view,
+ ruleIndex,
+ expectedSelectedNodeAfterClick
+) {
+ const selectContainerButton = getRuleViewAncestorRulesDataElementByIndex(
+ view,
+ ruleIndex
+ ).querySelector(".open-inspector");
+
+ // Ensure that the button can be targetted from EventUtils.
+ selectContainerButton.scrollIntoView();
+
+ const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } =
+ getHighlighterTestHelpers(inspector);
+
+ const onNodeHighlight = waitForHighlighterTypeShown(
+ inspector.highlighters.TYPES.BOXMODEL
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ selectContainerButton,
+ { type: "mouseover" },
+ selectContainerButton.ownerDocument.defaultView
+ );
+ const { nodeFront: highlightedNodeFront } = await onNodeHighlight;
+ is(
+ highlightedNodeFront.displayName,
+ expectedSelectedNodeAfterClick,
+ "The correct node was highlighted"
+ );
+
+ const onceNewNodeFront = inspector.selection.once("new-node-front");
+ const onNodeUnhighlight = waitForHighlighterTypeHidden(
+ inspector.highlighters.TYPES.BOXMODEL
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ selectContainerButton,
+ {},
+ selectContainerButton.ownerDocument.defaultView
+ );
+
+ const nodeFront = await onceNewNodeFront;
+ is(
+ nodeFront.displayName,
+ expectedSelectedNodeAfterClick,
+ "The correct node has been selected"
+ );
+
+ await onNodeUnhighlight;
+ ok("Highlighter was hidden when clicking on icon");
+}
+
+async function assertQueryContainerTooltip({
+ inspector,
+ view,
+ ruleIndex,
+ expectedHeaderText,
+ expectedBodyText,
+}) {
+ const tooltipTriggerEl = getRuleViewAncestorRulesDataElementByIndex(
+ view,
+ ruleIndex
+ ).querySelector(".container-query-declaration");
+
+ // Ensure that the element can be targetted from EventUtils.
+ tooltipTriggerEl.scrollIntoView();
+
+ const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } =
+ getHighlighterTestHelpers(inspector);
+
+ const onNodeHighlight = waitForHighlighterTypeShown(
+ inspector.highlighters.TYPES.BOXMODEL
+ );
+
+ const tooltip = view.tooltips.getTooltip("interactiveTooltip");
+ const onTooltipReady = tooltip.once("shown");
+ EventUtils.synthesizeMouseAtCenter(
+ tooltipTriggerEl,
+ { type: "mousemove" },
+ tooltipTriggerEl.ownerDocument.defaultView
+ );
+ await onTooltipReady;
+ await onNodeHighlight;
+
+ is(
+ tooltip.panel.querySelector("header").textContent,
+ expectedHeaderText,
+ "Tooltip has expected header content"
+ );
+
+ const lis = Array.from(tooltip.panel.querySelectorAll("li")).map(
+ li => li.textContent
+ );
+ Assert.deepEqual(lis, expectedBodyText, "Tooltip has expected body items");
+
+ info("Hide the tooltip");
+ const onHidden = tooltip.once("hidden");
+ const onNodeUnhighlight = waitForHighlighterTypeHidden(
+ inspector.highlighters.TYPES.BOXMODEL
+ );
+ // Move the mouse elsewhere to hide the tooltip
+ EventUtils.synthesizeMouse(
+ tooltipTriggerEl.ownerDocument.body,
+ 1,
+ 1,
+ { type: "mousemove" },
+ tooltipTriggerEl.ownerDocument.defaultView
+ );
+ await onHidden;
+ await onNodeUnhighlight;
+}
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 0000000000..ef3acbfd5c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_content_01.js
@@ -0,0 +1,72 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("#testid", inspector);
+ is(
+ view.element.querySelectorAll("#ruleview-no-results").length,
+ 0,
+ "After a highlight, no longer has a no-results element."
+ );
+
+ await clearCurrentNodeSelection(inspector);
+ is(
+ view.element.querySelectorAll("#ruleview-no-results").length,
+ 1,
+ "After highlighting null, has a no-results element again."
+ );
+
+ await selectNode("#testid", inspector);
+
+ let linkText = getRuleViewLinkTextByIndex(view, 1);
+ is(linkText, "inline:3", "link text at index 1 has expected content.");
+
+ const mediaText = getRuleViewAncestorRulesDataTextByIndex(view, 1);
+ is(
+ mediaText,
+ "@media screen and (min-width: 10px)",
+ "media text at index 1 has expected content"
+ );
+
+ linkText = getRuleViewLinkTextByIndex(view, 2);
+ is(linkText, "inline:7", "link text at index 2 has expected content.");
+ is(
+ getRuleViewAncestorRulesDataElementByIndex(view, 2),
+ null,
+ "There is no media text element for rule at index 2"
+ );
+
+ const 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 0000000000..5e48169625
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_content_02.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the rule-view content when the inspector gets opened via the page
+// ctx-menu "inspect element"
+
+const CONTENT = `
+ <body style="color:red;">
+ <div style="color:blue;">
+ <p style="color:green;">
+ <span style="color:yellow;">test element</span>
+ </p>
+ </div>
+ </body>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + CONTENT);
+
+ // const commands = await CommandsFactory.forTab(tab);
+ // // Initialize the TargetCommands which require some async stuff to be done
+ // // before being fully ready. This will define the `targetCommand.targetFront` attribute.
+ // await commands.targetCommand.startListening();
+ const inspector = await clickOnInspectMenuItem("span");
+
+ checkRuleViewContent(inspector.getPanel("ruleview").view);
+});
+
+function checkRuleViewContent({ styleDocument }) {
+ info("Making sure the rule-view contains the expected content");
+
+ const 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"
+ );
+
+ const rules = styleDocument.querySelectorAll(".ruleview-rule");
+ is(rules.length, 4, "There are 4 rules in the view");
+
+ for (const rule of rules) {
+ const selector = rule.querySelector(".ruleview-selectorcontainer");
+ is(
+ selector.textContent,
+ STYLE_INSPECTOR_L10N.getStr("rule.sourceElement"),
+ "The rule's selector is correct"
+ );
+
+ const propertyNames = [...rule.querySelectorAll(".ruleview-propertyname")];
+ is(propertyNames.length, 1, "There's only one property name, as expected");
+
+ const 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_copy_styles.js b/devtools/client/inspector/rules/test/browser_rules_copy_styles.js
new file mode 100644
index 0000000000..01fe365841
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_copy_styles.js
@@ -0,0 +1,359 @@
+/* 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_SSL + "doc_copystyles.html";
+
+add_task(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ const data = [
+ {
+ desc: "Test Copy Property Name",
+ node: ruleEditor.rule.textProps[0].editor.nameSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyPropertyName",
+ expectedPattern: "color",
+ visible: {
+ copyLocation: false,
+ copyDeclaration: 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,
+ copyDeclaration: 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,
+ copyDeclaration: true,
+ copyPropertyName: false,
+ copyPropertyValue: true,
+ copySelector: false,
+ copyRule: true,
+ },
+ },
+ {
+ desc: "Test Copy Property Declaration",
+ node: ruleEditor.rule.textProps[2].editor.nameSpan,
+ menuItemLabel: "styleinspector.contextmenu.copyDeclaration",
+ expectedPattern: "font-size: 12px;",
+ visible: {
+ copyLocation: false,
+ copyDeclaration: 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.copyDeclaration",
+ expectedPattern: "border-color: #00F !important;",
+ visible: {
+ copyLocation: false,
+ copyDeclaration: 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,
+ copyDeclaration: 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,
+ copyDeclaration: false,
+ copyPropertyName: false,
+ copyPropertyValue: false,
+ copySelector: true,
+ copyRule: true,
+ },
+ },
+ {
+ desc: "Test Copy Location",
+ node: ruleEditor.source,
+ menuItemLabel: "styleinspector.contextmenu.copyLocation",
+ expectedPattern:
+ "https://example.com/browser/devtools/client/" +
+ "inspector/rules/test/doc_copystyles.css",
+ visible: {
+ copyLocation: true,
+ copyDeclaration: false,
+ copyPropertyName: false,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true,
+ },
+ },
+ {
+ async setup() {
+ await 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,
+ copyDeclaration: true,
+ copyPropertyName: true,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true,
+ },
+ },
+ {
+ async setup() {
+ await 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,
+ copyDeclaration: 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.copyDeclaration",
+ expectedPattern: "/\\* color: #F00; \\*/",
+ visible: {
+ copyLocation: false,
+ copyDeclaration: true,
+ copyPropertyName: true,
+ copyPropertyValue: false,
+ copySelector: false,
+ copyRule: true,
+ },
+ },
+ ];
+
+ for (const {
+ setup,
+ desc,
+ node,
+ menuItemLabel,
+ expectedPattern,
+ visible,
+ } of data) {
+ if (setup) {
+ await setup();
+ }
+
+ info(desc);
+ await checkCopyStyle(view, node, menuItemLabel, expectedPattern, visible);
+ }
+});
+
+async function checkCopyStyle(
+ view,
+ node,
+ menuItemLabel,
+ expectedPattern,
+ visible
+) {
+ const allMenuItems = openStyleContextMenuAndGetAllItems(view, node);
+ const menuItem = allMenuItems.find(
+ item => item.label === STYLE_INSPECTOR_L10N.getStr(menuItemLabel)
+ );
+ const menuitemCopy = allMenuItems.find(
+ item =>
+ item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy")
+ );
+ const menuitemCopyLocation = allMenuItems.find(
+ item =>
+ item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyLocation")
+ );
+ const menuitemCopyDeclaration = allMenuItems.find(
+ item =>
+ item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyDeclaration")
+ );
+ const menuitemCopyPropertyName = allMenuItems.find(
+ item =>
+ item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyName")
+ );
+ const menuitemCopyPropertyValue = allMenuItems.find(
+ item =>
+ item.label ===
+ STYLE_INSPECTOR_L10N.getStr(
+ "styleinspector.contextmenu.copyPropertyValue"
+ )
+ );
+ const menuitemCopySelector = allMenuItems.find(
+ item =>
+ item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copySelector")
+ );
+ const 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(
+ menuitemCopyDeclaration.visible,
+ visible.copyDeclaration,
+ "Copy Property Declaration visible attribute is as expected: " +
+ visible.copyDeclaration
+ );
+
+ 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 {
+ await waitForClipboardPromise(
+ () => menuItem.click(),
+ () => checkClipboardData(expectedPattern)
+ );
+ } catch (e) {
+ failedClipboard(expectedPattern);
+ }
+}
+
+async function disableProperty(view, index) {
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+ const textProp = ruleEditor.rule.textProps[index];
+ await togglePropStatus(view, textProp);
+}
+
+function checkClipboardData(expectedPattern) {
+ const actual = SpecialPowers.getClipboardData("text/plain");
+ const expectedRegExp = new RegExp(expectedPattern, "g");
+ return expectedRegExp.test(actual);
+}
+
+function failedClipboard(expectedPattern) {
+ // Format expected text for comparison
+ const 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/plain");
+
+ // 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-compatibility-add-rename-rule.js b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-add-rename-rule.js
new file mode 100644
index 0000000000..510a558e7b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-add-rename-rule.js
@@ -0,0 +1,125 @@
+/* vim: set ft=javascript 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 - Add and rename rules
+// Test the correctness of the compatibility
+// status when the incompatible rules are added
+// or renamed to another universally compatible
+// rule
+
+const TEST_URI = `
+<style>
+ body {
+ user-select: none;
+ text-decoration-skip: none;
+ clip: auto;
+ }
+</style>
+<body>
+</body>`;
+
+const TEST_DATA_INITIAL = [
+ {
+ selector: "body",
+ rules: [
+ {},
+ {
+ "user-select": {
+ value: "none",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.default,
+ },
+ "text-decoration-skip": {
+ value: "none",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.experimental,
+ },
+ clip: {
+ value: "auto",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE["deprecated-supported"],
+ },
+ },
+ ],
+ },
+];
+
+const TEST_DATA_ADD_RULE = [
+ {
+ selector: "body",
+ rules: [
+ {
+ "-moz-float-edge": {
+ value: "content-box",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.deprecated,
+ },
+ },
+ {
+ "user-select": {
+ value: "none",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.default,
+ },
+ "text-decoration-skip": {
+ value: "none",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.experimental,
+ },
+ clip: {
+ value: "auto",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE["deprecated-supported"],
+ },
+ },
+ ],
+ },
+];
+
+const TEST_DATA_RENAME_RULE = [
+ {
+ selector: "body",
+ rules: [
+ {
+ "-moz-float-edge": {
+ value: "content-box",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.deprecated,
+ },
+ },
+ {
+ "background-color": {
+ value: "green",
+ },
+ "text-decoration-skip": {
+ value: "none",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.experimental,
+ },
+ clip: {
+ value: "auto",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE["deprecated-supported"],
+ },
+ },
+ ],
+ },
+];
+
+add_task(async function () {
+ await pushPref(
+ "devtools.inspector.ruleview.inline-compatibility-warning.enabled",
+ true
+ );
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ const userSelect = { "user-select": "none" };
+ const backgroundColor = { "background-color": "green" };
+
+ info("Check initial compatibility issues");
+ await runCSSCompatibilityTests(view, inspector, TEST_DATA_INITIAL);
+
+ info(
+ "Add an inheritable incompatible rule and check the compatibility status"
+ );
+ await addProperty(view, 0, "-moz-float-edge", "content-box");
+ await runCSSCompatibilityTests(view, inspector, TEST_DATA_ADD_RULE);
+
+ info("Rename user-select to color and check the compatibility status");
+ await updateDeclaration(view, 1, userSelect, backgroundColor);
+ await runCSSCompatibilityTests(view, inspector, TEST_DATA_RENAME_RULE);
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_css-compatibility-check-add-fix.js b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-check-add-fix.js
new file mode 100644
index 0000000000..17069756e2
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-check-add-fix.js
@@ -0,0 +1,134 @@
+/* vim: set ft=javascript 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 - Add fix for incompatible property
+// For properties like "user-select", there exists an alias
+// "-webkit-user-select", that is supported on all platform
+// as a result of its popularity. If such a universally
+// compatible alias exists, we shouldn't show compatibility
+// warning for the base declaration.
+// In this case "user-select" is marked compatible because the
+// universally compatible alias "-webkit-user-select" exists
+// alongside.
+
+const TARGET_BROWSERS = [
+ {
+ // Chrome doesn't need any prefix for both user-select and text-size-adjust.
+ id: "chrome",
+ status: "current",
+ },
+ {
+ // The safari_ios needs -webkit prefix for both properties.
+ id: "safari_ios",
+ status: "current",
+ },
+];
+
+const TEST_URI = `
+<style>
+ div {
+ color: green;
+ background-color: black;
+ user-select: none;
+ text-size-adjust: none;
+ }
+</style>
+<div>`;
+
+const TEST_DATA_INITIAL = [
+ {
+ rules: [
+ {},
+ {
+ color: { value: "green" },
+ "background-color": { value: "black" },
+ "user-select": {
+ value: "none",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.default,
+ },
+ "text-size-adjust": {
+ value: "none",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.experimental,
+ },
+ },
+ ],
+ },
+];
+
+const TEST_DATA_FIX_USER_SELECT = [
+ {
+ rules: [
+ {},
+ {
+ color: { value: "green" },
+ "background-color": { value: "black" },
+ "user-select": { value: "none" },
+ "-webkit-user-select": { value: "none" },
+ "text-size-adjust": {
+ value: "none",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.experimental,
+ },
+ },
+ ],
+ },
+];
+
+// text-size-adjust is an experimental property with aliases.
+// Adding -webkit makes it compatible on all platforms but will
+// still show an inline warning for its experimental status.
+const TEST_DATA_FIX_EXPERIMENTAL_SUPPORTED = [
+ {
+ rules: [
+ {},
+ {
+ color: { value: "green" },
+ "background-color": { value: "black" },
+ "user-select": { value: "none" },
+ "-webkit-user-select": { value: "none" },
+ "text-size-adjust": {
+ value: "none",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE["experimental-supported"],
+ },
+ },
+ ],
+ },
+];
+
+add_task(async function () {
+ await pushPref(
+ "devtools.inspector.compatibility.target-browsers",
+ JSON.stringify(TARGET_BROWSERS)
+ );
+ await pushPref(
+ "devtools.inspector.ruleview.inline-compatibility-warning.enabled",
+ true
+ );
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ // We're only looking for properties on this single node so select it here instead of
+ // passing `selector` to `runCSSCompatibilityTests` (otherwise addition requests are sent
+ // to the server and we may end up with pending promises when the toolbox closes).
+ await selectNode("div", inspector);
+
+ await runCSSCompatibilityTests(view, inspector, TEST_DATA_INITIAL);
+
+ info(
+ 'Add -webkit-user-select: "none" which solves the compatibility issue from user-select'
+ );
+ await addProperty(view, 1, "-webkit-user-select", "none");
+ await runCSSCompatibilityTests(view, inspector, TEST_DATA_FIX_USER_SELECT);
+
+ info(
+ 'Add -webkit-text-size-adjust: "none" fixing issue but leaving an inline warning of an experimental property'
+ );
+ await addProperty(view, 1, "-webkit-text-size-adjust", "none");
+ await runCSSCompatibilityTests(
+ view,
+ inspector,
+ TEST_DATA_FIX_EXPERIMENTAL_SUPPORTED
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_css-compatibility-learn-more-link.js b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-learn-more-link.js
new file mode 100644
index 0000000000..08e86e6202
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-learn-more-link.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";
+
+// Test that the Learn More link is displayed when possible,
+// and that it links to MDN or the spec if no MDN url is provided.
+
+const TEST_URI = `
+<style>
+ body {
+ user-select: none;
+ hyphenate-limit-chars: auto;
+ overflow-clip-box: padding-box;
+ }
+</style>
+<body>
+</body>`;
+
+const TEST_DATA_INITIAL = [
+ {
+ selector: "body",
+ rules: [
+ {},
+ {
+ "user-select": {
+ value: "none",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.default,
+ // MDN url
+ expectedLearnMoreUrl:
+ "https://developer.mozilla.org/docs/Web/CSS/user-select?utm_source=devtools&utm_medium=inspector-css-compatibility&utm_campaign=default",
+ },
+ "hyphenate-limit-chars": {
+ value: "auto",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.default,
+ // No MDN url, but a spec one
+ expectedLearnMoreUrl:
+ "https://drafts.csswg.org/css-text-4/#propdef-hyphenate-limit-chars",
+ },
+ "overflow-clip-box": {
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.default,
+ value: "padding-box",
+ // No MDN nor spec url
+ expectedLearnMoreUrl: null,
+ },
+ },
+ ],
+ },
+];
+
+add_task(async function () {
+ await pushPref(
+ "devtools.inspector.ruleview.inline-compatibility-warning.enabled",
+ true
+ );
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ // If the test fail because the properties used are no longer in the dataset, or they
+ // now have mdn/spec url although we expected them not to, uncomment the next line
+ // to get all the properties in the dataset that don't have a MDN url.
+ // logCssCompatDataPropertiesWithoutMDNUrl()
+
+ await runCSSCompatibilityTests(view, inspector, TEST_DATA_INITIAL);
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_css-compatibility-toggle-rules.js b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-toggle-rules.js
new file mode 100644
index 0000000000..7209adbc2e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-toggle-rules.js
@@ -0,0 +1,150 @@
+/* vim: set ft=javascript 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 rules linked to the element and the class
+// Checking whether the compatibility warning icon is displayed
+// correctly.
+// If a rule is disabled, it is marked compatible to keep
+// consistency with compatibility panel.
+// We test both the compatible and incompatible rules here
+
+const TEST_URI = `
+<style>
+ div {
+ color: green;
+ background-color: black;
+ -moz-float-edge: content-box;
+ }
+</style>
+<div class="test-inline" style="color:pink; user-select:none;"></div>
+<div class="test-class-linked"></div>`;
+
+const TEST_DATA_INITIAL = [
+ {
+ selector: ".test-class-linked",
+ rules: [
+ {},
+ {
+ color: { value: "green" },
+ "background-color": { value: "black" },
+ "-moz-float-edge": {
+ value: "content-box",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.deprecated,
+ },
+ },
+ ],
+ },
+ {
+ selector: ".test-inline",
+ rules: [
+ {
+ color: { value: "pink" },
+ "user-select": {
+ value: "none",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.default,
+ },
+ },
+ {
+ color: { value: "green" },
+ "background-color": { value: "black" },
+ "-moz-float-edge": {
+ value: "content-box",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.deprecated,
+ },
+ },
+ ],
+ },
+];
+
+const TEST_DATA_TOGGLE_CLASS_DECLARATION = [
+ {
+ selector: ".test-class-linked",
+ rules: [
+ {},
+ {
+ color: { value: "green" },
+ "background-color": { value: "black" },
+ "-moz-float-edge": { value: "content-box" },
+ },
+ ],
+ },
+ {
+ selector: ".test-inline",
+ rules: [
+ {
+ color: { value: "pink" },
+ "user-select": {
+ value: "none",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.default,
+ },
+ },
+ {
+ color: { value: "green" },
+ "background-color": { value: "black" },
+ "-moz-float-edge": { value: "content-box" },
+ },
+ ],
+ },
+];
+
+const TEST_DATA_TOGGLE_INLINE = [
+ {
+ selector: ".test-class-linked",
+ rules: [
+ {},
+ {
+ color: { value: "green" },
+ "background-color": { value: "black" },
+ "-moz-float-edge": { value: "content-box" },
+ },
+ ],
+ },
+ {
+ selector: ".test-inline",
+ rules: [
+ {
+ color: { value: "pink" },
+ "user-select": { value: "none" },
+ },
+ {
+ color: { value: "green" },
+ "background-color": { value: "black" },
+ "-moz-float-edge": { value: "content-box" },
+ },
+ ],
+ },
+];
+
+add_task(async function () {
+ await pushPref(
+ "devtools.inspector.ruleview.inline-compatibility-warning.enabled",
+ true
+ );
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ const mozFloatEdge = { "-moz-float-edge": "content-box" };
+ const userSelect = { "user-select": "none" };
+
+ await runCSSCompatibilityTests(view, inspector, TEST_DATA_INITIAL);
+
+ info(
+ 'Disable -moz-float-edge: "content-box" which is not cross browser compatible declaration'
+ );
+ await toggleDeclaration(view, 1, mozFloatEdge);
+ await runCSSCompatibilityTests(
+ view,
+ inspector,
+ TEST_DATA_TOGGLE_CLASS_DECLARATION
+ );
+
+ info(
+ 'Toggle inline declaration "user-select": "none" and check the compatibility status'
+ );
+ await selectNode(".test-inline", inspector);
+ await toggleDeclaration(view, 0, userSelect);
+ await runCSSCompatibilityTests(view, inspector, TEST_DATA_TOGGLE_INLINE);
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_css-compatibility-tooltip-telemetry.js b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-tooltip-telemetry.js
new file mode 100644
index 0000000000..4580f819aa
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-tooltip-telemetry.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";
+
+// Test - Toggling rules linked to the element and the class
+// Checking whether the compatibility warning icon is displayed
+// correctly.
+// If a rule is disabled, it is marked compatible to keep
+// consistency with compatibility panel.
+// We test both the compatible and incompatible rules here
+
+const TEST_URI = `
+<style>
+ div {
+ -moz-float-edge: content-box;
+ }
+</style>
+<div></div>`;
+
+const TEST_DATA = [
+ {
+ selector: "div",
+ rules: [
+ {},
+ {
+ "-moz-float-edge": {
+ value: "content-box",
+ expected: COMPATIBILITY_TOOLTIP_MESSAGE.deprecated,
+ },
+ },
+ ],
+ },
+];
+
+add_task(async function () {
+ startTelemetry();
+
+ await pushPref(
+ "devtools.inspector.ruleview.inline-compatibility-warning.enabled",
+ true
+ );
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ info("Check correctness of data by toggling tooltip open");
+ await runCSSCompatibilityTests(view, inspector, TEST_DATA);
+
+ checkResults();
+});
+
+function checkResults() {
+ info(
+ 'Check the telemetry against "devtools.tooltip.shown" for label "css-compatibility" and ensure it is set'
+ );
+ checkTelemetry("devtools.tooltip.shown", "", 1, "css-compatibility");
+}
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 0000000000..37a1214a52
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cssom.js
@@ -0,0 +1,32 @@
+/* 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(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+ await selectNode("#target", inspector);
+
+ const elementStyle = view._elementStyle;
+ let rule;
+
+ rule = elementStyle.rules[1];
+ is(rule.textProps.length, 1, "rule 1 should have one property");
+ is(rule.textProps[0].name, "color", "the property should be 'color'");
+ is(rule.ruleLine, -1, "the property has no source line");
+
+ rule = elementStyle.rules[2];
+ is(rule.textProps.length, 1, "rule 2 should have one property");
+ is(
+ rule.textProps[0].name,
+ "font-weight",
+ "the property should be 'font-weight'"
+ );
+ is(rule.ruleLine, -1, "the property has no source line");
+});
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 0000000000..8a967e1a8a
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-appears-on-swatch-click.js
@@ -0,0 +1,79 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ const 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 (const swatch of swatches) {
+ info("Testing that the cubic-bezier appears on cubicswatch click");
+ await testAppears(view, swatch);
+ }
+});
+
+async function testAppears(view, swatch) {
+ ok(swatch, "The cubic-swatch exists");
+
+ const bezier = view.tooltips.getTooltip("cubicBezier");
+ ok(bezier, "The rule-view has the expected cubicBezier property");
+
+ const bezierPanel = bezier.tooltip.panel;
+ ok(bezierPanel, "The XUL panel for the cubic-bezier tooltip exists");
+
+ const onBezierWidgetReady = bezier.once("ready");
+ swatch.click();
+ await 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"
+ );
+ await 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 0000000000..5fbd1da25b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-commit-on-ENTER.js
@@ -0,0 +1,82 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { view } = await openRuleView();
+
+ info("Getting the bezier swatch element");
+ const swatch = getRuleViewProperty(
+ view,
+ "body",
+ "transition"
+ ).valueSpan.querySelector(".ruleview-bezierswatch");
+
+ await testPressingEnterCommitsChanges(swatch, view);
+});
+
+async function testPressingEnterCommitsChanges(swatch, ruleView) {
+ const bezierTooltip = ruleView.tooltips.getTooltip("cubicBezier");
+
+ info("Showing the tooltip");
+ const onBezierWidgetReady = bezierTooltip.once("ready");
+ swatch.click();
+ await onBezierWidgetReady;
+
+ const widget = await bezierTooltip.widget;
+ info("Simulating a change of curve in the widget");
+ widget.coordinates = [0.1, 2, 0.9, -1];
+ const expected = "cubic-bezier(0.1, 2, 0.9, -1)";
+
+ await waitForSuccess(async function () {
+ const func = await 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.includes("cubic-bezier("),
+ "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.
+ const onRuleViewChanged = waitForNEvents(ruleView, "ruleview-changed", 2);
+ focusAndSendKey(widget.parent.ownerDocument.defaultView, "RETURN");
+ await onRuleViewChanged;
+
+ const style = await getComputedStyleProperty(
+ "body",
+ null,
+ "transition-timing-function"
+ );
+ is(style, expected, "The element's timing-function was kept after RETURN");
+
+ const ruleViewStyle = getRuleViewProperty(
+ ruleView,
+ "body",
+ "transition"
+ ).valueSpan.textContent.includes("cubic-bezier(");
+ 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 0000000000..91371fa548
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-revert-on-ESC.js
@@ -0,0 +1,67 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { view } = await openRuleView();
+ await testPressingEscapeRevertsChanges(view);
+});
+
+async function testPressingEscapeRevertsChanges(view) {
+ const { propEditor } = await 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."
+ );
+
+ await escapeTooltip(view);
+
+ await waitForComputedStyleProperty(
+ "body",
+ null,
+ "animation-timing-function",
+ "linear"
+ );
+ is(
+ propEditor.valueSpan.textContent,
+ "linear",
+ "Got expected property value."
+ );
+}
+
+async function escapeTooltip(view) {
+ info("Pressing ESCAPE to close the tooltip");
+
+ const bezierTooltip = view.tooltips.getTooltip("cubicBezier");
+ const widget = await bezierTooltip.widget;
+ const onHidden = bezierTooltip.tooltip.once("hidden");
+ const onModifications = view.once("ruleview-changed");
+ focusAndSendKey(widget.parent.ownerDocument.defaultView, "ESCAPE");
+ await onHidden;
+ await 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 0000000000..084a870eb8
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_custom.js
@@ -0,0 +1,89 @@
+/* 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(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+
+ await simpleCustomOverride(inspector, view);
+ await importantCustomOverride(inspector, view);
+ await disableCustomOverride(inspector, view);
+});
+
+async function simpleCustomOverride(inspector, view) {
+ await selectNode("#testidSimple", inspector);
+
+ const idRule = getRuleViewRuleEditor(view, 1).rule;
+ const 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.");
+
+ const classRule = getRuleViewRuleEditor(view, 2).rule;
+ const 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.
+ const elementProp = await 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");
+}
+
+async function importantCustomOverride(inspector, view) {
+ await selectNode("#testidImportant", inspector);
+
+ const idRule = getRuleViewRuleEditor(view, 1).rule;
+ const idRuleProp = idRule.textProps[0];
+ ok(idRuleProp.overridden, "Not-important rule should be overridden.");
+
+ const classRule = getRuleViewRuleEditor(view, 2).rule;
+ const classRuleProp = classRule.textProps[0];
+ ok(!classRuleProp.overridden, "Important rule should not be overridden.");
+}
+
+async function disableCustomOverride(inspector, view) {
+ await selectNode("#testidDisable", inspector);
+
+ const idRule = getRuleViewRuleEditor(view, 1).rule;
+ const idRuleProp = idRule.textProps[0];
+
+ await togglePropStatus(view, idRuleProp);
+
+ const classRule = getRuleViewRuleEditor(view, 2).rule;
+ const 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 0000000000..be85867c94
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cycle-angle.js
@@ -0,0 +1,122 @@
+/* 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">
+ .turn {
+ filter: hue-rotate(1turn);
+ }
+ .deg {
+ filter: hue-rotate(180deg);
+ }
+ </style>
+ <body><div class=turn>Test turn</div><div class=deg>Test deg</div>cycling angle units in the rule view!</body>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await checkAngleCycling(inspector, view);
+ await checkAngleCyclingPersist(inspector, view);
+});
+
+async function checkAngleCycling(inspector, view) {
+ await selectNode(".turn", inspector);
+
+ const container = (
+ await getRuleViewProperty(view, ".turn", "filter", { wait: true })
+ ).valueSpan;
+ const valueNode = container.querySelector(".ruleview-angle");
+ const win = view.styleWindow;
+
+ // turn
+ is(valueNode.textContent, "1turn", "Angle displayed as a turn value.");
+
+ const 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 (const test of tests) {
+ await checkSwatchShiftClick(container, win, test.value, test.comment);
+ }
+}
+
+async function checkAngleCyclingPersist(inspector, view) {
+ await selectNode(".deg", inspector);
+ let container = (
+ await getRuleViewProperty(view, ".deg", "filter", { wait: true })
+ ).valueSpan;
+ let valueNode = container.querySelector(".ruleview-angle");
+ const win = view.styleWindow;
+
+ is(valueNode.textContent, "180deg", "Angle displayed as a degree value.");
+
+ await checkSwatchShiftClick(
+ container,
+ win,
+ `${Math.round(Math.PI * 10000) / 10000}rad`,
+ "Angle displayed as a radian value."
+ );
+
+ // Select the .turn div and reselect the .deg div to see
+ // if the new angle unit persisted
+ await selectNode(".turn", inspector);
+ await selectNode(".deg", inspector);
+
+ // We have to query for the container and the swatch because
+ // they've been re-generated
+ container = (
+ await getRuleViewProperty(view, ".deg", "filter", { wait: true })
+ ).valueSpan;
+ valueNode = container.querySelector(".ruleview-angle");
+ is(
+ valueNode.textContent,
+ `${Math.round(Math.PI * 10000) / 10000}rad`,
+ "Angle still displayed as a radian value."
+ );
+}
+
+async function checkSwatchShiftClick(container, win, expectedValue, comment) {
+ // Wait for 500ms before attempting a click to workaround frequent
+ // intermittents.
+ //
+ // See intermittent bug at https://bugzilla.mozilla.org/show_bug.cgi?id=1721938
+ // See potentially related bugs:
+ // - browserLoaded + synthesizeMouse timeouts https://bugzilla.mozilla.org/show_bug.cgi?id=1727749
+ // - mochitest general synthesize events issue https://bugzilla.mozilla.org/show_bug.cgi?id=1720248
+ await wait(500);
+
+ const swatch = container.querySelector(".ruleview-angleswatch");
+ const valueNode = container.querySelector(".ruleview-angle");
+
+ const onUnitChange = swatch.once("unit-change");
+ EventUtils.synthesizeMouseAtCenter(
+ swatch,
+ {
+ type: "mousedown",
+ shiftKey: true,
+ },
+ win
+ );
+ await 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 0000000000..3d8349ebc8
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cycle-color.js
@@ -0,0 +1,225 @@
+/* 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;
+ }
+ div {
+ color: green;
+ }
+ p {
+ color: blue;
+ }
+ </style>
+ <body>
+ <span>Test</span>
+ <div>cycling color types in the rule view!</div>
+ <p>cycling color and using the color picker</p>
+ </body>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await checkColorCycling(view);
+ await checkAlphaColorCycling(inspector, view);
+ await checkColorCyclingWithDifferentDefaultType(inspector, view);
+ await checkColorCyclingWithColorPicker(inspector, view);
+});
+
+async function checkColorCycling(view) {
+ const { valueSpan } = getRuleViewProperty(view, "body", "color");
+
+ checkColorValue(
+ valueSpan,
+ "#f00",
+ "Color displayed as a hex value, its authored type"
+ );
+
+ await runSwatchShiftClickTests(view, valueSpan, [
+ {
+ 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: "hwb(0 0% 0%)",
+ comment: "Color displayed as an HWB 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",
+ },
+ ]);
+}
+
+async function checkAlphaColorCycling(inspector, view) {
+ await selectNode("span", inspector);
+ const { valueSpan } = getRuleViewProperty(view, "span", "border-color");
+
+ checkColorValue(
+ valueSpan,
+ "#ff000080",
+ "Color displayed as an alpha hex value, its authored type"
+ );
+
+ await runSwatchShiftClickTests(view, valueSpan, [
+ {
+ 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: "hwb(0 0% 0% / 0.5)",
+ comment: "Color displayed as an HWB value.",
+ },
+ {
+ value: "#ff000080",
+ comment: "Color displayed as an alpha hex value again",
+ },
+ ]);
+}
+
+async function checkColorCyclingWithDifferentDefaultType(inspector, view) {
+ info("Change the default color type pref to hex");
+ await pushPref("devtools.defaultColorUnit", "hex");
+
+ info(
+ "Select a new node that would normally have a color with a different type"
+ );
+ await selectNode("div", inspector);
+ const { valueSpan } = getRuleViewProperty(view, "div", "color");
+
+ checkColorValue(
+ valueSpan,
+ "#008000",
+ "Color displayed as a hex value, which is the type just selected"
+ );
+
+ info("Cycle through color types again");
+ await runSwatchShiftClickTests(view, valueSpan, [
+ {
+ value: "hsl(120, 100%, 25.1%)",
+ comment: "Color displayed as an HSL value",
+ },
+ {
+ value: "rgb(0, 128, 0)",
+ comment: "Color displayed as an RGB value",
+ },
+ {
+ value: "hwb(120 0% 49.8%)",
+ comment: "Color displayed as an HWB value.",
+ },
+ {
+ value: "green",
+ comment: "Color displayed as a color name",
+ },
+ {
+ value: "#008000",
+ comment: "Color displayed as an authored value",
+ },
+ {
+ value: "hsl(120, 100%, 25.1%)",
+ comment: "Color displayed as an HSL value again",
+ },
+ ]);
+}
+
+async function checkColorCyclingWithColorPicker(inspector, view) {
+ // Enforce hex format for this test
+ await pushPref("devtools.defaultColorUnit", "hex");
+
+ info("Select a new node for this test");
+ await selectNode("p", inspector);
+ const { valueSpan } = getRuleViewProperty(view, "p", "color");
+
+ checkColorValue(valueSpan, "#00f", "Color has the expected initial value");
+
+ checkSwatchShiftClick(
+ view,
+ valueSpan,
+ "hsl(240, 100%, 50%)",
+ "Color has the expected value after a shift+click"
+ );
+
+ info("Opening the color picker");
+ const swatchElement = valueSpan.querySelector(".ruleview-colorswatch");
+ const picker = view.tooltips.getTooltip("colorPicker");
+ const onColorPickerReady = picker.once("ready");
+ swatchElement.click();
+ await onColorPickerReady;
+
+ info("Hide the color picker with escape");
+ const cPicker = view.tooltips.getTooltip("colorPicker");
+ const { spectrum } = cPicker;
+ const onHidden = cPicker.tooltip.once("hidden");
+ const onModifications = view.once("ruleview-changed");
+ EventUtils.sendKey("ESCAPE", spectrum.element.ownerDocument.defaultView);
+ await onHidden;
+ await onModifications;
+
+ is(
+ swatchElement.parentNode.dataset.color,
+ "hsl(240, 100%, 50%)",
+ "data-color is still using the correct format"
+ );
+}
+
+async function runSwatchShiftClickTests(view, valueSpan, tests) {
+ for (const { value, comment } of tests) {
+ await checkSwatchShiftClick(view, valueSpan, value, comment);
+ }
+}
+
+async function checkSwatchShiftClick(view, valueSpan, expectedValue, comment) {
+ const swatchNode = valueSpan.querySelector(".ruleview-colorswatch");
+ const colorNode = valueSpan.querySelector(".ruleview-color");
+
+ info(
+ "Shift-click the color swatch and wait for the color type and ruleview to update"
+ );
+ const onUnitChange = swatchNode.once("unit-change");
+
+ EventUtils.synthesizeMouseAtCenter(
+ swatchNode,
+ {
+ type: "mousedown",
+ shiftKey: true,
+ },
+ view.styleWindow
+ );
+
+ await onUnitChange;
+
+ is(colorNode.textContent, expectedValue, comment);
+}
+
+function checkColorValue(valueSpan, expectedColorValue, comment) {
+ const colorNode = valueSpan.querySelector(".ruleview-color");
+ is(colorNode.textContent, expectedColorValue, 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 0000000000..e3556ed8ff
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js
@@ -0,0 +1,53 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const highlighters = view.highlighters;
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.GRID;
+ const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } =
+ getHighlighterTestHelpers(inspector);
+
+ await selectNode("#grid", inspector);
+ const container = (
+ await getRuleViewProperty(view, "#grid", "display", { wait: true })
+ ).valueSpan;
+ let gridToggle = container.querySelector(".js-toggle-grid-highlighter");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ gridToggle.click();
+ await onHighlighterShown;
+
+ info("Edit the 'grid' property value to 'block'.");
+ const editor = await focusEditableField(view, container);
+ const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE);
+ const onDone = view.once("ruleview-changed");
+ editor.input.value = "block;";
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ await onHighlighterHidden;
+ await onDone;
+
+ info("Check the grid highlighter and grid toggle button are hidden.");
+ gridToggle = container.querySelector(".js-toggle-grid-highlighter");
+ ok(!gridToggle, "Grid highlighter toggle is not visible.");
+ ok(!highlighters.gridHighlighters.size, "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 0000000000..443f8432f8
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js
@@ -0,0 +1,56 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+ const prop = getTextProperty(view, 1, { "background-color": "#00F" });
+ const propEditor = prop.editor;
+
+ await focusEditableField(view, propEditor.nameSpan);
+ await sendKeysAndWaitForFocus(view, ruleEditor.element, ["DELETE", "ESCAPE"]);
+
+ is(
+ propEditor.nameSpan.textContent,
+ "background-color",
+ "'background-color' property name is correctly set."
+ );
+ is(
+ await getComputedStyleProperty("#testid", null, "background-color"),
+ "rgb(0, 0, 255)",
+ "#00F background color is set."
+ );
+
+ await focusEditableField(view, propEditor.valueSpan);
+ const onValueDeleted = view.once("ruleview-changed");
+ await sendKeysAndWaitForFocus(view, ruleEditor.element, ["DELETE", "ESCAPE"]);
+ await onValueDeleted;
+
+ is(
+ propEditor.valueSpan.textContent,
+ "#00F",
+ "'#00F' property value is correctly set."
+ );
+ is(
+ await 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 0000000000..78fa56eb35
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-click.js
@@ -0,0 +1,60 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testEditPropertyAndCancel(inspector, view);
+});
+
+async function testEditPropertyAndCancel(inspector, view) {
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+ const propEditor = getTextProperty(view, 1, { margin: "0" }).editor;
+
+ info("Test editor is created when clicking on property name");
+ await focusEditableField(view, propEditor.nameSpan);
+ ok(propEditor.nameSpan.inplaceEditor, "Editor created for property name");
+ await sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]);
+
+ info("Test editor is created when clicking on ':' next to property name");
+ const nameRect = propEditor.nameSpan.getBoundingClientRect();
+ await focusEditableField(view, propEditor.nameSpan, nameRect.width + 1);
+ ok(propEditor.nameSpan.inplaceEditor, "Editor created for property name");
+ await sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]);
+
+ info("Test editor is created when clicking on property value");
+ await 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");
+ await sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]);
+ await onRuleviewChanged;
+
+ info("Test editor is created when clicking on ';' next to property value");
+ const valueRect = propEditor.valueSpan.getBoundingClientRect();
+ await 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");
+ await sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]);
+ await 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 0000000000..c27395a18d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js
@@ -0,0 +1,101 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ for (const data of testData) {
+ await runTestData(view, data);
+ }
+});
+
+async function runTestData(view, { value, commitKey, modifiers, expected }) {
+ const idRuleEditor = getRuleViewRuleEditor(view, 1);
+ const propEditor = idRuleEditor.rule.textProps[0].editor;
+
+ info("Focusing the inplace editor field");
+
+ const editor = await 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.debounce.flush();
+ await onRuleViewChanged;
+
+ info("Entering the commit key " + commitKey + " " + modifiers);
+ onRuleViewChanged = view.once("ruleview-changed");
+ const onBlur = once(editor.input, "blur");
+ EventUtils.synthesizeKey(commitKey, modifiers);
+ await onBlur;
+ await 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 0000000000..cbcbc506fe
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js
@@ -0,0 +1,108 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await editAndCheck(view);
+});
+
+async function editAndCheck(view) {
+ const prop = getTextProperty(view, 1, { padding: "10px" });
+ const propEditor = prop.editor;
+ const newPaddingValue = "20px";
+
+ info("Focusing the inplace editor field");
+ const editor = await focusEditableField(view, propEditor.valueSpan);
+ is(
+ inplaceEditor(propEditor.valueSpan),
+ editor,
+ "Focused editor should be the value span."
+ );
+
+ const onPropertyChange = waitForComputedStyleProperty(
+ "#testid",
+ null,
+ "padding-top",
+ newPaddingValue
+ );
+ const onRefreshAfterPreview = once(view, "ruleview-changed");
+
+ info("Entering a new value");
+ EventUtils.sendString(newPaddingValue, view.styleWindow);
+
+ info(
+ "Waiting for the debounced previewValue to apply the " +
+ "changes to document"
+ );
+
+ view.debounce.flush();
+ await onPropertyChange;
+
+ info("Waiting for ruleview-refreshed after previewValue was applied.");
+ await onRefreshAfterPreview;
+
+ const onBlur = once(editor.input, "blur");
+
+ info("Entering the commit key and finishing edit");
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ info("Waiting for blur on the field");
+ await onBlur;
+
+ info("Waiting for the style changes to be applied");
+ await once(view, "ruleview-changed");
+
+ const computed = prop.computed;
+ const 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();
+ const 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 0000000000..2072ea5f96
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js
@@ -0,0 +1,820 @@
+/* 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 lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
+});
+
+const TEST_URI = `
+ <style>
+ #test {
+ margin-top: 0px;
+ padding-top: 0px;
+ color: #000000;
+ background-color: #000000;
+ background: none;
+ transition: initial;
+ z-index: 0;
+ opacity: 1;
+ line-height: 1;
+ --custom: 0;
+ }
+ </style>
+ <div id="test"></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ const { inspector, view } = await openRuleView();
+ await selectNode("#test", inspector);
+
+ await testMarginIncrements(view);
+ await testVariousUnitIncrements(view);
+ await testHexIncrements(view);
+ await testAlphaHexIncrements(view);
+ await testRgbIncrements(view);
+ await testHslIncrements(view);
+ await testRgbCss4Increments(view);
+ await testHslCss4Increments(view);
+ await testHwbIncrements(view);
+ await testShorthandIncrements(view);
+ await testOddCases(view);
+ await testZeroValueIncrements(view);
+ await testOpacityIncrements(view);
+ await testLineHeightIncrements(view);
+ await testCssVariableIncrements(view);
+});
+
+async function testMarginIncrements(view) {
+ info("Testing keyboard increments on the margin property");
+
+ const marginPropEditor = getTextProperty(view, 1, {
+ "margin-top": "0px",
+ }).editor;
+
+ await runIncrementTest(marginPropEditor, view, {
+ 1: {
+ ...getSmallIncrementKey(),
+ 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,
+ ...getSmallIncrementKey(),
+ 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 },
+ });
+}
+
+async function testVariousUnitIncrements(view) {
+ info("Testing keyboard increments on values with various units");
+
+ const paddingPropEditor = getTextProperty(view, 1, {
+ "padding-top": "0px",
+ }).editor;
+
+ await 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 },
+ });
+}
+
+async function testHexIncrements(view) {
+ info("Testing keyboard increments with hex colors");
+
+ const hexColorPropEditor = getTextProperty(view, 1, {
+ color: "#000000",
+ }).editor;
+
+ await 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,
+ },
+ });
+}
+
+async function testAlphaHexIncrements(view) {
+ info("Testing keyboard increments with alpha hex colors");
+
+ const hexColorPropEditor = getTextProperty(view, 1, {
+ color: "#000000",
+ }).editor;
+
+ await 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,
+ },
+ });
+}
+
+async function testRgbIncrements(view) {
+ info("Testing keyboard increments with rgb(a) colors");
+
+ const rgbColorPropEditor = getTextProperty(view, 1, {
+ "background-color": "#000000",
+ }).editor;
+
+ await 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],
+ },
+ 7: {
+ start: "rgba(0,0,0,1)",
+ end: "rgba(0,0,0,1)",
+ selection: [11, 12],
+ },
+ 8: {
+ ...getSmallIncrementKey(),
+ start: "rgba(0,0,0,0.5)",
+ end: "rgba(0,0,0,0.6)",
+ selection: [12, 13],
+ },
+ 9: {
+ down: true,
+ start: "rgba(0,0,0,0)",
+ end: "rgba(0,0,0,0)",
+ selection: [11, 12],
+ },
+ });
+}
+
+async function testHslIncrements(view) {
+ info("Testing keyboard increments with hsl(a) colors");
+
+ const hslColorPropEditor = getTextProperty(view, 1, {
+ "background-color": "#000000",
+ }).editor;
+
+ await runIncrementTest(hslColorPropEditor, view, {
+ 1: { start: "hsl(0,0%,0%)", end: "hsl(0,1%,0%)", selection: [6, 8] },
+ 2: {
+ shift: true,
+ start: "hsl(0,0%,0%)",
+ end: "hsl(0,10%,0%)",
+ selection: [6, 8],
+ },
+ 3: { start: "hsl(0,100%,0%)", end: "hsl(0,100%,0%)", selection: [6, 10] },
+ 4: {
+ shift: true,
+ start: "hsl(0,95%,0%)",
+ end: "hsl(0,100%,0%)",
+ selection: [6, 10],
+ },
+ 5: {
+ down: true,
+ start: "hsl(0,0%,0%)",
+ end: "hsl(0,0%,0%)",
+ selection: [6, 8],
+ },
+ 6: {
+ down: true,
+ shift: true,
+ start: "hsl(0,5%,0%)",
+ end: "hsl(0,0%,0%)",
+ selection: [6, 8],
+ },
+ 7: {
+ start: "hsla(0,0%,0%,1)",
+ end: "hsla(0,0%,0%,1)",
+ selection: [13, 14],
+ },
+ 8: {
+ ...getSmallIncrementKey(),
+ start: "hsla(0,0%,0%,0.5)",
+ end: "hsla(0,0%,0%,0.6)",
+ selection: [14, 15],
+ },
+ 9: {
+ down: true,
+ start: "hsla(0,0%,0%,0)",
+ end: "hsla(0,0%,0%,0)",
+ selection: [13, 14],
+ },
+ });
+}
+
+async function testRgbCss4Increments(view) {
+ info("Testing keyboard increments with rgb colors using CSS 4 Color syntax");
+
+ const rgbColorPropEditor = getTextProperty(view, 1, {
+ "background-color": "#000000",
+ }).editor;
+
+ await 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],
+ },
+ 7: {
+ start: "rgb(0 0 0/1)",
+ end: "rgb(0 0 0/1)",
+ selection: [10, 11],
+ },
+ 8: {
+ ...getSmallIncrementKey(),
+ start: "rgb(0 0 0/0.5)",
+ end: "rgb(0 0 0/0.6)",
+ selection: [11, 12],
+ },
+ 9: {
+ down: true,
+ start: "rgb(0 0 0/0)",
+ end: "rgb(0 0 0/0)",
+ selection: [10, 11],
+ },
+ });
+}
+
+async function testHslCss4Increments(view) {
+ info("Testing keyboard increments with hsl colors using CSS 4 Color syntax");
+
+ const hslColorPropEditor = getTextProperty(view, 1, {
+ "background-color": "#000000",
+ }).editor;
+
+ await runIncrementTest(hslColorPropEditor, view, {
+ 1: { start: "hsl(0 0% 0%)", end: "hsl(0 1% 0%)", selection: [6, 8] },
+ 2: {
+ shift: true,
+ start: "hsl(0 0% 0%)",
+ end: "hsl(0 10% 0%)",
+ selection: [6, 8],
+ },
+ 3: { start: "hsl(0 100% 0%)", end: "hsl(0 100% 0%)", selection: [6, 10] },
+ 4: {
+ shift: true,
+ start: "hsl(0 95% 0%)",
+ end: "hsl(0 100% 0%)",
+ selection: [6, 10],
+ },
+ 5: {
+ down: true,
+ start: "hsl(0 0% 0%)",
+ end: "hsl(0 0% 0%)",
+ selection: [6, 8],
+ },
+ 6: {
+ down: true,
+ shift: true,
+ start: "hsl(0 5% 0%)",
+ end: "hsl(0 0% 0%)",
+ selection: [6, 8],
+ },
+ 7: {
+ start: "hsl(0 0% 0%/1)",
+ end: "hsl(0 0% 0%/1)",
+ selection: [12, 13],
+ },
+ 8: {
+ ...getSmallIncrementKey(),
+ start: "hsl(0 0% 0%/0.5)",
+ end: "hsl(0 0% 0%/0.6)",
+ selection: [13, 14],
+ },
+ 9: {
+ down: true,
+ start: "hsl(0 0% 0%/0)",
+ end: "hsl(0 0% 0%/0)",
+ selection: [12, 13],
+ },
+ });
+}
+
+async function testHwbIncrements(view) {
+ info("Testing keyboard increments with hwb colors");
+
+ const hwbColorPropEditor = getTextProperty(view, 1, {
+ "background-color": "#000000",
+ }).editor;
+
+ await runIncrementTest(hwbColorPropEditor, view, {
+ 1: { start: "hwb(0 0% 0%)", end: "hwb(0 1% 0%)", selection: [6, 8] },
+ 2: {
+ shift: true,
+ start: "hwb(0 0% 0%)",
+ end: "hwb(0 10% 0%)",
+ selection: [6, 8],
+ },
+ 3: { start: "hwb(0 100% 0%)", end: "hwb(0 100% 0%)", selection: [6, 10] },
+ 4: {
+ shift: true,
+ start: "hwb(0 95% 0%)",
+ end: "hwb(0 100% 0%)",
+ selection: [6, 10],
+ },
+ 5: {
+ down: true,
+ start: "hwb(0 0% 0%)",
+ end: "hwb(0 0% 0%)",
+ selection: [6, 8],
+ },
+ 6: {
+ down: true,
+ shift: true,
+ start: "hwb(0 5% 0%)",
+ end: "hwb(0 0% 0%)",
+ selection: [6, 8],
+ },
+ 7: {
+ start: "hwb(0 0% 0%/1)",
+ end: "hwb(0 0% 0%/1)",
+ selection: [12, 13],
+ },
+ 8: {
+ ...getSmallIncrementKey(),
+ start: "hwb(0 0% 0%/0.5)",
+ end: "hwb(0 0% 0%/0.6)",
+ selection: [13, 14],
+ },
+ 9: {
+ down: true,
+ start: "hwb(0 0% 0%/0)",
+ end: "hwb(0 0% 0%/0)",
+ selection: [12, 13],
+ },
+ });
+}
+
+async function testShorthandIncrements(view) {
+ info("Testing keyboard increments within shorthand values");
+
+ const paddingPropEditor = getTextProperty(view, 1, {
+ "padding-top": "0px",
+ }).editor;
+
+ await 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,
+ ...getSmallIncrementKey(),
+ 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],
+ },
+ });
+}
+
+async function testOddCases(view) {
+ info("Testing some more odd cases");
+
+ const marginPropEditor = getTextProperty(view, 1, {
+ "margin-top": "0px",
+ }).editor;
+
+ await runIncrementTest(marginPropEditor, view, {
+ 1: { start: "98.7%", end: "99.7%", selection: [3, 3] },
+ 2: {
+ ...getSmallIncrementKey(),
+ 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,
+ ...getSmallIncrementKey(),
+ start: "url('test-0.png')",
+ end: "url('test--0.1.png')",
+ selection: [10, 11],
+ },
+ 14: {
+ ...getSmallIncrementKey(),
+ start: "url('test--0.1.png')",
+ end: "url('test-0.png')",
+ selection: [10, 14],
+ },
+ });
+}
+
+async function testZeroValueIncrements(view) {
+ info("Testing a valid unit is added when incrementing from 0");
+
+ const backgroundPropEditor = getTextProperty(view, 1, {
+ background: "none",
+ }).editor;
+ await 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],
+ },
+ });
+
+ const transitionPropEditor = getTextProperty(view, 1, {
+ transition: "initial",
+ }).editor;
+ await 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],
+ },
+ });
+
+ const zIndexPropEditor = getTextProperty(view, 1, { "z-index": "0" }).editor;
+ await runIncrementTest(zIndexPropEditor, view, {
+ 1: { start: "0", end: "1", selection: [1, 1] },
+ });
+}
+
+async function testOpacityIncrements(view) {
+ info("Testing keyboard increments on the opacity property");
+
+ const opacityPropEditor = getTextProperty(view, 1, { opacity: "1" }).editor;
+
+ await runIncrementTest(opacityPropEditor, view, {
+ 1: {
+ ...getSmallIncrementKey(),
+ start: "0.5",
+ end: "0.51",
+ selectAll: true,
+ },
+ 2: { start: "0", end: "0.1", selectAll: true },
+ 3: { shift: true, start: "0", end: "1", selectAll: true },
+ 4: {
+ down: true,
+ ...getSmallIncrementKey(),
+ start: "0.1",
+ end: "0.09",
+ selectAll: true,
+ },
+ 5: { down: true, start: "0", end: "-0.1", selectAll: true },
+ 6: { down: true, shift: true, start: "0", end: "-1", selectAll: true },
+ 7: { pageUp: true, shift: true, start: "0", end: "10", selectAll: true },
+ 8: { pageDown: true, shift: true, start: "0", end: "-10", selectAll: true },
+ 9: { start: "0.7", end: "0.8", selectAll: true },
+ 10: { down: true, start: "0", end: "-0.1", selectAll: true },
+ });
+}
+
+async function testLineHeightIncrements(view) {
+ info("Testing keyboard increments on the line height property");
+
+ const opacityPropEditor = getTextProperty(view, 1, {
+ "line-height": "1",
+ }).editor;
+
+ // line-height accepts both values with or without units, check that we don't
+ // force using a unit if none was specified.
+ await runIncrementTest(opacityPropEditor, view, {
+ 1: {
+ ...getSmallIncrementKey(),
+ start: "0",
+ end: "0.1",
+ selectAll: true,
+ },
+ 2: {
+ ...getSmallIncrementKey(),
+ start: "0px",
+ end: "0.1px",
+ selectAll: true,
+ },
+ 3: {
+ start: "0",
+ end: "1",
+ selectAll: true,
+ },
+ 4: {
+ start: "0px",
+ end: "1px",
+ selectAll: true,
+ },
+ 5: {
+ down: true,
+ ...getSmallIncrementKey(),
+ start: "0",
+ end: "-0.1",
+ selectAll: true,
+ },
+ 6: {
+ down: true,
+ ...getSmallIncrementKey(),
+ start: "0px",
+ end: "-0.1px",
+ selectAll: true,
+ },
+ 7: {
+ down: true,
+ start: "0",
+ end: "-1",
+ selectAll: true,
+ },
+ 8: {
+ down: true,
+ start: "0px",
+ end: "-1px",
+ selectAll: true,
+ },
+ });
+}
+
+async function testCssVariableIncrements(view) {
+ info("Testing keyboard increments on the css variable property");
+
+ const opacityPropEditor = getTextProperty(view, 1, {
+ "--custom": "0",
+ }).editor;
+
+ await runIncrementTest(opacityPropEditor, view, {
+ 1: {
+ ...getSmallIncrementKey(),
+ start: "0",
+ end: "0.1",
+ selectAll: true,
+ },
+ 2: {
+ start: "0",
+ end: "1",
+ selectAll: true,
+ },
+ 3: {
+ down: true,
+ ...getSmallIncrementKey(),
+ start: "0",
+ end: "-0.1",
+ selectAll: true,
+ },
+ 4: {
+ down: true,
+ start: "0",
+ end: "-1",
+ selectAll: true,
+ },
+ });
+}
+
+async function runIncrementTest(propertyEditor, view, tests) {
+ propertyEditor.valueSpan.scrollIntoView();
+ const editor = await focusEditableField(view, propertyEditor.valueSpan);
+
+ for (const test in tests) {
+ await 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).
+ const onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ view.debounce.flush();
+ await onRuleViewChanged;
+}
+
+async function testIncrement(editor, options, view) {
+ editor.input.value = options.start;
+ const 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);
+
+ const onRuleViewChanged = view.once("ruleview-changed");
+ const 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";
+ }
+
+ let smallIncrementKey = { ctrlKey: options.ctrl };
+ if (lazy.AppConstants.platform === "macosx") {
+ smallIncrementKey = { altKey: options.alt };
+ }
+
+ EventUtils.synthesizeKey(
+ key,
+ { ...smallIncrementKey, shiftKey: options.shift },
+ view.styleWindow
+ );
+
+ await onKeyUp;
+
+ // Only expect a change if the value actually changed!
+ if (options.start !== options.end) {
+ view.debounce.flush();
+ await onRuleViewChanged;
+ }
+
+ is(input.value, options.end, "Value changed to " + options.end);
+}
+
+function getSmallIncrementKey() {
+ if (lazy.AppConstants.platform === "macosx") {
+ return { alt: true };
+ }
+ return { ctrl: true };
+}
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 0000000000..172c01d511
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-order.js
@@ -0,0 +1,130 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ const elementStyle = view._elementStyle;
+ const elementRule = elementStyle.rules[1];
+
+ info("Checking rules insertion order and checking the applied style");
+ const firstProp = await addProperty(view, 1, "background-color", "green");
+ let secondProp = await 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(
+ await 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");
+ await removeProperty(view, secondProp);
+ // rgb(0, 128, 0) = green
+ is(
+ await 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 = await addProperty(view, 1, "background-color", "blue");
+
+ is(
+ await 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");
+ await togglePropStatus(view, secondProp);
+
+ is(
+ await 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");
+ await togglePropStatus(view, firstProp);
+
+ is(
+ await getValue("#testid", "background-color"),
+ "rgba(0, 0, 0, 0)",
+ "After disabling both properties, value should be empty."
+ );
+
+ info("Re-enabling the second propertyt and checking the applied style");
+ await togglePropStatus(view, secondProp);
+
+ is(
+ await 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"
+ );
+ await togglePropStatus(view, firstProp);
+
+ is(
+ await 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");
+ await setProperty(view, firstProp, "purple");
+
+ is(
+ await getValue("#testid", "background-color"),
+ "rgb(0, 0, 255)",
+ "Modifying an earlier property shouldn't override a later property."
+ );
+});
+
+async function getValue(selector, propName) {
+ const value = await 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 0000000000..c929d252da
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_01.js
@@ -0,0 +1,66 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ info("Getting the first property in the #testid rule");
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[0];
+
+ info("Deleting the name of that property to remove the property");
+ await removeProperty(view, prop, false);
+
+ let newValue = await getRulePropertyValue(0, 0, "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();
+ await removeProperty(view, prop, false);
+
+ newValue = await getRulePropertyValue(0, 0, "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 0000000000..b5f0673b5d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_02.js
@@ -0,0 +1,66 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ info("Getting the first property in the rule");
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[0];
+
+ info("Clearing the property value");
+ await setProperty(view, prop, null, { blurNewProperty: false });
+
+ let newValue = await getRulePropertyValue(0, 0, "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");
+ await setProperty(view, prop, null, { blurNewProperty: false });
+
+ newValue = await getRulePropertyValue(0, 0, "background-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 0000000000..d5bc376dd4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_03.js
@@ -0,0 +1,84 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ info("Getting the second property in the rule");
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ let prop = rule.textProps[1];
+
+ info("Clearing the property value and pressing shift-tab");
+ let editor = await focusEditableField(view, prop.editor.valueSpan);
+ const onValueDone = view.once("ruleview-changed");
+ editor.input.value = "";
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, view.styleWindow);
+ await onValueDone;
+
+ let newValue = await getRulePropertyValue(0, 0, "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");
+ const onValueFocused = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, view.styleWindow);
+ await 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");
+ const onNameFocused = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, view.styleWindow);
+ await onNameFocused;
+
+ info("Removing the name and pressing shift-tab to focus the selector");
+ const onNameDeleted = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow);
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, view.styleWindow);
+ await onNameDeleted;
+
+ newValue = await getRulePropertyValue(0, 0, "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-remove_04.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_04.js
new file mode 100644
index 0000000000..0d9915285c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_04.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that removing the only declaration from a rule and unselecting then re-selecting
+// the element will not restore the removed declaration. Bug 1512956
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid {
+ color: #00F;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+ <div id='empty'></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ info("Select original node");
+ await selectNode("#testid", inspector);
+
+ info("Get the first property in the #testid rule");
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ const prop = rule.textProps[0];
+
+ info("Delete the property name to remove the declaration");
+ const onRuleViewChanged = view.once("ruleview-changed");
+ await removeProperty(view, prop, false);
+ info("Wait for Rule view to update");
+ await onRuleViewChanged;
+
+ is(rule.textProps.length, 0, "No CSS properties left on the rule");
+
+ info("Select another node");
+ await selectNode("#empty", inspector);
+
+ info("Select original node again");
+ await selectNode("#testid", inspector);
+
+ is(rule.textProps.length, 0, "Still no CSS properties on the rule");
+});
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 0000000000..258c0a0f88
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_01.js
@@ -0,0 +1,160 @@
+/* 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 ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS;
+
+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 },
+];
+
+const DATA = [
+ {
+ timestamp: null,
+ category: "devtools.main",
+ method: "edit_rule",
+ object: "ruleview",
+ },
+ {
+ timestamp: null,
+ category: "devtools.main",
+ method: "edit_rule",
+ object: "ruleview",
+ },
+ {
+ timestamp: null,
+ category: "devtools.main",
+ method: "edit_rule",
+ object: "ruleview",
+ },
+ {
+ timestamp: null,
+ category: "devtools.main",
+ method: "edit_rule",
+ object: "ruleview",
+ },
+ {
+ timestamp: null,
+ category: "devtools.main",
+ method: "edit_rule",
+ object: "ruleview",
+ },
+];
+
+add_task(async function () {
+ // Let's reset the counts.
+ Services.telemetry.clearEvents();
+
+ // Ensure no events have been logged
+ const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true);
+ ok(!snapshot.parent, "No events have been logged for the main process");
+
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ for (const { name, value, isValid } of TEST_DATA) {
+ await testEditProperty(view, rule, name, value, isValid);
+ }
+
+ checkResults();
+});
+
+async function testEditProperty(view, rule, name, value, isValid) {
+ info("Test editing existing property name/value fields");
+
+ const doc = rule.editor.doc;
+ const prop = rule.textProps[0];
+
+ info("Focusing an existing property name in the rule-view");
+ let editor = await 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"
+ );
+ const onValueFocus = once(rule.editor.element, "focus", true);
+ const onNameDone = view.once("ruleview-changed");
+ EventUtils.sendString(name + ":", doc.defaultView);
+ await onValueFocus;
+ await 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");
+ const onValueDone = view.once("ruleview-changed");
+ const onBlur = once(input, "blur");
+ EventUtils.sendString(value + ";", doc.defaultView);
+ await onBlur;
+ await onValueDone;
+
+ is(
+ prop.editor.isValid(),
+ isValid,
+ value + " is " + isValid ? "valid" : "invalid"
+ );
+
+ info("Checking that the style property was changed on the content page");
+ const propValue = await getRulePropertyValue(0, 0, name);
+ if (isValid) {
+ is(propValue, value, name + " should have been set.");
+ } else {
+ isnot(propValue, value, name + " shouldn't have been set.");
+ }
+}
+
+function checkResults() {
+ const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true);
+ const events = snapshot.parent.filter(
+ event =>
+ event[1] === "devtools.main" &&
+ event[2] === "edit_rule" &&
+ event[3] === "ruleview"
+ );
+
+ for (const i in DATA) {
+ const [timestamp, category, method, object] = events[i];
+ const expected = DATA[i];
+
+ // ignore timestamp
+ ok(timestamp > 0, "timestamp is greater than 0");
+ is(category, expected.category, "category is correct");
+ is(method, expected.method, "method is correct");
+ is(object, expected.object, "object is correct");
+ }
+}
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 0000000000..373f78eab6
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js
@@ -0,0 +1,143 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ await testEditProperty(inspector, view);
+ await testDisableProperty(inspector, view);
+ await testPropertyStillMarkedDirty(inspector, view);
+});
+
+async function testEditProperty(inspector, ruleView) {
+ const idRule = getRuleViewRuleEditor(ruleView, 1).rule;
+ const prop = getTextProperty(ruleView, 1, { "background-color": "blue" });
+
+ let editor = await 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"
+ );
+ const onNameDone = ruleView.once("ruleview-changed");
+ const onFocus = once(idRule.editor.element, "focus", true);
+ EventUtils.sendString("border-color:", ruleView.styleWindow);
+ await onFocus;
+ await 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");
+ const 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 (const ch of "red;") {
+ const onPreviewDone = ruleView.once("ruleview-changed");
+ EventUtils.sendChar(ch, ruleView.styleWindow);
+ ruleView.debounce.flush();
+ await onPreviewDone;
+ is(
+ prop.editor.warning.hidden,
+ true,
+ "warning triangle is hidden or shown as appropriate"
+ );
+ }
+ await onBlur;
+
+ const newValue = await getRulePropertyValue(0, 0, "border-color");
+ is(newValue, "red", "border-color should have been set.");
+
+ ruleView.styleDocument.activeElement.blur();
+ await addProperty(ruleView, 1, "color", "red", { commitValueWith: ";" });
+
+ const 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"
+ );
+ }
+}
+
+async function testDisableProperty(inspector, ruleView) {
+ const prop = getTextProperty(ruleView, 1, {
+ "border-color": "red",
+ color: "red",
+ });
+
+ info("Disabling a property");
+ await togglePropStatus(ruleView, prop);
+
+ let newValue = await getRulePropertyValue(0, 0, "border-color");
+ is(newValue, "", "Border-color should have been unset.");
+
+ info("Enabling the property again");
+ await togglePropStatus(ruleView, prop);
+
+ newValue = await getRulePropertyValue(0, 0, "border-color");
+ is(newValue, "red", "Border-color should have been reset.");
+}
+
+async function testPropertyStillMarkedDirty(inspector, ruleView) {
+ // Select an unstyled node.
+ await selectNode("#testid2", inspector);
+
+ // Select the original node again.
+ await selectNode("#testid", inspector);
+
+ const 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 0000000000..e6ec0aef33
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_03.js
@@ -0,0 +1,49 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+ const propEditor = getTextProperty(view, 1, {
+ "background-color": "blue",
+ }).editor;
+
+ await focusEditableField(view, propEditor.valueSpan);
+
+ info("Deleting all the text out of a value field");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ await sendKeysAndWaitForFocus(view, ruleEditor.element, ["DELETE", "RETURN"]);
+ await onRuleViewChanged;
+
+ info("Pressing enter a couple times to cycle through editors");
+ await sendKeysAndWaitForFocus(view, ruleEditor.element, ["RETURN"]);
+ onRuleViewChanged = view.once("ruleview-changed");
+ await sendKeysAndWaitForFocus(view, ruleEditor.element, ["RETURN"]);
+ await onRuleViewChanged;
+
+ isnot(propEditor.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 0000000000..070832710b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_04.js
@@ -0,0 +1,83 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ const prop = getTextProperty(view, 1, { "background-color": "blue" });
+
+ info("Disabling a property");
+ await togglePropStatus(view, prop);
+
+ const newValue = await getRulePropertyValue(0, 0, "background-color");
+ is(newValue, "", "background-color should have been unset.");
+
+ await testEditDisableProperty(view, prop, "name", "VK_ESCAPE");
+ await testEditDisableProperty(view, prop, "value", "VK_ESCAPE");
+ await testEditDisableProperty(view, prop, "value", "VK_TAB");
+ await testEditDisableProperty(view, prop, "value", "VK_RETURN");
+});
+
+async function testEditDisableProperty(view, prop, fieldType, commitKey) {
+ const field =
+ fieldType === "name" ? prop.editor.nameSpan : prop.editor.valueSpan;
+
+ const editor = await 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 = await getRulePropertyValue(0, 0, "background-color");
+ is(newValue, "", "background-color should remain unset.");
+
+ let onChangeDone;
+ if (fieldType === "value") {
+ onChangeDone = view.once("ruleview-changed");
+ }
+
+ const onBlur = once(editor.input, "blur");
+ EventUtils.synthesizeKey(commitKey, {}, view.styleWindow);
+ await onBlur;
+ await 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 = await getRulePropertyValue(0, 0, "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 0000000000..9bfc002a4a
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_05.js
@@ -0,0 +1,72 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ const prop = getTextProperty(view, 1, { "background-color": "blue" });
+
+ info("Disabling background-color property");
+ await togglePropStatus(view, prop);
+
+ let newValue = await getRulePropertyValue(0, 0, "background-color");
+ is(newValue, "", "background-color should have been unset.");
+
+ info(
+ "Entering a new property name, including : to commit and " +
+ "focus the value"
+ );
+
+ await focusEditableField(view, prop.editor.nameSpan);
+ const onNameDone = view.once("ruleview-changed");
+ EventUtils.sendString("border-color:", view.styleWindow);
+ await onNameDone;
+
+ info("Escape editing the property value");
+ const onValueDone = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ await onValueDone;
+
+ newValue = await getRulePropertyValue(0, 0, "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");
+ await togglePropStatus(view, prop);
+
+ newValue = await getRulePropertyValue(0, 0, "border-color");
+ is(newValue, "", "border-color should have been unset.");
+
+ info("Enter a new property value for the border-color property");
+ await setProperty(view, prop, "red");
+
+ newValue = await getRulePropertyValue(0, 0, "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"
+ );
+});
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 0000000000..1b9a04ba5d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_06.js
@@ -0,0 +1,68 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("body", inspector);
+
+ const prop = getTextProperty(view, 1, { "background-color": "red" });
+
+ is(
+ await getComputedStyleProperty("body", null, "background-color"),
+ "rgb(0, 128, 0)",
+ "green background color is set."
+ );
+
+ await setProperty(view, prop, "red !important");
+
+ is(
+ prop.editor.valueSpan.textContent,
+ "red !important",
+ "'red !important' property value is correctly set."
+ );
+ is(
+ await getComputedStyleProperty("body", null, "background-color"),
+ "rgb(255, 0, 0)",
+ "red background color is set."
+ );
+
+ info("Disabling red background color property");
+ await togglePropStatus(view, prop);
+
+ is(
+ await getComputedStyleProperty("body", null, "background-color"),
+ "rgb(0, 128, 0)",
+ "green background color is set."
+ );
+
+ await 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(
+ await 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 0000000000..9edd910fa6
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_07.js
@@ -0,0 +1,79 @@
+/* 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 STYLE = "#testid { background-color: #f00 }";
+
+const TEST_URI_INLINE_SHEET = `
+ <style>${STYLE}</style>
+ <div id='testid'>Styled Node</div>
+`;
+
+const TEST_URI_CONSTRUCTED_SHEET = `
+ <div id='testid'>Styled Node</div>
+ <script>
+ let sheet = new CSSStyleSheet();
+ sheet.replaceSync("${STYLE}");
+ document.adoptedStyleSheets.push(sheet);
+ </script>
+`;
+
+async function runTest(testUri) {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(testUri));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ const prop = rule.textProps[0];
+
+ info("Disabling red background color property");
+ await togglePropStatus(view, prop);
+ ok(!prop.enabled, "red background-color property is disabled.");
+
+ const editor = await focusEditableField(view, prop.editor.valueSpan);
+ const onDone = view.once("ruleview-changed");
+ editor.input.value = "red; color: red;";
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ await onDone;
+
+ is(
+ prop.editor.valueSpan.textContent,
+ "red",
+ "'red' property value is correctly set."
+ );
+ ok(prop.enabled, "red background-color property is enabled.");
+ is(
+ await getComputedStyleProperty("#testid", null, "background-color"),
+ "rgb(255, 0, 0)",
+ "red background color is set."
+ );
+
+ const 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(
+ await getComputedStyleProperty("#testid", null, "color"),
+ "rgb(255, 0, 0)",
+ "red color is set."
+ );
+}
+
+add_task(async function test_inline_sheet() {
+ await runTest(TEST_URI_INLINE_SHEET);
+});
+
+add_task(async function test_constructed_sheet() {
+ await runTest(TEST_URI_CONSTRUCTED_SHEET);
+});
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 0000000000..d00a7c2c24
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_08.js
@@ -0,0 +1,62 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ info("Get the color property editor");
+ const ruleEditor = getRuleViewRuleEditor(view, 0);
+ const propEditor = ruleEditor.rule.textProps[0].editor;
+ is(ruleEditor.rule.textProps[0].name, "color");
+
+ info("Focus the property name field");
+ await 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.
+ const onValueFocus = once(ruleEditor.element, "focus", true);
+ let onRuleViewChanged = ruleEditor.ruleView.once("ruleview-changed");
+ const onMutation = inspector.once("markupmutation");
+ EventUtils.sendString("background-color:", ruleEditor.doc.defaultView);
+ await onValueFocus;
+ await onRuleViewChanged;
+ await onMutation;
+
+ is(ruleEditor.rule.textProps[0].name, "background-color");
+ await waitForComputedStyleProperty(
+ "#testid",
+ null,
+ "background-color",
+ "rgb(255, 0, 0)"
+ );
+
+ is(
+ await 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("KEY_Escape");
+ await 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 0000000000..ab54c98729
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_09.js
@@ -0,0 +1,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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testClickOnEmptyAreaToCloseEditor(inspector, view);
+});
+
+function synthesizeMouseOnEmptyArea(view) {
+ // any text property editor will do
+ const prop = getTextProperty(view, 1, { "background-color": "blue" });
+ const propEditor = prop.editor;
+ const valueContainer = propEditor.valueContainer;
+ const valueRect = valueContainer.getBoundingClientRect();
+ // click right next to the ";" at the end of valueContainer
+ EventUtils.synthesizeMouse(
+ valueContainer,
+ valueRect.width + 1,
+ 1,
+ {},
+ view.styleWindow
+ );
+}
+
+async 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.
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+ const prop = getTextProperty(view, 1, { "background-color": "blue" });
+ const propEditor = prop.editor;
+
+ info("Create a property value editor");
+ let editor = await 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"
+ );
+ const onRuleViewChanged = view.once("ruleview-changed");
+ let onBlur = once(editor.input, "blur");
+ synthesizeMouseOnEmptyArea(view);
+ await onBlur;
+ await 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");
+ const onFocus = once(ruleEditor.element, "focus", true);
+ synthesizeMouseOnEmptyArea(view);
+ await 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(view);
+ await onBlur;
+
+ ok(!view.isEditing, "No inplace editor should be displayed in the ruleview");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_10.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_10.js
new file mode 100644
index 0000000000..3db2164b0c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_10.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = `
+ <style>
+ div {
+ color: red;
+ width: 10; /* This document is in quirks mode so this value should be valid */
+ }
+ </style>
+ <div></div>
+`;
+
+// Test that CSS property names are case insensitive when validating, and that
+// quirks mode is accounted for when validating.
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view: ruleView } = await openRuleView();
+
+ await selectNode("div", inspector);
+ let prop = getTextProperty(ruleView, 1, { color: "red" });
+
+ let onRuleViewChanged;
+
+ info(`Rename the CSS property name to "Color"`);
+ onRuleViewChanged = ruleView.once("ruleview-changed");
+ await renameProperty(ruleView, prop, "Color");
+ info("Wait for Rule view to update");
+ await onRuleViewChanged;
+
+ is(prop.overridden, false, "Titlecase property is not overriden");
+ is(prop.enabled, true, "Titlecase property is enabled");
+ is(prop.isNameValid(), true, "Titlecase property is valid");
+
+ info(`Rename the CSS property name to "COLOR"`);
+ onRuleViewChanged = ruleView.once("ruleview-changed");
+ await renameProperty(ruleView, prop, "COLOR");
+ info("Wait for Rule view to update");
+ await onRuleViewChanged;
+
+ is(prop.overridden, false, "Uppercase property is not overriden");
+ is(prop.enabled, true, "Uppercase property is enabled");
+ is(prop.isNameValid(), true, "Uppercase property is valid");
+
+ info(`Checking width validity`);
+ prop = getTextProperty(ruleView, 1, { width: "10" });
+ is(prop.isNameValid(), true, "width is a valid property");
+ is(prop.isValid(), true, "10 is a valid property value in quirks mode");
+});
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 0000000000..52e11097cc
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js
@@ -0,0 +1,96 @@
+/* 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(async function () {
+ info("Toolbox height should be small enough to force scrollbars to appear");
+ await new Promise(done => {
+ const options = { set: [["devtools.toolbox.footer.height", 200]] };
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode(".testclass", inspector);
+
+ info("Check we have an overflow on the ruleview container.");
+ const container = view.element;
+ const 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.");
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+ const editor = await focusEditableField(view, ruleEditor.selectorText);
+ is(
+ inplaceEditor(ruleEditor.selectorText),
+ editor,
+ "The selector editor is focused."
+ );
+
+ info("Click on the scrollbar element.");
+ await 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");
+ const newValue = ".testclass.a.b.c.d.e.f";
+ const onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Enter new value and commit.");
+ editor.input.value = newValue;
+ EventUtils.synthesizeKey("KEY_Enter");
+ await onRuleViewChanged;
+ ok(getRuleViewRule(view, newValue), "Rule with '" + newValue + " 'exists.");
+});
+
+async function clickOnRuleviewScrollbar(view) {
+ const container = view.element.parentNode;
+ const onScroll = once(container, "scroll");
+ const 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
+ );
+ await 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 0000000000..e3c1822201
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click.js
@@ -0,0 +1,84 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode(".testclass", inspector);
+ await testClickOnSelectorEditorInput(view);
+});
+
+async function testClickOnSelectorEditorInput(view) {
+ info("Test clicking inside the selector editor input");
+
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ const editor = await focusEditableField(view, ruleEditor.selectorText);
+ const editorInput = editor.input;
+ is(
+ inplaceEditor(ruleEditor.selectorText),
+ editor,
+ "The selector editor got focused"
+ );
+
+ info("Click inside the editor input");
+ const onClick = once(editorInput, "click");
+ EventUtils.synthesizeMouse(editor.input, 2, 1, {}, view.styleWindow);
+ await 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");
+ const onDoubleClick = once(editorInput, "dblclick");
+ EventUtils.synthesizeMouse(
+ editor.input,
+ 2,
+ 1,
+ { clickCount: 2 },
+ view.styleWindow
+ );
+ await 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");
+ const onBlur = once(editorInput, "blur");
+ const rect = editorInput.getBoundingClientRect();
+ EventUtils.synthesizeMouse(
+ editorInput,
+ rect.width + 5,
+ rect.height / 2,
+ {},
+ view.styleWindow
+ );
+ await 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 0000000000..ffed882701
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector-commit.js
@@ -0,0 +1,136 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ for (const data of TEST_DATA) {
+ await runTestData(inspector, view, data);
+ }
+});
+
+async function runTestData(inspector, view, data) {
+ const { node, value, commitKey, modifiers, expected } = data;
+
+ info(
+ "Updating " +
+ node +
+ " to " +
+ value +
+ " and committing with " +
+ commitKey +
+ ". Expecting: " +
+ expected
+ );
+
+ info("Selecting the test element");
+ await selectNode(node, inspector);
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ const editor = await 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);
+
+ const 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;
+ }
+
+ await once(view, "ruleview-changed");
+
+ ok(
+ getRuleViewRule(view, expected),
+ "Rule with " + expected + " selector exists."
+ );
+
+ if (modifiers.shiftKey) {
+ idRuleEditor = getRuleViewRuleEditor(view, 0);
+ }
+
+ const rule = idRuleEditor.rule;
+ if (rule.textProps.length) {
+ 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 0000000000..be18919f68
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_01.js
@@ -0,0 +1,66 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ info("Selecting the test element");
+ await selectNode("#testid", inspector);
+ await testEditSelector(view, "span");
+
+ info("Selecting the modified element with the new rule");
+ await selectNode("span", inspector);
+ await checkModifiedElement(view, "span");
+});
+
+async function testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ const idRuleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ const editor = await 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");
+ const onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await 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 0000000000..70b58d4e42
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_02.js
@@ -0,0 +1,92 @@
+/* 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(async function () {
+ // Expand the pseudo-elements section by default.
+ Services.prefs.setBoolPref(PSEUDO_PREF, true);
+
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ info("Selecting the test element");
+ await selectNode(".testclass", inspector);
+ await testEditSelector(view, "div:nth-child(1)");
+
+ info("Selecting the modified element");
+ await selectNode("#testid", inspector);
+ await checkModifiedElement(view, "div:nth-child(1)");
+
+ info("Selecting the test element");
+ await selectNode("#testid3", inspector);
+ await testEditSelector(view, ".testclass2::first-letter");
+
+ info("Selecting the modified element");
+ await selectNode(".testclass2", inspector);
+ await checkModifiedElement(view, ".testclass2::first-letter");
+
+ // Reset the pseudo-elements section pref to its default value.
+ Services.prefs.clearUserPref(PSEUDO_PREF);
+});
+
+async function testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ const idRuleEditor =
+ getRuleViewRuleEditor(view, 1) || getRuleViewRuleEditor(view, 1, 0);
+
+ info("Focusing an existing selector name in the rule-view");
+ const editor = await 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");
+ const onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await onRuleViewChanged;
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rule.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+
+ const 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 0000000000..bbc9175568
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_03.js
@@ -0,0 +1,55 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode(".testclass", inspector);
+ await testEditSelector(view, "asd@:::!");
+});
+
+async function testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ const editor = await 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;
+ const onRuleViewChanged = once(view, "ruleview-invalid-selector");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await 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 0000000000..02a1feaac3
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js
@@ -0,0 +1,70 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("p", inspector);
+
+ await testSelectorHighlight(view, "p");
+ await testEditSelector(inspector, view, "body");
+ await testSelectorHighlight(view, "body");
+});
+
+async function testSelectorHighlight(view, selector) {
+ info("Test creating selector highlighter");
+
+ info("Clicking on a selector icon");
+ const { highlighter, isShown } = await clickSelectorIcon(view, selector);
+
+ ok(highlighter, "The selector highlighter instance was created");
+ ok(isShown, "The selector highlighter was shown");
+}
+
+async function testEditSelector(inspector, view, name) {
+ info("Test editing existing selector fields");
+
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ const editor = await focusEditableField(view, ruleEditor.selectorText);
+
+ is(
+ inplaceEditor(ruleEditor.selectorText),
+ editor,
+ "The selector editor got focused"
+ );
+
+ const onRuleViewChanged = view.once("ruleview-changed");
+ const { waitForHighlighterTypeHidden } = getHighlighterTestHelpers(inspector);
+ const onSelectorHighlighterHidden = waitForHighlighterTypeHidden(
+ inspector.highlighters.TYPES.SELECTOR
+ );
+
+ info("Entering a new selector name and committing");
+ editor.input.value = name;
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ info("Waiting for Rules view to update");
+ await onRuleViewChanged;
+ await onSelectorHighlighterHidden;
+ const highlighter = inspector.highlighters.getActiveHighlighter(
+ inspector.highlighters.TYPES.SELECTOR
+ );
+
+ ok(!highlighter, "The highlighter instance was removed");
+}
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 0000000000..30d945608d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_05.js
@@ -0,0 +1,82 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ info("Selecting the test element");
+ await selectNode("#testid", inspector);
+ await testEditSelector(view, "span");
+ await testAddProperty(view);
+
+ info("Selecting the modified element with the new rule");
+ await selectNode("span", inspector);
+ await checkModifiedElement(view, "span");
+});
+
+async function testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ const editor = await 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");
+ const onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await 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
+ const onBlur = once(view.styleDocument.activeElement, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ await onBlur;
+}
+
+function checkModifiedElement(view, name) {
+ is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+}
+
+async function testAddProperty(view) {
+ info("Test creating a new property");
+ const textProp = await 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 0000000000..8f418d2320
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_06.js
@@ -0,0 +1,83 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode(".testclass", inspector);
+ await testEditClassSelector(view);
+ await testEditDivSelector(view);
+});
+
+async function testEditClassSelector(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ const editor = await focusEditableField(view, ruleEditor.selectorText);
+
+ editor.input.value = "body";
+ const onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await onRuleViewChanged;
+
+ // Get the new rule editor that replaced the original
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+ const 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."
+ );
+}
+
+async function testEditDivSelector(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 2);
+ const editor = await focusEditableField(view, ruleEditor.selectorText);
+
+ editor.input.value = "asdf";
+ const onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await 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 0000000000..0949a966e8
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_07.js
@@ -0,0 +1,67 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("#testid", inspector);
+ await testEditSelector(view, "span");
+});
+
+async 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");
+ const editor = await 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");
+ const onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await onRuleViewChanged;
+
+ // Get the new rule editor that replaced the original
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+ const rule = ruleEditor.rule;
+ const 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 0000000000..112e2c83c7
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_08.js
@@ -0,0 +1,78 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ info("Selecting the test element");
+ await selectNode("#testid", inspector);
+
+ let idRuleEditor = getRuleViewRuleEditor(view, 2);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = await 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("KEY_Enter");
+ await onRuleViewChanged;
+
+ info("Re-focusing the selector name in the rule-view");
+ idRuleEditor = getRuleViewRuleEditor(view, 2);
+ editor = await 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("KEY_Enter");
+ await 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 0000000000..f9ee73098c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_09.js
@@ -0,0 +1,120 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("#testid", inspector);
+ await testEditSelector(view, "span");
+ await testAddImportantProperty(view);
+ await testAddMatchedRule(view, "span, div");
+});
+
+async function testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ const editor = await 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");
+ const onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await 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
+ const onBlur = once(view.styleDocument.activeElement, "blur");
+ EventUtils.synthesizeKey("KEY_Escape", {}, view.styleWindow);
+ await onBlur;
+}
+
+async function testAddImportantProperty(view) {
+ info("Test creating a new property with !important");
+ const textProp = await 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");
+
+ const prop = getTextProperty(view, 1, { color: "black" });
+ ok(
+ !prop.overridden,
+ "Existing property on matched rule should not be overridden"
+ );
+}
+
+async function testAddMatchedRule(view, name) {
+ info("Test adding a matching selector");
+
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ const editor = await 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");
+ const onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await 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
+ const onBlur = once(view.styleDocument.activeElement, "blur");
+ EventUtils.synthesizeKey("KEY_Escape", {}, view.styleWindow);
+ await 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 0000000000..26ee52d64e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_10.js
@@ -0,0 +1,67 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode(".pickme", inspector);
+ await testEditSelector(view);
+});
+
+async function testEditSelector(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ const editor = await focusEditableField(view, ruleEditor.selectorText);
+
+ editor.input.value = "#testid span";
+ const onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await onRuleViewChanged;
+
+ // Escape the new property editor after editing the selector
+ const onBlur = once(view.styleDocument.activeElement, "blur");
+ EventUtils.synthesizeKey("KEY_Escape", {}, view.styleWindow);
+ await 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 0000000000..6882686eff
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_11.js
@@ -0,0 +1,72 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode(".pickme", inspector);
+ await testEditSelector(view);
+});
+
+async function testEditSelector(view) {
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ const editor = await focusEditableField(view, ruleEditor.selectorText);
+
+ editor.input.value = ".pickme";
+ const onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await onRuleViewChanged;
+
+ // Escape the new property editor after editing the selector
+ const onBlur = once(view.styleDocument.activeElement, "blur");
+ EventUtils.synthesizeKey("KEY_Escape", {}, view.styleWindow);
+ await 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-selector_12.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_12.js
new file mode 100644
index 0000000000..187836b860
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_12.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test editing selectors for rules inside @import'd stylesheets.
+// This is a regression test for bug 1355819.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_edit_imported_selector.html");
+ const { inspector, view } = await openRuleView();
+
+ info("Select the node styled by an @import'd rule");
+ await selectNode("#target", inspector);
+
+ info("Focus the selector in the rule-view");
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+ const editor = await focusEditableField(view, ruleEditor.selectorText);
+
+ info("Change the selector to something else");
+ editor.input.value = "div";
+ const onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await onRuleViewChanged;
+
+ info("Escape the new property editor after editing the selector");
+ const onBlur = once(view.styleDocument.activeElement, "blur");
+ EventUtils.synthesizeKey("KEY_Escape", {}, view.styleWindow);
+ await onBlur;
+
+ info("Check the rules are still displayed correctly");
+ is(view._elementStyle.rules.length, 3, "The element still has 3 rules.");
+
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+ is(
+ ruleEditor.element.getAttribute("unmatched"),
+ "false",
+ "Rule editor is matched."
+ );
+ is(ruleEditor.selectorText.textContent, "div", "The new selector is correct");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-size-property-dragging.js b/devtools/client/inspector/rules/test/browser_rules_edit-size-property-dragging.js
new file mode 100644
index 0000000000..c95506de50
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-size-property-dragging.js
@@ -0,0 +1,481 @@
+/* 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 by dragging with
+// the mouse works correctly.
+
+const TEST_URI = `
+ <style>
+ #test {
+ padding-top: 10px;
+ margin-top: unset;
+ margin-bottom: 0px;
+ width: 0px;
+ border: 1px solid red;
+ line-height: 2;
+ border-width: var(--12px);
+ max-height: +10.2e3vmin;
+ min-height: 1% !important;
+ font-size: 10Q;
+ transform: rotate(45deg);
+ margin-left: 28.3em;
+ animation-delay: +15s;
+ margin-right: -2px;
+ padding-bottom: .9px;
+ rotate: 90deg;
+ }
+ </style>
+ <div id="test"></div>
+`;
+
+const DRAGGABLE_VALUE_CLASSNAME = "ruleview-propertyvalue-draggable";
+
+add_task(async function () {
+ await pushPref("devtools.inspector.draggable_properties", true);
+
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ const { inspector, view } = await openRuleView();
+ await selectNode("#test", inspector);
+
+ testDraggingClassIsAddedWhenNeeded(view);
+
+ // Check that toggling the feature updates the UI immediately.
+ await pushPref("devtools.inspector.draggable_properties", false);
+ testDraggingClassIsRemovedAfterPrefChange(view);
+
+ await pushPref("devtools.inspector.draggable_properties", true);
+ testDraggingClassIsAddedWhenNeeded(view);
+
+ await testIncrementAngleValue(view);
+ await testPressingEscapeWhileDragging(view);
+ await testUpdateDisabledValue(view);
+ await testWidthIncrements(view);
+ await testDraggingClassIsAddedOnValueUpdate(view);
+});
+
+const PROPERTIES = [
+ {
+ name: "border",
+ value: "1px solid red",
+ shouldBeDraggable: false,
+ },
+ {
+ name: "line-height",
+ value: "2",
+ shouldBeDraggable: false,
+ },
+ {
+ name: "border-width",
+ value: "var(--12px)",
+ shouldBeDraggable: false,
+ },
+ {
+ name: "transform",
+ value: "rotate(45deg)",
+ shouldBeDraggable: false,
+ },
+ {
+ name: "max-height",
+ value: "+10.2e3vmin",
+ shouldBeDraggable: true,
+ },
+ {
+ name: "min-height",
+ value: "1%",
+ shouldBeDraggable: true,
+ },
+ {
+ name: "font-size",
+ value: "10Q",
+ shouldBeDraggable: true,
+ },
+ {
+ name: "margin-left",
+ value: "28.3em",
+ shouldBeDraggable: true,
+ },
+ {
+ name: "animation-delay",
+ value: "+15s",
+ shouldBeDraggable: true,
+ },
+ {
+ name: "margin-right",
+ value: "-2px",
+ shouldBeDraggable: true,
+ },
+ {
+ name: "padding-bottom",
+ value: ".9px",
+ shouldBeDraggable: true,
+ },
+ {
+ name: "rotate",
+ value: "90deg",
+ shouldBeDraggable: true,
+ },
+];
+
+function testDraggingClassIsAddedWhenNeeded(view) {
+ info("Testing class is added or not on different property values");
+ runIsDraggableTest(view, PROPERTIES);
+}
+
+function testDraggingClassIsRemovedAfterPrefChange(view) {
+ info("Testing class is removed if the feature is disabled");
+ runIsDraggableTest(
+ view,
+ // Create a temporary copy of the test PROPERTIES, where shouldBeDraggable is
+ // always false.
+ PROPERTIES.map(prop =>
+ Object.assign({}, prop, { shouldBeDraggable: false })
+ )
+ );
+}
+
+async function testIncrementAngleValue(view) {
+ info("Testing updating an angle value with the angle swatch span");
+ const rotatePropEditor = getTextProperty(view, 1, {
+ rotate: "90deg",
+ }).editor;
+ await runIncrementTest(rotatePropEditor, view, [
+ {
+ startValue: "90deg",
+ expectedEndValue: "100deg",
+ distance: 10,
+ description: "updating angle value",
+ },
+ ]);
+}
+
+async function testPressingEscapeWhileDragging(view) {
+ info("Testing pressing escape while dragging with mouse");
+ const marginPropEditor = getTextProperty(view, 1, {
+ "margin-bottom": "0px",
+ }).editor;
+ await runIncrementTest(marginPropEditor, view, [
+ {
+ startValue: "0px",
+ expectedEndValue: "0px",
+ expectedEndValueBeforeEscape: "100px",
+ escape: true,
+ distance: 100,
+ description: "Pressing escape to check if value has been reset",
+ },
+ ]);
+}
+
+async function testUpdateDisabledValue(view) {
+ info("Testing updating a disabled value by dragging mouse");
+
+ const textProperty = getTextProperty(view, 1, { "padding-top": "10px" });
+ const editor = textProperty.editor;
+
+ await togglePropStatus(view, textProperty);
+ ok(!editor.enable.checked, "Should be disabled");
+ await runIncrementTest(editor, view, [
+ {
+ startValue: "10px",
+ expectedEndValue: "110px",
+ distance: 100,
+ description: "Updating disabled value",
+ },
+ ]);
+ ok(editor.enable.checked, "Should be enabled");
+}
+
+async function testWidthIncrements(view) {
+ info("Testing dragging the mouse on the width property");
+
+ const marginPropEditor = getTextProperty(view, 1, { width: "0px" }).editor;
+ await runIncrementTest(marginPropEditor, view, [
+ {
+ startValue: "0px",
+ expectedEndValue: "20px",
+ distance: 20,
+ description: "Increasing value while dragging",
+ },
+ {
+ startValue: "20px",
+ expectedEndValue: "0px",
+ distance: -20,
+ description: "Decreasing value while dragging",
+ },
+ {
+ startValue: "0px",
+ expectedEndValue: "2px",
+ ...getSmallIncrementKey(),
+ distance: 20,
+ description:
+ "Increasing value with small increments by pressing ctrl or alt",
+ },
+ {
+ startValue: "2px",
+ expectedEndValue: "202px",
+ shift: true,
+ distance: 20,
+ description: "Increasing value with large increments by pressing shift",
+ },
+ {
+ startValue: "202px",
+ expectedEndValue: "402px",
+ distance: 200,
+ description: "Increasing value with long distance",
+ },
+ {
+ startValue: "402px",
+ expectedEndValue: "402px",
+ distance: marginPropEditor._DRAGGING_DEADZONE_DISTANCE - 1,
+ description: "No change in the deadzone (positive value)",
+ deadzoneIncluded: true,
+ },
+ {
+ startValue: "402px",
+ expectedEndValue: "402px",
+ distance: -1 * (marginPropEditor._DRAGGING_DEADZONE_DISTANCE - 1),
+ description: "No change in the deadzone (negative value)",
+ deadzoneIncluded: true,
+ },
+ {
+ startValue: "402px",
+ expectedEndValue: "403px",
+ distance: marginPropEditor._DRAGGING_DEADZONE_DISTANCE + 1,
+ description: "Changed by 1 when leaving the deadzone (positive value)",
+ deadzoneIncluded: true,
+ },
+ {
+ startValue: "403px",
+ expectedEndValue: "402px",
+ distance: -1 * (marginPropEditor._DRAGGING_DEADZONE_DISTANCE + 1),
+ description: "Changed by 1 when leaving the deadzone (negative value)",
+ deadzoneIncluded: true,
+ },
+ ]);
+}
+
+async function testDraggingClassIsAddedOnValueUpdate(view) {
+ info("Testing dragging class is added when a supported unit is detected");
+
+ const editor = getTextProperty(view, 1, { "margin-top": "unset" }).editor;
+ const valueSpan = editor.valueSpan;
+ ok(
+ !valueSpan.classList.contains(DRAGGABLE_VALUE_CLASSNAME),
+ "Should not be draggable"
+ );
+ valueSpan.scrollIntoView();
+ await setProperty(view, editor.prop, "23em");
+ ok(
+ valueSpan.classList.contains(DRAGGABLE_VALUE_CLASSNAME),
+ "Should be draggable"
+ );
+}
+
+/**
+ * Runs each test and check whether or not the property is draggable
+ *
+ * @param {CSSRuleView} view
+ * @param {Array.<{
+ * name: String,
+ * value: String,
+ * shouldBeDraggable: Boolean,
+ * }>} tests
+ */
+function runIsDraggableTest(view, tests) {
+ for (const test of tests) {
+ const property = test;
+ info(`Testing ${property.name} with value ${property.value}`);
+ const editor = getTextProperty(view, 1, {
+ [property.name]: property.value,
+ }).editor;
+ const valueSpan = editor.valueSpan;
+ if (property.shouldBeDraggable) {
+ ok(
+ valueSpan.classList.contains(DRAGGABLE_VALUE_CLASSNAME),
+ "Should be draggable"
+ );
+ } else {
+ ok(
+ !valueSpan.classList.contains(DRAGGABLE_VALUE_CLASSNAME),
+ "Should not be draggable"
+ );
+ }
+ }
+}
+
+/**
+ * Runs each test in the tests array by synthesizing a mouse dragging
+ *
+ * @param {TextPropertyEditor} editor
+ * @param {CSSRuleView} view
+ * @param {Array} tests
+ */
+async function runIncrementTest(editor, view, tests) {
+ for (const test of tests) {
+ await testIncrement(editor, test, view);
+ }
+ view.debounce.flush();
+}
+
+/**
+ * Runs an increment test
+ *
+ * 1. We initialize the TextProperty value with "startValue"
+ * 2. We synthesize a mouse dragging of "distance" length
+ * 3. We check the value of TextProperty is equal to "expectedEndValue"
+ *
+ * @param {TextPropertyEditor} editor
+ * @param {Array} options
+ * @param {String} options.startValue
+ * @param {String} options.expectedEndValue
+ * @param {Boolean} options.shift Whether or not we press the shift key
+ * @param {Number} options.distance Distance of the dragging
+ * @param {String} options.description
+ * @param {Boolean} options.ctrl Small increment key
+ * @param {Boolean} options.alt Small increment key for macosx
+ * @param {Boolean} option.deadzoneIncluded True if the provided distance
+ * accounts for the deadzone. When false, the deadzone will automatically
+ * be added to the distance.
+ * @param {CSSRuleView} view
+ */
+async function testIncrement(editor, options, view) {
+ info("Running subtest: " + options.description);
+
+ editor.valueSpan.scrollIntoView();
+ await setProperty(editor.ruleView, editor.prop, options.startValue);
+
+ is(
+ editor.prop.value,
+ options.startValue,
+ "Value initialized at " + options.startValue
+ );
+
+ const onMouseUp = once(editor.valueSpan, "mouseup");
+
+ await synthesizeMouseDragging(editor, options.distance, options);
+
+ // mouseup event not triggered when escape is pressed
+ if (!options.escape) {
+ info("Waiting mouseup");
+ await onMouseUp;
+ info("Received mouseup");
+ }
+
+ is(
+ editor.prop.value,
+ options.expectedEndValue,
+ "Value changed to " + editor.prop.value
+ );
+}
+
+/**
+ * Synthesizes mouse dragging (mousedown + mousemove + mouseup)
+ *
+ * @param {TextPropertyEditor} editor
+ * @param {Number} distance length of the horizontal dragging (negative if dragging left)
+ * @param {Object} option
+ * @param {Boolean} option.escape
+ * @param {Boolean} option.alt
+ * @param {Boolean} option.shift
+ * @param {Boolean} option.ctrl
+ * @param {Boolean} option.deadzoneIncluded
+ */
+async function synthesizeMouseDragging(editor, distance, options = {}) {
+ info(`Start to synthesize mouse dragging (from ${1} to ${1 + distance})`);
+
+ const styleWindow = editor.ruleView.styleWindow;
+ const elm = editor.valueSpan;
+ const startPosition = [1, 1];
+
+ // Handle the pixel based deadzone.
+ const deadzone = editor._DRAGGING_DEADZONE_DISTANCE;
+ if (!options.deadzoneIncluded) {
+ // Most tests do not care about the deadzone and the provided distance does
+ // not account for the deadzone. Add it automatically.
+ distance = distance + Math.sign(distance) * deadzone;
+ }
+ const updateExpected = Math.abs(options.distance) > deadzone;
+
+ const endPosition = [startPosition[0] + distance, startPosition[1]];
+
+ EventUtils.synthesizeMouse(
+ elm,
+ startPosition[0],
+ startPosition[1],
+ { type: "mousedown" },
+ styleWindow
+ );
+
+ // If the drag will not trigger any update, simply wait for 100ms.
+ // Otherwise, wait for the next property-updated-by-dragging event.
+ const updated = updateExpected
+ ? editor.ruleView.once("property-updated-by-dragging")
+ : wait(100);
+
+ EventUtils.synthesizeMouse(
+ elm,
+ endPosition[0],
+ endPosition[1],
+ {
+ type: "mousemove",
+ shiftKey: !!options.shift,
+ ctrlKey: !!options.ctrl,
+ altKey: !!options.alt,
+ },
+ styleWindow
+ );
+
+ // We wait because the mousemove event listener is throttled to 30ms
+ // in the TextPropertyEditor class
+ info("waiting for event property-updated-by-dragging");
+ await updated;
+ ok(true, "received event property-updated-by-dragging");
+
+ if (options.escape) {
+ is(
+ editor.prop.value,
+ options.expectedEndValueBeforeEscape,
+ "testing value before pressing escape"
+ );
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, styleWindow);
+ }
+
+ // If the drag will not trigger any update, simply wait for 100ms.
+ // Otherwise, wait for the next ruleview-changed event.
+ const done = updateExpected
+ ? editor.ruleView.once("ruleview-changed")
+ : wait(100);
+
+ EventUtils.synthesizeMouse(
+ elm,
+ endPosition[0],
+ endPosition[1],
+ {
+ type: "mouseup",
+ },
+ styleWindow
+ );
+ await done;
+
+ // If the drag did not trigger any update, mouseup might open an inline editor.
+ // Leave the editor.
+ const inplaceEditor = styleWindow.document.querySelector(
+ ".styleinspector-propertyeditor"
+ );
+ if (inplaceEditor) {
+ const onBlur = once(inplaceEditor, "blur");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, styleWindow);
+ await onBlur;
+ }
+
+ info("Finish to synthesize mouse dragging");
+}
+
+function getSmallIncrementKey() {
+ if (AppConstants.platform === "macosx") {
+ return { alt: true };
+ }
+ return { ctrl: true };
+}
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 0000000000..a3813f1f7b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js
@@ -0,0 +1,119 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("#testid", inspector);
+ const prop = getTextProperty(view, 1, { color: "red" });
+ const propEditor = prop.editor;
+
+ await testColorValueSpanClickWithoutNameChange(propEditor, view);
+ await testColorValueSpanClickAfterNameChange(propEditor, view);
+});
+
+async function testColorValueSpanClickWithoutNameChange(propEditor, view) {
+ info("Test click on color span while focusing property name editor");
+ const colorSpan = propEditor.valueSpan.querySelector(".ruleview-color");
+
+ info("Focus the color name span");
+ await 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)
+ const 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
+ const 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");
+ await 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"
+ );
+ const onRuleViewChanged = view.once("ruleview-changed");
+ editor.input.blur();
+ await onRuleViewChanged;
+}
+
+async function testColorValueSpanClickAfterNameChange(propEditor, view) {
+ info("Test click on color span after property name change");
+ const colorSpan = propEditor.valueSpan.querySelector(".ruleview-color");
+
+ info("Focus the color name span");
+ await 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");
+ const 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"
+ );
+ await onRuleViewChanged;
+
+ info("wait for the property value to be updated");
+ await 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();
+ await 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 0000000000..95b6dccea9
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.js
@@ -0,0 +1,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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ info("Test shift + click on color swatch while editing property name");
+
+ await selectNode("#testid", inspector);
+ const prop = getTextProperty(view, 1, {
+ background:
+ "linear-gradient( 90deg, rgb(183,222,237), rgb(33,180,226), rgb(31,170,217), rgba(200,170,140,0.5))",
+ });
+ const propEditor = prop.editor;
+
+ const swatchSpan = propEditor.valueSpan.querySelectorAll(
+ ".ruleview-colorswatch"
+ )[2];
+
+ info("Focus the background name span");
+ await focusEditableField(view, propEditor.nameSpan);
+ const editor = inplaceEditor(propEditor.doc.activeElement);
+
+ info(
+ "Modify the property to background-image to trigger the " +
+ "property-value-updated event"
+ );
+ editor.input.value = "background-image";
+
+ const onPropertyValueUpdate = view.once("property-value-updated");
+ const onSwatchUnitChange = swatchSpan.once("unit-change");
+ const 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"
+ );
+ await onRuleViewChanged;
+
+ info("wait for the color unit to change");
+ await onSwatchUnitChange;
+ ok(true, "the color unit was changed");
+
+ info("wait for the property value to be updated");
+ await 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 0000000000..c43751da1f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.js
@@ -0,0 +1,86 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ info("Test click on color swatch while editing property name");
+
+ await selectNode("#testid", inspector);
+ const prop = getTextProperty(view, 1, {
+ background:
+ "linear-gradient( 90deg, rgb(183,222,237), rgb(33,180,226), rgb(31,170,217), rgba(200,170,140,0.5))",
+ });
+ const propEditor = prop.editor;
+
+ const swatchSpan = propEditor.valueSpan.querySelectorAll(
+ ".ruleview-colorswatch"
+ )[3];
+ const colorPicker = view.tooltips.getTooltip("colorPicker");
+
+ info("Focus the background name span");
+ await focusEditableField(view, propEditor.nameSpan);
+ const 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";
+
+ const onRuleViewChanged = view.once("ruleview-changed");
+ const onPropertyValueUpdate = view.once("property-value-updated");
+ const 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"
+ );
+ await onRuleViewChanged;
+
+ info("wait for the property value to be updated");
+ await onPropertyValueUpdate;
+
+ info("wait for the color picker to be shown");
+ await 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"
+ );
+
+ const 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 0000000000..a315f5bec5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_04.js
@@ -0,0 +1,76 @@
+/* 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("https://example.com/browser/devtools/client/inspector/rules/test/doc_test_image.png"), linear-gradient(white, #F06 400px);
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ info("Test click on background-image url while editing property name");
+
+ await selectNode("#testid", inspector);
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+ const propEditor = ruleEditor.rule.textProps[0].editor;
+ const anchor = propEditor.valueSpan.querySelector(
+ ".ruleview-propertyvalue .theme-link"
+ );
+
+ info("Focus the background name span");
+ await focusEditableField(view, propEditor.nameSpan);
+ const editor = inplaceEditor(propEditor.doc.activeElement);
+
+ info(
+ "Modify the property to background to trigger the " +
+ "property-value-updated event"
+ );
+ editor.input.value = "background-image";
+
+ const onRuleViewChanged = view.once("ruleview-changed");
+ const onPropertyValueUpdate = view.once("property-value-updated");
+ const 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.
+ const 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"
+ );
+ await onRuleViewChanged;
+
+ info("wait for the property value to be updated");
+ await onPropertyValueUpdate;
+
+ info("wait for the image to be open in a new tab");
+ const tab = await 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_edit-variable-add.js b/devtools/client/inspector/rules/test/browser_rules_edit-variable-add.js
new file mode 100644
index 0000000000..3c4c9a25a4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-variable-add.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test adding a CSS variable.
+
+const TEST_URI = `
+ <style type='text/css'>
+ div {
+ color: var(--color);
+ }
+ </style>
+ <div></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ info("Check the initial state of --color which refers to an unset variable");
+ checkCSSVariableOutput(
+ view,
+ "div",
+ "color",
+ "ruleview-unmatched-variable",
+ "--color is not set"
+ );
+
+ info("Add the --color CSS variable");
+ await addProperty(view, 0, "--color", "lime");
+ checkCSSVariableOutput(
+ view,
+ "div",
+ "color",
+ "ruleview-variable",
+ "--color = lime"
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-variable-remove.js b/devtools/client/inspector/rules/test/browser_rules_edit-variable-remove.js
new file mode 100644
index 0000000000..322cc8e166
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-variable-remove.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test removing a CSS variable.
+
+const TEST_URI = `
+ <style type='text/css'>
+ div {
+ color: var(--color);
+ --color: lime;
+ }
+ </style>
+ <div></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ info("Check the initial state of the --color variable");
+ checkCSSVariableOutput(
+ view,
+ "div",
+ "color",
+ "ruleview-variable",
+ "--color = lime"
+ );
+
+ info("Remove the --color variable declaration");
+ const prop = getTextProperty(view, 1, { "--color": "lime" });
+ await removeProperty(view, prop);
+ checkCSSVariableOutput(
+ view,
+ "div",
+ "color",
+ "ruleview-unmatched-variable",
+ "--color is not set"
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-variable.js b/devtools/client/inspector/rules/test/browser_rules_edit-variable.js
new file mode 100644
index 0000000000..0df57d9bf1
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-variable.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test editing a CSS variable.
+
+const TEST_URI = `
+ <style type='text/css'>
+ div {
+ color: var(--color);
+ --color: lime;
+ }
+ </style>
+ <div></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ info("Check the initial state of the --color variable");
+ checkCSSVariableOutput(
+ view,
+ "div",
+ "color",
+ "ruleview-variable",
+ "--color = lime"
+ );
+
+ info("Edit the CSS variable");
+ const prop = getTextProperty(view, 1, { "--color": "lime" });
+ const propEditor = prop.editor;
+ const editor = await focusEditableField(view, propEditor.valueSpan);
+ editor.input.value = "blue";
+ const onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ await onRuleViewChanged;
+ checkCSSVariableOutput(
+ view,
+ "div",
+ "color",
+ "ruleview-variable",
+ "--color = blue"
+ );
+});
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 0000000000..5665d517db
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_01.js
@@ -0,0 +1,111 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testEditableFieldFocus(inspector, view, "KEY_Enter");
+ await testEditableFieldFocus(inspector, view, "KEY_Tab");
+});
+
+async function testEditableFieldFocus(inspector, view, commitKey) {
+ info("Click on the selector of the inline style ('element')");
+ let ruleEditor = getRuleViewRuleEditor(view, 0);
+ const onFocus = once(ruleEditor.element, "focus", true);
+ ruleEditor.selectorText.click();
+ await onFocus;
+ assertEditor(
+ view,
+ ruleEditor.newPropSpan,
+ "Focus should be in the element property span"
+ );
+
+ info("Focus the next field with " + commitKey);
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+ await 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++) {
+ const textProp = ruleEditor.rule.textProps[i];
+ const 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).
+ const onRuleViewChanged = i > 0 ? view.once("ruleview-changed") : null;
+ await focusNextEditableField(view, ruleEditor, commitKey);
+ await onRuleViewChanged;
+ assertEditor(
+ view,
+ propEditor.nameSpan,
+ "Focus should have moved to the property name"
+ );
+
+ info("Focus the next field with " + commitKey);
+ await 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.
+ const onRuleViewChanged = view.once("ruleview-changed");
+ await focusNextEditableField(view, ruleEditor, commitKey);
+ await onRuleViewChanged;
+ assertEditor(
+ view,
+ ruleEditor.newPropSpan,
+ "Focus should have moved to the new property span"
+ );
+
+ ruleEditor = getRuleViewRuleEditor(view, 2);
+
+ await focusNextEditableField(view, ruleEditor, commitKey);
+ assertEditor(
+ view,
+ ruleEditor.selectorText,
+ "Focus should have moved to the next rule selector"
+ );
+
+ info("Blur the selector field");
+ EventUtils.synthesizeKey("KEY_Escape");
+}
+
+async function focusNextEditableField(view, ruleEditor, commitKey) {
+ const onFocus = once(ruleEditor.element, "focus", true);
+ EventUtils.synthesizeKey(commitKey, {}, view.styleWindow);
+ await onFocus;
+}
+
+function assertEditor(view, element, message) {
+ const 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 0000000000..23c7397b46
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_02.js
@@ -0,0 +1,120 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testEditableFieldFocus(inspector, view, "VK_TAB", { shiftKey: true });
+});
+
+async function testEditableFieldFocus(
+ inspector,
+ view,
+ commitKey,
+ options = {}
+) {
+ let ruleEditor = getRuleViewRuleEditor(view, 2);
+ const editor = await focusEditableField(view, ruleEditor.selectorText);
+ is(
+ inplaceEditor(ruleEditor.selectorText),
+ editor,
+ "Focus should be in the 'div' rule selector"
+ );
+
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ await focusNextField(view, ruleEditor, commitKey, options);
+ assertEditor(
+ view,
+ ruleEditor.newPropSpan,
+ "Focus should have moved to the new property span"
+ );
+
+ for (const textProp of ruleEditor.rule.textProps.slice(0).reverse()) {
+ const propEditor = textProp.editor;
+
+ await focusNextField(view, ruleEditor, commitKey, options);
+ if (
+ ["background-color", "color"].includes(propEditor.nameSpan.textContent)
+ ) {
+ // background-color and color property value spans have inner focusable elements
+ // and so, focus needs to move to the inplace editor field where enter needs to be
+ // pressed to trigger click event on it
+ await focusNextField(view, ruleEditor, commitKey, options);
+ EventUtils.sendKey("Return");
+ }
+ await assertEditor(
+ view,
+ propEditor.valueSpan,
+ "Focus should have moved to the property value"
+ );
+
+ await focusNextFieldAndExpectChange(view, ruleEditor, commitKey, options);
+ await assertEditor(
+ view,
+ propEditor.nameSpan,
+ "Focus should have moved to the property name"
+ );
+ }
+
+ ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ await focusNextField(view, ruleEditor, commitKey, options);
+ await assertEditor(
+ view,
+ ruleEditor.selectorText,
+ "Focus should have moved to the '#testid' rule selector"
+ );
+
+ ruleEditor = getRuleViewRuleEditor(view, 0);
+
+ await focusNextField(view, ruleEditor, commitKey, options);
+ assertEditor(
+ view,
+ ruleEditor.newPropSpan,
+ "Focus should have moved to the new property span"
+ );
+}
+
+async function focusNextFieldAndExpectChange(
+ view,
+ ruleEditor,
+ commitKey,
+ options
+) {
+ const onRuleViewChanged = view.once("ruleview-changed");
+ await focusNextField(view, ruleEditor, commitKey, options);
+ await onRuleViewChanged;
+}
+
+async function focusNextField(view, ruleEditor, commitKey, options) {
+ const onFocus = once(ruleEditor.element, "focus", true);
+ EventUtils.synthesizeKey(commitKey, options, view.styleWindow);
+ await onFocus;
+}
+
+function assertEditor(view, element, message) {
+ const 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 0000000000..bf59ef4b91
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_eyedropper.js
@@ -0,0 +1,160 @@
+/* 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)";
+
+registerCleanupFunction(() => {
+ // Restore the default Toolbox host position after the test.
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+});
+
+add_task(async function () {
+ info("Add the test tab, open the rule-view and select the test node");
+
+ const url = "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI);
+ await addTab(url);
+
+ const { inspector, view, toolbox } = await openRuleView();
+
+ await runTest(inspector, view, false);
+
+ info("Reload the page to restore the initial state");
+ await navigateTo(url);
+
+ info("Change toolbox host to WINDOW");
+ await toolbox.switchHost("window");
+
+ // Switching hosts is not correctly waiting when DevTools run in content frame
+ // See Bug 1571421.
+ await wait(1000);
+
+ await runTest(inspector, view, true);
+});
+
+async function runTest(inspector, view, isWindowHost) {
+ await selectNode("#div2", inspector);
+
+ info("Get the background-color property from the rule-view");
+ const property = getRuleViewProperty(view, "#div2", "background-color");
+ const 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");
+ await openEyedropper(view, swatch);
+
+ const tooltip = view.tooltips.getTooltip("colorPicker").tooltip;
+ ok(
+ !tooltip.isVisible(),
+ "color picker tooltip is closed after opening eyedropper"
+ );
+
+ info("Test that pressing escape dismisses the eyedropper");
+ await testESC(swatch, inspector);
+
+ if (isWindowHost) {
+ // The following code is only needed on linux otherwise the test seems to
+ // timeout when clicking again on the swatch. Both the focus and the wait
+ // seem needed to make it pass.
+ // To be fixed in Bug 1571421.
+ info("Ensure the swatch window is focused");
+ const onWindowFocus = BrowserTestUtils.waitForEvent(
+ swatch.ownerGlobal,
+ "focus"
+ );
+ swatch.ownerGlobal.focus();
+ await onWindowFocus;
+ }
+
+ info("Open the eyedropper again");
+ await openEyedropper(view, swatch);
+
+ info("Test that a color can be selected with the eyedropper");
+ await testSelect(view, swatch, inspector);
+
+ const onHidden = tooltip.once("hidden");
+ tooltip.hide();
+ await onHidden;
+ ok(!tooltip.isVisible(), "color picker tooltip is closed");
+
+ await waitForTick();
+}
+
+async function testESC(swatch, inspector) {
+ info("Press escape");
+ const onCanceled = new Promise(resolve => {
+ inspector.inspectorFront.once("color-pick-canceled", resolve);
+ });
+ BrowserTestUtils.synthesizeKey(
+ "VK_ESCAPE",
+ {},
+ gBrowser.selectedTab.linkedBrowser
+ );
+ await onCanceled;
+
+ const color = swatch.style.backgroundColor;
+ is(color, ORIGINAL_COLOR, "swatch didn't change after pressing ESC");
+}
+
+async function testSelect(view, swatch, inspector) {
+ info("Click at x:10px y:10px");
+ const onPicked = new Promise(resolve => {
+ inspector.inspectorFront.once("color-picked", resolve);
+ });
+ // The change to the content is done async after rule view change
+ const onRuleViewChanged = view.once("ruleview-changed");
+
+ await safeSynthesizeMouseEventAtCenterInContentPage("#div1", {
+ type: "mousemove",
+ });
+ await safeSynthesizeMouseEventAtCenterInContentPage("#div1", {
+ type: "mousedown",
+ });
+ await safeSynthesizeMouseEventAtCenterInContentPage("#div1", {
+ type: "mouseup",
+ });
+
+ await onPicked;
+ await onRuleViewChanged;
+
+ const color = swatch.style.backgroundColor;
+ is(color, EXPECTED_COLOR, "swatch changed colors");
+
+ ok(!swatch.eyedropperOpen, "swatch eye dropper is closed");
+ ok(!swatch.activeSwatch, "no active swatch");
+
+ is(
+ await getComputedStyleProperty("div", null, "background-color"),
+ EXPECTED_COLOR,
+ "div's color set to body color after dropper"
+ );
+}
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 0000000000..037c3f8a82
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_filtereditor-appears-on-swatch-click.js
@@ -0,0 +1,69 @@
+/* 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(async function () {
+ await addTab(TEST_URL);
+
+ const { toolbox, view } = await openRuleView();
+
+ info("Getting the filter swatch element");
+ const property = await getRuleViewProperty(view, "body", "filter", {
+ wait: true,
+ });
+
+ const swatch = property.valueSpan.querySelector(".ruleview-filterswatch");
+
+ const filterTooltip = view.tooltips.getTooltip("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.
+ const onRuleViewChanged = view.once("ruleview-changed");
+ swatch.click();
+ await 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"
+ );
+
+ info("Get the cssfilter widget instance");
+ const widget = filterTooltip.widget;
+ const select = widget.el.querySelector("select");
+
+ // Next we will check that interacting with the select does not close the
+ // filter tooltip.
+ info("Show the filter select");
+ const onSelectPopupShown = BrowserTestUtils.waitForSelectPopupShown(window);
+ EventUtils.synthesizeMouseAtCenter(select, {}, toolbox.win);
+ const selectPopup = await onSelectPopupShown;
+ ok(
+ filterTooltip.tooltip.isVisible(),
+ "The tooltip was not hidden when opening the select"
+ );
+
+ info("Hide the filter select");
+ const onSelectPopupHidden = once(selectPopup, "popuphidden");
+ const blurMenuItem = selectPopup.querySelector("menuitem[label='blur']");
+ EventUtils.synthesizeMouseAtCenter(blurMenuItem, {}, window);
+ await onSelectPopupHidden;
+ await waitFor(() => select.value === "blur");
+ is(
+ select.value,
+ "blur",
+ "The filter select was updated with the correct value"
+ );
+ ok(
+ filterTooltip.tooltip.isVisible(),
+ "The tooltip was not hidden when using the select"
+ );
+
+ await hideTooltipAndWaitForRuleViewChanged(filterTooltip, view);
+ await 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 0000000000..7237f0c997
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_filtereditor-commit-on-ENTER.js
@@ -0,0 +1,51 @@
+/* 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(async function () {
+ await addTab(TEST_URL);
+ const { view } = await openRuleView();
+
+ info("Get the filter swatch element");
+ const 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();
+ await onRuleViewChanged;
+
+ info("Get the cssfilter widget instance");
+ const filterTooltip = view.tooltips.getTooltip("filterEditor");
+ const widget = filterTooltip.widget;
+
+ info("Set a new value in the cssfilter widget");
+ onRuleViewChanged = view.once("ruleview-changed");
+ widget.setCssValue("blur(2px)");
+ await waitForComputedStyleProperty("body", null, "filter", "blur(2px)");
+ await 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);
+ await onRuleViewChanged;
+
+ is(
+ await 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 0000000000..ea46af9997
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_filtereditor-revert-on-ESC.js
@@ -0,0 +1,74 @@
+/* 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(async function () {
+ await addTab(TEST_URL);
+ const { view } = await openRuleView();
+ await testPressingEscapeRevertsChanges(view);
+});
+
+async function testPressingEscapeRevertsChanges(view) {
+ const prop = getTextProperty(view, 1, { filter: "blur(2px) contrast(2)" });
+ const propEditor = prop.editor;
+ const swatch = propEditor.valueSpan.querySelector(".ruleview-filterswatch");
+
+ await clickOnFilterSwatch(swatch, view);
+ await setValueInFilterWidget("blur(2px)", view);
+
+ await waitForComputedStyleProperty("body", null, "filter", "blur(2px)");
+ is(
+ propEditor.valueSpan.textContent,
+ "blur(2px)",
+ "Got expected property value."
+ );
+
+ await pressEscapeToCloseTooltip(view);
+
+ await waitForComputedStyleProperty(
+ "body",
+ null,
+ "filter",
+ "blur(2px) contrast(2)"
+ );
+ is(
+ propEditor.valueSpan.textContent,
+ "blur(2px) contrast(2)",
+ "Got expected property value."
+ );
+}
+
+async 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.
+ const onRuleViewChanged = view.once("ruleview-changed");
+ swatch.click();
+ await onRuleViewChanged;
+}
+
+async function setValueInFilterWidget(value, view) {
+ info("Setting the CSS filter value in the tooltip");
+
+ const filterTooltip = view.tooltips.getTooltip("filterEditor");
+ const onRuleViewChanged = view.once("ruleview-changed");
+ filterTooltip.widget.setCssValue(value);
+ await onRuleViewChanged;
+}
+
+async function pressEscapeToCloseTooltip(view) {
+ info("Pressing ESCAPE to close the tooltip");
+
+ const filterTooltip = view.tooltips.getTooltip("filterEditor");
+ const onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.sendKey("ESCAPE", filterTooltip.widget.styleWindow);
+ await onRuleViewChanged;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-on-mutation.js b/devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-on-mutation.js
new file mode 100644
index 0000000000..eb887f9848
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-on-mutation.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the flexbox highlighter is hidden when the highlighted flexbox container is
+// removed from the page.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #flex {
+ display: flex;
+ }
+ </style>
+ <div id="flex"></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX;
+ const {
+ getNodeForActiveHighlighter,
+ waitForHighlighterTypeShown,
+ waitForHighlighterTypeHidden,
+ } = getHighlighterTestHelpers(inspector);
+
+ const onRuleViewUpdated = view.once("ruleview-refreshed");
+ await selectNode("#flex", inspector);
+ await onRuleViewUpdated;
+ const container = getRuleViewProperty(view, "#flex", "display").valueSpan;
+ const flexboxToggle = container.querySelector(
+ ".js-toggle-flexbox-highlighter"
+ );
+
+ info("Toggling ON the flexbox highlighter from the rule-view.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ flexboxToggle.click();
+ await onHighlighterShown;
+ ok(
+ getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "Flexbox highlighter is shown."
+ );
+
+ info("Remove the #flex container in the content page.");
+ const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () =>
+ content.document.querySelector("#flex").remove()
+ );
+ await onHighlighterHidden;
+ ok(
+ !getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "Flexbox highlighter is hidden."
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-on-navigate.js b/devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-on-navigate.js
new file mode 100644
index 0000000000..85b8f5429f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-on-navigate.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that flexbox highlighter is hidden on page navigation.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #flex {
+ display: flex;
+ }
+ </style>
+ <div id="flex"></div>
+`;
+
+const TEST_URI_2 = "data:text/html,<html><body>test</body></html>";
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX;
+ const { getNodeForActiveHighlighter, waitForHighlighterTypeShown } =
+ getHighlighterTestHelpers(inspector);
+
+ await selectNode("#flex", inspector);
+ const container = getRuleViewProperty(view, "#flex", "display").valueSpan;
+ const flexboxToggle = container.querySelector(
+ ".js-toggle-flexbox-highlighter"
+ );
+
+ info("Toggling ON the flexbox highlighter from the rule-view.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ flexboxToggle.click();
+ await onHighlighterShown;
+ ok(
+ getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "Flexbox highlighter is shown."
+ );
+
+ await navigateTo(TEST_URI_2);
+ ok(
+ !getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "Flexbox highlighter is hidden."
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-on-reload.js b/devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-on-reload.js
new file mode 100644
index 0000000000..aa35ffb322
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-on-reload.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that a flexbox highlighter after reloading the page.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #flex {
+ display: flex;
+ }
+ </style>
+ <div id="flex"></div>
+`;
+
+add_task(async function () {
+ const tab = await addTab(
+ "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)
+ );
+
+ info("Check that the flexbox highlighter can be displayed.");
+ await checkFlexboxHighlighter();
+
+ info("Close the toolbox before reloading the tab.");
+ await gDevTools.closeToolboxForTab(tab);
+
+ await reloadBrowser();
+
+ info(
+ "Check that the flexbox highlighter can be displayed after reloading the page."
+ );
+ await checkFlexboxHighlighter();
+});
+
+async function checkFlexboxHighlighter() {
+ const { inspector, view } = await openRuleView();
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX;
+ const { getNodeForActiveHighlighter, waitForHighlighterTypeShown } =
+ getHighlighterTestHelpers(inspector);
+
+ await selectNode("#flex", inspector);
+ const container = getRuleViewProperty(view, "#flex", "display").valueSpan;
+ const flexboxToggle = container.querySelector(
+ ".js-toggle-flexbox-highlighter"
+ );
+
+ info("Toggling ON the flexbox highlighter from the rule-view.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ flexboxToggle.click();
+ await onHighlighterShown;
+
+ ok(
+ getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "Flexbox highlighter is shown."
+ );
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-restored-after-reload.js b/devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-restored-after-reload.js
new file mode 100644
index 0000000000..dfa0368e1d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-restored-after-reload.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the flexbox highlighter is re-displayed after reloading a page.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #flex {
+ display: flex;
+ }
+ </style>
+ <div id="flex"></div>
+`;
+
+const OTHER_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid"></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ info("Check that the flexbox highlighter can be displayed.");
+ const { inspector, view } = await openRuleView();
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX;
+ const {
+ getActiveHighlighter,
+ waitForHighlighterTypeShown,
+ waitForHighlighterTypeRestored,
+ waitForHighlighterTypeDiscarded,
+ } = getHighlighterTestHelpers(inspector);
+
+ await selectNode("#flex", inspector);
+ const container = getRuleViewProperty(view, "#flex", "display").valueSpan;
+ const flexboxToggle = container.querySelector(
+ ".js-toggle-flexbox-highlighter"
+ );
+
+ info("Toggling ON the flexbox highlighter from the rule-view.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ flexboxToggle.click();
+ await onHighlighterShown;
+ ok(getActiveHighlighter(HIGHLIGHTER_TYPE), "Flexbox highlighter is shown.");
+
+ info("Reload the page, expect the highlighter to be displayed once again");
+ const onRestored = waitForHighlighterTypeRestored(HIGHLIGHTER_TYPE);
+ const onReloaded = inspector.once("reloaded");
+ await reloadBrowser();
+ info("Wait for inspector to be reloaded after page reload");
+ await onReloaded;
+ info("Wait for the highlighter to be restored");
+ await onRestored;
+ ok(getActiveHighlighter(HIGHLIGHTER_TYPE), "Flexbox highlighter restored.");
+
+ info("Navigate to another URL, and check that the highlighter is hidden");
+ const otherUri =
+ "data:text/html;charset=utf-8," + encodeURIComponent(OTHER_URI);
+ const onDiscarded = waitForHighlighterTypeDiscarded(HIGHLIGHTER_TYPE);
+ await navigateTo(otherUri);
+ info("Expect the highlighter not to be restored");
+ await onDiscarded;
+ ok(!getActiveHighlighter(HIGHLIGHTER_TYPE), "Flexbox highlighter not shown.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle-telemetry.js b/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle-telemetry.js
new file mode 100644
index 0000000000..cc58a25c5f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle-telemetry.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the telemetry is correct when the flexbox highlighter is activated from
+// the rules view.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #flex {
+ display: flex;
+ }
+ </style>
+ <div id="flex"></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ startTelemetry();
+ const { inspector, view } = await openRuleView();
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX;
+ const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } =
+ getHighlighterTestHelpers(inspector);
+
+ await selectNode("#flex", inspector);
+ const container = getRuleViewProperty(view, "#flex", "display").valueSpan;
+ const flexboxToggle = container.querySelector(
+ ".js-toggle-flexbox-highlighter"
+ );
+
+ info("Toggling ON the flexbox highlighter from the rule-view.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ flexboxToggle.click();
+ await onHighlighterShown;
+
+ info("Toggling OFF the flexbox highlighter from the rule-view.");
+ const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE);
+ flexboxToggle.click();
+ await onHighlighterHidden;
+
+ checkResults();
+});
+
+function checkResults() {
+ checkTelemetry("devtools.rules.flexboxhighlighter.opened", "", 1, "scalar");
+ checkTelemetry(
+ "DEVTOOLS_FLEXBOX_HIGHLIGHTER_TIME_ACTIVE_SECONDS",
+ "",
+ null,
+ "hasentries"
+ );
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_01.js b/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_01.js
new file mode 100644
index 0000000000..d09e28a0ec
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_01.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the flexbox highlighter in the rule view and the display of the
+// flexbox highlighter.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #flex {
+ display: flex;
+ }
+ </style>
+ <div id="flex"></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX;
+ const {
+ getActiveHighlighter,
+ getNodeForActiveHighlighter,
+ waitForHighlighterTypeShown,
+ waitForHighlighterTypeHidden,
+ } = getHighlighterTestHelpers(inspector);
+
+ await selectNode("#flex", inspector);
+ const container = getRuleViewProperty(view, "#flex", "display").valueSpan;
+ const flexboxToggle = container.querySelector(
+ ".js-toggle-flexbox-highlighter"
+ );
+
+ info("Checking the initial state of the flexbox toggle in the rule-view.");
+ ok(flexboxToggle, "Flexbox highlighter toggle is visible.");
+ ok(
+ !flexboxToggle.classList.contains("active"),
+ "Flexbox highlighter toggle button is not active."
+ );
+ ok(
+ !getActiveHighlighter(HIGHLIGHTER_TYPE),
+ "No flexbox highlighter exists in the rule-view."
+ );
+ ok(
+ !getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "No flexbox highlighter is shown."
+ );
+
+ info("Toggling ON the flexbox highlighter from the rule-view.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ flexboxToggle.click();
+ await onHighlighterShown;
+
+ info(
+ "Checking the flexbox highlighter is created and toggle button is active in " +
+ "the rule-view."
+ );
+ ok(
+ flexboxToggle.classList.contains("active"),
+ "Flexbox highlighter toggle is active."
+ );
+ ok(
+ getActiveHighlighter(HIGHLIGHTER_TYPE),
+ "Flexbox highlighter created in the rule-view."
+ );
+ ok(
+ getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "Flexbox highlighter is shown."
+ );
+
+ info("Toggling OFF the flexbox highlighter from the rule-view.");
+ const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE);
+ flexboxToggle.click();
+ await onHighlighterHidden;
+
+ info(
+ "Checking the flexbox highlighter is not shown and toggle button is not active " +
+ "in the rule-view."
+ );
+ ok(
+ !flexboxToggle.classList.contains("active"),
+ "Flexbox highlighter toggle button is not active."
+ );
+ ok(
+ !getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "No flexbox highlighter is shown."
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_01b.js b/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_01b.js
new file mode 100644
index 0000000000..16f55d75c2
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_01b.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the flebox highlighter in the rule view and the display of the
+// flexbox highlighter.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #flex {
+ display: inline-flex;
+ }
+ </style>
+ <div id="flex"></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX;
+ const {
+ getActiveHighlighter,
+ getNodeForActiveHighlighter,
+ waitForHighlighterTypeShown,
+ waitForHighlighterTypeHidden,
+ } = getHighlighterTestHelpers(inspector);
+
+ await selectNode("#flex", inspector);
+ const container = getRuleViewProperty(view, "#flex", "display").valueSpan;
+ const flexboxToggle = container.querySelector(
+ ".js-toggle-flexbox-highlighter"
+ );
+
+ info("Checking the initial state of the flexbox toggle in the rule-view.");
+ ok(flexboxToggle, "Flexbox highlighter toggle is visible.");
+ ok(
+ !flexboxToggle.classList.contains("active"),
+ "Flexbox highlighter toggle button is not active."
+ );
+ ok(
+ !getActiveHighlighter(HIGHLIGHTER_TYPE),
+ "No flexbox highlighter exists in the rule-view."
+ );
+ ok(
+ !getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "No flexbox highlighter is shown."
+ );
+
+ info("Toggling ON the flexbox highlighter from the rule-view.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ flexboxToggle.click();
+ await onHighlighterShown;
+
+ info(
+ "Checking the flexbox highlighter is created and toggle button is active in " +
+ "the rule-view."
+ );
+ ok(
+ flexboxToggle.classList.contains("active"),
+ "Flexbox highlighter toggle is active."
+ );
+ ok(
+ getActiveHighlighter(HIGHLIGHTER_TYPE),
+ "Flexbox highlighter created in the rule-view."
+ );
+ ok(
+ getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "Flexbox highlighter is shown."
+ );
+
+ info("Toggling OFF the flexbox highlighter from the rule-view.");
+ const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE);
+ flexboxToggle.click();
+ await onHighlighterHidden;
+
+ info(
+ "Checking the flexbox highlighter is not shown and toggle button is not active " +
+ "in the rule-view."
+ );
+ ok(
+ !flexboxToggle.classList.contains("active"),
+ "Flexbox highlighter toggle button is not active."
+ );
+ ok(
+ !getActiveHighlighter(HIGHLIGHTER_TYPE),
+ "No flexbox highlighter is shown."
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_02.js b/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_02.js
new file mode 100644
index 0000000000..858562a5ee
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_02.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the flexbox highlighter in the rule view from an overridden
+// 'display: flex' declaration.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #flex {
+ display: flex;
+ }
+ div, ul {
+ display: flex;
+ }
+ </style>
+ <ul id="flex">
+ <li>1</li>
+ <li>2</li>
+ </ul>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX;
+ const {
+ getActiveHighlighter,
+ getNodeForActiveHighlighter,
+ waitForHighlighterTypeShown,
+ waitForHighlighterTypeHidden,
+ } = getHighlighterTestHelpers(inspector);
+
+ await selectNode("#flex", inspector);
+ const container = getRuleViewProperty(view, "#flex", "display").valueSpan;
+ const flexboxToggle = container.querySelector(
+ ".js-toggle-flexbox-highlighter"
+ );
+ const overriddenContainer = getRuleViewProperty(
+ view,
+ "div, ul",
+ "display"
+ ).valueSpan;
+ const overriddenFlexboxToggle = overriddenContainer.querySelector(
+ ".js-toggle-flexbox-highlighter"
+ );
+
+ info("Checking the initial state of the flexbox toggle in the rule-view.");
+ ok(
+ flexboxToggle && overriddenFlexboxToggle,
+ "Flexbox highlighter toggles are visible."
+ );
+ ok(
+ !flexboxToggle.classList.contains("active") &&
+ !overriddenFlexboxToggle.classList.contains("active"),
+ "Flexbox highlighter toggle buttons are not active."
+ );
+ ok(
+ !getActiveHighlighter(HIGHLIGHTER_TYPE),
+ "No flexbox highlighter exists in the rule-view."
+ );
+ ok(
+ !getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "No flexbox highlighter is shown."
+ );
+
+ info(
+ "Toggling ON the flexbox highlighter from the overridden rule in the rule-view."
+ );
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ overriddenFlexboxToggle.click();
+ await onHighlighterShown;
+
+ info(
+ "Checking the flexbox highlighter is created and toggle buttons are active in " +
+ "the rule-view."
+ );
+ ok(
+ flexboxToggle.classList.contains("active") &&
+ overriddenFlexboxToggle.classList.contains("active"),
+ "Flexbox highlighter toggle is active."
+ );
+ ok(
+ getActiveHighlighter(HIGHLIGHTER_TYPE),
+ "Flexbox highlighter created in the rule-view."
+ );
+ ok(
+ getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "Flexbox highlighter is shown."
+ );
+
+ info(
+ "Toggling off the flexbox highlighter from the normal flexbox declaration in " +
+ "the rule-view."
+ );
+ const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE);
+ flexboxToggle.click();
+ await onHighlighterHidden;
+
+ info(
+ "Checking the flexbox highlighter is not shown and toggle buttons are not " +
+ "active in the rule-view."
+ );
+ ok(
+ !flexboxToggle.classList.contains("active") &&
+ !overriddenFlexboxToggle.classList.contains("active"),
+ "Flexbox highlighter toggle buttons are not active."
+ );
+ ok(
+ !getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "No flexbox highlighter is shown."
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_03.js b/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_03.js
new file mode 100644
index 0000000000..4f5ecbbd52
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_03.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the flexbox highlighter in the rule view with multiple flexboxes in the
+// page.
+
+const TEST_URI = `
+ <style type='text/css'>
+ .flex {
+ display: flex;
+ }
+ </style>
+ <div id="flex1" class="flex"></div>
+ <div id="flex2" class="flex"></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX;
+ const {
+ getActiveHighlighter,
+ getNodeForActiveHighlighter,
+ waitForHighlighterTypeShown,
+ } = getHighlighterTestHelpers(inspector);
+
+ info("Selecting the first flexbox container.");
+ await selectNode("#flex1", inspector);
+ let container = getRuleViewProperty(view, ".flex", "display").valueSpan;
+ let flexboxToggle = container.querySelector(".js-toggle-flexbox-highlighter");
+
+ info(
+ "Checking the state of the flexbox toggle for the first flexbox container in " +
+ "the rule-view."
+ );
+ ok(flexboxToggle, "flexbox highlighter toggle is visible.");
+ ok(
+ !flexboxToggle.classList.contains("active"),
+ "Flexbox highlighter toggle button is not active."
+ );
+ ok(
+ !getActiveHighlighter(HIGHLIGHTER_TYPE),
+ "No flexbox highlighter exists in the rule-view."
+ );
+ ok(
+ !getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "No flexbox highlighter is shown."
+ );
+
+ info(
+ "Toggling ON the flexbox highlighter for the first flexbox container from the " +
+ "rule-view."
+ );
+ let onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ flexboxToggle.click();
+ await onHighlighterShown;
+
+ info(
+ "Checking the flexbox highlighter is created and toggle button is active in " +
+ "the rule-view."
+ );
+ ok(
+ flexboxToggle.classList.contains("active"),
+ "Flexbox highlighter toggle is active."
+ );
+ ok(
+ getActiveHighlighter(HIGHLIGHTER_TYPE),
+ "Flexbox highlighter created in the rule-view."
+ );
+ ok(
+ getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "Flexbox highlighter is shown."
+ );
+
+ info("Selecting the second flexbox container.");
+ await selectNode("#flex2", inspector);
+ const firstFlexboxHighterShown =
+ getNodeForActiveHighlighter(HIGHLIGHTER_TYPE);
+ container = getRuleViewProperty(view, ".flex", "display").valueSpan;
+ flexboxToggle = container.querySelector(".js-toggle-flexbox-highlighter");
+
+ info(
+ "Checking the state of the CSS flexbox toggle for the second flexbox container " +
+ "in the rule-view."
+ );
+ ok(flexboxToggle, "Flexbox highlighter toggle is visible.");
+ ok(
+ !flexboxToggle.classList.contains("active"),
+ "Flexbox highlighter toggle button is not active."
+ );
+ ok(
+ getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "Flexbox highlighter is still shown."
+ );
+
+ info(
+ "Toggling ON the flexbox highlighter for the second flexbox container from the " +
+ "rule-view."
+ );
+ onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ flexboxToggle.click();
+ await onHighlighterShown;
+
+ info(
+ "Checking the flexbox highlighter is created for the second flexbox container " +
+ "and toggle button is active in the rule-view."
+ );
+ ok(
+ flexboxToggle.classList.contains("active"),
+ "Flexbox highlighter toggle is active."
+ );
+ ok(
+ getNodeForActiveHighlighter(HIGHLIGHTER_TYPE) != firstFlexboxHighterShown,
+ "Flexbox highlighter for the second flexbox container is shown."
+ );
+
+ info("Selecting the first flexbox container.");
+ await selectNode("#flex1", inspector);
+ container = getRuleViewProperty(view, ".flex", "display").valueSpan;
+ flexboxToggle = container.querySelector(".js-toggle-flexbox-highlighter");
+
+ info(
+ "Checking the state of the flexbox toggle for the first flexbox container in " +
+ "the rule-view."
+ );
+ ok(flexboxToggle, "Flexbox highlighter toggle is visible.");
+ ok(
+ !flexboxToggle.classList.contains("active"),
+ "Flexbox highlighter toggle button is not active."
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_04.js b/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_04.js
new file mode 100644
index 0000000000..abf1e1e1d5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_04.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the flexbox highlighter in the rule view from a
+// 'display: flex!important' declaration.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #flex {
+ display: flex !important;
+ }
+ </style>
+ <div id="flex"></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX;
+ const {
+ getActiveHighlighter,
+ getNodeForActiveHighlighter,
+ waitForHighlighterTypeShown,
+ waitForHighlighterTypeHidden,
+ } = getHighlighterTestHelpers(inspector);
+
+ await selectNode("#flex", inspector);
+ const container = getRuleViewProperty(view, "#flex", "display").valueSpan;
+ const flexboxToggle = container.querySelector(
+ ".js-toggle-flexbox-highlighter"
+ );
+
+ info("Checking the initial state of the flexbox toggle in the rule-view.");
+ ok(flexboxToggle, "Flexbox highlighter toggle is visible.");
+ ok(
+ !flexboxToggle.classList.contains("active"),
+ "Flexbox highlighter toggle button is not active."
+ );
+ ok(
+ !getActiveHighlighter(HIGHLIGHTER_TYPE),
+ "No flexbox highlighter exists in the rule-view."
+ );
+ ok(
+ !getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "No flexbox highlighter is shown."
+ );
+
+ info("Toggling ON the flexbox highlighter from the rule-view.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ flexboxToggle.click();
+ await onHighlighterShown;
+
+ info(
+ "Checking the flexbox highlighter is created and toggle button is active in " +
+ "the rule-view."
+ );
+ ok(
+ flexboxToggle.classList.contains("active"),
+ "Flexbox highlighter toggle is active."
+ );
+ ok(
+ getActiveHighlighter(HIGHLIGHTER_TYPE),
+ "Flexbox highlighter created in the rule-view."
+ );
+ ok(
+ getNodeForActiveHighlighter(HIGHLIGHTER_TYPE),
+ "Flexbox highlighter is shown."
+ );
+
+ info("Toggling OFF the flexbox highlighter from the rule-view.");
+ const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE);
+ flexboxToggle.click();
+ await onHighlighterHidden;
+
+ info(
+ "Checking the flexbox highlighter is not shown and toggle button is not active " +
+ "in the rule-view."
+ );
+ ok(
+ !flexboxToggle.classList.contains("active"),
+ "Flexbox highlighter toggle button is not active."
+ );
+ ok(
+ !getActiveHighlighter(HIGHLIGHTER_TYPE),
+ "No flexbox highlighter is shown."
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_font-family-parsing.js b/devtools/client/inspector/rules/test/browser_rules_font-family-parsing.js
new file mode 100644
index 0000000000..5e067be731
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_font-family-parsing.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the parsed font-family property value shown in the rules
+// pane is correct.
+
+const TEST_URI = `
+ <style type="text/css">
+ #id1 {
+ font-family: georgia, arial, sans-serif;
+ }
+ #id2 {
+ font-family: georgia,arial,sans-serif;
+ }
+ #id3 {
+ font-family: georgia ,arial ,sans-serif;
+ }
+ #id4 {
+ font-family: georgia , arial , sans-serif;
+ }
+ #id4 {
+ font-family: arial, georgia, sans-serif ;
+ }
+ #id5 {
+ font-family: helvetica !important;
+ }
+ </style>
+ <div id="id1">1</div>
+ <div id="id2">2</div>
+ <div id="id3">3</div>
+ <div id="id4">4</div>
+ <div id="id5">5</div>
+`;
+
+const TESTS = [
+ { selector: "#id1", expectedTextContent: "georgia, arial, sans-serif" },
+ { selector: "#id2", expectedTextContent: "georgia,arial,sans-serif" },
+ { selector: "#id3", expectedTextContent: "georgia ,arial ,sans-serif" },
+ { selector: "#id4", expectedTextContent: "arial, georgia, sans-serif" },
+ { selector: "#id5", expectedTextContent: "helvetica !important" },
+];
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ for (const { selector, expectedTextContent } of TESTS) {
+ await selectNode(selector, inspector);
+ info("Looking for font-family property value in selector " + selector);
+
+ const prop = getRuleViewProperty(view, selector, "font-family").valueSpan;
+ is(
+ prop.textContent,
+ expectedTextContent,
+ "The font-family property value is correct"
+ );
+ }
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-mutation.js b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-mutation.js
new file mode 100644
index 0000000000..1394c0532c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-mutation.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid highlighter is hidden when the highlighted grid container is
+// removed from the page.
+
+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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const highlighters = inspector.highlighters;
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.GRID;
+ const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } =
+ getHighlighterTestHelpers(inspector);
+
+ await selectNode("#grid", inspector);
+ const container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ const gridToggle = container.querySelector(".js-toggle-grid-highlighter");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ gridToggle.click();
+ await onHighlighterShown;
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+ const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE);
+ info("Remove the #grid container in the content page");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () =>
+ content.document.querySelector("#grid").remove()
+ );
+ await onHighlighterHidden;
+ ok(!highlighters.gridHighlighters.size, "CSS grid highlighter is hidden.");
+});
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 0000000000..6c2192b800
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.js
@@ -0,0 +1,42 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const highlighters = inspector.highlighters;
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.GRID;
+ const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector);
+
+ await selectNode("#grid", inspector);
+ const container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ const gridToggle = container.querySelector(".js-toggle-grid-highlighter");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ gridToggle.click();
+ await onHighlighterShown;
+
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+ await navigateTo(TEST_URI_2);
+ ok(!highlighters.gridHighlighters.size, "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 0000000000..7850b069ae
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-reload.js
@@ -0,0 +1,55 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ info("Check that the grid highlighter can be displayed");
+ await checkGridHighlighter();
+
+ info("Close the toolbox before reloading the tab");
+ await gDevTools.closeToolboxForTab(gBrowser.selectedTab);
+
+ await reloadBrowser();
+
+ info(
+ "Check that the grid highlighter can be displayed after reloading the page"
+ );
+ await checkGridHighlighter();
+});
+
+async function checkGridHighlighter() {
+ const { inspector, view } = await openRuleView();
+ const { highlighters } = inspector;
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.GRID;
+ const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector);
+
+ await selectNode("#grid", inspector);
+ const container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ const gridToggle = container.querySelector(".js-toggle-grid-highlighter");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ gridToggle.click();
+ await onHighlighterShown;
+
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-restored-after-reload.js b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-restored-after-reload.js
new file mode 100644
index 0000000000..6a2e5743af
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-restored-after-reload.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid highlighter is re-displayed after reloading a page.
+
+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 OTHER_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ <div id="cell3">cell3</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ info("Check that the grid highlighter can be displayed");
+ const { inspector, view } = await openRuleView();
+ const { highlighters } = inspector;
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.GRID;
+ const {
+ waitForHighlighterTypeShown,
+ waitForHighlighterTypeRestored,
+ waitForHighlighterTypeDiscarded,
+ } = getHighlighterTestHelpers(inspector);
+
+ await selectNode("#grid", inspector);
+ const container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ const gridToggle = container.querySelector(".js-toggle-grid-highlighter");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ gridToggle.click();
+ await onHighlighterShown;
+
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+ info("Reload the page, expect the highlighter to be displayed once again");
+ const onRestored = waitForHighlighterTypeRestored(HIGHLIGHTER_TYPE);
+
+ const onReloaded = inspector.once("reloaded");
+ await reloadBrowser();
+ info("Wait for inspector to be reloaded after page reload");
+ await onReloaded;
+
+ await onRestored;
+ is(
+ highlighters.gridHighlighters.size,
+ 1,
+ "CSS grid highlighter was restored."
+ );
+
+ info("Navigate to another URL, and check that the highlighter is hidden");
+ const otherUri =
+ "data:text/html;charset=utf-8," + encodeURIComponent(OTHER_URI);
+ const onDiscarded = waitForHighlighterTypeDiscarded(HIGHLIGHTER_TYPE);
+ await navigateTo(otherUri);
+ await onDiscarded;
+ is(
+ highlighters.gridHighlighters.size,
+ 0,
+ "CSS grid highlighter was not restored."
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-template-areas.js b/devtools/client/inspector/rules/test/browser_rules_grid-template-areas.js
new file mode 100644
index 0000000000..0decb7f7db
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-template-areas.js
@@ -0,0 +1,178 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the text editor correctly display the grid-template-areas value.
+// The CSS Grid spec allows to create grid-template-areas in an ascii-art style matrix.
+// It should show each string on its own line, when displaying rules for grid-template-areas.
+
+const TEST_URI = `
+<style type='text/css'>
+ #testid {
+ display: grid;
+ grid-template-areas: "a a bb"
+ 'a a bb'
+ "ccc ccc bb";
+ }
+
+ #testid-quotes {
+ quotes: "«" "»" "‹" "›";
+ }
+
+ #testid-invalid-strings {
+ grid-template-areas: "a a b"
+ "a a";
+ }
+
+ #testid-valid-quotefree-value {
+ grid-template-areas: inherit;
+ }
+
+ .a {
+ grid-area: a;
+ }
+
+ .b {
+ grid-area: bb;
+ }
+
+ .c {
+ grid-area: ccc;
+ }
+</style>
+<div id="testid">
+ <div class="a">cell a</div>
+ <div class="b">cell b</div>
+ <div class="c">cell c</div>
+</div>
+<q id="testid-quotes">
+ Show us the wonder-working <q>Brothers,</q> let them come out publicly—and we will believe in them!
+</q>
+<div id="testid-invalid-strings">
+ <div class="a">cell a</div>
+ <div class="b">cell b</div>
+</div>
+<div id="testid-valid-quotefree-value">
+ <div class="a">cell a</div>
+ <div class="b">cell b</div>
+</div>
+`;
+
+const multiLinesInnerText = '\n"a a bb" \n\'a a bb\' \n"ccc ccc bb"';
+const typedAndCopiedMultiLinesString = '"a a bb ccc" "a a bb ccc"';
+const typedAndCopiedMultiLinesInnerText = '\n"a a bb ccc" \n"a a bb ccc"';
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ info("Selecting the test node");
+ await selectNode("#testid", inspector);
+
+ info(
+ "Tests display of grid-template-areas value in an ascii-art style," +
+ "displaying each string on its own line"
+ );
+
+ const gridRuleProperty = await getRuleViewProperty(
+ view,
+ "#testid",
+ "grid-template-areas"
+ ).valueSpan;
+ is(
+ gridRuleProperty.innerText,
+ multiLinesInnerText,
+ "the grid-area is displayed with each string in its own line, and sufficient spacing for areas to align vertically"
+ );
+
+ // copy/paste the current value inside, to also make sure of the value copied is useful as text
+
+ // Calculate offsets to click in the value line which is below the property name line .
+ const rect = gridRuleProperty.getBoundingClientRect();
+ const previousProperty = await getRuleViewProperty(
+ view,
+ "#testid",
+ "display"
+ ).nameSpan.getBoundingClientRect();
+
+ const x = rect.width / 2;
+ const y = rect.y - previousProperty.y + 1;
+
+ info("Focusing the css property editable value");
+ await focusEditableField(view, gridRuleProperty, x, y);
+ info("typing a new value");
+ [...typedAndCopiedMultiLinesString].map(char =>
+ EventUtils.synthesizeKey(char, {}, view.styleWindow)
+ );
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ is(
+ gridRuleProperty.innerText,
+ typedAndCopiedMultiLinesInnerText,
+ "the typed value is correct, and a single quote is displayed on its own line"
+ );
+ info("copy-paste the 'grid-template-areas' property value to duplicate it");
+ const onDone = view.once("ruleview-changed");
+ await focusEditableField(view, gridRuleProperty, x, y);
+ EventUtils.synthesizeKey("C", { accelKey: true }, view.styleWindow);
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, view.styleWindow);
+ EventUtils.synthesizeKey("V", { accelKey: true }, view.styleWindow);
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+
+ await onDone;
+
+ info("Check copy-pasting the property value is not breaking it");
+
+ is(
+ gridRuleProperty.innerText,
+ typedAndCopiedMultiLinesInnerText + " " + typedAndCopiedMultiLinesInnerText,
+ "copy-pasting the current value duplicate the correct value, with each string of the multi strings grid-template-areas value is displayed on a new line"
+ );
+
+ // test that when "non grid-template-area", like quotes for example, its multi-string value is not displayed over multiple lines
+ await selectNode("#testid-quotes", inspector);
+
+ info(
+ "Tests display of content string value is NOT in an ascii-art style," +
+ "displaying each string on a single line"
+ );
+
+ const contentRuleProperty = await getRuleViewProperty(
+ view,
+ "#testid-quotes",
+ "quotes"
+ ).valueSpan;
+ is(
+ contentRuleProperty.innerText,
+ '"«" "»" "‹" "›"',
+ "the quotes strings values are all displayed on the same single line"
+ );
+
+ // test that when invalid strings values do not get formatted
+ info("testing it does not try to format invalid values");
+ await selectNode("#testid-invalid-strings", inspector);
+ const invalidGridRuleProperty = await getRuleViewProperty(
+ view,
+ "#testid-invalid-strings",
+ "grid-template-areas"
+ ).valueSpan;
+ is(
+ invalidGridRuleProperty.innerText,
+ '"a a b" "a a"',
+ "the invalid strings values do not get formatted"
+ );
+
+ // test that when a valid value without quotes such as `inherit` it does not get formatted
+ info("testing it does not try to format valid non-quote values");
+ await selectNode("#testid-valid-quotefree-value", inspector);
+ const validGridRuleNoQuoteProperty = await getRuleViewProperty(
+ view,
+ "#testid-valid-quotefree-value",
+ "grid-template-areas"
+ ).valueSpan;
+ is(
+ validGridRuleNoQuoteProperty.innerText,
+ "inherit",
+ "the valid quote-free values do not get formatted"
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-toggle-telemetry.js b/devtools/client/inspector/rules/test/browser_rules_grid-toggle-telemetry.js
new file mode 100644
index 0000000000..7ea229a734
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle-telemetry.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the telemetry count is correct when the grid highlighter is activated from
+// the rules view.
+
+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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ startTelemetry();
+ const { inspector, view } = await openRuleView();
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.GRID;
+ const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector);
+
+ await selectNode("#grid", inspector);
+ const container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ const gridToggle = container.querySelector(".js-toggle-grid-highlighter");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ gridToggle.click();
+ await onHighlighterShown;
+
+ checkResults();
+});
+
+function checkResults() {
+ checkTelemetry("devtools.rules.gridinspector.opened", "", 1, "scalar");
+}
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 0000000000..21097c907a
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js
@@ -0,0 +1,81 @@
+/* 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>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const highlighters = view.highlighters;
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.GRID;
+ const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } =
+ getHighlighterTestHelpers(inspector);
+
+ await selectNode("#grid", inspector);
+ const container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ const gridToggle = container.querySelector(".js-toggle-grid-highlighter");
+
+ info("Checking the initial state of the CSS grid toggle in the rule-view.");
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ !gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active."
+ );
+ ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ gridToggle.click();
+ await onHighlighterShown;
+
+ info(
+ "Checking the CSS grid highlighter is created and toggle button is active in " +
+ "the rule-view."
+ );
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ gridToggle.classList.contains("active"),
+ "Grid highlighter toggle is active."
+ );
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+ info("Toggling OFF the CSS grid highlighter from the rule-view.");
+ const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE);
+ gridToggle.click();
+ await onHighlighterHidden;
+
+ info(
+ "Checking the CSS grid highlighter is not shown and toggle button is not active " +
+ "in the rule-view."
+ );
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ !gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active."
+ );
+ ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01b.js b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01b.js
new file mode 100644
index 0000000000..0933d1b4ab
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01b.js
@@ -0,0 +1,81 @@
+/* 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: inline-grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const highlighters = view.highlighters;
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.GRID;
+ const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } =
+ getHighlighterTestHelpers(inspector);
+
+ await selectNode("#grid", inspector);
+ const container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ const gridToggle = container.querySelector(".js-toggle-grid-highlighter");
+
+ info("Checking the initial state of the CSS grid toggle in the rule-view.");
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ !gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active."
+ );
+ ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ gridToggle.click();
+ await onHighlighterShown;
+
+ info(
+ "Checking the CSS grid highlighter is created and toggle button is active in " +
+ "the rule-view."
+ );
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ gridToggle.classList.contains("active"),
+ "Grid highlighter toggle is active."
+ );
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+ info("Toggling OFF the CSS grid highlighter from the rule-view.");
+ const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE);
+ gridToggle.click();
+ await onHighlighterHidden;
+
+ info(
+ "Checking the CSS grid highlighter is not shown and toggle button is not active " +
+ "in the rule-view."
+ );
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ !gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active."
+ );
+ ok(!highlighters.gridHighlighters.size, "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 0000000000..2f168126a6
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js
@@ -0,0 +1,95 @@
+/* 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>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const highlighters = view.highlighters;
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.GRID;
+ const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } =
+ getHighlighterTestHelpers(inspector);
+
+ await selectNode("#grid", inspector);
+ const container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ const gridToggle = container.querySelector(".js-toggle-grid-highlighter");
+ const overriddenContainer = getRuleViewProperty(
+ view,
+ "div, ul",
+ "display"
+ ).valueSpan;
+ const overriddenGridToggle =
+ overriddenContainer.querySelector(".ruleview-grid");
+
+ info("Checking the initial state of the CSS grid toggle in the rule-view.");
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ !overriddenGridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ !gridToggle.classList.contains("active") &&
+ !overriddenGridToggle.classList.contains("active"),
+ "Grid highlighter toggle buttons are not active."
+ );
+ ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+
+ info(
+ "Toggling ON the CSS grid highlighter from the overridden rule in the rule-view."
+ );
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ overriddenGridToggle.click();
+ await 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."
+ );
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+ info(
+ "Toggling off the CSS grid highlighter from the normal grid declaration in the " +
+ "rule-view."
+ );
+ const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE);
+ gridToggle.click();
+ await 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.gridHighlighters.size, "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 0000000000..2beec28d0e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js
@@ -0,0 +1,137 @@
+/* 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>
+`;
+
+add_task(async function () {
+ await pushPref("devtools.gridinspector.maxHighlighters", 1);
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const highlighters = view.highlighters;
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.GRID;
+ const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector);
+
+ info("Selecting the first grid container.");
+ await selectNode("#grid1", inspector);
+ let container = getRuleViewProperty(view, ".grid", "display").valueSpan;
+ let gridToggle = container.querySelector(".js-toggle-grid-highlighter");
+
+ info(
+ "Checking the state of the CSS grid toggle for the first grid container in the " +
+ "rule-view."
+ );
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ !gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active."
+ );
+ ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+
+ info(
+ "Toggling ON the CSS grid highlighter for the first grid container from the " +
+ "rule-view."
+ );
+ let onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ gridToggle.click();
+ await onHighlighterShown;
+
+ info(
+ "Checking the CSS grid highlighter is created and toggle button is active in " +
+ "the rule-view."
+ );
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ gridToggle.classList.contains("active"),
+ "Grid highlighter toggle is active."
+ );
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+ info("Selecting the second grid container.");
+ await selectNode("#grid2", inspector);
+ const firstGridHighterShown = highlighters.gridHighlighters
+ .keys()
+ .next().value;
+ container = getRuleViewProperty(view, ".grid", "display").valueSpan;
+ gridToggle = container.querySelector(".js-toggle-grid-highlighter");
+
+ info(
+ "Checking the state of the CSS grid toggle for the second grid container in the " +
+ "rule-view."
+ );
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ !gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active."
+ );
+ is(
+ highlighters.gridHighlighters.size,
+ 1,
+ "CSS grid highlighter is still shown."
+ );
+
+ info(
+ "Toggling ON the CSS grid highlighter for the second grid container from the " +
+ "rule-view."
+ );
+ onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ gridToggle.click();
+ await 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.gridHighlighters.keys().next().value != firstGridHighterShown,
+ "Grid highlighter for the second grid container is shown."
+ );
+
+ info("Selecting the first grid container.");
+ await selectNode("#grid1", inspector);
+ container = getRuleViewProperty(view, ".grid", "display").valueSpan;
+ gridToggle = container.querySelector(".js-toggle-grid-highlighter");
+
+ info(
+ "Checking the state of the CSS grid toggle for the first grid container in the " +
+ "rule-view."
+ );
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ !gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active."
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_04.js b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_04.js
new file mode 100644
index 0000000000..70e4b91893
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_04.js
@@ -0,0 +1,81 @@
+/* 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 a 'display: grid !important'
+// declaration.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid !important;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const highlighters = view.highlighters;
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.GRID;
+ const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } =
+ getHighlighterTestHelpers(inspector);
+
+ await selectNode("#grid", inspector);
+ const container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+ const gridToggle = container.querySelector(".js-toggle-grid-highlighter");
+
+ info("Checking the initial state of the CSS grid toggle in the rule-view.");
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ !gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active."
+ );
+ ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ gridToggle.click();
+ await onHighlighterShown;
+
+ info(
+ "Checking the CSS grid highlighter is created and toggle button is active in " +
+ "the rule-view."
+ );
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ gridToggle.classList.contains("active"),
+ "Grid highlighter toggle is active."
+ );
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+ info("Toggling OFF the CSS grid highlighter from the rule-view.");
+ const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE);
+ gridToggle.click();
+ await onHighlighterHidden;
+
+ info(
+ "Checking the CSS grid highlighter is not shown and toggle button is not active " +
+ "in the rule-view."
+ );
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ !gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active."
+ );
+ ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_05.js b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_05.js
new file mode 100644
index 0000000000..54d2d0979f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_05.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the grid toggle is hidden when the maximum number of grid highlighters
+// have been reached.
+
+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>
+ <div id="grid3" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await pushPref("devtools.gridinspector.maxHighlighters", 2);
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, gridInspector } = await openLayoutView();
+ const ruleView = selectRuleView(inspector);
+ const { document: doc } = gridInspector;
+ const { highlighters } = inspector;
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.GRID;
+ const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } =
+ getHighlighterTestHelpers(inspector);
+
+ await selectNode("#grid1", inspector);
+ const gridList = doc.getElementById("grid-list");
+ const checkbox2 = gridList.children[1].querySelector("input");
+ const checkbox3 = gridList.children[2].querySelector("input");
+ const container = getRuleViewProperty(ruleView, ".grid", "display").valueSpan;
+ const gridToggle = container.querySelector(".js-toggle-grid-highlighter");
+
+ info("Checking the initial state of the CSS grid toggle in the rule-view.");
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ !gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active."
+ );
+ ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+
+ info("Toggling ON the CSS grid highlighter for #grid2.");
+ let onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ checkbox2.click();
+ await onHighlighterShown;
+
+ info(
+ "Checking the CSS grid toggle for #grid1 is not disabled and not active."
+ );
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ !gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active."
+ );
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+ info("Toggling ON the CSS grid highlighter for #grid3.");
+ onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ checkbox3.click();
+ await onHighlighterShown;
+
+ info("Checking the CSS grid toggle for #grid1 is disabled.");
+ ok(
+ gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is disabled."
+ );
+ is(highlighters.gridHighlighters.size, 2, "CSS grid highlighters are shown.");
+
+ info("Toggling OFF the CSS grid highlighter for #grid3.");
+ let onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE);
+ checkbox3.click();
+ await onHighlighterHidden;
+
+ info(
+ "Checking the CSS grid toggle for #grid1 is not disabled and not active."
+ );
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ !gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active."
+ );
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+ info("Toggling ON the CSS grid highlighter for #grid1 from the rule-view.");
+ onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ gridToggle.click();
+ await onHighlighterShown;
+
+ info("Checking the CSS grid toggle for #grid1 is not disabled.");
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ gridToggle.classList.contains("active"),
+ "Grid highlighter toggle is active."
+ );
+ is(highlighters.gridHighlighters.size, 2, "CSS grid highlighters are shown.");
+
+ info("Toggling OFF the CSS grid highlighter for #grid1 from the rule-view.");
+ onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE);
+ gridToggle.click();
+ await onHighlighterHidden;
+
+ info(
+ "Checking the CSS grid toggle for #grid1 is not disabled and not active."
+ );
+ ok(
+ !gridToggle.hasAttribute("disabled"),
+ "Grid highlighter toggle is not disabled."
+ );
+ ok(
+ !gridToggle.classList.contains("active"),
+ "Grid highlighter toggle button is not active."
+ );
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_gridline-names-are-shown-correctly.js b/devtools/client/inspector/rules/test/browser_rules_gridline-names-are-shown-correctly.js
new file mode 100644
index 0000000000..ec7f4a358f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_gridline-names-are-shown-correctly.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the text editor correctly calculates the grid line names shown in the
+// autocomplete popup. We generally want to show all the grid line names for a grid
+// container, except for implicit line names created by an implicitly named area.
+
+const TEST_URL = URL_ROOT + "doc_grid_area_gridline_names.html";
+
+add_task(async function () {
+ await addTab(TEST_URL);
+ const { inspector, view } = await openRuleView();
+
+ info(
+ "Test that implicit grid line names from explicit grid areas are shown."
+ );
+ await testExplicitNamedAreas(inspector, view);
+
+ info(
+ "Test that explicit grid line names creating implicit grid areas are shown."
+ );
+ await testImplicitNamedAreasWithExplicitGridLineNames(inspector, view);
+
+ info(
+ "Test that implicit grid line names creating implicit grid areas are not shown."
+ );
+ await testImplicitAreasWithImplicitGridLineNames(inspector, view);
+ await testImplicitNamedAreasWithReversedGridLineNames(inspector, view);
+});
+
+async function testExplicitNamedAreas(inspector, view) {
+ await selectNode(".header", inspector);
+
+ const gridColLines = ["header-start", "header-end", "main-start", "main-end"];
+
+ const propertyName = view.styleDocument.querySelectorAll(
+ ".ruleview-propertyvalue"
+ )[0];
+ const gridLineNamesUpdated = inspector.once("grid-line-names-updated");
+ const editor = await focusEditableField(view, propertyName);
+ const onPopupShown = once(editor.popup, "popup-opened");
+ await gridLineNamesUpdated;
+
+ EventUtils.synthesizeKey("VK_DOWN", { shiftKey: true }, view.styleWindow);
+
+ await onPopupShown;
+
+ info(
+ "Check that the expected grid line column names are shown in the editor popup."
+ );
+ for (const lineName of gridColLines) {
+ ok(
+ editor.gridLineNames.cols.indexOf(lineName) > -1,
+ `${lineName} is a suggested implicit grid line`
+ );
+ }
+}
+
+async function testImplicitNamedAreasWithExplicitGridLineNames(
+ inspector,
+ view
+) {
+ await selectNode(".contentArea", inspector);
+
+ const gridRowLines = [
+ "main-start",
+ "main-end",
+ "content-start",
+ "content-end",
+ ];
+
+ const propertyName = view.styleDocument.querySelectorAll(
+ ".ruleview-propertyvalue"
+ )[1];
+ const gridLineNamesUpdated = inspector.once("grid-line-names-updated");
+ const editor = await focusEditableField(view, propertyName);
+ const onPopupShown = once(editor.popup, "popup-opened");
+ await gridLineNamesUpdated;
+
+ EventUtils.synthesizeKey("VK_DOWN", { shiftKey: true }, view.styleWindow);
+
+ await onPopupShown;
+
+ info(
+ "Check that the expected grid line row names are shown in the editor popup."
+ );
+ for (const lineName of gridRowLines) {
+ ok(
+ editor.gridLineNames.rows.indexOf(lineName) > -1,
+ `${lineName} is a suggested explicit grid line`
+ );
+ }
+}
+
+async function testImplicitAreasWithImplicitGridLineNames(inspector, view) {
+ await selectNode(".a", inspector);
+
+ const propertyName = view.styleDocument.querySelectorAll(
+ ".ruleview-propertyvalue"
+ )[0];
+ const gridLineNamesUpdated = inspector.once("grid-line-names-updated");
+ const editor = await focusEditableField(view, propertyName);
+ const onPopupShown = once(editor.popup, "popup-opened");
+ await gridLineNamesUpdated;
+
+ EventUtils.synthesizeKey("VK_DOWN", { shiftKey: true }, view.styleWindow);
+
+ await onPopupShown;
+
+ info(
+ "Check that the implicit grid lines created by implicit areas are not shown."
+ );
+ ok(
+ !(editor.gridLineNames.cols.indexOf("a-end") > -1),
+ "a-end is not shown because it is created by an implicit named area."
+ );
+}
+
+async function testImplicitNamedAreasWithReversedGridLineNames(
+ inspector,
+ view
+) {
+ await selectNode(".b", inspector);
+
+ const propertyName = view.styleDocument.querySelectorAll(
+ ".ruleview-propertyvalue"
+ )[0];
+ const gridLineNamesUpdated = inspector.once("grid-line-names-updated");
+ const editor = await focusEditableField(view, propertyName);
+ const onPopupShown = once(editor.popup, "popup-opened");
+ await gridLineNamesUpdated;
+
+ EventUtils.synthesizeKey("VK_DOWN", { shiftKey: true }, view.styleWindow);
+
+ await onPopupShown;
+
+ info(
+ "Test that reversed implicit grid line names from implicit areas are not shown"
+ );
+ ok(
+ !(editor.gridLineNames.cols.indexOf("b-start") > -1),
+ "b-start is not shown because it is created by an implicit named area."
+ );
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_gridline-names-autocomplete.js b/devtools/client/inspector/rules/test/browser_rules_gridline-names-autocomplete.js
new file mode 100644
index 0000000000..e80e7f01ac
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_gridline-names-autocomplete.js
@@ -0,0 +1,205 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that CSS property values are autocompleted and cycled
+// correctly when editing an existing property 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 changeTestData = [
+ ["c", {}, "col1-start", OPEN, SELECTED, CHANGE],
+ ["o", {}, "col1-start", OPEN, SELECTED, CHANGE],
+ ["l", {}, "col1-start", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "col2-start", OPEN, SELECTED, CHANGE],
+ ["VK_RIGHT", {}, "col2-start", !OPEN, !SELECTED, !CHANGE],
+];
+
+// Creates a new CSS property value.
+// Checks that grid-area autocompletes column and row names.
+const newAreaTestData = [
+ ["g", {}, "gap", OPEN, SELECTED, !CHANGE],
+ ["VK_DOWN", {}, "grid", OPEN, SELECTED, !CHANGE],
+ ["VK_DOWN", {}, "grid-area", OPEN, SELECTED, !CHANGE],
+ ["VK_TAB", {}, "", !OPEN, !SELECTED, !CHANGE],
+ "grid-line-names-updated",
+ ["c", {}, "col1-start", OPEN, SELECTED, CHANGE],
+ ["VK_BACK_SPACE", {}, "c", !OPEN, !SELECTED, CHANGE],
+ ["VK_BACK_SPACE", {}, "", OPEN, !SELECTED, CHANGE],
+ ["r", {}, "revert", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "revert-layer", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "row1-start", OPEN, SELECTED, CHANGE],
+ ["r", {}, "rr", !OPEN, !SELECTED, CHANGE],
+ ["VK_BACK_SPACE", {}, "r", !OPEN, !SELECTED, CHANGE],
+ ["o", {}, "row1-start", OPEN, SELECTED, CHANGE],
+ ["VK_RETURN", {}, "", !OPEN, !SELECTED, CHANGE],
+];
+
+// Creates a new CSS property value.
+// Checks that grid-row only autocompletes row names.
+const newRowTestData = [
+ ["g", {}, "gap", OPEN, SELECTED, !CHANGE],
+ ["r", {}, "grid", OPEN, SELECTED, !CHANGE],
+ ["i", {}, "grid", OPEN, SELECTED, !CHANGE],
+ ["d", {}, "grid", OPEN, SELECTED, !CHANGE],
+ ["-", {}, "grid-area", OPEN, SELECTED, !CHANGE],
+ ["r", {}, "grid-row", OPEN, SELECTED, !CHANGE],
+ ["VK_RETURN", {}, "", !OPEN, !SELECTED, !CHANGE],
+ "grid-line-names-updated",
+ ["c", {}, "c", !OPEN, !SELECTED, CHANGE],
+ ["VK_BACK_SPACE", {}, "", OPEN, !SELECTED, CHANGE],
+ ["r", {}, "revert", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "revert-layer", OPEN, SELECTED, CHANGE],
+ ["VK_DOWN", {}, "row1-start", OPEN, SELECTED, CHANGE],
+ ["VK_TAB", {}, "", !OPEN, !SELECTED, CHANGE],
+];
+
+const TEST_URL = URL_ROOT + "doc_grid_names.html";
+
+add_task(async function () {
+ await addTab(TEST_URL);
+ const { toolbox, inspector, view } = await openRuleView();
+
+ info("Test autocompletion changing a preexisting property");
+ await runChangePropertyAutocompletionTest(
+ toolbox,
+ inspector,
+ view,
+ changeTestData
+ );
+
+ info("Test autocompletion creating a new property");
+ await runNewPropertyAutocompletionTest(
+ toolbox,
+ inspector,
+ view,
+ newAreaTestData
+ );
+
+ info("Test autocompletion creating a new property");
+ await runNewPropertyAutocompletionTest(
+ toolbox,
+ inspector,
+ view,
+ newRowTestData
+ );
+});
+
+async function runNewPropertyAutocompletionTest(
+ toolbox,
+ inspector,
+ view,
+ testData
+) {
+ info("Selecting the test node");
+ await selectNode("#cell2", inspector);
+
+ info("Focusing the css property editable field");
+ const ruleEditor = getRuleViewRuleEditor(view, 0);
+ const editor = await focusNewRuleViewProperty(ruleEditor);
+ const gridLineNamesUpdated = inspector.once("grid-line-names-updated");
+
+ info("Starting to test for css property completion");
+ for (const data of testData) {
+ if (data == "grid-line-names-updated") {
+ await gridLineNamesUpdated;
+ continue;
+ }
+ await testCompletion(data, editor, view);
+ }
+}
+
+async function runChangePropertyAutocompletionTest(
+ toolbox,
+ inspector,
+ view,
+ testData
+) {
+ info("Selecting the test node");
+ await selectNode("#cell3", inspector);
+
+ const ruleEditor = getRuleViewRuleEditor(view, 1).rule;
+ const prop = ruleEditor.textProps[0];
+
+ info("Focusing the css property editable value");
+ const gridLineNamesUpdated = inspector.once("grid-line-names-updated");
+ let editor = await focusEditableField(view, prop.editor.valueSpan);
+ await gridLineNamesUpdated;
+
+ info("Starting to test for css property completion");
+ for (const data of testData) {
+ // Re-define the editor at each iteration, because the focus may have moved
+ // from property to value and back
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ await testCompletion(data, editor, view);
+ }
+}
+
+async 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.
+ const popupEvent = open ? "popup-opened" : "popup-closed";
+ const 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 debounce for the preview text.
+ view.debounce.flush();
+
+ await onDone;
+ await 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_guessIndentation.js b/devtools/client/inspector/rules/test/browser_rules_guessIndentation.js
new file mode 100644
index 0000000000..22bec84378
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_guessIndentation.js
@@ -0,0 +1,46 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { toolbox, inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ info("Add a new property in the rule-view");
+ await addProperty(view, 2, "color", "chartreuse");
+
+ info("Switch to the style-editor");
+ const { UI } = await toolbox.selectTool("styleeditor");
+
+ const styleEditor = await UI.editors[0].getSourceEditor();
+ const text = styleEditor.sourceEditor.getText();
+ is(text, expectedText, "style inspector changes are synced");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_highlight-element-rule.js b/devtools/client/inspector/rules/test/browser_rules_highlight-element-rule.js
new file mode 100644
index 0000000000..f5e1357950
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_highlight-element-rule.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule view's highlightElementRule scrolls to the specified rule.
+
+const TEST_URI = `
+ <style type="text/css">
+ .test::after {
+ content: "!";
+ color: red;
+ }
+ </style>
+ <div class="test">Hello</div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await selectNode(".test", inspector);
+ const { rules, styleWindow } = view;
+
+ info("Highlight .test::after rule.");
+ const ruleId = rules[0].domRule.actorID;
+
+ info("Wait for the view to scroll to the property.");
+ const onHighlightProperty = view.once("scrolled-to-element");
+
+ view.highlightElementRule(ruleId);
+
+ await onHighlightProperty;
+
+ ok(
+ isInViewport(rules[0].editor.element, styleWindow),
+ ".test::after is in view."
+ );
+});
+
+function isInViewport(element, win) {
+ const { top, left, bottom, right } = element.getBoundingClientRect();
+ return (
+ top >= 0 &&
+ bottom <= win.innerHeight &&
+ left >= 0 &&
+ right <= win.innerWidth
+ );
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_highlight-property.js b/devtools/client/inspector/rules/test/browser_rules_highlight-property.js
new file mode 100644
index 0000000000..6c244cecfd
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_highlight-property.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule view's highlightProperty scrolls to the specified declaration.
+
+const TEST_URI = `
+ <style type="text/css">
+ .test {
+ margin: 5px;
+ font-size: 12px;
+ border: 1px solid blue;
+ margin-top: 20px;
+ }
+
+ .test::after {
+ content: "!";
+ color: red;
+ }
+ </style>
+ <div class="test">Hello this is a test</div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await selectNode(".test", inspector);
+ const { rules, styleWindow } = view;
+
+ info(
+ "Highlight the computed border-left-width declaration in the rule view."
+ );
+ const borderLeftWidthStyle = rules[2].textProps[2].computed.find(
+ ({ name }) => name === "border-left-width"
+ );
+
+ let onHighlightProperty = view.once("scrolled-to-element");
+ let isHighlighted = view.highlightProperty("border-left-width");
+ await onHighlightProperty;
+
+ ok(isHighlighted, "border-left-property is highlighted.");
+ ok(
+ isInViewport(borderLeftWidthStyle.element, styleWindow),
+ "border-left-width is in view."
+ );
+
+ info("Highlight the font-size declaration in the rule view.");
+ const fontSize = rules[2].textProps[1].editor;
+
+ info("Wait for the view to scroll to the property.");
+ onHighlightProperty = view.once("scrolled-to-element");
+ isHighlighted = view.highlightProperty("font-size");
+ await onHighlightProperty;
+
+ ok(isHighlighted, "font-size property is highlighted.");
+ ok(isInViewport(fontSize.element, styleWindow), "font-size is in view.");
+
+ info("Highlight the pseudo-element's color declaration in the rule view.");
+ const color = rules[0].textProps[1].editor;
+
+ info("Wait for the view to scroll to the property.");
+ onHighlightProperty = view.once("scrolled-to-element");
+ isHighlighted = view.highlightProperty("color");
+ await onHighlightProperty;
+
+ ok(isHighlighted, "color property is highlighted.");
+ ok(isInViewport(color.element, styleWindow), "color property is in view.");
+
+ info("Highlight margin-top declaration in the rules view.");
+ const marginTop = rules[2].textProps[3].editor;
+
+ info("Wait for the view to scroll to the property.");
+ onHighlightProperty = view.once("scrolled-to-element");
+ isHighlighted = view.highlightProperty("margin-top");
+ await onHighlightProperty;
+
+ ok(isHighlighted, "margin-top property is highlighted.");
+ ok(
+ isInViewport(marginTop.element, styleWindow),
+ "margin-top property is in view."
+ );
+});
+
+function isInViewport(element, win) {
+ const { top, left, bottom, right } = element.getBoundingClientRect();
+ return (
+ top >= 0 &&
+ bottom <= win.innerHeight &&
+ left >= 0 &&
+ right <= win.innerWidth
+ );
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_highlight-used-fonts.js b/devtools/client/inspector/rules/test/browser_rules_highlight-used-fonts.js
new file mode 100644
index 0000000000..f0dc95f15c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_highlight-used-fonts.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that a used font-family is highlighted in the rule-view.
+
+const TEST_URI = `
+ <style type="text/css">
+ #id1 {
+ font-family: foo, bar, sans-serif;
+ }
+ #id2 {
+ font-family: serif;
+ }
+ #id3 {
+ font-family: foo, monospace, monospace, serif;
+ }
+ #id4 {
+ font-family: foo, bar;
+ }
+ #id5 {
+ font-family: "monospace";
+ }
+ #id6 {
+ font-family: georgia, arial;
+ }
+ #id7 {
+ font-family: foo, serif !important;
+ }
+ #id8 {
+ font-family: important;
+ }
+ #id9::before {
+ content: ' ';
+ font-family: foo, monospace;
+ }
+ </style>
+ <div id="id1">Text</div>
+ <div id="id2">Text</div>
+ <div id="id3">Text</div>
+ <div id="id4">Text</div>
+ <div id="id5">Text</div>
+ <div id="id6">A &#586;</div>
+ <div id="id7">Text</div>
+ <div id="id8">Text</div>
+ <div id="id9">Text</div>
+`;
+
+// Tests that font-family properties in the rule-view correctly
+// indicates which font is in use.
+// Each entry in the test array should contain:
+// {
+// baseSelector: the rule-view selector to look for font-family in
+// nb: the number of fonts this property should have
+// used: the indexes of all the fonts that should be highlighted, or null if none should
+// be highlighted
+// selectBeforePseudoElement: Whether the before pseudo element should be selectd or not
+// }
+const TESTS = [
+ { baseSelector: "#id1", nb: 3, used: [2] }, // sans-serif
+ { baseSelector: "#id2", nb: 1, used: [0] }, // serif
+ { baseSelector: "#id3", nb: 4, used: [1] }, // monospace
+ { baseSelector: "#id4", nb: 2, used: null },
+ { baseSelector: "#id5", nb: 1, used: [0] }, // monospace
+ { baseSelector: "#id7", nb: 2, used: [1] }, // serif
+ { baseSelector: "#id8", nb: 1, used: null },
+ { baseSelector: "#id9", nb: 2, used: [1], selectBeforePseudoElement: true }, // monospace
+];
+
+if (Services.appinfo.OS !== "Linux") {
+ // Both georgia and arial are used because the second character can't be rendered with
+ // georgia, so the browser falls back. Also, skip this on Linux which has neither of
+ // these fonts.
+ TESTS.push({ baseSelector: "#id6", nb: 2, used: [0, 1] });
+}
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ for (const { baseSelector, nb, used, selectBeforePseudoElement } of TESTS) {
+ const onFontHighlighted = view.once("font-highlighted");
+
+ if (selectBeforePseudoElement) {
+ // Query the first children node to get the before pseudo element:
+ const baseNode = await getNodeFront(baseSelector, inspector);
+ const pseudoElement = (await inspector.walker.children(baseNode))
+ .nodes[0];
+ await selectNode(pseudoElement, inspector);
+ } else {
+ await selectNode(baseSelector, inspector);
+ }
+ await onFontHighlighted;
+
+ const selector = !selectBeforePseudoElement
+ ? baseSelector
+ : `${baseSelector}::before`;
+ info(`Looking for fonts in font-family property for: <${selector}>`);
+
+ const prop = getRuleViewProperty(view, selector, "font-family").valueSpan;
+ const fonts = prop.querySelectorAll(".ruleview-font-family");
+
+ ok(fonts.length, "Fonts found in the property");
+ is(fonts.length, nb, "Correct number of fonts found in the property");
+
+ const highlighted = [...fonts].filter(span =>
+ span.classList.contains("used-font")
+ );
+ const expectedHighlightedNb = used === null ? 0 : used.length;
+ is(
+ highlighted.length,
+ expectedHighlightedNb,
+ "Correct number of used fonts found"
+ );
+
+ let highlightedIndex = 0;
+ [...fonts].forEach((font, index) => {
+ if (font === highlighted[highlightedIndex]) {
+ is(index, used[highlightedIndex], "The right font is highlighted");
+ highlightedIndex++;
+ }
+ });
+ }
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_imported_stylesheet_edit.js b/devtools/client/inspector/rules/test/browser_rules_imported_stylesheet_edit.js
new file mode 100644
index 0000000000..76c2b4a5ba
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_imported_stylesheet_edit.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const TEST_URI = URL_ROOT_SSL + "doc_rules_imported_stylesheet_edit.html";
+const SJS_URI = URL_ROOT_SSL + "sjs_imported_stylesheet_edit.sjs";
+/**
+ * Test that imported stylesheets are correctly handled by the inspector after
+ * being updated.
+ * The inspector used to retrieve an outdated version of the stylesheet text,
+ * which triggered many issues: outdated values, blank panels etc...
+ *
+ * This test involves an imported CSS which is generated by a sjs file.
+ * Using sjs here allows us to simulate an "update" of a stylesheet while still
+ * fetching the same URL, which closely matches what a developer would experience
+ * when manually editing a stylesheet in an IDE before reloading a page.
+ */
+add_task(async function () {
+ info("Call `?setup` on the test sjs");
+ await fetch(SJS_URI + "?setup");
+
+ info("Add the test tab, open the rule-view and select the test node");
+ await addTab(TEST_URI);
+
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("div", inspector);
+ const redColorProp = getTextProperty(view, 1, { color: "red" });
+ ok(redColorProp, "RuleView displays a color:red property");
+
+ // The "?update-stylesheet" call will change the CSS returned by sjs_imported_stylesheet_edit.sjs:
+ // - some rules are added before the matching `div {}` rule
+ // - the value of the `color` property changes
+ info("Call `?update-stylesheet` on the test sjs");
+ await fetch(SJS_URI + "?update-stylesheet");
+
+ info("Reload the page to restore the initial state");
+ await navigateTo(TEST_URI);
+
+ info("Wait until a rule is displayed at index 1");
+ await waitFor(() => view.element.children[1]);
+
+ info("Check that the displayed rule has been correctly updated.");
+ const goldColorProp = getTextProperty(view, 1, { color: "gold" });
+ ok(goldColorProp, "RuleView displays a color:gold property");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_inactive_css_display-justify.js b/devtools/client/inspector/rules/test/browser_rules_inactive_css_display-justify.js
new file mode 100644
index 0000000000..a196fcca40
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inactive_css_display-justify.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 a declaration's inactive state doesn't linger on its previous state when
+// the declaration it depends on changes. Bug 1593944
+
+const TEST_URI = `
+<style>
+ div {
+ justify-content: center;
+ /*! display: flex */
+ }
+</style>
+<div>`;
+
+add_task(async function () {
+ await pushPref("devtools.inspector.inactive.css.enabled", true);
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("div", inspector);
+
+ const justifyContent = { "justify-content": "center" };
+ const justifyItems = { "justify-items": "center" };
+ const displayFlex = { display: "flex" };
+ const displayGrid = { display: "grid" };
+
+ info("Enable display:flex and check that justify-content becomes active");
+ await checkDeclarationIsInactive(view, 1, justifyContent);
+ await toggleDeclaration(view, 1, displayFlex);
+ await checkDeclarationIsActive(view, 1, justifyContent);
+
+ info(
+ "Rename justify-content to justify-items and check that it becomes inactive"
+ );
+ await updateDeclaration(view, 1, justifyContent, justifyItems);
+ await checkDeclarationIsInactive(view, 1, justifyItems);
+
+ info(
+ "Rename display:flex to display:grid and check that justify-items becomes active"
+ );
+ await updateDeclaration(view, 1, displayFlex, displayGrid);
+ await checkDeclarationIsActive(view, 1, justifyItems);
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_inactive_css_flexbox.js b/devtools/client/inspector/rules/test/browser_rules_inactive_css_flexbox.js
new file mode 100644
index 0000000000..23f1b39845
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inactive_css_flexbox.js
@@ -0,0 +1,161 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test inactive flex properties.
+
+const TEST_URI = `
+<head>
+ <style>
+ #container {
+ width: 200px;
+ height: 100px;
+ border: 1px solid #000;
+ align-content: space-between;
+ order: 1;
+ }
+
+ .flex-item {
+ flex-basis: auto;
+ flex-grow: 1;
+ flex-shrink: 1;
+ flex-direction: row;
+ }
+
+ #self-aligned {
+ align-self: stretch;
+ }
+ </style>
+</head>
+<body>
+ <h1>browser_rules_inactive_css_flexbox.js</h1>
+ <div id="container" style="display:flex">
+ <div class="flex-item item-1" style="order:1">1</div>
+ <div class="flex-item item-2" style="order:2">2</div>
+ <div class="flex-item item-3" style="order:3">3</div>
+ </div>
+ <div id="self-aligned"></div>
+</body>`;
+
+const BEFORE = [
+ {
+ selector: "#self-aligned",
+ inactiveDeclarations: [
+ {
+ declaration: {
+ "align-self": "stretch",
+ },
+ ruleIndex: 1,
+ },
+ ],
+ },
+ {
+ selector: ".item-2",
+ activeDeclarations: [
+ {
+ declarations: {
+ order: "2",
+ },
+ ruleIndex: 0,
+ },
+ {
+ declarations: {
+ "flex-basis": "auto",
+ "flex-grow": "1",
+ "flex-shrink": "1",
+ },
+ ruleIndex: 1,
+ },
+ ],
+ inactiveDeclarations: [
+ {
+ declaration: {
+ "flex-direction": "row",
+ },
+ ruleIndex: 1,
+ },
+ ],
+ },
+ {
+ selector: "#container",
+ activeDeclarations: [
+ {
+ declarations: {
+ display: "flex",
+ },
+ ruleIndex: 0,
+ },
+ {
+ declarations: {
+ width: "200px",
+ height: "100px",
+ border: "1px solid #000",
+ "align-content": "space-between",
+ },
+ ruleIndex: 1,
+ },
+ ],
+ inactiveDeclarations: [
+ {
+ declaration: {
+ order: "1",
+ },
+ ruleIndex: 1,
+ },
+ ],
+ },
+];
+
+const AFTER = [
+ {
+ selector: ".item-2",
+ inactiveDeclarations: [
+ {
+ declaration: {
+ order: "2",
+ },
+ ruleIndex: 0,
+ },
+ {
+ declaration: {
+ "flex-basis": "auto",
+ },
+ ruleIndex: 1,
+ },
+ {
+ declaration: {
+ "flex-grow": "1",
+ },
+ ruleIndex: 1,
+ },
+ {
+ declaration: {
+ "flex-shrink": "1",
+ },
+ ruleIndex: 1,
+ },
+ {
+ declaration: {
+ "flex-direction": "row",
+ },
+ ruleIndex: 1,
+ },
+ ],
+ },
+];
+
+add_task(async function () {
+ await pushPref("devtools.inspector.inactive.css.enabled", true);
+
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await runInactiveCSSTests(view, inspector, BEFORE);
+
+ // Toggle `display:flex` to disabled.
+ await toggleDeclaration(view, 0, {
+ display: "flex",
+ });
+ await runInactiveCSSTests(view, inspector, AFTER);
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_inactive_css_grid.js b/devtools/client/inspector/rules/test/browser_rules_inactive_css_grid.js
new file mode 100644
index 0000000000..a0afad08f7
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inactive_css_grid.js
@@ -0,0 +1,267 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test inactive grid properties.
+
+const TEST_URI = `
+<head>
+ <style>
+ html {
+ grid-area: foo;
+ }
+ #container {
+ width: 200px;
+ height: 100px;
+ border: 1px solid #000;
+ column-gap: 10px;
+ row-gap: 10px;
+ align-self: start;
+ position: relative;
+ }
+
+ .item-1 {
+ grid-column-start: 1;
+ grid-column-end: 3;
+ grid-row-start: 1;
+ grid-row-end: auto;
+ flex-direction: row
+ }
+
+ #abspos {
+ position: absolute;
+ grid-column: 2;
+ }
+
+ #self-aligned {
+ align-self: stretch;
+ }
+ </style>
+</head>
+<body>
+ <h1>browser_rules_inactive_css_grid.js</h1>
+ <div id="container" style="display:grid">
+ <div class="grid-item item-1">1</div>
+ <div class="grid-item item-2">2</div>
+ <div class="grid-item item-3">3</div>
+ <div class="grid-item item-4">4</div>
+ <div class="grid-item item-5">
+ <div id="abspos">AbsPos item</div>
+ </div>
+ </div>
+ <div id="self-aligned"></div>
+</body>`;
+
+const BEFORE = [
+ {
+ // Check first that the getting grid-related data about the <html> node doesn't break.
+ // See bug 1576484.
+ selector: "html",
+ inactiveDeclarations: [
+ {
+ declaration: {
+ "grid-area": "foo",
+ },
+ ruleIndex: 1,
+ },
+ ],
+ },
+ {
+ selector: "#self-aligned",
+ inactiveDeclarations: [
+ {
+ declaration: {
+ "align-self": "stretch",
+ },
+ ruleIndex: 1,
+ },
+ ],
+ },
+ {
+ selector: ".item-1",
+ activeDeclarations: [
+ {
+ declarations: {
+ "grid-column-start": "1",
+ "grid-column-end": "3",
+ "grid-row-start": "1",
+ "grid-row-end": "auto",
+ },
+ ruleIndex: 1,
+ },
+ ],
+ inactiveDeclarations: [
+ {
+ declaration: {
+ "flex-direction": "row",
+ },
+ ruleIndex: 1,
+ },
+ ],
+ },
+ {
+ selector: "#abspos",
+ activeDeclarations: [
+ {
+ declarations: {
+ "grid-column": 2,
+ },
+ ruleIndex: 1,
+ },
+ ],
+ },
+ {
+ selector: "#container",
+ activeDeclarations: [
+ {
+ declarations: {
+ display: "grid",
+ },
+ ruleIndex: 0,
+ },
+ {
+ declarations: {
+ width: "200px",
+ height: "100px",
+ border: "1px solid #000",
+ "column-gap": "10px",
+ "row-gap": "10px",
+ },
+ ruleIndex: 1,
+ },
+ ],
+ inactiveDeclarations: [
+ {
+ declaration: {
+ "align-self": "start",
+ },
+ ruleIndex: 1,
+ },
+ ],
+ },
+];
+
+const AFTER = [
+ {
+ activeDeclarations: [
+ {
+ declarations: {
+ display: "grid",
+ },
+ ruleIndex: 0,
+ },
+ {
+ declarations: {
+ width: "200px",
+ height: "100px",
+ border: "1px solid #000",
+ },
+ ruleIndex: 1,
+ },
+ ],
+ inactiveDeclarations: [
+ {
+ declaration: {
+ "column-gap": "10px",
+ },
+ ruleIndex: 1,
+ },
+ {
+ declaration: {
+ "row-gap": "10px",
+ },
+ ruleIndex: 1,
+ },
+ {
+ declaration: {
+ "align-self": "start",
+ },
+ ruleIndex: 1,
+ },
+ ],
+ },
+ {
+ selector: "#abspos",
+ inactiveDeclarations: [
+ {
+ declaration: {
+ "grid-column": 2,
+ },
+ ruleIndex: 1,
+ },
+ ],
+ },
+];
+
+add_task(async function () {
+ await pushPref("devtools.inspector.inactive.css.enabled", true);
+
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await runInactiveCSSTests(view, inspector, BEFORE);
+
+ // Toggle `display:grid` to disabled.
+ await toggleDeclaration(view, 0, {
+ display: "grid",
+ });
+ await view.once("ruleview-refreshed");
+ await runInactiveCSSTests(view, inspector, AFTER);
+
+ info("Toggle `display: grid` to enabled again.");
+ await selectNode("#container", inspector);
+ await toggleDeclaration(view, 0, {
+ display: "grid",
+ });
+ await runAbsPosGridElementTests(view, inspector);
+});
+
+/**
+ * Tests for absolute positioned elements in a grid.
+ */
+async function runAbsPosGridElementTests(view, inspector) {
+ info("Toggling `position: relative` to disabled.");
+ await toggleDeclaration(view, 1, {
+ position: "relative",
+ });
+ await runInactiveCSSTests(view, inspector, [
+ {
+ selector: "#abspos",
+ inactiveDeclarations: [
+ {
+ declaration: {
+ "grid-column": 2,
+ },
+ ruleIndex: 1,
+ },
+ ],
+ },
+ ]);
+
+ info("Toggling `position: relative` back to enabled.");
+ await selectNode("#container", inspector);
+ await toggleDeclaration(view, 1, {
+ position: "relative",
+ });
+
+ info("Toggling `position: absolute` on grid element to disabled.");
+ await selectNode("#abspos", inspector);
+ await toggleDeclaration(view, 1, {
+ position: "absolute",
+ });
+
+ await runInactiveCSSTests(view, inspector, [
+ {
+ selector: "#abspos",
+ inactiveDeclarations: [
+ {
+ declaration: {
+ "grid-column": 2,
+ },
+ ruleIndex: 1,
+ },
+ ],
+ },
+ ]);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_inactive_css_inline.js b/devtools/client/inspector/rules/test/browser_rules_inactive_css_inline.js
new file mode 100644
index 0000000000..2670e50d10
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inactive_css_inline.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test css properties that are inactive on block-level elements.
+
+const TEST_URI = `
+<style>
+#block {
+ border: 1px solid #000;
+ vertical-align: sub;
+}
+td {
+ vertical-align: super;
+}
+#flex {
+ display: inline-flex;
+ vertical-align: text-bottom;
+}
+</style>
+<h1 style="vertical-align:text-bottom;">browser_rules_inactive_css_inline.js</h1>
+<div id="block">Block</div>
+<table>
+ <tr><td>A table cell</td></tr>
+</table>
+<div id="flex">Inline flex element</div>
+`;
+
+const TEST_DATA = [
+ {
+ selector: "h1",
+ inactiveDeclarations: [
+ {
+ declaration: { "vertical-align": "text-bottom" },
+ ruleIndex: 0,
+ },
+ ],
+ },
+ {
+ selector: "#block",
+ inactiveDeclarations: [
+ {
+ declaration: { "vertical-align": "sub" },
+ ruleIndex: 1,
+ },
+ ],
+ },
+ {
+ selector: "td",
+ activeDeclarations: [
+ {
+ declarations: { "vertical-align": "super" },
+ ruleIndex: 1,
+ },
+ ],
+ },
+ {
+ selector: "#flex",
+ activeDeclarations: [
+ {
+ declarations: { "vertical-align": "text-bottom" },
+ ruleIndex: 1,
+ },
+ ],
+ },
+];
+
+add_task(async function () {
+ await pushPref("devtools.inspector.inactive.css.enabled", true);
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await runInactiveCSSTests(view, inspector, TEST_DATA);
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_inactive_css_split-condition.js b/devtools/client/inspector/rules/test/browser_rules_inactive_css_split-condition.js
new file mode 100644
index 0000000000..d03bcd9e63
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inactive_css_split-condition.js
@@ -0,0 +1,31 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that a CSS property is marked as inactive when a condition
+// changes in other CSS rule matching the element.
+
+const TEST_URI = `
+<style>
+ .display {
+ display: grid;
+ }
+ .gap {
+ gap: 1em;
+ }
+</style>
+<div class="display gap">`;
+
+add_task(async function () {
+ await pushPref("devtools.inspector.inactive.css.enabled", true);
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("div", inspector);
+
+ await checkDeclarationIsActive(view, 1, { gap: "1em" });
+ await toggleDeclaration(view, 2, { display: "grid" });
+ await checkDeclarationIsInactive(view, 1, { gap: "1em" });
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_inactive_css_visited.js b/devtools/client/inspector/rules/test/browser_rules_inactive_css_visited.js
new file mode 100644
index 0000000000..6b452f23bc
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inactive_css_visited.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test css properties that are inactive in :visited rule.
+
+const TEST_URI = URL_ROOT + "doc_visited.html";
+
+const TEST_DATA = [
+ {
+ selector: "#visited",
+ inactiveDeclarations: [
+ {
+ declaration: { "font-size": "100px" },
+ ruleIndex: 2,
+ },
+ {
+ declaration: { "margin-left": "50px" },
+ ruleIndex: 2,
+ },
+ ],
+ activeDeclarations: [
+ {
+ declarations: {
+ "background-color": "transparent",
+ "border-color": "lime",
+ color: "rgba(0, 255, 0, 0.8)",
+ "text-decoration-color": "lime",
+ "text-emphasis-color": "seagreen",
+ },
+ ruleIndex: 2,
+ },
+ ],
+ },
+ {
+ selector: "#visited-and-other-matched-selector",
+ activeDeclarations: [
+ {
+ declarations: {
+ "background-color": "transparent",
+ "border-color": "lime",
+ color: "rgba(0, 255, 0, 0.8)",
+ "font-size": "100px",
+ "margin-left": "50px",
+ "text-decoration-color": "lime",
+ "text-emphasis-color": "seagreen",
+ },
+ ruleIndex: 1,
+ },
+ ],
+ },
+];
+
+add_task(async () => {
+ info("Open a url which has visited links");
+ const tab = await addTab(TEST_URI);
+
+ info("Wait until the visited links are available");
+ const selectors = TEST_DATA.map(t => t.selector);
+ await waitUntilVisitedState(tab, selectors);
+
+ info("Open the inspector");
+ const { inspector, view } = await openRuleView();
+
+ await runInactiveCSSTests(view, inspector, TEST_DATA);
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_inactive_css_xul.js b/devtools/client/inspector/rules/test/browser_rules_inactive_css_xul.js
new file mode 100644
index 0000000000..b9536f1f9e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inactive_css_xul.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test inactive css properties in XUL documents.
+
+const TEST_URI = URL_ROOT_SSL + "doc_inactive_css_xul.xhtml";
+
+const TEST_DATA = [
+ {
+ selector: "#test-img-in-xul",
+ inactiveDeclarations: [
+ {
+ declaration: { "grid-column-gap": "5px" },
+ ruleIndex: 0,
+ },
+ ],
+ activeDeclarations: [
+ {
+ declarations: {
+ width: "10px",
+ height: "10px",
+ },
+ ruleIndex: 0,
+ },
+ ],
+ },
+];
+
+add_task(async () => {
+ await SpecialPowers.pushPermissions([
+ { type: "allowXULXBL", allow: true, context: URL_ROOT_SSL },
+ ]);
+
+ info("Open a url to a XUL document");
+ await addTab(TEST_URI);
+
+ info("Open the inspector");
+ const { inspector, view } = await openRuleView();
+
+ await runInactiveCSSTests(view, inspector, TEST_DATA);
+});
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 0000000000..60e7966528
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_01.js
@@ -0,0 +1,53 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#test1", inspector);
+ await simpleInherit(inspector, view);
+});
+
+function simpleInherit(inspector, view) {
+ const elementStyle = view._elementStyle;
+ is(elementStyle.rules.length, 2, "Should have 2 rules.");
+
+ const elementRule = elementStyle.rules[0];
+ ok(
+ !elementRule.inherited,
+ "Element style attribute should not consider itself inherited."
+ );
+
+ const 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");
+ const bgcProp = inheritRule.textProps[0];
+ is(
+ bgcProp.name,
+ "background-color",
+ "background-color property should exist"
+ );
+ ok(bgcProp.invisible, "background-color property should be invisible");
+ const 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 0000000000..da489b45d1
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_02.js
@@ -0,0 +1,35 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#test1", inspector);
+ await emptyInherit(inspector, view);
+});
+
+function emptyInherit(inspector, view) {
+ // No inheritable styles, this rule shouldn't show up.
+ const elementStyle = view._elementStyle;
+ is(elementStyle.rules.length, 1, "Should have 1 rule.");
+
+ const 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 0000000000..995fd7f88d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_03.js
@@ -0,0 +1,49 @@
+/* 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 {
+ style: { ELEMENT_STYLE },
+} = require("resource://devtools/shared/constants.js");
+
+const TEST_URI = `
+ <div id="test2" style="color: red">
+ <div id="test1">Styled Node</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#test1", inspector);
+ await elementStyleInherit(inspector, view);
+});
+
+function elementStyleInherit(inspector, view) {
+ const elementStyle = view._elementStyle;
+ is(elementStyle.rules.length, 2, "Should have 2 rules.");
+
+ const elementRule = elementStyle.rules[0];
+ ok(
+ !elementRule.inherited,
+ "Element style attribute should not consider itself inherited."
+ );
+
+ const 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"
+ );
+ const inheritProp = inheritRule.textProps[0];
+ is(inheritProp.name, "color", "color should have been inherited.");
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_inherited-properties_04.js b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_04.js
new file mode 100644
index 0000000000..2416b01910
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_04.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that different inherited properties sections are created for rules
+// inherited from several elements of the same type.
+
+const TEST_URI = `
+ <div style="cursor:pointer">
+ A
+ <div style="cursor:pointer">
+ B<a>Cursor</a>
+ </div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("a", inspector);
+ await elementStyleInherit(inspector, view);
+});
+
+function elementStyleInherit(inspector, view) {
+ const gutters = view.element.querySelectorAll(".ruleview-header");
+ is(gutters.length, 2, "Gutters should contains 2 sections");
+ ok(gutters[0].textContent, "Inherited from div");
+ ok(gutters[1].textContent, "Inherited from div");
+}
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 0000000000..34a9943b56
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inline-source-map.js
@@ -0,0 +1,25 @@
+/* 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.source-map.client-service.enabled";
+
+add_task(async function () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ await addTab(TESTCASE_URI);
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("div", inspector);
+
+ const 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_inline-style-order.js b/devtools/client/inspector/rules/test/browser_rules_inline-style-order.js
new file mode 100644
index 0000000000..6018b04a85
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inline-style-order.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that when the order of properties in the inline style changes, the inline style
+// rule updates accordingly.
+// This can happen in cases such as this one:
+// Given this DOM node:
+// <div style="margin:0;color:red;"></div>
+// Executing this:
+// element.style.margin = "10px";
+// Will result in the following attribute value:
+// <div style="color: red; margin: 10px;"></div>
+// The inline style rule in the rule-view need to update to reflect the new order of
+// properties accordingly.
+// Note however that we do not want to expect a specific order in this test, and be
+// subject to failures if it changes again. Instead, the test compares the order against
+// what is in the style DOM attribute.
+// See bug 1467076.
+
+// Test cases, these are { name, value } objects used to change the DOM element's style
+// property. After each of these changes, the inline style rule's content will be checked
+// against the style DOM attribute's content.
+const TEST_CASES = [
+ { name: "margin", value: "10px" },
+ { name: "color", value: "blue" },
+ { name: "padding", value: "20px" },
+ { name: "margin", value: "0px" },
+ { name: "color", value: "black" },
+];
+
+add_task(async function () {
+ const { linkedBrowser: browser } = await addTab(
+ `data:text/html;charset=utf-8,<div style="margin:0;color:red;">Inspect me!</div>`
+ );
+
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ for (const { name, value } of TEST_CASES) {
+ info(`Setting style.${name} to ${value} on the test node`);
+
+ const onStyleMutation = waitForStyleModification(inspector);
+ const onRuleRefreshed = inspector.once("rule-view-refreshed");
+ await SpecialPowers.spawn(
+ browser,
+ [{ name, value }],
+ async function (change) {
+ content.document.querySelector("div").style[change.name] = change.value;
+ }
+ );
+ await Promise.all([onStyleMutation, onRuleRefreshed]);
+
+ info("Getting and parsing the content of the node's style attribute");
+ const markupContainer = inspector.markup.getContainer(
+ inspector.selection.nodeFront
+ );
+ const styleAttrValue =
+ markupContainer.elt.querySelector(".attr-value").textContent;
+ const parsedStyleAttr = styleAttrValue
+ .split(";")
+ .filter(v => v.trim())
+ .map(decl => {
+ const nameValue = decl.split(":").map(v => v.trim());
+ return { name: nameValue[0], value: nameValue[1] };
+ });
+
+ info("Checking the content of the rule-view");
+ const ruleEditor = getRuleViewRuleEditor(view, 0);
+ const propertiesEls = ruleEditor.propertyList.children;
+
+ parsedStyleAttr.forEach((expected, i) => {
+ is(
+ propertiesEls[i].querySelector(".ruleview-propertyname").textContent,
+ expected.name,
+ `Correct name found for property ${i}`
+ );
+ is(
+ propertiesEls[i].querySelector(".ruleview-propertyvalue").textContent,
+ expected.value,
+ `Correct value found for property ${i}`
+ );
+ });
+ }
+});
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 0000000000..0e99d11789
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_invalid-source-map.js
@@ -0,0 +1,44 @@
+/* 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.source-map.client-service.enabled";
+const CSS_LOC = "doc_invalid_sourcemap.css:1";
+
+add_task(async function () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ await addTab(TESTCASE_URI);
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("div", inspector);
+
+ const ruleEl = getRuleViewRule(view, "div");
+ ok(ruleEl, "The 'div' rule exists in the rule-view");
+
+ const prop = getRuleViewProperty(view, "div", "color");
+ ok(prop, "The 'color' property exists in this rule");
+
+ const value = getRuleViewPropertyValue(view, "div", "color");
+ is(value, "gold", "The 'color' property has the right value");
+
+ await verifyLinkText(view, CSS_LOC);
+
+ Services.prefs.clearUserPref(PREF);
+});
+
+function verifyLinkText(view, text) {
+ info("Verifying that the rule-view stylesheet link is " + text);
+ const 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 0000000000..f6776b47c4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_invalid.js
@@ -0,0 +1,32 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ const { inspector, view } = await openRuleView();
+ await 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 0000000000..ec4d1c5bf5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_keybindings.js
@@ -0,0 +1,48 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8,<h1>Some header text</h1>");
+ const { inspector, view } = await openRuleView();
+ await selectNode("h1", inspector);
+
+ info("Getting the ruleclose brace element");
+ const brace = view.styleDocument.querySelector(".ruleview-ruleclose");
+
+ info("Focus the new property editable field to create a color property");
+ const ruleEditor = getRuleViewRuleEditor(view, 0);
+ let editor = await 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");
+
+ await onFocus;
+ await 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");
+ await onFocus;
+ await 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 0000000000..98b0452a9b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_keyframeLineNumbers.js
@@ -0,0 +1,24 @@
+/* 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(async function () {
+ await addTab(TESTCASE_URI);
+ const { inspector, view } = await openRuleView();
+ await selectNode("#outer", inspector);
+
+ info("Insert a new property, which will affect the line numbers");
+ await addProperty(view, 1, "font-size", "72px");
+
+ await selectNode("#inner", inspector);
+
+ const 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-shadowdom.js b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule-shadowdom.js
new file mode 100644
index 0000000000..1200fa3ab0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule-shadowdom.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that keyframes are displayed for elements nested under a shadow-root.
+
+const TEST_URI = `data:text/html;charset=utf-8,
+ <div></div>
+ <script>
+ document.querySelector('div').attachShadow({mode: 'open'}).innerHTML = \`
+ <span>text</span>
+ <style>
+ @keyframes blink {
+ 0% {
+ border: rgba(255,0,0,1) 2px dashed;
+ }
+ 100% {
+ border: rgba(255,0,0,0) 2px dashed;
+ }
+ }
+ span {
+ animation: blink .5s 0s infinite;
+ }
+ </style>\`;
+ </script>`;
+
+add_task(async function () {
+ await addTab(TEST_URI);
+
+ const { inspector, view } = await openRuleView();
+
+ info("Expand the shadow-root parent");
+ const divFront = await getNodeFront("div", inspector);
+ await inspector.markup.expandNode(divFront);
+ await waitForMultipleChildrenUpdates(inspector);
+
+ const { markup } = inspector;
+ const divContainer = markup.getContainer(divFront);
+
+ info("Expand the shadow-root");
+ const shadowRootContainer = divContainer.getChildContainers()[0];
+ await expandContainer(inspector, shadowRootContainer);
+
+ info("Retrieve the rules displayed for the span under the shadow root");
+ const spanContainer = shadowRootContainer.getChildContainers()[0];
+ const rules = await getKeyframeRules(spanContainer.node, inspector, view);
+
+ is(
+ convertTextPropsToString(rules.keyframeRules[0].textProps),
+ "border: rgba(255,0,0,1) 2px dashed",
+ "Keyframe blink (0%) property is correct"
+ );
+
+ is(
+ convertTextPropsToString(rules.keyframeRules[1].textProps),
+ "border: rgba(255,0,0,0) 2px dashed",
+ "Keyframe blink (100%) property is correct"
+ );
+});
+
+function convertTextPropsToString(textProps) {
+ return textProps.map(t => t.name + ": " + t.value).join("; ");
+}
+
+async function getKeyframeRules(selector, inspector, view) {
+ await selectNode(selector, inspector);
+ const elementStyle = view._elementStyle;
+
+ const 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_keyframes-rule_01.js b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js
new file mode 100644
index 0000000000..a5e7cf720a
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js
@@ -0,0 +1,121 @@
+/* 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(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+ await testPacman(inspector, view);
+ await testBoxy(inspector, view);
+ await testMoxy(inspector, view);
+});
+
+async function testPacman(inspector, view) {
+ info("Test content and gutter in the keyframes rule of #pacman");
+
+ await assertKeyframeRules("#pacman", inspector, view, {
+ elementRulesNb: 2,
+ keyframeRulesNb: 2,
+ keyframesRules: ["pacman", "pacman"],
+ keyframeRules: ["100%", "100%"],
+ });
+
+ assertGutters(view, {
+ guttersNbs: 2,
+ gutterHeading: ["Keyframes pacman", "Keyframes pacman"],
+ });
+}
+
+async function testBoxy(inspector, view) {
+ info("Test content and gutter in the keyframes rule of #boxy");
+
+ await assertKeyframeRules("#boxy", inspector, view, {
+ elementRulesNb: 3,
+ keyframeRulesNb: 3,
+ keyframesRules: ["boxy", "boxy", "boxy"],
+ keyframeRules: ["10%", "20%", "100%"],
+ });
+
+ assertGutters(view, {
+ guttersNbs: 1,
+ gutterHeading: ["Keyframes boxy"],
+ });
+}
+
+async function testMoxy(inspector, view) {
+ info("Test content and gutter in the keyframes rule of #moxy");
+
+ await 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"],
+ });
+}
+
+async function assertKeyframeRules(selector, inspector, view, expected) {
+ await selectNode(selector, inspector);
+ const elementStyle = view._elementStyle;
+
+ const 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 (const 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) {
+ const gutters = view.element.querySelectorAll(".ruleview-header");
+
+ is(
+ gutters.length,
+ expected.guttersNbs,
+ "There are " + gutters.length + " gutter headings"
+ );
+
+ let i = 0;
+ for (const 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 0000000000..63e8ae4e65
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_02.js
@@ -0,0 +1,95 @@
+/* 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(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+ await testPacman(inspector, view);
+ await testBoxy(inspector, view);
+});
+
+async function testPacman(inspector, view) {
+ info("Test content in the keyframes rule of #pacman");
+
+ const rules = await 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.");
+}
+
+async function testBoxy(inspector, view) {
+ info("Test content in the keyframes rule of #boxy");
+
+ const rules = await 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("; ");
+}
+
+async function getKeyframeRules(selector, inspector, view) {
+ await selectNode(selector, inspector);
+ const elementStyle = view._elementStyle;
+
+ const 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_large_base64_background_image.js b/devtools/client/inspector/rules/test/browser_rules_large_base64_background_image.js
new file mode 100644
index 0000000000..eb4bd1e050
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_large_base64_background_image.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// 1px red dot
+const shortDataUrl =
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P4z8DwHwAFAAH/F1FwBgAAAABJRU5ErkJggg==";
+
+// Not a valid base64 image, but will still generate a long text property
+const longDataUrl = new Array(1000 * 1000).join("a");
+
+const TEST_STYLESHEET = `
+body {
+ background-image: url(data:image/png;base64,${shortDataUrl});
+ background-image: url(data:image/png;base64,${longDataUrl});
+}`;
+
+// Serve the stylesheet dynamically from a test HTTPServer to avoid logging an
+// extremely long data-url when adding the tab using our usual test helpers.
+const server = createTestHTTPServer();
+const filepath = "/style.css";
+const cssuri = `http://localhost:${server.identity.primaryPort}${filepath}`;
+server.registerContentType("css", "text/css");
+server.registerPathHandler(filepath, (metadata, response) => {
+ response.write(TEST_STYLESHEET);
+});
+
+const TEST_URL =
+ "data:text/html," +
+ encodeURIComponent(`
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <link href="${cssuri}" rel="stylesheet" />
+ </head>
+ <body></body>
+ </html>
+`);
+
+// Check that long URLs are rendered correctly in the rule view.
+add_task(async function () {
+ const { inspector } = await openInspectorForURL(TEST_URL);
+ const view = selectRuleView(inspector);
+
+ await selectNode("body", inspector);
+
+ const propertyValues = view.styleDocument.querySelectorAll(
+ ".ruleview-propertyvalue"
+ );
+
+ is(propertyValues.length, 2, "Ruleview has 2 propertyvalue elements");
+ ok(
+ propertyValues[0].textContent.startsWith("url(data:image/png"),
+ "Property value for the background image was correctly rendered"
+ );
+
+ ok(
+ !propertyValues[0].querySelector(".propertyvalue-long-text"),
+ "The first background-image is short enough and does not need to be truncated"
+ );
+ ok(
+ !!propertyValues[1].querySelector(".propertyvalue-long-text"),
+ "The second background-image has a special CSS class to be truncated"
+ );
+ const ruleviewContainer =
+ view.styleDocument.getElementById("ruleview-container");
+ ok(
+ ruleviewContainer.scrollHeight === ruleviewContainer.clientHeight,
+ "The ruleview container does not have a scrollbar"
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_layer.js b/devtools/client/inspector/rules/test/browser_rules_layer.js
new file mode 100644
index 0000000000..9db3577f20
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_layer.js
@@ -0,0 +1,107 @@
+/* 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 when the page defines layers.
+
+const TEST_URI = `
+ <style type="text/css">
+ @import url(${URL_ROOT_COM_SSL}doc_imported_anonymous_layer.css) layer;
+ @import url(${URL_ROOT_COM_SSL}doc_imported_named_layer.css) layer(importedLayer);
+ @import url(${URL_ROOT_COM_SSL}doc_imported_no_layer.css);
+
+ @layer myLayer {
+ h1, [test-hint=named-layer] {
+ background-color: tomato;
+ color: lightgreen;
+ }
+ }
+
+ @layer {
+ h1, [test-hint=anonymous-layer] {
+ color: green;
+ font-variant: small-caps
+ }
+ }
+
+ h1, [test-hint=no-rule-layer] {
+ color: pink;
+ }
+ </style>
+ <h1>Hello @layer!</h1>
+`;
+
+add_task(async function () {
+ await addTab(
+ "https://example.com/document-builder.sjs?html=" +
+ encodeURIComponent(TEST_URI)
+ );
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("h1", inspector);
+
+ const expectedRules = [
+ { selector: "element", ancestorRulesData: null },
+ { selector: `h1, [test-hint="no-rule-layer"]`, ancestorRulesData: null },
+ {
+ selector: `h1, [test-hint="imported-no-layer--no-rule-layer"]`,
+ ancestorRulesData: null,
+ },
+ {
+ selector: `h1, [test-hint="anonymous-layer"]`,
+ ancestorRulesData: ["@layer"],
+ },
+ {
+ selector: `h1, [test-hint="named-layer"]`,
+ ancestorRulesData: ["@layer myLayer"],
+ },
+ {
+ selector: `h1, [test-hint="imported-named-layer--no-rule-layer"]`,
+ ancestorRulesData: ["@layer importedLayer", "@media screen"],
+ },
+ {
+ selector: `h1, [test-hint="imported-named-layer--named-layer"]`,
+ ancestorRulesData: [
+ "@layer importedLayer",
+ "@media screen",
+ "@layer in-imported-stylesheet",
+ ],
+ },
+ {
+ selector: `h1, [test-hint="imported-anonymous-layer--no-rule-layer"]`,
+ ancestorRulesData: ["@layer"],
+ },
+ ];
+
+ const rulesInView = Array.from(view.element.children);
+ is(
+ rulesInView.length,
+ expectedRules.length,
+ "All expected rules are displayed"
+ );
+
+ for (let i = 0; i < expectedRules.length; i++) {
+ const expectedRule = expectedRules[i];
+ info(`Checking rule #${i}: ${expectedRule.selector}`);
+
+ const selector = rulesInView[i].querySelector(
+ ".ruleview-selectorcontainer"
+ ).innerText;
+ is(selector, expectedRule.selector, `Expected selector for ${selector}`);
+
+ if (expectedRule.ancestorRulesData == null) {
+ is(
+ getRuleViewAncestorRulesDataElementByIndex(view, i),
+ null,
+ `No ancestor rules data displayed for ${selector}`
+ );
+ } else {
+ is(
+ getRuleViewAncestorRulesDataTextByIndex(view, i),
+ expectedRule.ancestorRulesData.join("\n"),
+ `Expected ancestor rules data displayed for ${selector}`
+ );
+ }
+ }
+});
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 0000000000..9c9d1ef9ab
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_lineNumbers.js
@@ -0,0 +1,31 @@
+/* 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(async function () {
+ await addTab(TESTCASE_URI);
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ const bodyRuleEditor = getRuleViewRuleEditor(view, 3);
+ const value = getRuleViewLinkTextByIndex(view, 2);
+ // Note that this is relative to the <style>.
+ is(value.slice(-2), ":6", "initial rule line number is 6");
+
+ const onLocationChanged = once(
+ bodyRuleEditor.rule.domRule,
+ "location-changed"
+ );
+ await addProperty(view, 1, "font-size", "23px");
+ await onLocationChanged;
+
+ const 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_linear-easing-swatch.js b/devtools/client/inspector/rules/test/browser_rules_linear-easing-swatch.js
new file mode 100644
index 0000000000..3739e6ff9c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_linear-easing-swatch.js
@@ -0,0 +1,487 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that linear easing widget pickers appear when clicking on linear
+// swatches.
+
+const TEST_URI = `
+ <style type="text/css">
+ div {
+ animation: move 3s linear(0, 0.2, 1);
+ transition: top 4s linear(0 10%, 0.5 20% 80%, 0 90%);
+ }
+ .test {
+ animation-timing-function: linear(0, 1 50% 100%);
+ transition-timing-function: linear(1 -10%, 0, -1 110%);
+ }
+ </style>
+ <div class="test">Testing the linear easing tooltip!</div>
+`;
+
+add_task(async function testSwatches() {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ const tooltip = view.tooltips.getTooltip("linearEaseFunction");
+ ok(tooltip, "The rule-view has the expected linear tooltip");
+ const panel = tooltip.tooltip.panel;
+ ok(panel, "The XUL panel for the linear tooltip exists");
+
+ const expectedData = [
+ {
+ selector: "div",
+ property: "animation",
+ expectedPoints: [
+ [0, 0],
+ [0.5, 0.2],
+ [1, 1],
+ ],
+ },
+ {
+ selector: "div",
+ property: "transition",
+ expectedPoints: [
+ [0.1, 0],
+ [0.2, 0.5],
+ [0.8, 0.5],
+ [0.9, 0],
+ ],
+ },
+ {
+ selector: ".test",
+ property: "animation-timing-function",
+ expectedPoints: [
+ [0, 0],
+ [0.5, 1],
+ [1, 1],
+ ],
+ },
+ {
+ selector: ".test",
+ property: "transition-timing-function",
+ expectedPoints: [
+ [-0.1, 1],
+ [0.5, 0],
+ [1.1, -1],
+ ],
+ },
+ ];
+
+ for (const { selector, property, expectedPoints } of expectedData) {
+ const messagePrefix = `[${selector}|${property}]`;
+ const swatch = getRuleViewLinearEasingSwatch(view, selector, property);
+ ok(swatch, `${messagePrefix} the swatch exists`);
+
+ const onWidgetReady = tooltip.once("ready");
+ swatch.click();
+ await onWidgetReady;
+ ok(true, `${messagePrefix} clicking the swatch displayed the tooltip`);
+
+ ok(
+ !inplaceEditor(swatch.parentNode),
+ `${messagePrefix} inplace editor wasn't shown`
+ );
+
+ checkChartState(panel, expectedPoints);
+
+ await hideTooltipAndWaitForRuleViewChanged(tooltip, view);
+ }
+});
+
+add_task(async function testChart() {
+ await pushPref("ui.prefersReducedMotion", 0);
+
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ const tooltip = view.tooltips.getTooltip("linearEaseFunction");
+ ok(tooltip, "The rule-view has the expected linear tooltip");
+ const panel = tooltip.tooltip.panel;
+ ok(panel, "The XUL panel for the linear tooltip exists");
+
+ const selector = ".test";
+ const property = "animation-timing-function";
+
+ const swatch = getRuleViewLinearEasingSwatch(view, selector, property);
+ const onWidgetReady = tooltip.once("ready");
+ swatch.click();
+ await onWidgetReady;
+ const widget = await tooltip.widget;
+
+ const svgEl = panel.querySelector(`svg.chart`);
+ const svgRect = svgEl.getBoundingClientRect();
+
+ checkChartState(
+ panel,
+ [
+ [0, 0],
+ [0.5, 1],
+ [1, 1],
+ ],
+ "testChart - initial state:"
+ );
+
+ info("Check that double clicking a point removes it");
+ const middlePoint = panel.querySelector(
+ `svg.chart .control-points-group .control-point:nth-of-type(2)`
+ );
+ let onWidgetUpdated = widget.once("updated");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.sendMouseEvent(
+ { type: "dblclick" },
+ middlePoint,
+ widget.parent.ownerGlobal
+ );
+
+ let newValue = await onWidgetUpdated;
+ is(newValue, `linear(0 0%, 1 100%)`);
+ checkChartState(
+ panel,
+ [
+ [0, 0],
+ [1, 1],
+ ],
+ "testChart - after point removed:"
+ );
+ await onRuleViewChanged;
+ await checkRuleView(view, selector, property, newValue);
+
+ info(
+ "Check that double clicking a point when there are only 2 points on the line does not remove it"
+ );
+ const timeoutRes = Symbol();
+ let onTimeout = wait(1000).then(() => timeoutRes);
+ onWidgetUpdated = widget.once("updated");
+ EventUtils.sendMouseEvent(
+ { type: "dblclick" },
+ panel.querySelector(`svg.chart .control-points-group .control-point`),
+ widget.parent.ownerGlobal
+ );
+ let raceWinner = await Promise.race([onWidgetUpdated, onTimeout]);
+ is(
+ raceWinner,
+ timeoutRes,
+ "The widget wasn't updated after double clicking one of the 2 last points"
+ );
+ checkChartState(
+ panel,
+ [
+ [0, 0],
+ [1, 1],
+ ],
+ "testChart - no point removed:"
+ );
+
+ info("Check that double clicking on the svg does add a point");
+ onWidgetUpdated = widget.once("updated");
+ onRuleViewChanged = view.once("ruleview-changed");
+ // Clicking on svg center with shiftKey so it snaps to the grid
+ EventUtils.synthesizeMouseAtCenter(
+ panel.querySelector(`svg.chart`),
+ { clickCount: 2, shiftKey: true },
+ widget.parent.ownerGlobal
+ );
+
+ newValue = await onWidgetUpdated;
+ is(
+ newValue,
+ `linear(0 0%, 0.5 50%, 1 100%)`,
+ "Widget was updated with expected value"
+ );
+ checkChartState(
+ panel,
+ [
+ [0, 0],
+ [0.5, 0.5],
+ [1, 1],
+ ],
+ "testChart - new point added"
+ );
+ await onRuleViewChanged;
+ await checkRuleView(view, selector, property, newValue);
+
+ info("Check that points can be moved");
+ onWidgetUpdated = widget.once("updated");
+ onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeMouseAtCenter(
+ panel.querySelector(`svg.chart .control-points-group .control-point`),
+ { type: "mousedown" },
+ widget.parent.ownerGlobal
+ );
+
+ EventUtils.synthesizeMouse(
+ svgEl,
+ svgRect.width / 3,
+ svgRect.height / 3,
+ { type: "mousemove", shiftKey: true },
+ widget.parent.ownerGlobal
+ );
+ newValue = await onWidgetUpdated;
+ is(
+ newValue,
+ `linear(0.7 30%, 0.5 50%, 1 100%)`,
+ "Widget was updated with expected value"
+ );
+ checkChartState(
+ panel,
+ [
+ [0.3, 0.7],
+ [0.5, 0.5],
+ [1, 1],
+ ],
+ "testChart - point moved"
+ );
+ await onRuleViewChanged;
+ await checkRuleView(view, selector, property, newValue);
+
+ info("Check that the points can be moved past the next point");
+ onWidgetUpdated = widget.once("updated");
+ onRuleViewChanged = view.once("ruleview-changed");
+
+ // the second point is at 50%, so simulate a mousemove all the way to the right (which
+ // should be ~100%)
+ EventUtils.synthesizeMouse(
+ svgEl,
+ svgRect.width,
+ svgRect.height / 3,
+ { type: "mousemove", shiftKey: true },
+ widget.parent.ownerGlobal
+ );
+ newValue = await onWidgetUpdated;
+ is(
+ newValue,
+ `linear(0.7 50%, 0.5 50%, 1 100%)`,
+ "point wasn't moved past the next point (50%)"
+ );
+ checkChartState(
+ panel,
+ [
+ [0.5, 0.7],
+ [0.5, 0.5],
+ [1, 1],
+ ],
+ "testChart - point moved constrained by next point"
+ );
+ await onRuleViewChanged;
+ await checkRuleView(view, selector, property, newValue);
+
+ info("Stop dragging");
+ EventUtils.synthesizeMouseAtCenter(
+ svgEl,
+ { type: "mouseup" },
+ widget.parent.ownerGlobal
+ );
+
+ onTimeout = wait(1000).then(() => timeoutRes);
+ onWidgetUpdated = widget.once("updated");
+ EventUtils.synthesizeMouseAtCenter(
+ svgEl,
+ { type: "mousemove" },
+ widget.parent.ownerGlobal
+ );
+ raceWinner = await Promise.race([onWidgetUpdated, onTimeout]);
+ is(raceWinner, timeoutRes, "Dragging is disabled after mouseup");
+
+ info("Add another point, which should be the first one for the line");
+ onWidgetUpdated = widget.once("updated");
+ onRuleViewChanged = view.once("ruleview-changed");
+
+ EventUtils.synthesizeMouse(
+ svgEl,
+ svgRect.width / 3,
+ svgRect.height - 1,
+ { clickCount: 2, shiftKey: true },
+ widget.parent.ownerGlobal
+ );
+
+ newValue = await onWidgetUpdated;
+ is(
+ newValue,
+ `linear(0 30%, 0.7 50%, 0.5 50%, 1 100%)`,
+ "Widget was updated with expected value"
+ );
+ checkChartState(
+ panel,
+ [
+ [0.3, 0],
+ [0.5, 0.7],
+ [0.5, 0.5],
+ [1, 1],
+ ],
+ "testChart - point added at beginning"
+ );
+ await onRuleViewChanged;
+ await checkRuleView(view, selector, property, newValue);
+
+ info("Check that the points can't be moved past previous point");
+ onWidgetUpdated = widget.once("updated");
+ onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.synthesizeMouseAtCenter(
+ panel.querySelector(
+ `svg.chart .control-points-group .control-point:nth-of-type(2)`
+ ),
+ { type: "mousedown" },
+ widget.parent.ownerGlobal
+ );
+
+ EventUtils.synthesizeMouse(
+ svgEl,
+ 0,
+ svgRect.height / 3,
+ { type: "mousemove", shiftKey: true },
+ widget.parent.ownerGlobal
+ );
+ newValue = await onWidgetUpdated;
+ is(
+ newValue,
+ `linear(0 30%, 0.7 30%, 0.5 50%, 1 100%)`,
+ "point wasn't moved past previous point (30%)"
+ );
+ checkChartState(
+ panel,
+ [
+ [0.3, 0],
+ [0.3, 0.7],
+ [0.5, 0.5],
+ [1, 1],
+ ],
+ "testChart - point moved constrained by previous point"
+ );
+ await onRuleViewChanged;
+ await checkRuleView(view, selector, property, newValue);
+
+ info("Stop dragging");
+ EventUtils.synthesizeMouseAtCenter(
+ svgEl,
+ { type: "mouseup" },
+ widget.parent.ownerGlobal
+ );
+
+ info(
+ "Check that timing preview is destroyed if prefers-reduced-motion gets enabled"
+ );
+ const getTimingFunctionPreview = () =>
+ panel.querySelector(".timing-function-preview");
+ ok(getTimingFunctionPreview(), "By default, timing preview is visible");
+ info("Enable prefersReducedMotion");
+ await pushPref("ui.prefersReducedMotion", 1);
+ await waitFor(() => !getTimingFunctionPreview());
+ ok(true, "timing preview was removed after enabling prefersReducedMotion");
+
+ info("Disable prefersReducedMotion");
+ await pushPref("ui.prefersReducedMotion", 0);
+ await waitFor(() => getTimingFunctionPreview());
+ ok(
+ true,
+ "timing preview was added back after disabling prefersReducedMotion"
+ );
+
+ info("Hide tooltip with escape to cancel modification");
+ const onHidden = tooltip.tooltip.once("hidden");
+ const onModifications = view.once("ruleview-changed");
+ focusAndSendKey(widget.parent.ownerDocument.defaultView, "ESCAPE");
+ await onHidden;
+ await onModifications;
+
+ await checkRuleView(
+ view,
+ selector,
+ property,
+ "linear(0, 1 50% 100%)",
+ "linear(0 0%, 1 50%, 1 100%)"
+ );
+});
+
+/**
+ * Check that the svg chart line and control points are placed where we expect them.
+ *
+ * @param {ToolipPanel} panel
+ * @param {Array<Array<Number>>} expectedPoints: Array of coordinated
+ * @param {String} messagePrefix
+ */
+function checkChartState(panel, expectedPoints, messagePrefix = "") {
+ const svgLine = panel.querySelector("svg.chart .chart-linear");
+ is(
+ svgLine.getAttribute("points"),
+ expectedPoints.map(([x, y]) => `${x},${1 - y}`).join(" "),
+ `${messagePrefix} line has the expected points`
+ );
+
+ const controlPoints = panel.querySelectorAll(
+ `svg.chart .control-points-group .control-point`
+ );
+
+ is(
+ controlPoints.length,
+ expectedPoints.length,
+ `${messagePrefix} the expected number of control points were created`
+ );
+ controlPoints.forEach((controlPoint, i) => {
+ is(
+ parseFloat(controlPoint.getAttribute("cx")),
+ expectedPoints[i][0],
+ `${messagePrefix} Control point ${i} has correct cx`
+ );
+ is(
+ parseFloat(controlPoint.getAttribute("cy")),
+ // XXX work around floating point issues
+ Math.round((1 - expectedPoints[i][1]) * 10) / 10,
+ `${messagePrefix} Control point ${i} has correct cy`
+ );
+ });
+}
+
+/**
+ * Checks if the property in the rule view has the expected state
+ *
+ * @param {RuleView} view
+ * @param {String} selector
+ * @param {String} property
+ * @param {String} expectedLinearValue: Expected value in the rule view
+ * @param {String} expectedComputedLinearValue: Expected computed value. Defaults to expectedLinearValue.
+ * @returns {Element|null}
+ */
+async function checkRuleView(
+ view,
+ selector,
+ property,
+ expectedLinearValue,
+ expectedComputedLinearValue = expectedLinearValue
+) {
+ await waitForComputedStyleProperty(
+ selector,
+ null,
+ property,
+ expectedComputedLinearValue
+ );
+
+ is(
+ getRuleViewProperty(view, selector, property).valueSpan.textContent,
+ expectedLinearValue,
+ `${selector} ${property} has expected value`
+ );
+ const swatch = getRuleViewLinearEasingSwatch(view, selector, property);
+ is(
+ swatch.getAttribute("data-linear"),
+ expectedLinearValue,
+ `${selector} ${property} swatch has expected "data-linear" attribute`
+ );
+}
+
+/**
+ * Returns the linear easing swatch for a rule (defined by its selector), and a property.
+ *
+ * @param {RuleView} view
+ * @param {String} selector
+ * @param {String} property
+ * @returns {Element|null}
+ */
+function getRuleViewLinearEasingSwatch(view, selector, property) {
+ return getRuleViewProperty(view, selector, property).valueSpan.querySelector(
+ ".ruleview-lineareasingswatch"
+ );
+}
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 0000000000..38ad5ee1d0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_livepreview.js
@@ -0,0 +1,76 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ for (const data of TEST_DATA) {
+ await testLivePreviewData(data, view, "#testid");
+ }
+});
+
+async function testLivePreviewData(data, ruleView, selector) {
+ const rule = getRuleViewRuleEditor(ruleView, 1).rule;
+ const propEditor = rule.textProps[0].editor;
+
+ info("Focusing the property value inplace-editor");
+ const editor = await focusEditableField(ruleView, propEditor.valueSpan);
+ is(
+ inplaceEditor(propEditor.valueSpan),
+ editor,
+ "The focused editor is the value"
+ );
+
+ info("Entering value in the editor: " + data.value);
+ const onPreviewDone = ruleView.once("ruleview-changed");
+ EventUtils.sendString(data.value, ruleView.styleWindow);
+ ruleView.debounce.flush();
+ await onPreviewDone;
+
+ const onValueDone = ruleView.once("ruleview-changed");
+ if (data.escape) {
+ EventUtils.synthesizeKey("KEY_Escape");
+ } else {
+ EventUtils.synthesizeKey("KEY_Enter");
+ }
+ await onValueDone;
+
+ // While the editor is still focused in, the display should have
+ // changed already
+ is(
+ await 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 0000000000..4ac9ea3498
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_01.js
@@ -0,0 +1,57 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ const idProp = getTextProperty(view, 1, { "background-color": "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"
+ );
+
+ const classProp = getTextProperty(view, 2, { "background-color": "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.
+ const elementProp = await 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 0000000000..283419def9
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_02.js
@@ -0,0 +1,48 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testMarkOverridden(inspector, view);
+});
+
+function testMarkOverridden(inspector, view) {
+ const elementStyle = view._elementStyle;
+
+ const classRule = elementStyle.rules[2];
+ const classProp = classRule.textProps[0];
+ ok(
+ !classProp.overridden,
+ "Class prop shouldn't be overridden, some props are still being used."
+ );
+
+ for (const 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 0000000000..9acb4ae8cd
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_03.js
@@ -0,0 +1,33 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ const idProp = getTextProperty(view, 1, { "background-color": "blue" });
+ ok(idProp.overridden, "Not-important rule should be overridden.");
+
+ const classProp = getTextProperty(view, 2, { "background-color": "green" });
+ ok(!classProp.overridden, "Important rule should not be overridden.");
+
+ ok(idProp.overridden, "ID 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 0000000000..5a3de5b3fa
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_04.js
@@ -0,0 +1,34 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ const idProp = getTextProperty(view, 1, { "background-color": "blue" });
+
+ await togglePropStatus(view, idProp);
+ const classProp = getTextProperty(view, 2, { "background-color": "green" });
+ 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 0000000000..892ebaa955
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_05.js
@@ -0,0 +1,30 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ await addProperty(view, 1, "background-color", "red");
+
+ const firstProp = getTextProperty(view, 1, { "background-color": "green" });
+ const secondProp = getTextProperty(view, 1, { "background-color": "red" });
+
+ 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 0000000000..7ae4e77a02
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_06.js
@@ -0,0 +1,65 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testMarkOverridden(inspector, view);
+});
+
+async function testMarkOverridden(inspector, view) {
+ const elementStyle = view._elementStyle;
+ const rule = elementStyle.rules[1];
+ checkProperties(rule);
+
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+ info("Focusing an existing selector name in the rule-view");
+ const editor = await focusEditableField(view, ruleEditor.selectorText);
+
+ info("Entering a new selector name and committing");
+ editor.input.value = "div[class]";
+
+ const onRuleViewChanged = once(view, "ruleview-changed");
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await 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 0000000000..bdfe34a307
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_07.js
@@ -0,0 +1,93 @@
+/* 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 {
+ color: blue;
+ }
+
+ body {
+ margin-right: 1px !important;
+ font-size: 79px;
+ line-height: 100px !important;
+ color: green !important;
+ }
+
+ body {
+ color: red;
+ }
+
+ span {
+ font-size: 12px;
+ line-height: 10px;
+ }
+ </style>
+ <body>
+ <span>
+ <div id='testid' class='testclass'>Styled Node</div>
+ </span>
+ </body>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ testMarkOverridden(inspector, view);
+});
+
+function testMarkOverridden(inspector, view) {
+ const elementStyle = view._elementStyle;
+
+ const 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: "line-height", value: "10px", overridden: false },
+ ],
+ [{ name: "color", value: "red", overridden: true }],
+ [
+ { name: "margin-right", value: "1px", overridden: true },
+ { name: "font-size", value: "79px", overridden: true },
+ { name: "line-height", value: "100px", overridden: true },
+ { name: "color", value: "green", overridden: false },
+ ],
+ [{ name: "color", value: "blue", overridden: true }],
+ ];
+
+ for (let i = 1; i < RESULTS.length; ++i) {
+ const idRule = elementStyle.rules[i];
+
+ for (const propIndex in RESULTS[i]) {
+ const expected = RESULTS[i][propIndex];
+ const 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_mark_overridden_08.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_08.js
new file mode 100644
index 0000000000..444c87cbd7
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_08.js
@@ -0,0 +1,51 @@
+/* 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 in pseudo-elements when
+// selecting their host node.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #testid::before {
+ content: 'Pseudo-element';
+ color: red;
+ color: green;
+ }
+ #testid {
+ color: blue;
+ }
+ </style>
+ <div id='testid'>Styled Node</div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ info(
+ "Check the CSS declarations for ::before in the Pseudo-elements accordion."
+ );
+ const pseudoRule = getRuleViewRuleEditor(view, 1, 0).rule;
+ const pseudoProp1 = pseudoRule.textProps[1];
+ const pseudoProp2 = pseudoRule.textProps[2];
+ ok(
+ pseudoProp1.overridden,
+ "First declaration of color in pseudo-element should be overridden."
+ );
+ ok(
+ !pseudoProp2.overridden,
+ "Second declaration of color in pseudo-element should not be overridden."
+ );
+
+ info(
+ "Check that pseudo-element declarations do not override the host's declarations"
+ );
+ const idProp = getTextProperty(view, 4, { color: "blue" });
+ ok(
+ !idProp.overridden,
+ "The single declaration of color in ID selector should not be 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 0000000000..275426b105
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_mathml-element.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 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ info("Select the DIV node and verify the rule-view shows rules");
+ await 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");
+ await selectNode("math", inspector);
+ ok(
+ !view.element.querySelectorAll(".ruleview-rule").length,
+ "The rule-view is empty for the math element"
+ );
+
+ await selectNode("msubsup", inspector);
+ ok(
+ !view.element.querySelectorAll(".ruleview-rule").length,
+ "The rule-view is empty for the msubsup element"
+ );
+
+ await 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");
+ await 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 0000000000..09be8da801
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_media-queries.js
@@ -0,0 +1,35 @@
+/* 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 information in the rule view.
+
+const TEST_URI = URL_ROOT + "doc_media_queries.html?constructed";
+
+add_task(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ const elementStyle = view._elementStyle;
+
+ const inline = STYLE_INSPECTOR_L10N.getStr("rule.sourceInline");
+ const constructed = STYLE_INSPECTOR_L10N.getStr("rule.sourceConstructed");
+
+ is(elementStyle.rules.length, 4, "Should have 4 rules.");
+ is(elementStyle.rules[0].title, inline, "check rule 0 title");
+ is(
+ elementStyle.rules[1].title,
+ constructed,
+ "check constracted sheet rule title"
+ );
+ is(elementStyle.rules[2].title, inline + ":9", "check rule 2 title");
+ is(elementStyle.rules[3].title, inline + ":2", "check rule 3 title");
+
+ is(
+ getRuleViewAncestorRulesDataTextByIndex(view, 2),
+ "@media screen and (min-width: 1px)",
+ "Media queries information are displayed"
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_media-queries_reload.js b/devtools/client/inspector/rules/test/browser_rules_media-queries_reload.js
new file mode 100644
index 0000000000..79bc9b9f8f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_media-queries_reload.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that applicable media queries are updated in the Rule view after reloading
+// the page and resizing the window.
+
+const TEST_URI = `
+ <style type='text/css'>
+ @media all and (max-width: 500px) {
+ div {
+ color: red;
+ }
+ }
+ @media all and (min-width: 500px) {
+ div {
+ color: green;
+ }
+ }
+ </style>
+ <div></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view: ruleView, toolbox } = await openRuleView();
+ const hostWindow = toolbox.win.parent;
+
+ const originalWidth = hostWindow.outerWidth;
+ const originalHeight = hostWindow.outerHeight;
+
+ await selectNode("div", inspector);
+
+ info("Resize window so the media query for small viewports applies");
+ hostWindow.resizeTo(400, 400);
+
+ await waitForMediaRuleColor(ruleView, "red");
+ ok(true, "Small viewport media query inspected");
+
+ info("Reload the current page");
+ await reloadBrowser();
+ await selectNode("div", inspector);
+
+ info("Resize window so the media query for large viewports applies");
+ hostWindow.resizeTo(800, 800);
+
+ info("Reselect the rule after page reload.");
+ await waitForMediaRuleColor(ruleView, "green");
+ ok(true, "Large viewport media query inspected");
+
+ info("Resize window to original dimentions");
+ const onResize = once(hostWindow, "resize");
+ hostWindow.resizeTo(originalWidth, originalHeight);
+ await onResize;
+});
+
+function waitForMediaRuleColor(ruleView, color) {
+ return waitUntil(() => {
+ try {
+ const { value } = getTextProperty(ruleView, 1, { color });
+ return value === color;
+ } catch (e) {
+ return false;
+ }
+ });
+}
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 0000000000..2eb93e4b70
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-duplicates.js
@@ -0,0 +1,117 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ const 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).
+ const onMutation = inspector.once("markupmutation");
+ const onRuleViewChanged = view.once("ruleview-changed");
+ await createNewRuleViewProperty(
+ ruleEditor,
+ "color:red;color:orange;color:yellow;color:green;color:blue;color:indigo;" +
+ "color:violet;"
+ );
+ await onMutation;
+ await 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(
+ getTextProperty(view, 0, { color: "red" }).name,
+ "color",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { color: "red" }).value,
+ "red",
+ "Should have correct property value"
+ );
+
+ is(
+ getTextProperty(view, 0, { color: "orange" }).name,
+ "color",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { color: "orange" }).value,
+ "orange",
+ "Should have correct property value"
+ );
+
+ is(
+ getTextProperty(view, 0, { color: "yellow" }).name,
+ "color",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { color: "yellow" }).value,
+ "yellow",
+ "Should have correct property value"
+ );
+
+ is(
+ getTextProperty(view, 0, { color: "green" }).name,
+ "color",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { color: "green" }).value,
+ "green",
+ "Should have correct property value"
+ );
+
+ is(
+ getTextProperty(view, 0, { color: "blue" }).name,
+ "color",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { color: "blue" }).value,
+ "blue",
+ "Should have correct property value"
+ );
+
+ is(
+ getTextProperty(view, 0, { color: "indigo" }).name,
+ "color",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { color: "indigo" }).value,
+ "indigo",
+ "Should have correct property value"
+ );
+
+ is(
+ getTextProperty(view, 0, { color: "violet" }).name,
+ "color",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { color: "violet" }).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 0000000000..f7713529b5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-priority.js
@@ -0,0 +1,72 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ const 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).
+ const onMutation = inspector.once("markupmutation");
+ const onRuleViewChanged = view.once("ruleview-changed");
+ await createNewRuleViewProperty(
+ ruleEditor,
+ "color:red;width:100px;height: 100px;"
+ );
+ await onMutation;
+ await 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(
+ getTextProperty(view, 0, { color: "red" }).name,
+ "color",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { color: "red" }).value,
+ "red",
+ "Should have correct property value"
+ );
+
+ is(
+ getTextProperty(view, 0, { width: "100px" }).name,
+ "width",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { width: "100px" }).value,
+ "100px",
+ "Should have correct property value"
+ );
+
+ is(
+ getTextProperty(view, 0, { height: "100px" }).name,
+ "height",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { height: "100px" }).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 0000000000..e451950aba
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_01.js
@@ -0,0 +1,99 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+ await testCreateNewMultiUnfinished(inspector, view);
+});
+
+async function testCreateNewMultiUnfinished(inspector, view) {
+ const ruleEditor = getRuleViewRuleEditor(view, 0);
+ const onMutation = inspector.once("markupmutation");
+ let onRuleViewChanged = view.once("ruleview-changed");
+ await createNewRuleViewProperty(
+ ruleEditor,
+ "color:blue;background : orange ; text-align:center; border-color: "
+ );
+ await onMutation;
+ await 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);
+ await 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(
+ getTextProperty(view, 0, { color: "blue" }).name,
+ "color",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { color: "blue" }).value,
+ "blue",
+ "Should have correct property value"
+ );
+
+ is(
+ getTextProperty(view, 0, { background: "orange" }).name,
+ "background",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { background: "orange" }).value,
+ "orange",
+ "Should have correct property value"
+ );
+
+ is(
+ getTextProperty(view, 0, { "text-align": "center" }).name,
+ "text-align",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { "text-align": "center" }).value,
+ "center",
+ "Should have correct property value"
+ );
+
+ is(
+ getTextProperty(view, 0, { "border-color": "red" }).name,
+ "border-color",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { "border-color": "red" }).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 0000000000..9c52d33e58
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js
@@ -0,0 +1,107 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ // Turn off throttling, which can cause intermittents. Throttling is used by
+ // the TextPropertyEditor.
+ view.debounce = () => {};
+
+ await selectNode("div", inspector);
+
+ const 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");
+ await createNewRuleViewProperty(ruleEditor, "width: 100px; heig");
+ await onMutation;
+ await 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");
+ const valueEditor = ruleEditor.propertyList.children[1].querySelector(
+ ".styleinspector-propertyeditor"
+ );
+ valueEditor.value = "10px;background:orangered;color: black;";
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ await onMutation;
+ await 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(
+ getTextProperty(view, 0, { width: "100px" }).name,
+ "width",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { width: "100px" }).value,
+ "100px",
+ "Should have correct property value"
+ );
+
+ is(
+ getTextProperty(view, 0, { heig: "10px" }).name,
+ "heig",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { heig: "10px" }).value,
+ "10px",
+ "Should have correct property value"
+ );
+
+ is(
+ getTextProperty(view, 0, { background: "orangered" }).name,
+ "background",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { background: "orangered" }).value,
+ "orangered",
+ "Should have correct property value"
+ );
+
+ is(
+ getTextProperty(view, 0, { color: "black" }).name,
+ "color",
+ "Should have correct property name"
+ );
+ is(
+ getTextProperty(view, 0, { color: "black" }).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 0000000000..65babeddf6
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_01.js
@@ -0,0 +1,84 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ const 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).
+ const onMutation = inspector.once("markupmutation");
+ const onRuleViewChanged = view.once("ruleview-changed");
+ await createNewRuleViewProperty(
+ ruleEditor,
+ "color:blue;background : orange ; text-align:center; " +
+ "border-color: green;"
+ );
+ await onMutation;
+ await 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 0000000000..c2ed334bbe
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_02.js
@@ -0,0 +1,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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ const ruleEditor = getRuleViewRuleEditor(view, 0);
+ let onDone = view.once("ruleview-changed");
+ await createNewRuleViewProperty(ruleEditor, "width:");
+ await 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");
+ const onMutation = inspector.once("markupmutation");
+ const input = view.styleDocument.activeElement;
+ input.value = "height: 10px;color:blue";
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ await onMutation;
+ await 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_nested_at_rules.js b/devtools/client/inspector/rules/test/browser_rules_nested_at_rules.js
new file mode 100644
index 0000000000..db0f4d39c5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_nested_at_rules.js
@@ -0,0 +1,85 @@
+/* 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 when the page defines nested at-rules (@media, @layer, @supports, …)
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ container: mycontainer / inline-size;
+ }
+
+ @layer mylayer {
+ @supports (container-name: mycontainer) {
+ @container mycontainer (min-width: 1px) {
+ @media screen {
+ @container mycontainer (min-width: 2rem) {
+ h1, [test-hint="nested"] {
+ background: gold;
+ }
+ }
+ }
+ }
+ }
+ }
+ </style>
+ <h1>Hello nested at-rules!</h1>
+`;
+
+add_task(async function () {
+ await pushPref("layout.css.container-queries.enabled", true);
+
+ await addTab(
+ "https://example.com/document-builder.sjs?html=" +
+ encodeURIComponent(TEST_URI)
+ );
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("h1", inspector);
+
+ const expectedRules = [
+ { selector: "element", ancestorRulesData: null },
+ {
+ selector: `h1, [test-hint="nested"]`,
+ ancestorRulesData: [
+ `@layer mylayer`,
+ `@supports (container-name: mycontainer)`,
+ `@container mycontainer (min-width: 1px)`,
+ `@media screen`,
+ `@container mycontainer (min-width: 2rem)`,
+ ],
+ },
+ ];
+
+ const rulesInView = Array.from(view.element.children);
+ is(
+ rulesInView.length,
+ expectedRules.length,
+ "All expected rules are displayed"
+ );
+
+ for (let i = 0; i < expectedRules.length; i++) {
+ const expectedRule = expectedRules[i];
+ info(`Checking rule #${i}: ${expectedRule.selector}`);
+
+ const selector = rulesInView[i].querySelector(
+ ".ruleview-selectorcontainer"
+ ).innerText;
+ is(selector, expectedRule.selector, `Expected selector for ${selector}`);
+
+ if (expectedRule.ancestorRulesData == null) {
+ is(
+ getRuleViewAncestorRulesDataElementByIndex(view, i),
+ null,
+ `No ancestor rules data displayed for ${selector}`
+ );
+ } else {
+ is(
+ getRuleViewAncestorRulesDataTextByIndex(view, i),
+ expectedRule.ancestorRulesData.join("\n"),
+ `Expected ancestor rules data displayed for ${selector}`
+ );
+ }
+ }
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_non_ascii.js b/devtools/client/inspector/rules/test/browser_rules_non_ascii.js
new file mode 100644
index 0000000000..b124c7513b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_non_ascii.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that rule view can open when there are non-ASCII characters in
+// the style sheet. Regression test for bug 1390455.
+
+// Use a few 4-byte UTF-8 sequences to make it so the rule column
+// would be wrong when we had the bug.
+const SHEET_TEXT = "/*🆒🆒🆒🆒🆒🆒🆒🆒🆒🆒🆒🆒🆒🆒🆒*/#q{color:orange}";
+const HTML = `<style type="text/css">\n${SHEET_TEXT}
+ </style><div id="q">Styled Node</div>`;
+const TEST_URI = "data:text/html;charset=utf-8," + encodeURIComponent(HTML);
+
+add_task(async function () {
+ await addTab(TEST_URI);
+
+ const { inspector, view } = await openRuleView();
+ await selectNode("#q", inspector);
+
+ const elementStyle = view._elementStyle;
+
+ const expected = [{ name: "color", overridden: false }];
+
+ const rule = elementStyle.rules[1];
+
+ for (let i = 0; i < expected.length; ++i) {
+ const prop = rule.textProps[i];
+ is(prop.name, expected[i].name, `Got expected property name ${prop.name}`);
+ is(
+ prop.overridden,
+ expected[i].overridden,
+ `Got expected overridden value ${prop.overridden}`
+ );
+ }
+});
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 0000000000..30096791eb
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_original-source-link.js
@@ -0,0 +1,93 @@
+/* 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_SSL + "doc_sourcemaps.html";
+const PREF = "devtools.source-map.client-service.enabled";
+const SCSS_LOC = "doc_sourcemaps.scss:4";
+const CSS_LOC = "doc_sourcemaps.css:1";
+
+add_task(async function () {
+ info("Setting the " + PREF + " pref to true");
+ Services.prefs.setBoolPref(PREF, true);
+
+ await addTab(TESTCASE_URI);
+ const { toolbox, inspector, view } = await openRuleView();
+
+ info("Selecting the test node");
+ await selectNode("div", inspector);
+
+ await verifyLinkText(SCSS_LOC, view);
+
+ info("Setting the " + PREF + " pref to false");
+ Services.prefs.setBoolPref(PREF, false);
+ await verifyLinkText(CSS_LOC, view);
+
+ info("Setting the " + PREF + " pref to true again");
+ Services.prefs.setBoolPref(PREF, true);
+
+ await testClickingLink(toolbox, view);
+ const selectedEditor = await waitForOriginalStyleSheetEditorSelection(
+ toolbox
+ );
+
+ const href = selectedEditor.styleSheet.href;
+ ok(
+ href.endsWith("doc_sourcemaps.scss"),
+ "selected stylesheet is correct one"
+ );
+
+ await selectedEditor.getSourceEditor();
+ const { line } = selectedEditor.sourceEditor.getCursor();
+ is(line, 3, "cursor is at correct line number in original source");
+
+ info("Clearing the " + PREF + " pref");
+ Services.prefs.clearUserPref(PREF);
+});
+
+async function testClickingLink(toolbox, view) {
+ info("Listening for switch to the style editor");
+ const onStyleEditorReady = toolbox.once("styleeditor-selected");
+
+ info("Finding the stylesheet link and clicking it");
+ const link = getRuleViewLinkByIndex(view, 1);
+ link.scrollIntoView();
+ link.click();
+ await onStyleEditorReady;
+}
+
+function waitForOriginalStyleSheetEditorSelection(toolbox) {
+ const panel = toolbox.getCurrentPanel();
+ return new Promise((resolve, reject) => {
+ const maybeContinue = 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");
+ off();
+ resolve(editor);
+ }
+ };
+ const off = panel.UI.on("editor-selected", maybeContinue);
+ if (panel.UI.selectedEditor) {
+ maybeContinue(panel.UI.selectedEditor);
+ }
+ });
+}
+
+function verifyLinkText(text, view) {
+ info("Verifying that the rule-view stylesheet link is " + text);
+ const label = getRuleViewLinkByIndex(view, 1).querySelector(
+ ".ruleview-rule-source-label"
+ );
+ return waitForSuccess(function () {
+ return (
+ label.textContent == text &&
+ label.getAttribute("title") === URL_ROOT_SSL + text
+ );
+ }, "Link text changed to display correct location: " + text);
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_original-source-link2.js b/devtools/client/inspector/rules/test/browser_rules_original-source-link2.js
new file mode 100644
index 0000000000..87963f9ec5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_original-source-link2.js
@@ -0,0 +1,89 @@
+/* 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_sourcemaps2.html";
+const PREF = "devtools.source-map.client-service.enabled";
+const SCSS_LOC = "doc_sourcemaps.scss:4";
+const CSS_LOC = "doc_sourcemaps2.css:1";
+
+add_task(async function () {
+ info("Setting the " + PREF + " pref to true");
+ Services.prefs.setBoolPref(PREF, true);
+
+ await addTab(TESTCASE_URI);
+ const { toolbox, inspector, view } = await openRuleView();
+
+ info("Selecting the test node");
+ await selectNode("div", inspector);
+
+ await verifyLinkText(SCSS_LOC, view);
+
+ info("Setting the " + PREF + " pref to false");
+ Services.prefs.setBoolPref(PREF, false);
+ await verifyLinkText(CSS_LOC, view);
+
+ info("Setting the " + PREF + " pref to true again");
+ Services.prefs.setBoolPref(PREF, true);
+
+ await testClickingLink(toolbox, view);
+ const selectedEditor = await waitForOriginalStyleSheetEditorSelection(
+ toolbox
+ );
+ const href = selectedEditor.styleSheet.href;
+ ok(
+ href.endsWith("doc_sourcemaps.scss"),
+ "selected stylesheet is correct one"
+ );
+ await selectedEditor.getSourceEditor();
+
+ const { line } = selectedEditor.sourceEditor.getCursor();
+ is(line, 3, "cursor is at correct line number in original source");
+
+ info("Clearing the " + PREF + " pref");
+ Services.prefs.clearUserPref(PREF);
+});
+
+async function testClickingLink(toolbox, view) {
+ info("Listening for switch to the style editor");
+ const onStyleEditorReady = toolbox.once("styleeditor-selected");
+
+ info("Finding the stylesheet link and clicking it");
+ const link = getRuleViewLinkByIndex(view, 1);
+ link.scrollIntoView();
+ link.click();
+ await onStyleEditorReady;
+}
+
+function waitForOriginalStyleSheetEditorSelection(toolbox) {
+ const panel = toolbox.getCurrentPanel();
+ return new Promise((resolve, reject) => {
+ const maybeContinue = 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");
+ off();
+ resolve(editor);
+ }
+ };
+ const off = panel.UI.on("editor-selected", maybeContinue);
+ if (panel.UI.selectedEditor) {
+ maybeContinue(panel.UI.selectedEditor);
+ }
+ });
+}
+
+function verifyLinkText(text, view) {
+ info("Verifying that the rule-view stylesheet link is " + text);
+ const 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_preview-tooltips-sizes.js b/devtools/client/inspector/rules/test/browser_rules_preview-tooltips-sizes.js
new file mode 100644
index 0000000000..a90b6250cd
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_preview-tooltips-sizes.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the dimensions of the preview tooltips are correctly updated to fit their
+// content.
+
+// Small 32x32 image.
+const BASE_64_URL =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr" +
+ "0AAAAUElEQVRYR+3UsQkAQAhD0TjJ7T+Wk3gbxMIizbcVITwwJWlkZtptpXp+v94TAAEE4gLTvgfOf770RB" +
+ "EAAQTiAvEiIgACCMQF4kVEAAQQSAt8xsyeAW6R8eIAAAAASUVORK5CYII=";
+
+add_task(async function () {
+ await addTab(
+ "data:text/html;charset=utf-8," +
+ encodeURIComponent(`
+ <style>
+ html {
+ /* Using a long variable name to ensure preview tooltip for variable will be */
+ /* wider than the preview tooltip for the test 32x32 image. */
+ --test-var-wider-than-image: red;
+ }
+
+ #target {
+ color: var(--test-var-wider-than-image);
+ background: url(${BASE_64_URL});
+ }
+ </style>
+ <div id="target">inspect me</div>
+ `)
+ );
+ const { inspector, view } = await openRuleView();
+ await selectNode("#target", inspector);
+
+ // Note: See intermittent Bug 1721743.
+ // On linux webrender opt, the inspector might open the ruleview before it has
+ // been populated with the rules for the div.
+ info("Wait until the rule view property is rendered");
+ const colorPropertyElement = await waitFor(() =>
+ getRuleViewProperty(view, "#target", "color")
+ );
+
+ // Retrieve the element for `--test-var` on which the CSS variable tooltip will appear.
+ const colorPropertySpan = colorPropertyElement.valueSpan;
+ const colorVariableElement =
+ colorPropertySpan.querySelector(".ruleview-variable");
+
+ // Retrieve the element for the background url on which the image preview will appear.
+ const backgroundPropertySpan = getRuleViewProperty(
+ view,
+ "#target",
+ "background"
+ ).valueSpan;
+ const backgroundUrlElement =
+ backgroundPropertySpan.querySelector(".theme-link");
+
+ info("Show preview tooltip for CSS variable");
+ let previewTooltip = await assertShowPreviewTooltip(
+ view,
+ colorVariableElement
+ );
+ // Measure tooltip dimensions.
+ let tooltipRect = previewTooltip.panel.getBoundingClientRect();
+ const originalHeight = tooltipRect.height;
+ const originalWidth = tooltipRect.width;
+ info(`Original dimensions: ${originalWidth} x ${originalHeight}`);
+ await assertTooltipHiddenOnMouseOut(previewTooltip, colorVariableElement);
+
+ info("Show preview tooltip for background url");
+ previewTooltip = await assertShowPreviewTooltip(view, backgroundUrlElement);
+ // Compare new tooltip dimensions to previous measures.
+ tooltipRect = previewTooltip.panel.getBoundingClientRect();
+ info(
+ `Image preview dimensions: ${tooltipRect.width} x ${tooltipRect.height}`
+ );
+ ok(
+ tooltipRect.height > originalHeight,
+ "Tooltip is taller for image preview"
+ );
+ ok(
+ tooltipRect.width < originalWidth,
+ "Tooltip is narrower for image preview"
+ );
+ await assertTooltipHiddenOnMouseOut(previewTooltip, colorVariableElement);
+
+ info("Show preview tooltip for CSS variable again");
+ previewTooltip = await assertShowPreviewTooltip(view, colorVariableElement);
+ // Check measures are identical to initial ones.
+ tooltipRect = previewTooltip.panel.getBoundingClientRect();
+ info(
+ `CSS variable tooltip dimensions: ${tooltipRect.width} x ${tooltipRect.height}`
+ );
+ is(
+ tooltipRect.height,
+ originalHeight,
+ "Tooltip has the same height as the original"
+ );
+ is(
+ tooltipRect.width,
+ originalWidth,
+ "Tooltip has the same width as the original"
+ );
+ await assertTooltipHiddenOnMouseOut(previewTooltip, colorVariableElement);
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_print_media_simulation.js b/devtools/client/inspector/rules/test/browser_rules_print_media_simulation.js
new file mode 100644
index 0000000000..1ba411b56a
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_print_media_simulation.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test print media simulation.
+
+// Load the test page under .com TLD, to make the inner .org iframe remote with
+// Fission.
+const TEST_URI = URL_ROOT_COM_SSL + "doc_print_media_simulation.html";
+
+add_task(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+
+ info("Check that the print simulation button exists");
+ const button = inspector.panelDoc.querySelector("#print-simulation-toggle");
+ ok(button, "The print simulation button exists");
+
+ ok(!button.classList.contains("checked"), "The print button is not checked");
+
+ // Helper to retrieve the background-color property of the selected element
+ // All the test elements are expected to have a single background-color rule
+ // for this test.
+ const ruleViewHasColor = async color =>
+ (await getPropertiesForRuleIndex(view, 1)).has("background-color:" + color);
+
+ info("Select a div that will change according to print simulation");
+ await selectNode("div", inspector);
+ ok(
+ await ruleViewHasColor("#f00"),
+ "The rule view shows the expected initial rule"
+ );
+ is(
+ getRuleViewAncestorRulesDataElementByIndex(view, 1),
+ null,
+ "No media query information are displayed initially"
+ );
+
+ info("Click on the button and wait for print media to be applied");
+ button.click();
+
+ await waitFor(() => button.classList.contains("checked"));
+ ok(true, "The button is now checked");
+
+ await waitFor(() => ruleViewHasColor("#00f"));
+ ok(
+ true,
+ "The rules view was updated with the rule view from the print media query"
+ );
+ is(
+ getRuleViewAncestorRulesDataTextByIndex(view, 1),
+ "@media print",
+ "Media queries information are displayed"
+ );
+
+ info("Select the node from the remote iframe");
+ await selectNodeInFrames(["iframe", "html"], inspector);
+
+ ok(
+ await ruleViewHasColor("#0ff"),
+ "The simulation is also applied on the remote iframe"
+ );
+ is(
+ getRuleViewAncestorRulesDataTextByIndex(view, 1),
+ "@media print",
+ "Media queries information are displayed for the node on the remote iframe as well"
+ );
+
+ info("Select the top level div again");
+ await selectNode("div", inspector);
+
+ info("Click the button again to disable print simulation");
+ button.click();
+
+ await waitFor(() => !button.classList.contains("checked"));
+ ok(true, "The button is no longer checked");
+
+ await waitFor(() => ruleViewHasColor("#f00"));
+ is(
+ getRuleViewAncestorRulesDataElementByIndex(view, 1),
+ null,
+ "media query is no longer displayed"
+ );
+
+ info("Select the node from the remote iframe again");
+ await selectNodeInFrames(["iframe", "html"], inspector);
+
+ await waitFor(() => ruleViewHasColor("#ff0"));
+ ok(true, "The simulation stopped on the remote iframe as well");
+ is(
+ getRuleViewAncestorRulesDataElementByIndex(view, 1),
+ null,
+ "media query is no longer displayed on the remote iframe as well"
+ );
+});
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 0000000000..ce49501687
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js
@@ -0,0 +1,395 @@
+/* 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(async function () {
+ await pushPref(PSEUDO_PREF, true);
+
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+
+ await testTopLeft(inspector, view);
+ await testTopRight(inspector, view);
+ await testBottomRight(inspector, view);
+ await testBottomLeft(inspector, view);
+ await testParagraph(inspector, view);
+ await testBody(inspector, view);
+ await testList(inspector, view);
+ await testDialogBackdrop(inspector, view);
+});
+
+async function testTopLeft(inspector, view) {
+ const id = "#topleft";
+ const rules = await assertPseudoElementRulesNumbers(id, inspector, view, {
+ elementRulesNb: 4,
+ firstLineRulesNb: 2,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 1,
+ markerRulesNb: 0,
+ afterRulesNb: 1,
+ beforeRulesNb: 2,
+ });
+
+ const gutters = assertGutters(view);
+
+ info("Make sure that clicking on the twisty hides pseudo elements");
+ const 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"
+ );
+
+ const elementRuleView = getRuleViewRuleEditor(view, 3);
+
+ const elementFirstLineRule = rules.firstLineRules[0];
+ const 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
+ );
+ await onAdded;
+
+ onAdded = view.once("ruleview-changed");
+ const secondProp = elementFirstLineRuleView.addProperty(
+ "font-style",
+ "italic",
+ "",
+ true
+ );
+ await 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(
+ await getComputedStyleProperty(id, ":first-line", "background-color"),
+ "rgb(0, 255, 0)",
+ "Added property should have been used."
+ );
+ is(
+ await getComputedStyleProperty(id, ":first-line", "font-style"),
+ "italic",
+ "Added property should have been used."
+ );
+ is(
+ await getComputedStyleProperty(id, null, "text-decoration-line"),
+ "none",
+ "Added property should not apply to element"
+ );
+
+ await togglePropStatus(view, firstProp);
+
+ is(
+ await getComputedStyleProperty(id, ":first-line", "background-color"),
+ "rgb(255, 0, 0)",
+ "Disabled property should now have been used."
+ );
+ is(
+ await getComputedStyleProperty(id, null, "background-color"),
+ "rgb(221, 221, 221)",
+ "Added property should not apply to element"
+ );
+
+ await togglePropStatus(view, firstProp);
+
+ is(
+ await getComputedStyleProperty(id, ":first-line", "background-color"),
+ "rgb(0, 255, 0)",
+ "Added property should have been used."
+ );
+ is(
+ await getComputedStyleProperty(id, null, "text-decoration-line"),
+ "none",
+ "Added property should not apply to element"
+ );
+
+ onAdded = view.once("ruleview-changed");
+ firstProp = elementRuleView.addProperty(
+ "background-color",
+ "rgb(0, 0, 255)",
+ "",
+ true
+ );
+ await onAdded;
+
+ is(
+ await getComputedStyleProperty(id, null, "background-color"),
+ "rgb(0, 0, 255)",
+ "Added property should have been used."
+ );
+ is(
+ await getComputedStyleProperty(id, ":first-line", "background-color"),
+ "rgb(0, 255, 0)",
+ "Added prop does not apply to pseudo"
+ );
+}
+
+async function testTopRight(inspector, view) {
+ await assertPseudoElementRulesNumbers("#topright", inspector, view, {
+ elementRulesNb: 4,
+ firstLineRulesNb: 1,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 0,
+ markerRulesNb: 0,
+ beforeRulesNb: 2,
+ afterRulesNb: 1,
+ });
+
+ const gutters = assertGutters(view);
+
+ const 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"
+ );
+}
+
+async function testBottomRight(inspector, view) {
+ await assertPseudoElementRulesNumbers("#bottomright", inspector, view, {
+ elementRulesNb: 4,
+ firstLineRulesNb: 1,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 0,
+ markerRulesNb: 0,
+ beforeRulesNb: 3,
+ afterRulesNb: 1,
+ });
+}
+
+async function testBottomLeft(inspector, view) {
+ await assertPseudoElementRulesNumbers("#bottomleft", inspector, view, {
+ elementRulesNb: 4,
+ firstLineRulesNb: 1,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 0,
+ markerRulesNb: 0,
+ beforeRulesNb: 2,
+ afterRulesNb: 1,
+ });
+}
+
+async function testParagraph(inspector, view) {
+ const rules = await assertPseudoElementRulesNumbers(
+ "#bottomleft p",
+ inspector,
+ view,
+ {
+ elementRulesNb: 3,
+ firstLineRulesNb: 1,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 2,
+ markerRulesNb: 0,
+ beforeRulesNb: 0,
+ afterRulesNb: 0,
+ }
+ );
+
+ assertGutters(view);
+
+ const elementFirstLineRule = rules.firstLineRules[0];
+ is(
+ convertTextPropsToString(elementFirstLineRule.textProps),
+ "background: blue",
+ "Paragraph first-line properties are correct"
+ );
+
+ const elementFirstLetterRule = rules.firstLetterRules[0];
+ is(
+ convertTextPropsToString(elementFirstLetterRule.textProps),
+ "color: red; font-size: 130%",
+ "Paragraph first-letter properties are correct"
+ );
+
+ const elementSelectionRule = rules.selectionRules[0];
+ is(
+ convertTextPropsToString(elementSelectionRule.textProps),
+ "color: white; background: black",
+ "Paragraph first-letter properties are correct"
+ );
+}
+
+async function testBody(inspector, view) {
+ await testNode("body", inspector, view);
+
+ const gutters = getGutters(view);
+ is(gutters.length, 0, "There are no gutter headings");
+}
+
+async function testList(inspector, view) {
+ await assertPseudoElementRulesNumbers("#list", inspector, view, {
+ elementRulesNb: 4,
+ firstLineRulesNb: 1,
+ firstLetterRulesNb: 1,
+ selectionRulesNb: 0,
+ markerRulesNb: 1,
+ beforeRulesNb: 1,
+ afterRulesNb: 1,
+ });
+
+ assertGutters(view);
+}
+
+async function testDialogBackdrop(inspector, view) {
+ await assertPseudoElementRulesNumbers("dialog", inspector, view, {
+ elementRulesNb: 3,
+ backdropRules: 1,
+ });
+
+ assertGutters(view);
+}
+
+function convertTextPropsToString(textProps) {
+ return textProps.map(t => t.name + ": " + t.value).join("; ");
+}
+
+async function testNode(selector, inspector, view) {
+ await selectNode(selector, inspector);
+ const elementStyle = view._elementStyle;
+ return elementStyle;
+}
+
+async function assertPseudoElementRulesNumbers(
+ selector,
+ inspector,
+ view,
+ ruleNbs
+) {
+ const elementStyle = await testNode(selector, inspector, view);
+
+ const 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 === "::selection"
+ ),
+ markerRules: elementStyle.rules.filter(
+ rule => rule.pseudoElement === "::marker"
+ ),
+ beforeRules: elementStyle.rules.filter(
+ rule => rule.pseudoElement === "::before"
+ ),
+ afterRules: elementStyle.rules.filter(
+ rule => rule.pseudoElement === "::after"
+ ),
+ backdropRules: elementStyle.rules.filter(
+ rule => rule.pseudoElement === "::backdrop"
+ ),
+ };
+
+ is(
+ rules.elementRules.length,
+ ruleNbs.elementRulesNb || 0,
+ selector + " has the correct number of non pseudo element rules"
+ );
+ is(
+ rules.firstLineRules.length,
+ ruleNbs.firstLineRulesNb || 0,
+ selector + " has the correct number of ::first-line rules"
+ );
+ is(
+ rules.firstLetterRules.length,
+ ruleNbs.firstLetterRulesNb || 0,
+ selector + " has the correct number of ::first-letter rules"
+ );
+ is(
+ rules.selectionRules.length,
+ ruleNbs.selectionRulesNb || 0,
+ selector + " has the correct number of ::selection rules"
+ );
+ is(
+ rules.markerRules.length,
+ ruleNbs.markerRulesNb || 0,
+ selector + " has the correct number of ::marker rules"
+ );
+ is(
+ rules.beforeRules.length,
+ ruleNbs.beforeRulesNb || 0,
+ selector + " has the correct number of ::before rules"
+ );
+ is(
+ rules.afterRules.length,
+ ruleNbs.afterRulesNb || 0,
+ selector + " has the correct number of ::after rules"
+ );
+
+ return rules;
+}
+
+function getGutters(view) {
+ return view.element.querySelectorAll(".ruleview-header");
+}
+
+function assertGutters(view) {
+ const 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 0000000000..d098542758
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js
@@ -0,0 +1,54 @@
+/* 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(async function () {
+ await addTab(TEST_URI);
+ const { inspector } = await openRuleView();
+
+ const node = await getNodeFront("#topleft", inspector);
+ const children = await inspector.markup.walker.children(node);
+
+ is(children.nodes.length, 3, "Element has correct number of children");
+
+ const beforeElement = children.nodes[0];
+ is(
+ beforeElement.tagName,
+ "_moz_generated_content_before",
+ "tag name is correct"
+ );
+ await selectNode(beforeElement, inspector);
+
+ const afterElement = children.nodes[children.nodes.length - 1];
+ is(
+ afterElement.tagName,
+ "_moz_generated_content_after",
+ "tag name is correct"
+ );
+ await selectNode(afterElement, inspector);
+
+ const listNode = await getNodeFront("#list", inspector);
+ const listChildren = await inspector.markup.walker.children(listNode);
+
+ is(listChildren.nodes.length, 4, "<li> has correct number of children");
+ const markerElement = listChildren.nodes[0];
+ is(
+ markerElement.tagName,
+ "_moz_generated_content_marker",
+ "tag name is correct"
+ );
+ await selectNode(markerElement, inspector);
+
+ const listBeforeElement = listChildren.nodes[1];
+ is(
+ listBeforeElement.tagName,
+ "_moz_generated_content_before",
+ "tag name is correct"
+ );
+ await selectNode(listBeforeElement, inspector);
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_pseudo-visited.js b/devtools/client/inspector/rules/test/browser_rules_pseudo-visited.js
new file mode 100644
index 0000000000..6d37829160
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_pseudo-visited.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests for visited/unvisited rule.
+
+const TEST_URI = URL_ROOT + "doc_visited.html";
+
+add_task(async () => {
+ info("Open a url which has a visited and an unvisited link");
+ const tab = await addTab(TEST_URI);
+
+ info("Wait until the visited link is available");
+ await waitUntilVisitedState(tab, ["#visited"]);
+
+ info("Open the inspector");
+ const { inspector, view } = await openRuleView();
+
+ info("Check whether the rule view is shown correctly for visited element");
+ await selectNode("#visited", inspector);
+ ok(getRuleViewRule(view, "a:visited"), "Rule of a:visited is shown");
+ ok(!getRuleViewRule(view, "a:link"), "Rule of a:link is not shown");
+ ok(getRuleViewRule(view, "a"), "Rule of a is shown");
+
+ info("Check whether the rule view is shown correctly for unvisited element");
+ await selectNode("#unvisited", inspector);
+ ok(!getRuleViewRule(view, "a:visited"), "Rule of a:visited is not shown");
+ ok(getRuleViewRule(view, "a:link"), "Rule of a:link is shown");
+ ok(getRuleViewRule(view, "a"), "Rule of a is shown");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_pseudo-visited_in_media-query.js b/devtools/client/inspector/rules/test/browser_rules_pseudo-visited_in_media-query.js
new file mode 100644
index 0000000000..48f62a2fa9
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_pseudo-visited_in_media-query.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests for visited/unvisited rule.
+
+const TEST_URI = URL_ROOT + "doc_visited_in_media_query.html";
+
+add_task(async () => {
+ info("Open a url which has a visited link and the style in the media query");
+ const tab = await addTab(TEST_URI);
+
+ info("Wait until the visited link is available");
+ await waitUntilVisitedState(tab, ["#visited"]);
+
+ info("Open the inspector");
+ const { inspector, view } = await openRuleView();
+
+ info("Check whether the rule view is shown correctly for visited element");
+ await selectNode("#visited", inspector);
+ ok(getRuleViewRule(view, "a"), "Rule of a is shown");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_pseudo-visited_with_style-attribute.js b/devtools/client/inspector/rules/test/browser_rules_pseudo-visited_with_style-attribute.js
new file mode 100644
index 0000000000..97d1458b72
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_pseudo-visited_with_style-attribute.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule view works for an element with a visited pseudo class
+// with a style attribute defined.
+
+const TEST_URI = URL_ROOT + "doc_visited_with_style_attribute.html";
+
+add_task(async () => {
+ info(
+ "Open a page which has an element with a visited pseudo class and a style attribute"
+ );
+ const tab = await addTab(TEST_URI);
+
+ info("Wait until the link has been visited");
+ await waitUntilVisitedState(tab, ["#visited"]);
+
+ info("Open the inspector");
+ const { inspector, view } = await openRuleView();
+
+ info("Check whether the rule view is shown correctly for visited element");
+ await selectNode("#visited", inspector);
+ ok(getRuleViewRule(view, "element"), "Rule of a is shown");
+});
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 0000000000..62a8c8684d
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_pseudo_lock_options.js
@@ -0,0 +1,159 @@
+/* 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 {
+ PSEUDO_CLASSES,
+} = require("resource://devtools/shared/css/constants.js");
+const TEST_URI = `
+ <style type='text/css'>
+ div {
+ color: red;
+ }
+ div:hover {
+ color: blue;
+ }
+ div:active {
+ color: yellow;
+ }
+ div:focus {
+ color: green;
+ }
+ div:focus-within {
+ color: papayawhip;
+ }
+ div:visited {
+ color: orange;
+ }
+ div:focus-visible {
+ color: wheat;
+ }
+ div:target {
+ color: crimson;
+ }
+ </style>
+ <div>test div</div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ await assertPseudoPanelClosed(view);
+
+ info("Toggle the pseudo class panel open");
+ view.pseudoClassToggle.click();
+ await assertPseudoPanelOpened(view);
+
+ info("Toggle each pseudo lock and check that the pseudo lock is added");
+ for (const pseudo of PSEUDO_CLASSES) {
+ await togglePseudoClass(inspector, view, pseudo);
+ await assertPseudoAdded(inspector, view, pseudo, 3, 1);
+ await togglePseudoClass(inspector, view, pseudo);
+ await assertPseudoRemoved(inspector, view, 2);
+ }
+
+ info("Toggle all pseudo locks and check that the pseudo lock is added");
+ await togglePseudoClass(inspector, view, ":hover");
+ await togglePseudoClass(inspector, view, ":active");
+ await togglePseudoClass(inspector, view, ":focus");
+ await togglePseudoClass(inspector, view, ":target");
+ await assertPseudoAdded(inspector, view, ":target", 6, 1);
+ await assertPseudoAdded(inspector, view, ":focus", 6, 2);
+ await assertPseudoAdded(inspector, view, ":active", 6, 3);
+ await assertPseudoAdded(inspector, view, ":hover", 6, 4);
+ await togglePseudoClass(inspector, view, ":hover");
+ await togglePseudoClass(inspector, view, ":active");
+ await togglePseudoClass(inspector, view, ":focus");
+ await togglePseudoClass(inspector, view, ":target");
+ await assertPseudoRemoved(inspector, view, 2);
+
+ info("Select a null element");
+ await view.selectElement(null);
+
+ info("Check that all pseudo locks are unchecked and disabled");
+ for (const pseudo of PSEUDO_CLASSES) {
+ const checkbox = getPseudoClassCheckbox(view, pseudo);
+ ok(
+ !checkbox.checked && checkbox.disabled,
+ `${pseudo} checkbox is unchecked and disabled`
+ );
+ }
+
+ info("Toggle the pseudo class panel close");
+ view.pseudoClassToggle.click();
+ await assertPseudoPanelClosed(view);
+});
+
+async function togglePseudoClass(inspector, view, pseudoClass) {
+ info(`Toggle the pseudo-class ${pseudoClass}, wait for it to be applied`);
+ const onRefresh = inspector.once("rule-view-refreshed");
+ const checkbox = getPseudoClassCheckbox(view, pseudoClass);
+ if (checkbox) {
+ checkbox.click();
+ }
+ await onRefresh;
+}
+
+function assertPseudoAdded(inspector, view, pseudoClass, numRules, childIndex) {
+ info("Check that the rule view 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 rule view 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");
+
+ for (const pseudo of PSEUDO_CLASSES) {
+ const checkbox = getPseudoClassCheckbox(view, pseudo);
+ ok(!checkbox.disabled, `${pseudo} checkbox is not disabled`);
+ is(
+ checkbox.getAttribute("tabindex"),
+ "0",
+ `${pseudo} 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");
+
+ for (const pseudo of PSEUDO_CLASSES) {
+ const checkbox = getPseudoClassCheckbox(view, pseudo);
+ is(
+ checkbox.getAttribute("tabindex"),
+ "-1",
+ `${pseudo} 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 0000000000..05574ab4c7
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_refresh-no-flicker.js
@@ -0,0 +1,47 @@
+/* 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(async function () {
+ await addTab(TESTCASE_URI);
+
+ info("Opening the rule view and selecting the test node");
+ const { inspector, view } = await openRuleView();
+ const testdiv = await getNodeFront("#testdiv", inspector);
+ await selectNode(testdiv, inspector);
+
+ const 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");
+ const 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."
+ );
+ await 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 0000000000..d41ee2b00c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_01.js
@@ -0,0 +1,71 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await 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");
+ await setContentPageElementAttribute("#testid", "id", "differentid");
+ await 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");
+ await setContentPageElementAttribute("#differentid", "id", "testid");
+ await ruleViewRefreshed;
+
+ info("Checking that the rule-view has all the selectors again");
+ checkRuleViewContent(view, ["element", "#testid", ".testclass"]);
+});
+
+function checkRuleViewContent(view, expectedSelectors) {
+ const 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-style-change.js b/devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.js
new file mode 100644
index 0000000000..9287f161de
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.js
@@ -0,0 +1,43 @@
+/* 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(async function () {
+ Services.prefs.setCharPref("devtools.defaultColorUnit", "name");
+
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await 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");
+ const onUpdated = inspector.once("rule-view-refreshed");
+ await setContentPageElementAttribute(
+ "#testdiv",
+ "style",
+ "font-size: 3em; color: lightgoldenrodyellow; " +
+ "text-align: right; text-transform: uppercase"
+ );
+ await onUpdated;
+
+ const textAlign = getRuleViewPropertyValue(view, "element", "text-align");
+ is(textAlign, "right", "The rule view shows the new text align.");
+ const 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.");
+ const 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 0000000000..8a4d78e843
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_01.js
@@ -0,0 +1,180 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testAddTextInFilter(inspector, view);
+});
+
+async function testAddTextInFilter(inspector, view) {
+ for (const data of TEST_DATA) {
+ info(data.desc);
+ await setSearchFilter(view, data.search);
+ await checkRules(view, data);
+ await 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."
+ );
+
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ const textPropEditor = rule.textProps[0].editor;
+ const 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."
+ );
+}
+
+async function clearSearchAndCheckRules(view) {
+ const win = view.styleWindow;
+ const searchField = view.searchField;
+ const searchClearButton = view.searchClearButton;
+
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ const textPropEditor = rule.textProps[0].editor;
+ const computed = textPropEditor.computed;
+
+ info("Clearing the search filter");
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
+ await 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 0000000000..98c48c9f79
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_02.js
@@ -0,0 +1,118 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testAddTextInFilter(inspector, view);
+ await testRemoveTextInFilter(inspector, view);
+});
+
+async function testAddTextInFilter(inspector, view) {
+ await 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."
+ );
+
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ const ruleEditor = getTextProperty(view, 1, { margin: "4px 0px" }).editor;
+ const 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."
+ );
+}
+
+async function testRemoveTextInFilter(inspector, view) {
+ info('Press backspace and set filter text to "margin"');
+
+ const win = view.styleWindow;
+ const searchField = view.searchField;
+
+ searchField.focus();
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win);
+ await 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."
+ );
+
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ const ruleEditor = getTextProperty(view, 1, { margin: "4px 0px" }).editor;
+ const 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 0000000000..5c2a48a7fb
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_03.js
@@ -0,0 +1,55 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode(".testclass", inspector);
+ await testAddTextInFilter(inspector, view);
+});
+
+async function testAddTextInFilter(inspector, view) {
+ await 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."
+ );
+
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ const ruleEditor = rule.textProps[0].editor;
+ const 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 0000000000..6b2344b6a6
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_04.js
@@ -0,0 +1,75 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testModifyPropertyValueFilter(inspector, view);
+});
+
+async function testModifyPropertyValueFilter(inspector, view) {
+ await setSearchFilter(view, SEARCH);
+
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ const propEditor = getTextProperty(view, 1, { margin: "4px" }).editor;
+ const computed = propEditor.computed;
+ const editor = await 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."
+ );
+
+ const onBlur = once(editor.input, "blur");
+ const onModification = view.once("ruleview-changed");
+ EventUtils.sendString("4px 0px", view.styleWindow);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await onBlur;
+ await 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 0000000000..5eeff9b539
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_expander.js
@@ -0,0 +1,113 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testOpenExpanderAndAddTextInFilter(inspector, view);
+ await testClearSearchFilter(inspector, view);
+});
+
+async function testOpenExpanderAndAddTextInFilter(inspector, view) {
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ const ruleEditor = getTextProperty(view, 1, { margin: "4px 0px" }).editor;
+ const computed = ruleEditor.computed;
+
+ info("Opening the computed list of margin property");
+ ruleEditor.expander.click();
+
+ await 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."
+ );
+}
+
+async function testClearSearchFilter(inspector, view) {
+ info("Clearing the search filter");
+
+ const searchField = view.searchField;
+ const searchClearButton = view.searchClearButton;
+ const onRuleViewFiltered = inspector.once("ruleview-filtered");
+
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, view.styleWindow);
+
+ await 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"
+ );
+
+ const ruleEditor = getRuleViewRuleEditor(view, 1).rule.textProps[0].editor;
+ const 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-media-queries-layers.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-media-queries-layers.js
new file mode 100644
index 0000000000..fb95e6adf8
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-media-queries-layers.js
@@ -0,0 +1,195 @@
+/* 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 @media / @layer rules.
+// The document uses selectors so we can identify rule more easily
+const TEST_URI = `
+ <!DOCTYPE html>
+ <style type='text/css'>
+ h1, simple {
+ color: tomato;
+ }
+ @layer {
+ h1, anonymous {
+ color: tomato;
+ }
+ }
+ @layer myLayer {
+ h1, named {
+ color: tomato;
+ }
+ }
+ @media screen {
+ h1, skreen {
+ color: tomato;
+ }
+ }
+ @layer {
+ @layer myLayer {
+ @media (min-width: 1px) {
+ @media (min-height: 1px) {
+ h1, nested {
+ color: tomato;
+ }
+ }
+ }
+ }
+ }
+ </style>
+ <h1>Hello Mochi</h1>`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("h1", inspector);
+
+ info(`Check initial state and rules order`);
+ await checkRuleView(view, {
+ rules: [
+ { selector: "h1, skreen", highlighted: [] },
+ { selector: "h1, simple", highlighted: [] },
+ { selector: "h1, nested", highlighted: [] },
+ { selector: "h1, named", highlighted: [] },
+ { selector: "h1, anonymous", highlighted: [] },
+ ],
+ });
+
+ info(`Check filtering on "layer"`);
+ await setSearchFilter(view, `layer`);
+ await checkRuleView(view, {
+ rules: [
+ { selector: "h1, nested", highlighted: ["@layer", "@layer myLayer"] },
+ { selector: "h1, named", highlighted: ["@layer myLayer"] },
+ { selector: "h1, anonymous", highlighted: ["@layer"] },
+ ],
+ });
+
+ info(`Check filtering on "@layer"`);
+ await setNewSearchFilter(view, `@layer`);
+ await checkRuleView(view, {
+ rules: [
+ { selector: "h1, nested", highlighted: ["@layer", "@layer myLayer"] },
+ { selector: "h1, named", highlighted: ["@layer myLayer"] },
+ { selector: "h1, anonymous", highlighted: ["@layer"] },
+ ],
+ });
+
+ info("Check filtering on exact `@layer`");
+ await setNewSearchFilter(view, "`@layer`");
+ await checkRuleView(view, {
+ rules: [
+ { selector: "h1, nested", highlighted: ["@layer"] },
+ { selector: "h1, anonymous", highlighted: ["@layer"] },
+ ],
+ });
+
+ info(`Check filtering on layer name "myLayer"`);
+ await setNewSearchFilter(view, `myLayer`);
+ await checkRuleView(view, {
+ rules: [
+ { selector: "h1, nested", highlighted: ["@layer myLayer"] },
+ { selector: "h1, named", highlighted: ["@layer myLayer"] },
+ ],
+ });
+
+ info(`Check filtering on "@layer myLayer"`);
+ await setNewSearchFilter(view, `@layer myLayer`);
+ await checkRuleView(view, {
+ rules: [
+ { selector: "h1, nested", highlighted: ["@layer myLayer"] },
+ { selector: "h1, named", highlighted: ["@layer myLayer"] },
+ ],
+ });
+
+ info(`Check filtering on "media"`);
+ await setNewSearchFilter(view, `media`);
+ await checkRuleView(view, {
+ rules: [
+ { selector: "h1, skreen", highlighted: ["@media screen"] },
+ {
+ selector: "h1, nested",
+ highlighted: ["@media (min-width: 1px)", "@media (min-height: 1px)"],
+ },
+ ],
+ });
+
+ info(`Check filtering on "@media"`);
+ await setNewSearchFilter(view, `@media`);
+ await checkRuleView(view, {
+ rules: [
+ { selector: "h1, skreen", highlighted: ["@media screen"] },
+ {
+ selector: "h1, nested",
+ highlighted: ["@media (min-width: 1px)", "@media (min-height: 1px)"],
+ },
+ ],
+ });
+
+ info(`Check filtering on media query content "1px"`);
+ await setNewSearchFilter(view, `1px`);
+ await checkRuleView(view, {
+ rules: [
+ {
+ selector: "h1, nested",
+ highlighted: ["@media (min-width: 1px)", "@media (min-height: 1px)"],
+ },
+ ],
+ });
+
+ info(`Check filtering on media query content "height"`);
+ await setNewSearchFilter(view, `height`);
+ await checkRuleView(view, {
+ rules: [
+ {
+ selector: "h1, nested",
+ highlighted: ["@media (min-height: 1px)"],
+ },
+ ],
+ });
+
+ info("Check filtering on exact `@media`");
+ await setNewSearchFilter(view, "`@media`");
+ await checkRuleView(view, {
+ rules: [],
+ });
+});
+
+async function checkRuleView(view, { rules }) {
+ info("Check that the correct rules are visible");
+
+ const rulesInView = Array.from(view.element.children);
+ // The `element` "rule" is never filtered, so remove it from the list of element we check.
+ rulesInView.shift();
+
+ is(rulesInView.length, rules.length, "All expected rules are displayed");
+
+ for (let i = 0; i < rulesInView.length; i++) {
+ const rule = rulesInView[i];
+ const selector = rule.querySelector(
+ ".ruleview-selectorcontainer"
+ ).innerText;
+ is(selector, rules[i]?.selector, `Expected selector at index ${i}`);
+
+ const highlightedElements = Array.from(
+ rule.querySelectorAll(".ruleview-highlight")
+ ).map(el => el.innerText);
+ Assert.deepEqual(
+ highlightedElements,
+ rules[i]?.highlighted,
+ "The expected ancestor rules information element are highlighted"
+ );
+ }
+}
+
+async function setNewSearchFilter(view, newSearchText) {
+ const win = view.styleWindow;
+ const searchClearButton = view.searchClearButton;
+
+ const onRuleViewCleared = view.inspector.once("ruleview-filtered");
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
+ await onRuleViewCleared;
+
+ await setSearchFilter(view, newSearchText);
+}
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 0000000000..442c543289
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-overridden-property.js
@@ -0,0 +1,89 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testFilterOverriddenProperty(inspector, view);
+});
+
+async 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 = getTextProperty(ruleView, 1, { width: "100%" }).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 = getTextProperty(ruleView, 2, { width: "50%" }).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."
+ );
+
+ const searchField = ruleView.searchField;
+ const onRuleViewFiltered = inspector.once("ruleview-filtered");
+
+ info("Click the overridden search");
+ textPropEditor.filterProperty.click();
+ await 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 = getTextProperty(ruleView, 1, { width: "100%" }).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 = getTextProperty(ruleView, 2, { width: "50%" }).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 0000000000..a0a3ed32ff
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_01.js
@@ -0,0 +1,98 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testAddTextInFilter(inspector, view);
+});
+
+async function testAddTextInFilter(inspector, view) {
+ for (const data of TEST_DATA) {
+ info(data.desc);
+ await setSearchFilter(view, data.search);
+ await checkRules(view);
+ await 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."
+ );
+
+ const 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."
+ );
+}
+
+async function clearSearchAndCheckRules(view) {
+ const doc = view.styleDocument;
+ const win = view.styleWindow;
+ const searchField = view.searchField;
+ const searchClearButton = view.searchClearButton;
+
+ info("Clearing the search filter");
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
+ await 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 0000000000..173da66296
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_02.js
@@ -0,0 +1,36 @@
+/* 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(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+ await selectNode("#boxy", inspector);
+ await testAddTextInFilter(inspector, view);
+});
+
+async function testAddTextInFilter(inspector, view) {
+ await setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(
+ getRuleViewRuleEditor(view, 0).rule.selectorText,
+ "element",
+ "First rule is inline element."
+ );
+
+ const 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 0000000000..18335c121c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_03.js
@@ -0,0 +1,39 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testAddTextInFilter(inspector, view);
+});
+
+async function testAddTextInFilter(inspector, view) {
+ await setSearchFilter(view, SEARCH);
+
+ info("Check that the correct rules are visible");
+ is(view.element.children.length, 1, "Should have 1 rule.");
+
+ const 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 0000000000..fc0eee9f19
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_04.js
@@ -0,0 +1,82 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testAddTextInFilter(inspector, view);
+ await testRemoveTextInFilter(inspector, view);
+});
+
+async function testAddTextInFilter(inspector, view) {
+ await 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."
+ );
+
+ const 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."
+ );
+}
+
+async function testRemoveTextInFilter(inspector, view) {
+ info('Press backspace and set filter text to "00"');
+
+ const win = view.styleWindow;
+ const searchField = view.searchField;
+
+ searchField.focus();
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win);
+ await 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 0000000000..8138528ee4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_05.js
@@ -0,0 +1,37 @@
+/* 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(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+ await selectNode(".relative1", inspector);
+ await testAddTextInFilter(inspector, view);
+});
+
+async function testAddTextInFilter(inspector, view) {
+ await 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."
+ );
+
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ const 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 0000000000..a73fb87ff9
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_06.js
@@ -0,0 +1,29 @@
+/* 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(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+ await selectNode(".relative1", inspector);
+ await testAddTextInFilter(inspector, view);
+});
+
+async function testAddTextInFilter(inspector, view) {
+ await 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 0000000000..e2c7494bf7
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_07.js
@@ -0,0 +1,66 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ info("Enter the test value in the search filter");
+ await setSearchFilter(view, SEARCH);
+
+ info("Focus the width property name");
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+ const rule = ruleEditor.rule;
+ const propEditor = rule.textProps[0].editor;
+ await 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("KEY_Enter");
+ await 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("KEY_Escape");
+ await 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 0000000000..b35afd7d10
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_08.js
@@ -0,0 +1,57 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ info("Enter the test value in the search filter");
+ await setSearchFilter(view, SEARCH);
+
+ info("Focus the height property value");
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+ const rule = ruleEditor.rule;
+ const propEditor = rule.textProps[1].editor;
+ await 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%");
+ const onRuleViewChanged = view.once("ruleview-changed");
+ EventUtils.sendString("100%", view.styleWindow);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await 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 0000000000..0d1c8c8a45
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js
@@ -0,0 +1,79 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ info("Enter the test value in the search filter");
+ await setSearchFilter(view, SEARCH);
+
+ info("Start entering a new property in the rule");
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+ const rule = ruleEditor.rule;
+ const prop = getTextProperty(view, 1, { width: "100%" });
+ let editor = await 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(
+ prop.editor.container.classList.contains("ruleview-highlight"),
+ "width text property is correctly highlighted."
+ );
+ ok(
+ !getTextProperty(view, 1, {
+ height: "50%",
+ }).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);
+ await onRuleViewChanged;
+
+ // Getting the new value editor after focus
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ const 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.debounce.flush();
+ await onRuleViewChanged;
+
+ onRuleViewChanged = view.once("ruleview-changed");
+ editor.input.blur();
+ await 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 0000000000..b3ee69c2b9
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_10.js
@@ -0,0 +1,96 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testAddTextInFilter(inspector, view);
+});
+
+async function testAddTextInFilter(inspector, view) {
+ for (const data of TEST_DATA) {
+ info(data.desc);
+ await setSearchFilter(view, data.search);
+ await checkRules(view, data);
+ await 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."
+ );
+
+ const 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."
+ );
+}
+
+async function clearSearchAndCheckRules(view) {
+ const doc = view.styleDocument;
+ const win = view.styleWindow;
+ const searchField = view.searchField;
+ const searchClearButton = view.searchClearButton;
+
+ info("Clearing the search filter");
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
+ await 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 0000000000..881b5274ee
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js
@@ -0,0 +1,99 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { toolbox, inspector, view } = await openRuleView();
+ await selectNode("h1", inspector);
+
+ const searchField = view.searchField;
+
+ info("Opening context menu");
+
+ emptyClipboard();
+
+ const onFocus = once(searchField, "focus");
+ searchField.focus();
+ await onFocus;
+
+ let onContextMenuOpen = toolbox.once("menu-open");
+ synthesizeContextMenuEvent(searchField);
+ await onContextMenuOpen;
+
+ let searchContextMenu = toolbox.getTextBoxContextMenu();
+ ok(
+ searchContextMenu,
+ "The search filter context menu is loaded in the rule view"
+ );
+
+ let cmdUndo = searchContextMenu.querySelector("#editmenu-undo");
+ let cmdDelete = searchContextMenu.querySelector("#editmenu-delete");
+ let cmdSelectAll = searchContextMenu.querySelector("#editmenu-selectAll");
+ let cmdCut = searchContextMenu.querySelector("#editmenu-cut");
+ let cmdCopy = searchContextMenu.querySelector("#editmenu-copy");
+ let cmdPaste = searchContextMenu.querySelector("#editmenu-paste");
+
+ is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled");
+ is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled");
+ is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled");
+ is(cmdCut.getAttribute("disabled"), "true", "cmdCut is disabled");
+ is(cmdCopy.getAttribute("disabled"), "true", "cmdCopy is disabled");
+
+ 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 onContextMenuClose = toolbox.once("menu-close");
+ searchContextMenu.hidePopup();
+ await onContextMenuClose;
+
+ info("Copy text in search field using the context menu");
+ searchField.setUserInput(TEST_INPUT);
+ searchField.select();
+
+ onContextMenuOpen = toolbox.once("menu-open");
+ synthesizeContextMenuEvent(searchField);
+ await onContextMenuOpen;
+
+ searchContextMenu = toolbox.getTextBoxContextMenu();
+ cmdCopy = searchContextMenu.querySelector("#editmenu-copy");
+ await waitForClipboardPromise(() => cmdCopy.click(), TEST_INPUT);
+
+ onContextMenuClose = toolbox.once("menu-close");
+ searchContextMenu.hidePopup();
+ await onContextMenuClose;
+
+ info("Reopen context menu and check command properties");
+
+ onContextMenuOpen = toolbox.once("menu-open");
+ synthesizeContextMenuEvent(searchField);
+ await onContextMenuOpen;
+
+ searchContextMenu = toolbox.getTextBoxContextMenu();
+ cmdUndo = searchContextMenu.querySelector("#editmenu-undo");
+ cmdDelete = searchContextMenu.querySelector("#editmenu-delete");
+ cmdSelectAll = searchContextMenu.querySelector("#editmenu-selectAll");
+ cmdCut = searchContextMenu.querySelector("#editmenu-cut");
+ cmdCopy = searchContextMenu.querySelector("#editmenu-copy");
+ cmdPaste = searchContextMenu.querySelector("#editmenu-paste");
+
+ 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");
+
+ const onContextMenuHidden = toolbox.once("menu-close");
+ searchContextMenu.hidePopup();
+ await onContextMenuHidden;
+});
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 0000000000..600aefe536
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_escape-keypress.js
@@ -0,0 +1,71 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testAddTextInFilter(inspector, view);
+ await testEscapeKeypress(inspector, view);
+});
+
+async function testAddTextInFilter(inspector, view) {
+ await 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."
+ );
+
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ const prop = getTextProperty(view, 1, { "background-color": "#00F" });
+
+ is(rule.selectorText, "#testid", "Second rule is #testid.");
+ ok(
+ prop.editor.container.classList.contains("ruleview-highlight"),
+ "background-color text property is correctly highlighted."
+ );
+}
+
+async function testEscapeKeypress(inspector, view) {
+ info("Pressing the escape key on search filter");
+
+ const doc = view.styleDocument;
+ const win = view.styleWindow;
+ const searchField = view.searchField;
+ const onRuleViewFiltered = inspector.once("ruleview-filtered");
+
+ searchField.focus();
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+ await 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 0000000000..8558c40cab
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js
@@ -0,0 +1,210 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+ await checkCopySelection(view);
+ await checkSelectAll(view);
+ await checkCopyEditorValue(view);
+});
+
+async function checkCopySelection(view) {
+ info("Testing selection copy");
+
+ const contentDoc = view.styleDocument;
+ const win = view.styleWindow;
+ const prop = contentDoc.querySelector(".ruleview-property");
+ const 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");
+
+ const 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]*";
+
+ const allMenuItems = openStyleContextMenuAndGetAllItems(view, prop);
+ const menuitemCopy = allMenuItems.find(
+ item =>
+ item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy")
+ );
+
+ ok(menuitemCopy.visible, "Copy menu item is displayed as expected");
+
+ try {
+ await waitForClipboardPromise(
+ () => menuitemCopy.click(),
+ () => checkClipboardData(expectedPattern)
+ );
+ } catch (e) {
+ failedClipboard(expectedPattern);
+ }
+
+ info("Check copying from keyboard");
+ win.getSelection().removeRange(range);
+ // Selecting the declaration `margin: 10em;`
+ range = contentDoc.createRange();
+ range.setStart(prop, 0);
+ range.setEnd(prop, 1);
+ win.getSelection().addRange(range);
+
+ // Dispatching the copy event from the checkbox to make sure we cover Bug 1680893.
+ const declarationCheckbox = contentDoc.querySelector(
+ "input[type=checkbox].ruleview-enableproperty"
+ );
+ const copyEvent = new win.Event("copy", { bubbles: true });
+ await waitForClipboardPromise(
+ () => declarationCheckbox.dispatchEvent(copyEvent),
+ () => checkClipboardData("^margin: 10em;$")
+ );
+}
+
+async function checkSelectAll(view) {
+ info("Testing select-all copy");
+
+ const contentDoc = view.styleDocument;
+ const prop = contentDoc.querySelector(".ruleview-property");
+
+ info(
+ "Checking that _SelectAll() then copy returns the correct " +
+ "clipboard value"
+ );
+ view.contextMenu._onSelectAll();
+ const 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]*";
+
+ const allMenuItems = openStyleContextMenuAndGetAllItems(view, prop);
+ const menuitemCopy = allMenuItems.find(
+ item =>
+ item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy")
+ );
+
+ ok(menuitemCopy.visible, "Copy menu item is displayed as expected");
+
+ try {
+ await waitForClipboardPromise(
+ () => menuitemCopy.click(),
+ () => checkClipboardData(expectedPattern)
+ );
+ } catch (e) {
+ failedClipboard(expectedPattern);
+ }
+}
+
+async function checkCopyEditorValue(view) {
+ info("Testing CSS property editor value copy");
+
+ const ruleEditor = getRuleViewRuleEditor(view, 0);
+ const propEditor = ruleEditor.rule.textProps[0].editor;
+
+ const editor = await focusEditableField(view, propEditor.valueSpan);
+
+ info(
+ "Checking that copying a css property value editor returns the correct" +
+ " clipboard value"
+ );
+
+ const expectedPattern = "10em";
+
+ const allMenuItems = openStyleContextMenuAndGetAllItems(view, editor.input);
+ const menuitemCopy = allMenuItems.find(
+ item =>
+ item.label ===
+ STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy")
+ );
+
+ ok(menuitemCopy.visible, "Copy menu item is displayed as expected");
+
+ try {
+ await waitForClipboardPromise(
+ () => menuitemCopy.click(),
+ () => checkClipboardData(expectedPattern)
+ );
+ } catch (e) {
+ failedClipboard(expectedPattern);
+ }
+}
+
+function checkClipboardData(expectedPattern) {
+ const actual = SpecialPowers.getClipboardData("text/plain");
+ const expectedRegExp = new RegExp(expectedPattern, "g");
+ return expectedRegExp.test(actual);
+}
+
+function failedClipboard(expectedPattern) {
+ // Format expected text for comparison
+ const 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/plain");
+
+ // 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-iframe-picker.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter-iframe-picker.js
new file mode 100644
index 0000000000..51d4a9b371
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter-iframe-picker.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the selector highlighter is hidden when selecting frames in the iframe picker
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ background: red;
+ }
+ </style>
+ <h1>Test the selector highlighter</h1>
+ <iframe src="data:text/html,<meta charset=utf8><style>h2 {background: yellow;}</style><h2>In iframe</h2>">
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, toolbox, view } = await openRuleView();
+
+ info("Clicking on a selector icon");
+ const { highlighter, isShown } = await clickSelectorIcon(view, "body");
+
+ ok(highlighter, "The selector highlighter instance was created");
+ ok(isShown, "The selector highlighter was shown");
+ is(
+ highlighter,
+ inspector.highlighters.getActiveHighlighter(
+ inspector.highlighters.TYPES.SELECTOR
+ ),
+ "The selector highlighter is the active highlighter"
+ );
+
+ // Open frame menu and wait till it's available on the screen.
+ const panel = toolbox.doc.getElementById("command-button-frames-panel");
+ const btn = toolbox.doc.getElementById("command-button-frames");
+ btn.click();
+ ok(panel, "popup panel has created.");
+ await waitUntil(() => panel.classList.contains("tooltip-visible"));
+
+ // Verify that the menu is populated.
+ const menuList = toolbox.doc.getElementById("toolbox-frame-menu");
+ const frames = Array.from(menuList.querySelectorAll(".command"));
+
+ // Wait for the inspector to be reloaded
+ // (instead of only new-root) in order to wait for full
+ // async update of the inspector.
+ const onNewRoot = inspector.once("reloaded");
+ frames[1].click();
+ await onNewRoot;
+
+ await waitFor(
+ () =>
+ !inspector.highlighters.getActiveHighlighter(
+ inspector.highlighters.TYPES.SELECTOR
+ )
+ );
+ ok(true, "The selector highlighter gets hidden after selecting a frame");
+});
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 0000000000..3a014a68f7
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js
@@ -0,0 +1,35 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ info("Clicking on a selector icon");
+ const { highlighter, isShown } = await clickSelectorIcon(view, "body, p, td");
+
+ ok(highlighter, "The selector highlighter instance was created");
+ ok(isShown, "The selector highlighter was shown");
+
+ await navigateTo(TEST_URI_2);
+
+ const activeHighlighter = inspector.highlighters.getActiveHighlighter(
+ inspector.highlighters.TYPES.SELECTOR
+ );
+ ok(!activeHighlighter, "No selector highlighter is active");
+});
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 0000000000..a1010131a9
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_01.js
@@ -0,0 +1,32 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ const activeHighlighter = inspector.highlighters.getActiveHighlighter(
+ inspector.highlighters.TYPES.SELECTOR
+ );
+ ok(!activeHighlighter, "No selector highlighter is active");
+
+ info("Clicking on a selector icon");
+ const { highlighter, isShown } = await clickSelectorIcon(view, "body, p, td");
+
+ ok(highlighter, "The selector highlighter instance was created");
+ ok(isShown, "The selector highlighter was shown");
+});
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 0000000000..3d6ca9cbb4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_02.js
@@ -0,0 +1,48 @@
+/* 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
+
+const TEST_URI = `
+ <style type="text/css">
+ body {
+ background: red;
+ }
+ p {
+ color: white;
+ }
+ </style>
+ <p>Testing the selector highlighter</p>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ let data;
+
+ info("Clicking once on the body selector highlighter icon");
+ data = await clickSelectorIcon(view, "body");
+ ok(data.isShown, "The highlighter is shown");
+
+ info("Clicking once again on the body selector highlighter icon");
+ data = await clickSelectorIcon(view, "body");
+ ok(!data.isShown, "The highlighter is hidden");
+
+ info("Checking that the right NodeFront reference and options are passed");
+ await selectNode("p", inspector);
+ data = await clickSelectorIcon(view, "p");
+
+ is(
+ data.nodeFront.tagName,
+ "P",
+ "The right NodeFront is passed to the highlighter"
+ );
+ is(
+ data.options.selector,
+ "p",
+ "The right selector option is passed to the highlighter"
+ );
+});
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 0000000000..1c43e6adad
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_03.js
@@ -0,0 +1,64 @@
+/* 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.
+
+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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ let data;
+
+ info("Select .node-1 and click on the .node-1 selector icon");
+ await selectNode(".node-1", inspector);
+ data = await clickSelectorIcon(view, ".node-1");
+ ok(data.isShown, "The highlighter is shown");
+
+ info("With .node-1 still selected, click again on the .node-1 selector icon");
+ data = await clickSelectorIcon(view, ".node-1");
+ ok(!data.isShown, "The highlighter is now hidden");
+
+ info("With .node-1 still selected, click on the div selector icon");
+ data = await clickSelectorIcon(view, "div");
+ ok(data.isShown, "The highlighter is shown again");
+
+ info("With .node-1 still selected, click again on the .node-1 selector icon");
+ data = await clickSelectorIcon(view, ".node-1");
+ ok(
+ data.isShown,
+ "The highlighter is shown again since the clicked selector was different"
+ );
+
+ info("Selecting .node-2");
+ await selectNode(".node-2", inspector);
+ const activeHighlighter = inspector.highlighters.getActiveHighlighter(
+ inspector.highlighters.TYPES.SELECTOR
+ );
+ ok(activeHighlighter, "The highlighter is still shown after selection");
+
+ info("With .node-2 selected, click on the div selector icon");
+ data = await clickSelectorIcon(view, "div");
+ ok(
+ data.isShown,
+ "The highlighter is shown still since the selected was different"
+ );
+
+ info("Switching back to .node-1 and clicking on the div selector");
+ await selectNode(".node-1", inspector);
+ data = await clickSelectorIcon(view, "div");
+ ok(
+ !data.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 0000000000..46def76f05
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_04.js
@@ -0,0 +1,35 @@
+/* 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
+
+const TEST_URI = `
+<p>Testing the selector highlighter for the 'element {}' rule</p>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ let data;
+
+ info("Checking that the right NodeFront reference and options are passed");
+ await selectNode("p", inspector);
+ data = await clickSelectorIcon(view, "element");
+ is(
+ data.nodeFront.tagName,
+ "P",
+ "The right NodeFront is passed to the highlighter (1)"
+ );
+ is(
+ data.options.selector,
+ "body > p:nth-child(1)",
+ "The right selector option is passed to the highlighter (1)"
+ );
+ ok(data.isShown, "The toggle event says the highlighter is visible");
+
+ data = await clickSelectorIcon(view, "element");
+ ok(!data.isShown, "The toggle event says the highlighter is not visible");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_05.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_05.js
new file mode 100644
index 0000000000..e5538c682f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_05.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the selector highlighter is correctly shown when clicking on a
+// inherited element
+
+const TEST_URI = `
+<div style="cursor:pointer">
+ A
+ <div style="cursor:pointer">
+ B<a>Cursor</a>
+ </div>
+</div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ let data;
+
+ info("Checking that the right NodeFront reference and options are passed");
+ await selectNode("a", inspector);
+
+ data = await clickSelectorIcon(view, "element");
+ is(
+ data.options.selector,
+ "body > div:nth-child(1) > div:nth-child(1) > a:nth-child(1)",
+ "The right selector option is passed to the highlighter (1)"
+ );
+
+ data = await clickSelectorIcon(view, "element", 1);
+ is(
+ data.options.selector,
+ "body > div:nth-child(1) > div:nth-child(1)",
+ "The right selector option is passed to the highlighter (1)"
+ );
+
+ data = await clickSelectorIcon(view, "element", 2);
+ is(
+ data.options.selector,
+ "body > div:nth-child(1)",
+ "The right selector option is passed to the highlighter (1)"
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_order.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_order.js
new file mode 100644
index 0000000000..856ab7a840
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_order.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = `
+<style type="text/css">
+ #rule-from-stylesheet {
+ color: red;
+ }
+</style>
+<div id=inline style="cursor:pointer">
+ A
+ <div id=inherited>B</div>
+</div>
+<div id=rule-from-stylesheet>C</a>
+`;
+
+// This test will assert that specific elements of a ruleview rule have been
+// rendered in the expected order. This is specifically done to check the fix
+// for Bug 1664511, where some elements were rendered out of order due to
+// unexpected async processing.
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("#inline", inspector);
+ checkRuleViewRuleMarkupOrder(view, "element");
+ await selectNode("#inherited", inspector);
+ checkRuleViewRuleMarkupOrder(view, "element", 1);
+ await selectNode("#rule-from-stylesheet", inspector);
+ checkRuleViewRuleMarkupOrder(view, "#rule-from-stylesheet");
+});
+
+function checkRuleViewRuleMarkupOrder(view, selectorText, index = 0) {
+ const rule = getRuleViewRule(view, selectorText, index);
+
+ // Retrieve the individual elements to assert.
+ const selectorContainer = rule.querySelector(".ruleview-selectorcontainer");
+ const highlighterIcon = rule.querySelector(".js-toggle-selector-highlighter");
+ const ruleOpenBrace = rule.querySelector(".ruleview-ruleopen");
+
+ const parentNode = selectorContainer.parentNode;
+ const childNodes = [...parentNode.childNodes];
+
+ ok(
+ childNodes.indexOf(selectorContainer) < childNodes.indexOf(highlighterIcon),
+ "Selector text is rendered before the highlighter icon"
+ );
+ ok(
+ childNodes.indexOf(highlighterIcon) < childNodes.indexOf(ruleOpenBrace),
+ "Highlighter icon is rendered before the opening brace"
+ );
+}
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 0000000000..1d3620e24b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector_highlight.js
@@ -0,0 +1,152 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ for (const { node, pseudoClass, expected } of TEST_DATA) {
+ await selectNode(node, inspector);
+
+ if (pseudoClass) {
+ const onRefresh = inspector.once("rule-view-refreshed");
+ inspector.togglePseudoClass(pseudoClass);
+ await onRefresh;
+ }
+
+ const 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 (const 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_shadowdom_slot_rules.js b/devtools/client/inspector/rules/test/browser_rules_shadowdom_slot_rules.js
new file mode 100644
index 0000000000..f0d3a17d1e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shadowdom_slot_rules.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that when selecting a slot element, the rule view displays the rules for the
+// corresponding element.
+
+const TEST_URL =
+ `data:text/html;charset=utf-8,` +
+ encodeURIComponent(`
+ <html>
+ <head>
+ <style>
+ #el1 { color: red }
+ #el2 { color: blue }
+ </style>
+ </head>
+ <body>
+ <test-component>
+ <div slot="slot1" id="el1">slot1-1</div>
+ <div slot="slot1" id="el2">slot1-2</div>
+ <div slot="slot1" id="el3">slot1-2</div>
+ </test-component>
+
+ <script>
+ 'use strict';
+ customElements.define('test-component', class extends HTMLElement {
+ constructor() {
+ super();
+ let shadowRoot = this.attachShadow({mode: 'open'});
+ shadowRoot.innerHTML = \`
+ <style>
+ ::slotted(#el3) {
+ color: green;
+ }
+ </style>
+ <slot name="slot1"></slot>
+ \`;
+ }
+ });
+ </script>
+ </body>
+ </html>
+`);
+
+add_task(async function () {
+ const { inspector } = await openInspectorForURL(TEST_URL);
+ const { markup } = inspector;
+ const ruleview = inspector.getPanel("ruleview").view;
+
+ // <test-component> is a shadow host.
+ info("Find and expand the test-component shadow DOM host.");
+ const hostFront = await getNodeFront("test-component", inspector);
+
+ await markup.expandNode(hostFront);
+ await waitForMultipleChildrenUpdates(inspector);
+
+ info(
+ "Test that expanding a shadow host shows shadow root and one host child."
+ );
+ const hostContainer = markup.getContainer(hostFront);
+
+ info("Expand the shadow root");
+ const childContainers = hostContainer.getChildContainers();
+ const shadowRootContainer = childContainers[0];
+ await expandContainer(inspector, shadowRootContainer);
+
+ info("Expand the slot");
+ const shadowChildContainers = shadowRootContainer.getChildContainers();
+ // shadowChildContainers[0] is the style node.
+ const slotContainer = shadowChildContainers[1];
+ await expandContainer(inspector, slotContainer);
+
+ const slotChildContainers = slotContainer.getChildContainers();
+ is(slotChildContainers.length, 3, "Expecting 3 slotted children");
+
+ info(
+ "Select slotted node and check that the rule view displays correct content"
+ );
+ await selectNode(slotChildContainers[0].node, inspector);
+ checkRule(ruleview, "#el1", "color", "red");
+
+ info("Select another slotted node and check the rule view");
+ await selectNode(slotChildContainers[1].node, inspector);
+ checkRule(ruleview, "#el2", "color", "blue");
+
+ info("Select the last slotted node and check the rule view");
+ await selectNode(slotChildContainers[2].node, inspector);
+ checkRule(ruleview, "::slotted(#el3)", "color", "green");
+});
+
+function checkRule(ruleview, selector, name, expectedValue) {
+ const rule = getRuleViewRule(ruleview, selector);
+ ok(rule, "ruleview shows the expected rule for slotted " + selector);
+ const value = getRuleViewPropertyValue(ruleview, selector, name);
+ is(
+ value,
+ expectedValue,
+ "ruleview shows the expected value for slotted " + selector
+ );
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_01.js b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_01.js
new file mode 100644
index 0000000000..a3fd4ccd3e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_01.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the shapes highlighter in the rule view and the display of the
+// shapes highlighter.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #shape {
+ width: 800px;
+ height: 800px;
+ clip-path: circle(25%);
+ }
+ </style>
+ <div id="shape"></div>
+`;
+
+const HIGHLIGHTER_TYPE = "ShapesHighlighter";
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const highlighters = view.highlighters;
+
+ info("Select a node with a shape value");
+ await selectNode("#shape", inspector);
+ const container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan;
+ const shapesToggle = container.querySelector(".ruleview-shapeswatch");
+
+ info("Checking the initial state of the CSS shape toggle in the rule-view.");
+ ok(shapesToggle, "Shapes highlighter toggle is visible.");
+ ok(
+ !shapesToggle.classList.contains("active"),
+ "Shapes highlighter toggle button is not active."
+ );
+ ok(
+ !highlighters.highlighters[HIGHLIGHTER_TYPE],
+ "No CSS shapes highlighter exists in the rule-view."
+ );
+ ok(
+ !highlighters.shapesHighlighterShown,
+ "No CSS shapes highlighter is shown."
+ );
+ info("Toggling ON the CSS shapes highlighter from the rule-view.");
+ const onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+ shapesToggle.click();
+ await onHighlighterShown;
+
+ info(
+ "Checking the CSS shapes highlighter is created and toggle button is active in " +
+ "the rule-view."
+ );
+ ok(
+ shapesToggle.classList.contains("active"),
+ "Shapes highlighter toggle is active."
+ );
+ ok(
+ inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE).actorID,
+ "CSS shapes highlighter created in the rule-view."
+ );
+ ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown.");
+
+ info("Toggling OFF the CSS shapes highlighter from the rule-view.");
+ const onHighlighterHidden = highlighters.once("shapes-highlighter-hidden");
+ shapesToggle.click();
+ await onHighlighterHidden;
+
+ info(
+ "Checking the CSS shapes highlighter is not shown and toggle button is not " +
+ "active in the rule-view."
+ );
+ ok(
+ !shapesToggle.classList.contains("active"),
+ "shapes highlighter toggle button is not active."
+ );
+ ok(
+ !highlighters.shapesHighlighterShown,
+ "No CSS shapes highlighter is shown."
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_02.js b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_02.js
new file mode 100644
index 0000000000..3d2cd0d9e0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_02.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the swatch to toggle a shapes highlighter does not show up
+// on overwritten properties.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #shape {
+ width: 800px;
+ height: 800px;
+ clip-path: circle(25%);
+ }
+ div {
+ clip-path: circle(30%);
+ }
+ </style>
+ <div id="shape"></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("#shape", inspector);
+ const container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan;
+ const shapeToggle = container.querySelector(".ruleview-shapeswatch");
+ const shapeToggleStyle = getComputedStyle(shapeToggle);
+ const overriddenContainer = getRuleViewProperty(
+ view,
+ "div",
+ "clip-path"
+ ).valueSpan;
+ const overriddenShapeToggle = overriddenContainer.querySelector(
+ ".ruleview-shapeswatch"
+ );
+ const overriddenShapeToggleStyle = getComputedStyle(overriddenShapeToggle);
+
+ ok(
+ shapeToggle && overriddenShapeToggle,
+ "Shapes highlighter toggles exist in the DOM."
+ );
+ ok(
+ !shapeToggle.classList.contains("active") &&
+ !overriddenShapeToggle.classList.contains("active"),
+ "Shapes highlighter toggle buttons are not active."
+ );
+
+ isnot(
+ shapeToggleStyle.display,
+ "none",
+ "Shape highlighter toggle is not hidden"
+ );
+ is(
+ overriddenShapeToggleStyle.display,
+ "none",
+ "Overwritten shape highlighter toggle is not visible"
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_03.js b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_03.js
new file mode 100644
index 0000000000..9af14e3c8b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_03.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the shapes highlighter in the rule view with multiple shapes in the page.
+
+const TEST_URI = `
+ <style type='text/css'>
+ .shape {
+ width: 800px;
+ height: 800px;
+ clip-path: circle(25%);
+ }
+ </style>
+ <div class="shape" id="shape1"></div>
+ <div class="shape" id="shape2"></div>
+`;
+
+const HIGHLIGHTER_TYPE = "ShapesHighlighter";
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const highlighters = view.highlighters;
+
+ info("Selecting the first shape container.");
+ await selectNode("#shape1", inspector);
+ let container = getRuleViewProperty(view, ".shape", "clip-path").valueSpan;
+ let shapeToggle = container.querySelector(".ruleview-shapeswatch");
+
+ info(
+ "Checking the state of the CSS shape toggle for the first shape container " +
+ "in the rule-view."
+ );
+ ok(shapeToggle, "shape highlighter toggle is visible.");
+ ok(
+ !shapeToggle.classList.contains("active"),
+ "shape highlighter toggle button is not active."
+ );
+ ok(
+ !highlighters.highlighters[HIGHLIGHTER_TYPE],
+ "No CSS shape highlighter exists in the rule-view."
+ );
+ ok(
+ !highlighters.shapesHighlighterShown,
+ "No CSS shapes highlighter is shown."
+ );
+
+ info(
+ "Toggling ON the CSS shapes highlighter for the first shapes container from the " +
+ "rule-view."
+ );
+ let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+ shapeToggle.click();
+ await onHighlighterShown;
+
+ info(
+ "Checking the CSS shapes highlighter is created and toggle button is active in " +
+ "the rule-view."
+ );
+ ok(
+ shapeToggle.classList.contains("active"),
+ "shapes highlighter toggle is active."
+ );
+ ok(
+ inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE).actorID,
+ "CSS shapes highlighter created in the rule-view."
+ );
+ ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown.");
+
+ info("Selecting the second shapes container.");
+ await selectNode("#shape2", inspector);
+ const firstShapesHighlighterShown = highlighters.shapesHighlighterShown;
+ container = getRuleViewProperty(view, ".shape", "clip-path").valueSpan;
+ shapeToggle = container.querySelector(".ruleview-shapeswatch");
+
+ info(
+ "Checking the state of the CSS shapes toggle for the second shapes container " +
+ "in the rule-view."
+ );
+ ok(shapeToggle, "shapes highlighter toggle is visible.");
+ ok(
+ !shapeToggle.classList.contains("active"),
+ "shapes highlighter toggle button is not active."
+ );
+ ok(
+ !highlighters.shapesHighlighterShown,
+ "CSS shapes highlighter is still no longer" +
+ "shown due to selecting another node."
+ );
+
+ info(
+ "Toggling ON the CSS shapes highlighter for the second shapes container " +
+ "from the rule-view."
+ );
+ onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+ shapeToggle.click();
+ await onHighlighterShown;
+
+ info(
+ "Checking the CSS shapes highlighter is created for the second shapes container " +
+ "and toggle button is active in the rule-view."
+ );
+ ok(
+ shapeToggle.classList.contains("active"),
+ "shapes highlighter toggle is active."
+ );
+ ok(
+ highlighters.shapesHighlighterShown != firstShapesHighlighterShown,
+ "shapes highlighter for the second shapes container is shown."
+ );
+
+ info("Selecting the first shapes container.");
+ await selectNode("#shape1", inspector);
+ container = getRuleViewProperty(view, ".shape", "clip-path").valueSpan;
+ shapeToggle = container.querySelector(".ruleview-shapeswatch");
+
+ info(
+ "Checking the state of the CSS shapes toggle for the first shapes container " +
+ "in the rule-view."
+ );
+ ok(shapeToggle, "shapes highlighter toggle is visible.");
+ ok(
+ !shapeToggle.classList.contains("active"),
+ "shapes highlighter toggle button is not active."
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_04.js b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_04.js
new file mode 100644
index 0000000000..0add21381f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_04.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the shapes highlighter in the rule view and modifying the 'clip-path'
+// declaration.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #shape {
+ width: 800px;
+ height: 800px;
+ clip-path: circle(25%);
+ }
+ </style>
+ <div id="shape"></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const highlighters = view.highlighters;
+
+ info("Select a node with a shape value");
+ await selectNode("#shape", inspector);
+ const container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan;
+ let shapeToggle = container.querySelector(".ruleview-shapeswatch");
+
+ info("Toggling ON the CSS shape highlighter from the rule-view.");
+ const onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+ shapeToggle.click();
+ await onHighlighterShown;
+
+ info("Edit the clip-path property to ellipse.");
+ const editor = await focusEditableField(view, container, 30);
+ const onDone = view.once("ruleview-changed");
+ editor.input.value = "ellipse(30% 20%);";
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ await onDone;
+
+ info(
+ "Check the shape highlighter and shape toggle button are still visible."
+ );
+ shapeToggle = container.querySelector(".ruleview-shapeswatch");
+ ok(shapeToggle, "Shape highlighter toggle is visible.");
+ ok(highlighters.shapesHighlighterShown, "CSS shape highlighter is shown.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_05.js b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_05.js
new file mode 100644
index 0000000000..cf5722144f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_05.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the shapes highlighter is hidden when the highlighted shape container is
+// removed from the page.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #shape {
+ width: 800px;
+ height: 800px;
+ clip-path: circle(25%);
+ }
+ </style>
+ <div id="shape"></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const highlighters = view.highlighters;
+
+ info("Select a node with a shape value");
+ await selectNode("#shape", inspector);
+ const container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan;
+ const shapeToggle = container.querySelector(".ruleview-shapeswatch");
+
+ info("Toggling ON the CSS shapes highlighter from the rule-view.");
+ const onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+ shapeToggle.click();
+ await onHighlighterShown;
+ ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown.");
+
+ const onHighlighterHidden = highlighters.once("shapes-highlighter-hidden");
+ info("Remove the #shapes container in the content page");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () =>
+ content.document.querySelector("#shape").remove()
+ );
+ await onHighlighterHidden;
+ ok(!highlighters.shapesHighlighterShown, "CSS shapes highlighter is hidden.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_06.js b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_06.js
new file mode 100644
index 0000000000..f1395cc1c4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_06.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the shapes highlighter in the rule view with clip-path and shape-outside
+// on the same element.
+
+const TEST_URI = `
+ <style type='text/css'>
+ .shape {
+ width: 800px;
+ height: 800px;
+ clip-path: circle(25%);
+ shape-outside: circle(25%);
+ }
+ </style>
+ <div class="shape" id="shape1"></div>
+ <div class="shape" id="shape2"></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const highlighters = view.highlighters;
+
+ info("Selecting the first shapes container.");
+ await selectNode("#shape1", inspector);
+ let clipPathContainer = getRuleViewProperty(
+ view,
+ ".shape",
+ "clip-path"
+ ).valueSpan;
+ let clipPathShapeToggle = clipPathContainer.querySelector(
+ ".ruleview-shapeswatch"
+ );
+ let shapeOutsideContainer = getRuleViewProperty(
+ view,
+ ".shape",
+ "shape-outside"
+ ).valueSpan;
+ let shapeOutsideToggle = shapeOutsideContainer.querySelector(
+ ".ruleview-shapeswatch"
+ );
+
+ info(
+ "Toggling ON the CSS shapes highlighter for clip-path from the rule-view."
+ );
+ let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+ clipPathShapeToggle.click();
+ await onHighlighterShown;
+ ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown.");
+ ok(
+ clipPathShapeToggle.classList.contains("active"),
+ "clip-path toggle button is active."
+ );
+ ok(
+ !shapeOutsideToggle.classList.contains("active"),
+ "shape-outside toggle button is not active."
+ );
+
+ info(
+ "Toggling ON the CSS shapes highlighter for shape-outside from the rule-view."
+ );
+ onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+ shapeOutsideToggle.click();
+ await onHighlighterShown;
+ ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown.");
+ ok(
+ !clipPathShapeToggle.classList.contains("active"),
+ "clip-path toggle button is not active."
+ );
+ ok(
+ shapeOutsideToggle.classList.contains("active"),
+ "shape-outside toggle button is active."
+ );
+
+ info("Selecting the second shapes container.");
+ await selectNode("#shape2", inspector);
+ clipPathContainer = getRuleViewProperty(
+ view,
+ ".shape",
+ "clip-path"
+ ).valueSpan;
+ clipPathShapeToggle = clipPathContainer.querySelector(
+ ".ruleview-shapeswatch"
+ );
+ shapeOutsideContainer = getRuleViewProperty(
+ view,
+ ".shape",
+ "shape-outside"
+ ).valueSpan;
+ shapeOutsideToggle = shapeOutsideContainer.querySelector(
+ ".ruleview-shapeswatch"
+ );
+ ok(
+ !clipPathShapeToggle.classList.contains("active"),
+ "clip-path toggle button is not active."
+ );
+ ok(
+ !shapeOutsideToggle.classList.contains("active"),
+ "shape-outside toggle button is not active."
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_07.js b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_07.js
new file mode 100644
index 0000000000..d117d51b91
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_07.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling transform mode of the shapes highlighter
+
+const TEST_URI = `
+ <style type='text/css'>
+ #shape {
+ width: 800px;
+ height: 800px;
+ clip-path: circle(25%);
+ }
+ </style>
+ <div id="shape"></div>
+`;
+
+const HIGHLIGHTER_TYPE = "ShapesHighlighter";
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const highlighters = view.highlighters;
+
+ info("Select a node with a shape value");
+ await selectNode("#shape", inspector);
+ const container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan;
+ const shapesToggle = container.querySelector(".ruleview-shapeswatch");
+
+ info("Toggling ON the CSS shapes highlighter with transform mode on.");
+ let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+ EventUtils.sendMouseEvent(
+ { type: "click", metaKey: true, ctrlKey: true },
+ shapesToggle,
+ view.styleWindow
+ );
+ await onHighlighterShown;
+
+ info(
+ "Checking the CSS shapes highlighter is created and transform mode is on"
+ );
+ ok(
+ inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE).actorID,
+ "CSS shapes highlighter created in the rule-view."
+ );
+ ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown.");
+ ok(highlighters.state.shapes.options.transformMode, "Transform mode is on.");
+
+ info("Toggling OFF the CSS shapes highlighter from the rule-view.");
+ const onHighlighterHidden = highlighters.once("shapes-highlighter-hidden");
+ EventUtils.sendMouseEvent({ type: "click" }, shapesToggle, view.styleWindow);
+ await onHighlighterHidden;
+
+ info("Checking the CSS shapes highlighter is not shown.");
+ ok(
+ !highlighters.shapesHighlighterShown,
+ "No CSS shapes highlighter is shown."
+ );
+
+ info("Toggling ON the CSS shapes highlighter with transform mode off.");
+ onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+ EventUtils.sendMouseEvent({ type: "click" }, shapesToggle, view.styleWindow);
+ await onHighlighterShown;
+
+ info(
+ "Checking the CSS shapes highlighter is created and transform mode is off"
+ );
+ ok(
+ inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE).actorID,
+ "CSS shapes highlighter created in the rule-view."
+ );
+ ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown.");
+ ok(
+ !highlighters.state.shapes.options.transformMode,
+ "Transform mode is off."
+ );
+
+ info(
+ "Clicking shapes toggle to turn on transform mode while highlighter is shown."
+ );
+ onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+ EventUtils.sendMouseEvent(
+ { type: "click", metaKey: true, ctrlKey: true },
+ shapesToggle,
+ view.styleWindow
+ );
+ await onHighlighterShown;
+
+ info(
+ "Checking the CSS shapes highlighter is created and transform mode is on"
+ );
+ ok(
+ inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE).actorID,
+ "CSS shapes highlighter created in the rule-view."
+ );
+ ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown.");
+ ok(highlighters.state.shapes.options.transformMode, "Transform mode is on.");
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_basic-shapes-default.js b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_basic-shapes-default.js
new file mode 100644
index 0000000000..46afb2ac57
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_basic-shapes-default.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the shapes highlighter can be toggled for basic shapes with default values.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #shape-circle {
+ clip-path: circle();
+ }
+ #shape-ellipse {
+ clip-path: ellipse();
+ }
+ #shape-inset {
+ clip-path: inset();
+ }
+ #shape-polygon {
+ clip-path: polygon();
+ }
+ </style>
+ <div id="shape-circle"></div>
+ <div id="shape-ellipse"></div>
+ <div id="shape-inset"></div>
+ <div id="shape-polygon"></div>
+`;
+
+const HIGHLIGHTER_TYPE = "ShapesHighlighter";
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ const highlighters = view.highlighters;
+
+ const selectors = new Map([
+ ["#shape-circle", true],
+ ["#shape-ellipse", true],
+ // Basic shapes inset() and polygon() expect explicit coordinates.
+ // They don't have default values and are invalid without coordinates.
+ ["#shape-inset", false],
+ ["#shape-polygon", false],
+ ]);
+
+ for (const [selector, expectShapesToogle] of selectors) {
+ await selectNode(selector, inspector);
+ const container = getRuleViewProperty(
+ view,
+ selector,
+ "clip-path"
+ ).valueSpan;
+ const shapesToggle = container.querySelector(".ruleview-shapeswatch");
+
+ if (expectShapesToogle) {
+ ok(
+ shapesToggle,
+ `Shapes highlighter toggle expected and found for ${selector}`
+ );
+ } else {
+ is(
+ shapesToggle,
+ null,
+ `Shapes highlighter toggle not expected and not found for ${selector}`
+ );
+
+ // Skip the rest of the test.
+ continue;
+ }
+
+ info(`Toggling ON the shapes highlighter for ${selector}`);
+ const onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+ shapesToggle.click();
+ await onHighlighterShown;
+
+ ok(
+ shapesToggle.classList.contains("active"),
+ `Shapes highlighter toggle active for ${selector}`
+ );
+ ok(
+ inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE).actorID,
+ `Shapes highlighter instance created for ${selector}`
+ );
+ ok(
+ highlighters.shapesHighlighterShown,
+ `Shapes highlighter shown for ${selector}`
+ );
+
+ info(`Toggling OFF the shapes highlighter for ${selector}`);
+ const onHighlighterHidden = highlighters.once("shapes-highlighter-hidden");
+ shapesToggle.click();
+ await onHighlighterHidden;
+
+ ok(
+ !shapesToggle.classList.contains("active"),
+ `Shapes highlighter toggle no longer active for ${selector}`
+ );
+ ok(
+ !highlighters.shapesHighlighterShown,
+ `Shapes highlighter no longer shown for ${selector}`
+ );
+ }
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_shorthand-overridden-lists.js b/devtools/client/inspector/rules/test/browser_rules_shorthand-overridden-lists.js
new file mode 100644
index 0000000000..2ad037d443
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shorthand-overridden-lists.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule view shorthand overridden list works correctly,
+// can be shown and hidden correctly, and contain the right subproperties.
+
+var TEST_URI = `
+ <style type="text/css">
+ div {
+ margin: 0px 1px 2px 3px;
+ top: 0px;
+ }
+ #testid {
+ margin-left: 10px;
+ margin-right: 10px;
+ }
+ </style>
+ <div id="testid">Styled Node</div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testComputedList(inspector, view);
+});
+
+function testComputedList(inspector, view) {
+ const rule = getRuleViewRuleEditor(view, 2).rule;
+ const propEditor = rule.textProps[0].editor;
+ const expander = propEditor.expander;
+ const overriddenItems = propEditor.shorthandOverridden.children;
+ const propNames = ["margin-right", "margin-left"];
+
+ ok(!expander.hasAttribute("open"), "margin computed list is closed.");
+ ok(
+ !propEditor.shorthandOverridden.hasAttribute("hidden"),
+ "The shorthandOverridden list should be open."
+ );
+
+ is(
+ overriddenItems.length,
+ propNames.length,
+ "There should be 2 overridden shorthand value."
+ );
+ for (let i = 0; i < propNames.length; i++) {
+ const overriddenItem = overriddenItems[i].querySelector(
+ ".ruleview-propertyname"
+ );
+ is(
+ overriddenItem.textContent,
+ propNames[i],
+ "The overridden item #" + i + " should be " + propNames[i]
+ );
+ }
+
+ info("Opening the computed list of margin property.");
+ expander.click();
+ ok(expander.hasAttribute("open"), "margin computed list is open.");
+ ok(
+ propEditor.shorthandOverridden.hasAttribute("hidden"),
+ "The shorthandOverridden list should be hidden."
+ );
+
+ info("Closing the computed list of margin property.");
+ expander.click();
+ ok(!expander.hasAttribute("open"), "margin computed list is closed.");
+ ok(
+ !propEditor.shorthandOverridden.hasAttribute("hidden"),
+ "The shorthandOverridden list should be open."
+ );
+
+ for (let i = 0; i < propNames.length; i++) {
+ const overriddenItem = overriddenItems[i].querySelector(
+ ".ruleview-propertyname"
+ );
+ is(
+ overriddenItem.textContent,
+ propNames[i],
+ "The overridden item #" + i + " should still be " + propNames[i]
+ );
+ }
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_shorthand-overridden-lists_01.js b/devtools/client/inspector/rules/test/browser_rules_shorthand-overridden-lists_01.js
new file mode 100644
index 0000000000..f33fd57148
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shorthand-overridden-lists_01.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that overridden longhand properties aren't shown when the shorthand's value
+// contains a CSS variable. When this happens, the longhand values can't be computed
+// properly and are hidden. So the overridden longhand that are normally auto-expanded
+// should be hidden too.
+
+var TEST_URI = `
+ <style type="text/css">
+ div {
+ --color: red;
+ background: var(--color);
+ background-repeat: no-repeat;
+ }
+ </style>
+ <div>Inspect me</div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ const shorthandOverridden = rule.textProps[1].editor.shorthandOverridden;
+
+ is(
+ shorthandOverridden.children.length,
+ 0,
+ "The shorthandOverridden list is empty"
+ );
+});
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 0000000000..d1c51acedc
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter-computed-list_01.js
@@ -0,0 +1,208 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testAddTextInFilter(inspector, view);
+});
+
+async function testAddTextInFilter(inspector, view) {
+ for (const data of TEST_DATA) {
+ info(data.desc);
+ await setSearchFilter(view, data.search);
+ await checkRules(view, data);
+ await 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."
+ );
+
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ const textPropEditor = rule.textProps[0].editor;
+ const 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."
+ );
+}
+
+async function clearSearchAndCheckRules(view) {
+ const win = view.styleWindow;
+ const searchField = view.searchField;
+ const searchClearButton = view.searchClearButton;
+
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ const textPropEditor = rule.textProps[0].editor;
+ const computed = textPropEditor.computed;
+
+ info("Clearing the search filter");
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
+ await 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 0000000000..e70f02e37e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_01.js
@@ -0,0 +1,150 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testAddTextInFilter(inspector, view);
+});
+
+async function testAddTextInFilter(inspector, view) {
+ for (const data of TEST_DATA) {
+ info(data.desc);
+ await setSearchFilter(view, data.search);
+ await checkRules(view, data);
+ await 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."
+ );
+ }
+}
+
+async function clearSearchAndCheckRules(view) {
+ const doc = view.styleDocument;
+ const win = view.styleWindow;
+ const searchField = view.searchField;
+ const searchClearButton = view.searchClearButton;
+
+ info("Clearing the search filter");
+ EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
+ await 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 0000000000..a1c3824adf
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_02.js
@@ -0,0 +1,38 @@
+/* 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(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+ await selectNode(".relative1", inspector);
+ await testAddTextInFilter(inspector, view);
+});
+
+async function testAddTextInFilter(inspector, view) {
+ await 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."
+ );
+
+ const rule = getRuleViewRuleEditor(view, 1).rule;
+ const 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 0000000000..3bd34fd2c4
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_03.js
@@ -0,0 +1,50 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ await testAddTextInFilter(inspector, view);
+});
+
+async function testAddTextInFilter(inspector, view) {
+ await 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."
+ );
+
+ const 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 0000000000..a3c59bfa79
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js
@@ -0,0 +1,251 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the links from the rule-view to the styleeditor
+
+const STYLESHEET_DATA_URL_CONTENTS = `#first {
+color: blue
+}`;
+const STYLESHEET_DATA_URL = `data:text/css,${encodeURIComponent(
+ STYLESHEET_DATA_URL_CONTENTS
+)}`;
+
+const EXTERNAL_STYLESHEET_FILE_NAME = "doc_style_editor_link.css";
+const EXTERNAL_STYLESHEET_URL = URL_ROOT_SSL + EXTERNAL_STYLESHEET_FILE_NAME;
+
+const DOCUMENT_HTML = 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>
+ <style>
+ div { font-weight: bold; }
+ </style>
+ <link rel="stylesheet" type="text/css" href="${STYLESHEET_DATA_URL}">
+ <link rel="stylesheet" type="text/css" href="${EXTERNAL_STYLESHEET_URL}">
+ </head>
+ <body>
+ <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" 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>
+`);
+
+const DOCUMENT_DATA_URL = "data:text/html;charset=utf-8," + DOCUMENT_HTML;
+const EXAMPLE_ORG_DOCUMENT_URL =
+ "https://example.org/document-builder.sjs?html=" + DOCUMENT_HTML;
+
+add_task(async function () {
+ await addTab(DOCUMENT_DATA_URL);
+ const { toolbox, inspector, view } = await openRuleView();
+
+ await testAllStylesheets(inspector, view, toolbox);
+
+ info("Navigate to the example.org document");
+ await navigateTo(EXAMPLE_ORG_DOCUMENT_URL);
+ await testAllStylesheets(inspector, view, toolbox);
+});
+
+add_task(async function () {
+ info("Check that link to the style editor works after tab reload");
+ await addTab(EXAMPLE_ORG_DOCUMENT_URL);
+ const { toolbox, inspector, view } = await openRuleView();
+
+ info("Reload the example.org document");
+ // Use navigateTo as it waits for the inspector to be ready.
+ await navigateTo(EXAMPLE_ORG_DOCUMENT_URL);
+ await testAllStylesheets(inspector, view, toolbox);
+});
+
+async function testAllStylesheets(inspector, view, toolbox) {
+ await selectNode("div", inspector);
+ await testRuleViewLinkLabel(view);
+ await testDisabledStyleEditor(view, toolbox);
+ await testFirstInlineStyleSheet(view, toolbox);
+ await testSecondInlineStyleSheet(view, toolbox);
+ await testExternalStyleSheet(view, toolbox);
+
+ info("Switch back to the inspector panel");
+ await toolbox.selectTool("inspector");
+ await selectNode("body", inspector);
+}
+
+async function testFirstInlineStyleSheet(view, toolbox) {
+ info("Testing inline stylesheet");
+
+ info("Listening for toolbox switch to the styleeditor");
+ const onSwitch = waitForStyleEditor(toolbox);
+
+ info("Clicking an inline stylesheet");
+ clickLinkByIndex(view, 4);
+ const editor = await onSwitch;
+
+ ok(true, "Switched to the style-editor panel in the toolbox");
+
+ await validateStyleEditorSheet(toolbox, editor, 0);
+}
+
+async function testSecondInlineStyleSheet(view, toolbox) {
+ info("Testing second inline stylesheet");
+
+ const styleEditorPanel = toolbox.getCurrentPanel();
+ const onEditorSelected = styleEditorPanel.UI.once("editor-selected");
+
+ info("Switching back to the inspector panel in the toolbox");
+ await toolbox.selectTool("inspector");
+ const onToolSelected = toolbox.once("styleeditor-selected");
+
+ info("Clicking on second inline stylesheet link");
+ clickLinkByIndex(view, 3);
+
+ info("Wait for the stylesheet editor to be selected");
+ const editor = await onEditorSelected;
+ await onToolSelected;
+
+ is(
+ toolbox.currentToolId,
+ "styleeditor",
+ "The style editor is selected again"
+ );
+ await validateStyleEditorSheet(toolbox, editor, 1);
+}
+
+async function testExternalStyleSheet(view, toolbox) {
+ info("Testing external stylesheet");
+ const styleEditorPanel = toolbox.getCurrentPanel();
+ const onEditorSelected = styleEditorPanel.UI.once("editor-selected");
+
+ info("Switching back to the inspector panel in the toolbox");
+ await toolbox.selectTool("inspector");
+ const onToolSelected = toolbox.once("styleeditor-selected");
+
+ info("Clicking on an external stylesheet link");
+ clickLinkByIndex(view, 1);
+
+ info("Wait for the stylesheet editor to be selected");
+ const editor = await onEditorSelected;
+ await onToolSelected;
+
+ is(
+ toolbox.currentToolId,
+ "styleeditor",
+ "The style editor is selected again"
+ );
+ await validateStyleEditorSheet(toolbox, editor, 2);
+}
+
+async function validateStyleEditorSheet(toolbox, editor, expectedSheetIndex) {
+ info("validating style editor stylesheet");
+ is(
+ editor.styleSheet.styleSheetIndex,
+ expectedSheetIndex,
+ "loaded stylesheet index matches document stylesheet"
+ );
+
+ const href = editor.styleSheet.href || editor.styleSheet.nodeHref;
+
+ const expectedHref = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [expectedSheetIndex],
+ _expectedSheetIndex => {
+ return (
+ content.document.styleSheets[_expectedSheetIndex].href ||
+ content.document.location.href
+ );
+ }
+ );
+
+ is(href, expectedHref, "loaded stylesheet href matches document stylesheet");
+}
+
+async function testDisabledStyleEditor(view, toolbox) {
+ info("Testing with the style editor disabled");
+
+ info("Switching to the inspector panel in the toolbox");
+ await 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);
+ // Wait for a bit just to make sure the click didn't had an impact
+ await wait(2000);
+
+ 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");
+
+ Services.prefs.clearUserPref("devtools.styleeditor.enabled");
+}
+
+async function testRuleViewLinkLabel(view) {
+ info("Checking the data URL link label");
+ let link = getRuleViewLinkByIndex(view, 1);
+ let labelElem = link.querySelector(".ruleview-rule-source-label");
+ let value = labelElem.textContent;
+ let tooltipText = labelElem.getAttribute("title");
+
+ is(
+ value,
+ encodeURIComponent(STYLESHEET_DATA_URL_CONTENTS) + ":1",
+ "Rule view data URL stylesheet display value matches contents"
+ );
+ is(
+ tooltipText,
+ STYLESHEET_DATA_URL + ":1",
+ "Rule view data URL stylesheet tooltip text matches the full URI path"
+ );
+
+ info("Checking the external link label");
+ link = getRuleViewLinkByIndex(view, 2);
+ labelElem = link.querySelector(".ruleview-rule-source-label");
+ value = labelElem.textContent;
+ tooltipText = labelElem.getAttribute("title");
+
+ is(
+ value,
+ `${EXTERNAL_STYLESHEET_FILE_NAME}:1`,
+ "Rule view external stylesheet display value matches filename and line number"
+ );
+ is(
+ tooltipText,
+ `${EXTERNAL_STYLESHEET_URL}:1`,
+ "Rule view external stylesheet tooltip text matches the full URI path"
+ );
+}
+
+function testUnselectableRuleViewLink(view, index) {
+ const link = getRuleViewLinkByIndex(view, index);
+ const unselectable = link.hasAttribute("unselectable");
+
+ ok(unselectable, "Rule view is unselectable");
+}
+
+function clickLinkByIndex(view, index) {
+ const link = getRuleViewLinkByIndex(view, index);
+ link.scrollIntoView();
+ link.click();
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_update_mask_image_cors.js b/devtools/client/inspector/rules/test/browser_rules_update_mask_image_cors.js
new file mode 100644
index 0000000000..bac63a9572
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_update_mask_image_cors.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/shared-head.js",
+ this
+);
+
+// The mask image is served from example.com while the test page is served from
+// example.org.
+const MASK_SRC = URL_ROOT_COM_SSL + "square_svg.sjs";
+const STYLE_ATTRIBUTE = `mask-image: url("${MASK_SRC}"); width:10px; height: 10px; background: red;`;
+const TEST_URL = `https://example.org/document-builder.sjs?html=<div style='${STYLE_ATTRIBUTE}'>`;
+
+// Used to assert screenshot colors.
+const RED = { r: 255, g: 0, b: 0 };
+
+add_task(async function () {
+ await addTab(TEST_URL);
+ const { inspector, toolbox, view } = await openRuleView();
+
+ info("Open the splitconsole to check for CORS messages");
+ await toolbox.toggleSplitConsole();
+
+ await selectNode("div", inspector);
+
+ info("Take a node screenshot, mask is applied, should be red");
+ const beforeScreenshot = await takeNodeScreenshot(inspector);
+ await assertSingleColorScreenshotImage(beforeScreenshot, 10, 10, RED);
+
+ info("Update a property from the rule view");
+ const heightProperty = getTextProperty(view, 0, { height: "10px" });
+ await setProperty(view, heightProperty, "11px");
+
+ info("Wait until the style has been applied in the content page");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector("div").style.height == "11px"
+ );
+ });
+
+ // Wait for some time in case the image needs to be reloaded, and to allow
+ // error messages (if any) to be rendered.
+ await wait(1000);
+
+ info("Take another screenshot, mask should still apply, should be red");
+ const afterScreenshot = await takeNodeScreenshot(inspector);
+ await assertSingleColorScreenshotImage(afterScreenshot, 10, 11, RED);
+
+ const hud = toolbox.getPanel("webconsole").hud;
+ ok(
+ !findMessageByType(hud, "Cross-Origin Request Blocked", ".error"),
+ "No message was logged about a CORS issue"
+ );
+
+ info("Close split console");
+ await toolbox.toggleSplitConsole();
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_url-click-opens-new-tab.js b/devtools/client/inspector/rules/test/browser_rules_url-click-opens-new-tab.js
new file mode 100644
index 0000000000..0fe12dc859
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_url-click-opens-new-tab.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Bug 1326626 - Tests that clicking a background url opens a new tab
+// even when the devtools is opened in a separate window.
+
+const TEST_URL =
+ "data:text/html,<style>body{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGD4DwABBAEAfbLI3wAAAABJRU5ErkJggg==) no-repeat}";
+
+add_task(async function () {
+ const { inspector } = await openInspectorForURL(TEST_URL, "window");
+ const view = selectRuleView(inspector);
+
+ await selectNode("body", inspector);
+
+ const anchor = view.styleDocument.querySelector(
+ ".ruleview-propertyvaluecontainer a"
+ );
+ ok(anchor, "Link exists for style tag node");
+
+ const onTabOpened = waitForTab();
+ anchor.click();
+
+ info("Wait for the image to open in a new tab");
+ const tab = await onTabOpened;
+ ok(tab, "A new tab opened");
+
+ is(
+ tab.linkedBrowser.currentURI.spec,
+ anchor.href,
+ "The new tab has the expected URL"
+ );
+});
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 0000000000..1d9574d73f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_urls-clickable.js
@@ -0,0 +1,77 @@
+/* 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_SSL + "doc_urls_clickable.html";
+const TEST_IMAGE = URL_ROOT_SSL + "doc_test_image.png";
+const BASE_64_URL =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAA" +
+ "FCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAA" +
+ "BJRU5ErkJggg==";
+
+add_task(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+ await selectNodes(inspector, view);
+});
+
+async function selectNodes(inspector, ruleView) {
+ const relative1 = ".relative1";
+ const relative2 = ".relative2";
+ const absolute = ".absolute";
+ const inline = ".inline";
+ const base64 = ".base64";
+ const noimage = ".noimage";
+ const inlineresolved = ".inline-resolved";
+
+ await 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");
+
+ await 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");
+
+ await selectNode(absolute, inspector);
+ const absoluteLink = ruleView.styleDocument.querySelector(
+ ".ruleview-propertyvaluecontainer a"
+ );
+ ok(absoluteLink, "Link exists for absolute node");
+ is(absoluteLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ await selectNode(inline, inspector);
+ const inlineLink = ruleView.styleDocument.querySelector(
+ ".ruleview-propertyvaluecontainer a"
+ );
+ ok(inlineLink, "Link exists for inline node");
+ is(inlineLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ await selectNode(base64, inspector);
+ const base64Link = ruleView.styleDocument.querySelector(
+ ".ruleview-propertyvaluecontainer a"
+ );
+ ok(base64Link, "Link exists for base64 node");
+ is(base64Link.getAttribute("href"), BASE_64_URL, "href matches");
+
+ await selectNode(inlineresolved, inspector);
+ const inlineResolvedLink = ruleView.styleDocument.querySelector(
+ ".ruleview-propertyvaluecontainer a"
+ );
+ ok(inlineResolvedLink, "Link exists for style tag node");
+ is(inlineResolvedLink.getAttribute("href"), TEST_IMAGE, "href matches");
+
+ await selectNode(noimage, inspector);
+ const 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 0000000000..07a2b6abb8
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles-uneditable.js
@@ -0,0 +1,62 @@
+/* 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(async function () {
+ info("Starting the test with the pref set to true before toolbox is opened");
+ Services.prefs.setBoolPref(PREF_UA_STYLES, true);
+
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await userAgentStylesUneditable(inspector, view);
+
+ info("Resetting " + PREF_UA_STYLES);
+ Services.prefs.clearUserPref(PREF_UA_STYLES);
+});
+
+async function userAgentStylesUneditable(inspector, view) {
+ info("Making sure that UI is not editable for user agent styles");
+
+ await selectNode("a", inspector);
+ const uaRules = view._elementStyle.rules.filter(
+ rule => !rule.editor.isEditable
+ );
+
+ for (const rule of uaRules) {
+ ok(
+ rule.editor.element.hasAttribute("uneditable"),
+ "UA rules have uneditable attribute"
+ );
+
+ const 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");
+
+ const colorswatch = rule.editor.element.querySelector(
+ ".ruleview-colorswatch"
+ );
+ if (colorswatch) {
+ ok(
+ !view.tooltips.getTooltip("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 0000000000..6692badeda
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles.js
@@ -0,0 +1,216 @@
+/* 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("resource://devtools/client/shared/prefs.js");
+
+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(async function () {
+ // Bug 1517210: GC heuristics are broken for this test, so that the test ends up
+ // running out of memory if we don't force to reduce the GC side before/after the test.
+ Cu.forceShrinkingGC();
+
+ requestLongerTimeout(4);
+
+ info("Starting the test with the pref set to true before toolbox is opened");
+ await setUserAgentStylesPref(true);
+
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+
+ info("Making sure that UA styles are visible on initial load");
+ await userAgentStylesVisible(inspector, view);
+
+ info("Making sure that setting the pref to false hides UA styles");
+ await setUserAgentStylesPref(false);
+ await userAgentStylesNotVisible(inspector, view);
+
+ info("Making sure that resetting the pref to true shows UA styles again");
+ await setUserAgentStylesPref(true);
+ await userAgentStylesVisible(inspector, view);
+
+ info("Resetting " + PREF_UA_STYLES);
+ Services.prefs.clearUserPref(PREF_UA_STYLES);
+
+ // Bug 1517210: GC heuristics are broken for this test, so that the test ends up
+ // running out of memory if we don't force to reduce the GC side before/after the test.
+ Cu.forceShrinkingGC();
+});
+
+async 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.
+ const prefObserver = new PrefObserver("devtools.");
+ const oncePrefChanged = new Promise(resolve => {
+ prefObserver.on(PREF_UA_STYLES, onPrefChanged);
+
+ function onPrefChanged() {
+ prefObserver.off(PREF_UA_STYLES, onPrefChanged);
+ resolve();
+ }
+ });
+ Services.prefs.setBoolPref(PREF_UA_STYLES, val);
+ await oncePrefChanged;
+}
+
+async function userAgentStylesVisible(inspector, view) {
+ info("Making sure that user agent styles are currently visible");
+
+ let userRules;
+ let uaRules;
+
+ for (const data of TEST_DATA) {
+ await selectNode(data.selector, inspector);
+ await 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.includes(":any-link");
+ }),
+ "There is a rule for :any-link"
+ );
+ ok(
+ uaRules.some(rule => {
+ return rule.matchedSelectors.includes(":link");
+ }),
+ "There is a rule for :link"
+ );
+ ok(
+ uaRules.some(rule => {
+ return rule.matchedSelectors.length === 1;
+ }),
+ "Inline styles for ua styles"
+ );
+}
+
+async function userAgentStylesNotVisible(inspector, view) {
+ info("Making sure that user agent styles are not currently visible");
+
+ let userRules;
+ let uaRules;
+
+ for (const data of TEST_DATA) {
+ await selectNode(data.selector, inspector);
+ await 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");
+ }
+}
+
+async function compareAppliedStylesWithUI(inspector, view, filter) {
+ info("Making sure that UI is consistent with pageStyle.getApplied");
+
+ const pageStyle = inspector.selection.nodeFront.inspectorFront.pageStyle;
+ let entries = await pageStyle.getApplied(inspector.selection.nodeFront, {
+ inherited: true,
+ matchedSelectors: true,
+ filter,
+ });
+
+ // We may see multiple entries that map to a given rule; filter the
+ // duplicates here to match what the UI does.
+ const entryMap = new Map();
+ for (const entry of entries) {
+ entryMap.set(entry.rule, entry);
+ }
+ entries = [...entryMap.values()];
+
+ const 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) => {
+ const 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 0000000000..649547e514
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_user-property-reset.js
@@ -0,0 +1,117 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+
+ await selectNode("#id1", inspector);
+ await modifyRuleViewWidth("300px", view, inspector);
+ await assertRuleAndMarkupViewWidth("id1", "300px", view, inspector);
+
+ await selectNode("#id2", inspector);
+ await assertRuleAndMarkupViewWidth("id2", "100px", view, inspector);
+ await modifyRuleViewWidth("50px", view, inspector);
+ await assertRuleAndMarkupViewWidth("id2", "50px", view, inspector);
+
+ is(
+ view.store.userProperties.map.size,
+ 2,
+ "The modifications are stored as expected"
+ );
+
+ await reloadBrowser();
+
+ is(
+ view.store.userProperties.map.size,
+ 0,
+ "Properties storing user modifications is cleared after a reload"
+ );
+
+ await selectNode("#id1", inspector);
+ await assertRuleAndMarkupViewWidth("id1", "200px", view, inspector);
+ await selectNode("#id2", inspector);
+ await assertRuleAndMarkupViewWidth("id2", "100px", view, inspector);
+});
+
+function getStyleRule(ruleView) {
+ return ruleView.styleDocument.querySelector(".ruleview-rule");
+}
+
+async function modifyRuleViewWidth(value, ruleView, inspector) {
+ info("Getting the property value element");
+ const valueSpan = getStyleRule(ruleView).querySelector(
+ ".ruleview-propertyvalue"
+ );
+
+ info("Focusing the property value to set it to edit mode");
+ const editor = await 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"
+ );
+ const onBlur = once(editor.input, "blur", true);
+ const onStyleChanged = waitForStyleModification(inspector);
+ EventUtils.sendKey("return");
+ await onBlur;
+ await onStyleChanged;
+
+ info(
+ "Escaping out of the new property field that has been created after " +
+ "the value was edited"
+ );
+ const onNewFieldBlur = once(
+ ruleView.styleDocument.activeElement,
+ "blur",
+ true
+ );
+ EventUtils.sendKey("escape");
+ await onNewFieldBlur;
+}
+
+async function getContainerStyleAttrValue(id, { walker, markup }) {
+ const front = await walker.querySelector(walker.rootNode, "#" + id);
+ const container = markup.getContainer(front);
+
+ let attrIndex = 0;
+ for (const attrName of container.elt.querySelectorAll(".attr-name")) {
+ if (attrName.textContent === "style") {
+ return container.elt.querySelectorAll(".attr-value")[attrIndex];
+ }
+ attrIndex++;
+ }
+ return undefined;
+}
+
+async function assertRuleAndMarkupViewWidth(id, value, ruleView, inspector) {
+ const valueSpan = getStyleRule(ruleView).querySelector(
+ ".ruleview-propertyvalue"
+ );
+ is(
+ valueSpan.textContent,
+ value,
+ "Rule-view style width is " + value + " as expected"
+ );
+
+ const attr = await 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/browser_rules_variables-in-pseudo-element_01.js b/devtools/client/inspector/rules/test/browser_rules_variables-in-pseudo-element_01.js
new file mode 100644
index 0000000000..9e9a2d1d50
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_variables-in-pseudo-element_01.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for pseudo element which defines CSS variable.
+
+const TEST_URI = `
+ <style type='text/css'>
+ div::before {
+ color: var(--color);
+ --color: orange;
+ }
+
+ div {
+ color: var(--color);
+ --color: lime;
+ }
+ </style>
+ <div></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ info("Test the CSS variable which normal element is referring to");
+ checkCSSVariableOutput(
+ view,
+ "div",
+ "color",
+ "ruleview-variable",
+ "--color = lime"
+ );
+
+ info("Test the CSS variable which pseudo element is referring to");
+ checkCSSVariableOutput(
+ view,
+ "div::before",
+ "color",
+ "ruleview-variable",
+ "--color = orange"
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_variables-in-pseudo-element_02.js b/devtools/client/inspector/rules/test/browser_rules_variables-in-pseudo-element_02.js
new file mode 100644
index 0000000000..fba5c163c1
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_variables-in-pseudo-element_02.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for pseudo element which inherits CSS variable.
+
+const TEST_URI = `
+ <style type='text/css'>
+ div::before {
+ color: var(--color);
+ }
+
+ div {
+ color: var(--color);
+ --color: lime;
+ }
+ </style>
+ <div></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ info("Test the CSS variable which normal element is referring to");
+ checkCSSVariableOutput(
+ view,
+ "div",
+ "color",
+ "ruleview-variable",
+ "--color = lime"
+ );
+
+ info("Test the CSS variable which pseudo element is referring to");
+ checkCSSVariableOutput(
+ view,
+ "div::before",
+ "color",
+ "ruleview-variable",
+ "--color = lime"
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_variables_01.js b/devtools/client/inspector/rules/test/browser_rules_variables_01.js
new file mode 100644
index 0000000000..255367fcbb
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_variables_01.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for variables in rule view.
+
+const TEST_URI = URL_ROOT + "doc_variables_1.html";
+
+add_task(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+ await selectNode("#target", inspector);
+
+ info(
+ "Tests basic support for CSS Variables for both single variable " +
+ "and double variable. Formats tested: var(x, constant), var(x, var(y))"
+ );
+
+ const unsetColor = getRuleViewProperty(
+ view,
+ "div",
+ "color"
+ ).valueSpan.querySelector(".ruleview-unmatched-variable");
+ const setColor = unsetColor.previousElementSibling;
+ is(unsetColor.textContent, " red", "red is unmatched in color");
+ is(setColor.textContent, "--color", "--color is not set correctly");
+ is(
+ setColor.dataset.variable,
+ "--color = chartreuse",
+ "--color's dataset.variable is not set correctly"
+ );
+ let previewTooltip = await assertShowPreviewTooltip(view, setColor);
+ await assertTooltipHiddenOnMouseOut(previewTooltip, setColor);
+
+ ok(
+ previewTooltip.panel.textContent.includes("--color = chartreuse"),
+ "CSS variable preview tooltip shows the expected CSS variable"
+ );
+
+ const unsetVar = getRuleViewProperty(
+ view,
+ "div",
+ "background-color"
+ ).valueSpan.querySelector(".ruleview-unmatched-variable");
+ const setVar = unsetVar.nextElementSibling;
+ const setVarName = setVar.querySelector(".ruleview-variable");
+ is(
+ unsetVar.textContent,
+ "--not-set",
+ "--not-set is unmatched in background-color"
+ );
+ is(setVar.textContent, " var(--bg)", "var(--bg) parsed incorrectly");
+ is(setVarName.textContent, "--bg", "--bg is not set correctly");
+ is(
+ setVarName.dataset.variable,
+ "--bg = seagreen",
+ "--bg's dataset.variable is not set correctly"
+ );
+ previewTooltip = await assertShowPreviewTooltip(view, setVarName);
+
+ ok(
+ !previewTooltip.panel.textContent.includes("--color = chartreuse"),
+ "CSS variable preview tooltip no longer shows the previous CSS variable"
+ );
+ ok(
+ previewTooltip.panel.textContent.includes("--bg = seagreen"),
+ "CSS variable preview tooltip shows the new CSS variable"
+ );
+
+ await assertTooltipHiddenOnMouseOut(previewTooltip, setVarName);
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_variables_02.js b/devtools/client/inspector/rules/test/browser_rules_variables_02.js
new file mode 100644
index 0000000000..4100859fb9
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_variables_02.js
@@ -0,0 +1,321 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for variables in rule view.
+
+const TEST_URI = URL_ROOT + "doc_variables_2.html";
+
+add_task(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+
+ await testBasic(inspector, view);
+ await testNestedCssFunctions(inspector, view);
+ await testBorderShorthandAndInheritance(inspector, view);
+ await testSingleLevelVariable(inspector, view);
+ await testDoubleLevelVariable(inspector, view);
+ await testTripleLevelVariable(inspector, view);
+});
+
+async function testBasic(inspector, view) {
+ info(
+ "Test support for basic variable functionality for var() with 2 variables." +
+ "Format: var(--var1, var(--var2))"
+ );
+
+ await selectNode("#a", inspector);
+ const unsetVar = getRuleViewProperty(
+ view,
+ "#a",
+ "font-size"
+ ).valueSpan.querySelector(".ruleview-unmatched-variable");
+ const setVarParent = unsetVar.nextElementSibling;
+ const setVar = getVarFromParent(setVarParent);
+ is(
+ unsetVar.textContent,
+ "--var-not-defined",
+ "--var-not-defined is not set correctly"
+ );
+ is(
+ unsetVar.dataset.variable,
+ "--var-not-defined is not set",
+ "--var-not-defined's dataset.variable is not set correctly"
+ );
+ is(
+ setVarParent.textContent,
+ " var(--var-defined-font-size)",
+ "var(--var-defined-font-size) parsed incorrectly"
+ );
+ is(
+ setVar.textContent,
+ "--var-defined-font-size",
+ "--var-defined-font-size is not set correctly"
+ );
+ is(
+ setVar.dataset.variable,
+ "--var-defined-font-size = 60px",
+ "--bg's dataset.variable is not set correctly"
+ );
+}
+
+async function testNestedCssFunctions(inspector, view) {
+ info(
+ "Test support for variable functionality for a var() nested inside " +
+ "another CSS function. Format: rgb(0, 0, var(--var1, var(--var2)))"
+ );
+
+ await selectNode("#b", inspector);
+ const unsetVarParent = getRuleViewProperty(
+ view,
+ "#b",
+ "color"
+ ).valueSpan.querySelector(".ruleview-unmatched-variable");
+ const unsetVar = getVarFromParent(unsetVarParent);
+ const setVar = unsetVarParent.previousElementSibling;
+ is(
+ unsetVarParent.textContent,
+ " var(--var-defined-r-2)",
+ "var(--var-defined-r-2) not parsed correctly"
+ );
+ is(
+ unsetVar.textContent,
+ "--var-defined-r-2",
+ "--var-defined-r-2 is not set correctly"
+ );
+ is(
+ unsetVar.dataset.variable,
+ "--var-defined-r-2 = 0",
+ "--var-defined-r-2's dataset.variable is not set correctly"
+ );
+ is(
+ setVar.textContent,
+ "--var-defined-r-1",
+ "--var-defined-r-1 is not set correctly"
+ );
+ is(
+ setVar.dataset.variable,
+ "--var-defined-r-1 = 255",
+ "--var-defined-r-1's dataset.variable is not set correctly"
+ );
+}
+
+async function testBorderShorthandAndInheritance(inspector, view) {
+ info(
+ "Test support for variable functionality for shorthands/CSS styles with spaces " +
+ 'like "margin: w x y z". Also tests functionality for inherticance of CSS' +
+ " variables. Format: var(l, var(m)) var(x) rgb(var(r) var(g) var(b))"
+ );
+
+ await selectNode("#c", inspector);
+ const unsetVarL = getRuleViewProperty(
+ view,
+ "#c",
+ "border"
+ ).valueSpan.querySelector(".ruleview-unmatched-variable");
+ const setVarMParent = unsetVarL.nextElementSibling;
+
+ // var(x) is the next sibling of the parent of M
+ const setVarXParent = setVarMParent.parentNode.nextElementSibling;
+
+ // var(r) is the next sibling of var(x), and var(g) is the next sibling of var(r), etc.
+ const setVarRParent = setVarXParent.nextElementSibling;
+ const setVarGParent = setVarRParent.nextElementSibling;
+ const setVarBParent = setVarGParent.nextElementSibling;
+
+ const setVarM = getVarFromParent(setVarMParent);
+ const setVarX = setVarXParent.firstElementChild;
+ const setVarR = setVarRParent.firstElementChild;
+ const setVarG = setVarGParent.firstElementChild;
+ const setVarB = setVarBParent.firstElementChild;
+
+ is(
+ unsetVarL.textContent,
+ "--var-undefined",
+ "--var-undefined is not set correctly"
+ );
+ is(
+ unsetVarL.dataset.variable,
+ "--var-undefined is not set",
+ "--var-undefined's dataset.variable is not set correctly"
+ );
+
+ is(
+ setVarM.textContent,
+ "--var-border-px",
+ "--var-border-px is not set correctly"
+ );
+ is(
+ setVarM.dataset.variable,
+ "--var-border-px = 10px",
+ "--var-border-px's dataset.variable is not set correctly"
+ );
+
+ is(
+ setVarX.textContent,
+ "--var-border-style",
+ "--var-border-style is not set correctly"
+ );
+ is(
+ setVarX.dataset.variable,
+ "--var-border-style = solid",
+ "var-border-style's dataset.variable is not set correctly"
+ );
+
+ is(
+ setVarR.textContent,
+ "--var-border-r",
+ "--var-defined-r is not set correctly"
+ );
+ is(
+ setVarR.dataset.variable,
+ "--var-border-r = 255",
+ "--var-defined-r's dataset.variable is not set correctly"
+ );
+
+ is(
+ setVarG.textContent,
+ "--var-border-g",
+ "--var-defined-g is not set correctly"
+ );
+ is(
+ setVarG.dataset.variable,
+ "--var-border-g = 0",
+ "--var-defined-g's dataset.variable is not set correctly"
+ );
+
+ is(
+ setVarB.textContent,
+ "--var-border-b",
+ "--var-defined-b is not set correctly"
+ );
+ is(
+ setVarB.dataset.variable,
+ "--var-border-b = 0",
+ "--var-defined-b's dataset.variable is not set correctly"
+ );
+}
+
+async function testSingleLevelVariable(inspector, view) {
+ info(
+ "Test support for variable functionality of a single level of " +
+ "undefined variables. Format: var(x, constant)"
+ );
+
+ await selectNode("#d", inspector);
+ const unsetVar = getRuleViewProperty(
+ view,
+ "#d",
+ "font-size"
+ ).valueSpan.querySelector(".ruleview-unmatched-variable");
+
+ is(
+ unsetVar.textContent,
+ "--var-undefined",
+ "--var-undefined is not set correctly"
+ );
+ is(
+ unsetVar.dataset.variable,
+ "--var-undefined is not set",
+ "--var-undefined's dataset.variable is not set correctly"
+ );
+}
+
+async function testDoubleLevelVariable(inspector, view) {
+ info(
+ "Test support for variable functionality of double level of " +
+ "undefined variables. Format: var(x, var(y, constant))"
+ );
+
+ await selectNode("#e", inspector);
+ const allUnsetVars = getRuleViewProperty(
+ view,
+ "#e",
+ "color"
+ ).valueSpan.querySelectorAll(".ruleview-unmatched-variable");
+
+ is(allUnsetVars.length, 2, "The number of unset variables is mismatched.");
+
+ const unsetVar1 = allUnsetVars[0];
+ const unsetVar2 = allUnsetVars[1];
+
+ is(
+ unsetVar1.textContent,
+ "--var-undefined",
+ "--var-undefined is not set correctly"
+ );
+ is(
+ unsetVar1.dataset.variable,
+ "--var-undefined is not set",
+ "--var-undefined's dataset.variable is not set correctly"
+ );
+
+ is(
+ unsetVar2.textContent,
+ "--var-undefined-2",
+ "--var-undefined is not set correctly"
+ );
+ is(
+ unsetVar2.dataset.variable,
+ "--var-undefined-2 is not set",
+ "--var-undefined-2's dataset.variable is not set correctly"
+ );
+}
+
+async function testTripleLevelVariable(inspector, view) {
+ info(
+ "Test support for variable functionality of triple level of " +
+ "undefined variables. Format: var(x, var(y, var(z, constant)))"
+ );
+
+ await selectNode("#f", inspector);
+ const allUnsetVars = getRuleViewProperty(
+ view,
+ "#f",
+ "border-style"
+ ).valueSpan.querySelectorAll(".ruleview-unmatched-variable");
+
+ is(allUnsetVars.length, 3, "The number of unset variables is mismatched.");
+
+ const unsetVar1 = allUnsetVars[0];
+ const unsetVar2 = allUnsetVars[1];
+ const unsetVar3 = allUnsetVars[2];
+
+ is(
+ unsetVar1.textContent,
+ "--var-undefined",
+ "--var-undefined is not set correctly"
+ );
+ is(
+ unsetVar1.dataset.variable,
+ "--var-undefined is not set",
+ "--var-undefined's dataset.variable is not set correctly"
+ );
+
+ is(
+ unsetVar2.textContent,
+ "--var-undefined-2",
+ "--var-undefined-2 is not set correctly"
+ );
+ is(
+ unsetVar2.dataset.variable,
+ "--var-undefined-2 is not set",
+ "--var-defined-r-2's dataset.variable is not set correctly"
+ );
+
+ is(
+ unsetVar3.textContent,
+ "--var-undefined-3",
+ "--var-undefined-3 is not set correctly"
+ );
+ is(
+ unsetVar3.dataset.variable,
+ "--var-undefined-3 is not set",
+ "--var-defined-r-3's dataset.variable is not set correctly"
+ );
+}
+
+function getVarFromParent(varParent) {
+ return varParent.firstElementChild.firstElementChild;
+}
diff --git a/devtools/client/inspector/rules/test/browser_rules_variables_03-case-sensitive.js b/devtools/client/inspector/rules/test/browser_rules_variables_03-case-sensitive.js
new file mode 100644
index 0000000000..21fd00ec42
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_variables_03-case-sensitive.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that inherited CSS variables are case senstive.
+
+const TEST_URI = URL_ROOT + "doc_variables_3.html";
+
+add_task(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+ await selectNode("#target", inspector);
+
+ const upperCaseVarEl = getRuleViewProperty(
+ view,
+ "div",
+ "color"
+ ).valueSpan.querySelector(".ruleview-variable");
+ const lowerCaseVarEl = getRuleViewProperty(
+ view,
+ "div",
+ "background"
+ ).valueSpan.querySelector(".ruleview-variable");
+
+ is(upperCaseVarEl.textContent, "--COLOR", "upper case variable is matched");
+ is(
+ lowerCaseVarEl.textContent,
+ "--background",
+ "lower case variable is matched"
+ );
+});
diff --git a/devtools/client/inspector/rules/test/browser_rules_variables_04-valid-chars.js b/devtools/client/inspector/rules/test/browser_rules_variables_04-valid-chars.js
new file mode 100644
index 0000000000..4e163b1fb7
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_variables_04-valid-chars.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for variables in rule view.
+
+const TEST_URI = URL_ROOT + "doc_variables_4.html";
+
+add_task(async function () {
+ await addTab(TEST_URI);
+ const { inspector, view } = await openRuleView();
+
+ await testNumber(inspector, view);
+ await testDash(inspector, view);
+});
+
+async function testNumber(inspector, view) {
+ info(
+ "Test support for allowing vars that begin with a number" +
+ "Format: --10: 10px;"
+ );
+
+ await selectNode("#a", inspector);
+
+ const upperCaseVarEl = getRuleViewProperty(
+ view,
+ "#a",
+ "font-size"
+ ).valueSpan.querySelector(".ruleview-variable");
+
+ is(
+ upperCaseVarEl.dataset.variable,
+ "--10 = 10px",
+ "variable that starts with a number is valid"
+ );
+}
+
+async function testDash(inspector, view) {
+ info(
+ "Test support for allowing vars that begin with a dash" +
+ "Format: ---blue: blue;"
+ );
+
+ await selectNode("#b", inspector);
+
+ const upperCaseVarEl = getRuleViewProperty(
+ view,
+ "#b",
+ "color"
+ ).valueSpan.querySelector(".ruleview-variable");
+
+ is(
+ upperCaseVarEl.dataset.variable,
+ "---blue = blue",
+ "variable that starts with a dash is valid"
+ );
+}
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 0000000000..7595bc2e79
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_author-sheet.html
@@ -0,0 +1,34 @@
+<!-- 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 style = "data:text/css,a { background-color: seagreen; }";
+ var uri = SpecialPowers.Services.io.newURI(style);
+ var windowUtils = SpecialPowers.getDOMWindowUtils(window);
+ 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 0000000000..b408c2f6b0
--- /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_class_panel_autocomplete.html b/devtools/client/inspector/rules/test/doc_class_panel_autocomplete.html
new file mode 100644
index 0000000000..9b5e3f2c01
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_class_panel_autocomplete.html
@@ -0,0 +1,41 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html class="auto-html-class-1 auto-html-class-2 auto-bold">
+<head>
+ <title>Class panel autocomplete test</title>
+
+ <link href="./doc_class_panel_autocomplete_stylesheet.css" rel="stylesheet" type="text/css">
+ <style>
+ .auto-inline-class-1 {
+ padding: 1em;
+ }
+ .auto-inline-class-2 {
+ padding: 2em;
+ }
+ .auto-inline-class-3 {
+ padding: 3em;
+ }
+
+ .auto-inline-class-1,
+ div.auto-inline-class-2,
+ p:first-of-type.auto-inline-class-3,
+ .auto-inline-class-4 {
+ background-color: blue;
+ }
+
+ :root .auto-bold .auto-inline-class-5 {
+ font-size: bold;
+ }
+ </style>
+ <script defer>
+ "use strict";
+ const x = document.styleSheets[0];
+ x.insertRule(".auto-cssom-primary-color { color: tomato; }", 1);
+ </script>
+</head>
+<body class="auto-body-class-1 auto-body-class-2 auto-bold">
+ <div id="auto-div-id-1" class="auto-div-class-1 auto-div-class-2 auto-bold"> the ocean </div>
+ <div id="auto-div-id-2" class="auto-div-class-1 auto-div-class-2 auto-bold"> roaring </div>
+ <div id="auto-div-id-3"> ahead </div>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_class_panel_autocomplete_stylesheet.css b/devtools/client/inspector/rules/test/doc_class_panel_autocomplete_stylesheet.css
new file mode 100644
index 0000000000..c8b83ac6fc
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_class_panel_autocomplete_stylesheet.css
@@ -0,0 +1,20 @@
+.auto-stylesheet-class-1 {
+ padding: 1em;
+}
+.auto-stylesheet-class-2 {
+ padding: 2em;
+}
+.auto-stylesheet-class-3 {
+ padding: 3em;
+}
+
+.auto-stylesheet-class-1,
+div.auto-stylesheet-class-2,
+p:first-of-type.auto-stylesheet-class-3,
+.auto-stylesheet-class-4 {
+ background-color: blue;
+}
+
+:root .auto-bold .auto-stylesheet-class-5 {
+ font-size: bold;
+}
diff --git a/devtools/client/inspector/rules/test/doc_conditional_import.css b/devtools/client/inspector/rules/test/doc_conditional_import.css
new file mode 100644
index 0000000000..32529fc608
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_conditional_import.css
@@ -0,0 +1,3 @@
+h1, [test-hint=imported-conditional] {
+ color: rebeccapurple;
+}
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 0000000000..8af8ae950f
--- /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() {
+ const 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 0000000000..ea1a3d986b
--- /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 0000000000..77c73299ea
--- /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 0000000000..712ba78fb6
--- /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 0000000000..5aa5e2c6cb
--- /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 0000000000..83f0c87b12
--- /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 0000000000..da1b4c0b3f
--- /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 0000000000..6c44cfcf98
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_cssom.html
@@ -0,0 +1,25 @@
+<!-- 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() {
+ const x = document.styleSheets[0];
+ x.insertRule("div { color: seagreen; }", 1);
+
+ // Add a rule with a leading newline, to test that inspector can handle it.
+ x.insertRule("\ndiv { font-weight: bold; }", 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 0000000000..09bf501d59
--- /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_edit_imported_selector.html b/devtools/client/inspector/rules/test/doc_edit_imported_selector.html
new file mode 100644
index 0000000000..5b2120850a
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_edit_imported_selector.html
@@ -0,0 +1,13 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Edit a selector in an imported stylesheet</title>
+ <link rel="stylesheet" type="text/css" href="doc_content_stylesheet_script.css">
+</head>
+<body>
+ <div id="target">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 0000000000..cb2df9feb6
--- /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_grid_area_gridline_names.html b/devtools/client/inspector/rules/test/doc_grid_area_gridline_names.html
new file mode 100644
index 0000000000..0fe8527c01
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_grid_area_gridline_names.html
@@ -0,0 +1,59 @@
+<!doctype html>
+<style type='text/css'>
+ /* Implicit gridlines created from explicit grid areas. */
+ .wrapperA {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ grid-auto-rows: minmax(100px, auto);
+ grid-template-areas:
+ "header header header"
+ "main main main";
+ }
+
+ .header {
+ grid-column: header-start / header-end;
+ grid-row: header-start / header-end;
+ }
+
+ .main {
+ grid-area: main;
+ }
+
+ /* Implicit grid areas created from explicit gridlines */
+ .wrapperB {
+ display: grid;
+ grid-template-columns: [main-start] 1fr [content-start] 1fr [content-end main-end];
+ grid-template-rows: [main-start] 100px [content-start] 100px [content-end main-end];
+ }
+
+ .contentArea {
+ grid-column: content-start / content-end;
+ grid-row: content-start / content-end;
+ }
+
+ .wrapperC {
+ display: grid;
+ grid-template-columns: [a-start b-end] 1fr [c];
+ }
+
+ .a {
+ grid-column: a-start / a-end;
+ }
+
+ .b {
+ grid-column: b-start / b-end;
+ }
+</style>
+<div>
+ <div class="wrapperA">
+ <div class="header">Header</div>
+ <div class="main">Content</div>
+ </div>
+ <div class="wrapperB">
+ <div class="contentArea">Implicit area named "content".</div>
+ </div>
+ <div class="wrapperC">
+ <div class="a">A.</div>
+ <div class="b">B.</div>
+ </div>
+</div> \ No newline at end of file
diff --git a/devtools/client/inspector/rules/test/doc_grid_names.html b/devtools/client/inspector/rules/test/doc_grid_names.html
new file mode 100644
index 0000000000..0aefb18585
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_grid_names.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<style type='text/css'>
+ #grid {
+ display: grid;
+ grid-template-rows: [row1-start] auto [row2-start] auto [row2-end];
+ grid-template-columns: [col1-start] 100px [col2-start] 100px [col3-start] 100px [col3-end];
+ }
+ #cell3 {
+ grid-column: "col3-start";
+ grid-row: "row2-start";
+ }
+</style>
+<div id="grid">
+ <div>cell1</div>
+ <div id="cell2">cell2</div>
+ <div id="cell3">cell3</div>
+</div> \ No newline at end of file
diff --git a/devtools/client/inspector/rules/test/doc_imported_anonymous_layer.css b/devtools/client/inspector/rules/test/doc_imported_anonymous_layer.css
new file mode 100644
index 0000000000..fb537b53ae
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_imported_anonymous_layer.css
@@ -0,0 +1,4 @@
+h1, [test-hint=imported-anonymous-layer--no-rule-layer] {
+ color:cyan;
+ outline: 10px solid cyan;
+}
diff --git a/devtools/client/inspector/rules/test/doc_imported_named_layer.css b/devtools/client/inspector/rules/test/doc_imported_named_layer.css
new file mode 100644
index 0000000000..0817f84e9f
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_imported_named_layer.css
@@ -0,0 +1,12 @@
+@media screen {
+ h1, [test-hint=imported-named-layer--no-rule-layer] {
+ color:tomato;
+ border: 10px dotted currentColor;
+ }
+
+ @layer in-imported-stylesheet {
+ h1, [test-hint=imported-named-layer--named-layer] {
+ color: purple;
+ }
+ }
+}
diff --git a/devtools/client/inspector/rules/test/doc_imported_no_layer.css b/devtools/client/inspector/rules/test/doc_imported_no_layer.css
new file mode 100644
index 0000000000..9290eebc08
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_imported_no_layer.css
@@ -0,0 +1,3 @@
+h1, [test-hint=imported-no-layer--no-rule-layer] {
+ color: gold;
+}
diff --git a/devtools/client/inspector/rules/test/doc_inactive_css_xul.xhtml b/devtools/client/inspector/rules/test/doc_inactive_css_xul.xhtml
new file mode 100644
index 0000000000..ebe347997b
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_inactive_css_xul.xhtml
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://devtools/skin/light-theme.css"?>
+<window class="theme-light"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Inactive CSS for XUL documents">
+
+ <vbox>
+ <html:img
+ id="test-img-in-xul"
+ style="width:10px; height: 10px; grid-column-gap: 5px;">
+ </html:img>
+ </vbox>
+</window>
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 0000000000..cb107d4244
--- /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 0000000000..ff96a6b542
--- /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 0000000000..cd3a74817f
--- /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 0000000000..9964bf5069
--- /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: #f06;
+ }
+}
+
+@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 0000000000..64582ed358
--- /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 0000000000..4e02c32f05
--- /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 0000000000..f4706dad87
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_media_queries.html
@@ -0,0 +1,42 @@
+<html>
+<head>
+ <title>test</title>
+ <script type="application/javascript">
+
+ </script>
+ <style>
+ div {
+ width: 1000px;
+ height: 100px;
+ background-color: #f00;
+ }
+
+ @media screen and (min-width: 1px) {
+ div {
+ width: 200px;
+ background-color: yellow;
+ }
+ }
+
+ @media (prefers-color-scheme: dark) {
+ div {
+ background-color: darkblue;
+ }
+ }
+ </style>
+ <script>
+ "use strict";
+ if (window.location.search == "?constructed") {
+ const sheet = new CSSStyleSheet();
+ sheet.replaceSync(`div { z-index: 0 }`);
+ document.adoptedStyleSheets.push(sheet);
+ }
+ </script>
+</head>
+<body>
+<div></div>
+<iframe
+ src='https://example.org/document-builder.sjs?html=<style>html { background: cyan;} @media (prefers-color-scheme: dark) {html {background: darkred;}}'>
+</iframe>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_print_media_simulation.html b/devtools/client/inspector/rules/test/doc_print_media_simulation.html
new file mode 100644
index 0000000000..e0caa25296
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_print_media_simulation.html
@@ -0,0 +1,27 @@
+<html>
+<head>
+ <title>test print media simulation</title>
+ <script type="application/javascript">
+
+ </script>
+ <style>
+ div {
+ width: 1000px;
+ height: 100px;
+ background-color: #f00;
+ }
+
+ @media print {
+ div {
+ background-color: #00f;
+ }
+ }
+ </style>
+</head>
+<body>
+<div></div>
+<iframe
+ src='https://example.org/document-builder.sjs?html=<style>html { background-color: %23ff0;} @media print {html {background-color: %230ff;}}'>
+</iframe>
+</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 0000000000..26f94c6d3e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_pseudoelement.html
@@ -0,0 +1,149 @@
+<!-- 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;
+}
+
+#list::marker {
+ color: purple;
+}
+
+dialog::backdrop {
+ background-color: tomato;
+}
+
+ </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>
+
+ <ol>
+ <li id="list" class="box">List element</li>
+ </ol>
+
+ <dialog>In dialog</dialog>
+ <script>
+ "use strict";
+ // This is the only way to have the ::backdrop style to be applied
+ document.querySelector("dialog").showModal()
+ </script>
+ </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 0000000000..5a157f384c
--- /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_rules_imported_stylesheet_edit.html b/devtools/client/inspector/rules/test/doc_rules_imported_stylesheet_edit.html
new file mode 100644
index 0000000000..f1c87fa0ab
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_rules_imported_stylesheet_edit.html
@@ -0,0 +1,4 @@
+<style type="text/css">
+ @import url("./sjs_imported_stylesheet_edit.sjs");
+</style>
+<body><div></div></body>
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 0000000000..a9b437a408
--- /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 0000000000..0f7486fd91
--- /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 0000000000..0014e55fe9
--- /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 0000000000..0ff6c471bb
--- /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_sourcemaps2.css b/devtools/client/inspector/rules/test/doc_sourcemaps2.css
new file mode 100644
index 0000000000..c3e4ae7b40
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_sourcemaps2.css
@@ -0,0 +1,5 @@
+div {
+ color: #ff0066; }
+
+span {
+ background-color: #EEE; }
diff --git a/devtools/client/inspector/rules/test/doc_sourcemaps2.css^headers^ b/devtools/client/inspector/rules/test/doc_sourcemaps2.css^headers^
new file mode 100644
index 0000000000..a5e1f3c7a0
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_sourcemaps2.css^headers^
@@ -0,0 +1 @@
+X-SourceMap: doc_sourcemaps.css.map
diff --git a/devtools/client/inspector/rules/test/doc_sourcemaps2.html b/devtools/client/inspector/rules/test/doc_sourcemaps2.html
new file mode 100644
index 0000000000..e6990700e7
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_sourcemaps2.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_sourcemaps2.css"/>
+</head>
+<body>
+ <div>source maps <span>testcase</span></div>
+</body>
+</html>
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 0000000000..e49e1f5871
--- /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 0000000000..769c636340
--- /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 0000000000..e2f77934e2
--- /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("https://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 0000000000..b0265a703e
--- /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/doc_variables_1.html b/devtools/client/inspector/rules/test/doc_variables_1.html
new file mode 100644
index 0000000000..5b7905f47e
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_variables_1.html
@@ -0,0 +1,23 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <title>variables test</title>
+
+ <style>
+ * {
+ --color: tomato;
+ --bg: violet;
+ }
+
+ div {
+ --color: chartreuse;
+ color: var(--color, red);
+ background-color: var(--not-set, var(--bg));
+ }
+ </style>
+</head>
+<body>
+ <div id="target" style="--bg: seagreen;"> the ocean </div>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_variables_2.html b/devtools/client/inspector/rules/test/doc_variables_2.html
new file mode 100644
index 0000000000..4215ea87c6
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_variables_2.html
@@ -0,0 +1,45 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <title>variables test</title>
+ <style>
+ :root {
+ --var-border-px: 10px;
+ --var-border-style: solid;
+ --var-border-r: 255;
+ --var-border-g: 0;
+ --var-border-b: 0;
+ }
+ #a {
+ --var-defined-font-size: 60px;
+ font-size: var(--var-not-defined, var(--var-defined-font-size));
+ }
+ #b {
+ --var-defined-r-1: 255;
+ --var-defined-r-2: 0;
+ color: rgb(var(--var-defined-r-1, var(--var-defined-r-2)), 0, 0);
+ }
+ #c {
+ border: var(--var-undefined, var(--var-border-px)) var(--var-border-style) rgb(var(--var-border-r), var(--var-border-g), var(--var-border-b))
+ }
+ #d {
+ font-size: var(--var-undefined, 30px);
+ }
+ #e {
+ color: var(--var-undefined, var(--var-undefined-2, blue));
+ }
+ #f {
+ border-style: var(--var-undefined, var(--var-undefined-2, var(--var-undefined-3, solid)));
+ }
+ </style>
+</head>
+<body>
+ <div id="a">A</div><br>
+ <div id="b">B</div><br>
+ <div id="c">C</div><br>
+ <div id="d">D</div><br>
+ <div id="e">E</div><br>
+ <div id="f">F</div>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_variables_3.html b/devtools/client/inspector/rules/test/doc_variables_3.html
new file mode 100644
index 0000000000..61027c7b23
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_variables_3.html
@@ -0,0 +1,16 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html style="--COLOR: green; --background: black">
+<head>
+
+ <style>
+ div {
+ background: var(--background);
+ color: var(--COLOR);
+ }
+ </style>
+</head>
+<body>
+ <div id="target">test</div>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_variables_4.html b/devtools/client/inspector/rules/test/doc_variables_4.html
new file mode 100644
index 0000000000..81441c67c2
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_variables_4.html
@@ -0,0 +1,23 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <title>variables test</title>
+ <style>
+ :root {
+ --10: 10px;
+ ---blue: blue;
+ }
+ #a {
+ font-size: var(--10);
+ }
+ #b {
+ color: var(---blue);
+ }
+ </style>
+</head>
+<body>
+ <div id="a">A</div><br>
+ <div id="b">B</div>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_visited.html b/devtools/client/inspector/rules/test/doc_visited.html
new file mode 100644
index 0000000000..b18e8c3da1
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_visited.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <style type='text/css'>
+ a:visited, #visited-and-other-matched-selector {
+ background-color: transparent;
+ border-color: lime;
+ color: rgba(0, 255, 0, 0.8);
+ font-size: 100px;
+ margin-left: 50px;
+ text-decoration-color: lime;
+ text-emphasis-color: seagreen;
+ }
+ a:visited { color: lime; }
+ a:link { color: blue; }
+ a { color: pink; }
+ </style>
+ </head>
+ <body>
+ <a href="./doc_visited.html" id="visited">visited link</a>
+ <a href="#" id="unvisited">unvisited link</a>
+ <a href="./doc_visited.html" id="visited-and-other-matched-selector">
+ visited and other matched selector
+ </a>
+ </body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_visited_in_media_query.html b/devtools/client/inspector/rules/test/doc_visited_in_media_query.html
new file mode 100644
index 0000000000..ff95cfbc73
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_visited_in_media_query.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <style type='text/css'>
+ @media (min-width:1px)
+ {
+ a {
+ color: lime;
+ margin-left: 1px;
+ }
+ }
+ </style>
+ </head>
+ <body>
+ <a href="./doc_visited_in_media_query.html" id="visited">visited link</a>
+ </body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_visited_with_style_attribute.html b/devtools/client/inspector/rules/test/doc_visited_with_style_attribute.html
new file mode 100644
index 0000000000..0f07fb9d48
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_visited_with_style_attribute.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ </head>
+ <body>
+ <a href="./doc_visited_with_style_attribute.html" style="margin: 0;" id="visited">visited link</a>
+ </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 0000000000..40c7bbe3d5
--- /dev/null
+++ b/devtools/client/inspector/rules/test/head.js
@@ -0,0 +1,1284 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+"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("resource://devtools/client/shared/inplace-editor.js");
+
+const {
+ COMPATIBILITY_TOOLTIP_MESSAGE,
+} = require("resource://devtools/client/inspector/rules/constants.js");
+
+const ROOT_TEST_DIR = getRootDirectory(gTestPath);
+
+const STYLE_INSPECTOR_L10N = new LocalizationHelper(
+ "devtools/shared/locales/styleinspector.properties"
+);
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.defaultColorUnit");
+});
+
+/**
+ * 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
+ */
+async function hideTooltipAndWaitForRuleViewChanged(editorTooltip, view) {
+ const onModified = view.once("ruleview-changed");
+ const onHidden = editorTooltip.tooltip.once("hidden");
+ editorTooltip.hide();
+ await onModified;
+ await 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 = async function (validatorFn, desc = "untitled") {
+ let i = 0;
+ while (true) {
+ info("Checking: " + desc);
+ if (await validatorFn()) {
+ ok(true, "Success: " + desc);
+ break;
+ }
+ i++;
+ if (i > 10) {
+ ok(false, "Failure: " + desc);
+ break;
+ }
+ await new Promise(r => setTimeout(r, 200));
+ }
+};
+
+/**
+ * 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 = async function (
+ ruleView,
+ colorPicker,
+ newRgba,
+ expectedChange
+) {
+ let onComputedStyleChanged;
+ if (expectedChange) {
+ const { selector, name, value } = expectedChange;
+ onComputedStyleChanged = waitForComputedStyleProperty(
+ selector,
+ null,
+ name,
+ value
+ );
+ }
+ const onRuleViewChanged = ruleView.once("ruleview-changed");
+ info("Getting the spectrum colorpicker object");
+ const 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");
+ await onRuleViewChanged;
+
+ if (expectedChange) {
+ info("Waiting for the style to be applied on the page");
+ await 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 = async function (
+ view,
+ ruleIndex,
+ propIndex,
+ newRgba,
+ expectedChange
+) {
+ const ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
+ const propEditor = ruleEditor.rule.textProps[propIndex].editor;
+ const swatch = propEditor.valueSpan.querySelector(".ruleview-colorswatch");
+ const cPicker = view.tooltips.getTooltip("colorPicker");
+
+ info("Opening the colorpicker by clicking the color swatch");
+ const onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ await onColorPickerReady;
+
+ await 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 = async function (
+ view,
+ ruleIndex,
+ propIndex,
+ coords,
+ expectedChange
+) {
+ const ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
+ const propEditor = ruleEditor.rule.textProps[propIndex].editor;
+ const swatch = propEditor.valueSpan.querySelector(".ruleview-bezierswatch");
+ const bezierTooltip = view.tooltips.getTooltip("cubicBezier");
+
+ info("Opening the cubicBezier by clicking the swatch");
+ const onBezierWidgetReady = bezierTooltip.once("ready");
+ swatch.click();
+ await onBezierWidgetReady;
+
+ const widget = await bezierTooltip.widget;
+
+ info("Simulating a change of curve in the widget");
+ const onRuleViewChanged = view.once("ruleview-changed");
+ widget.coordinates = coords;
+ await onRuleViewChanged;
+
+ if (expectedChange) {
+ info("Waiting for the style to be applied on the page");
+ const { selector, name, value } = expectedChange;
+ await waitForComputedStyleProperty(selector, null, name, value);
+ }
+
+ return { propEditor, swatch, bezierTooltip };
+};
+
+/**
+ * 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.
+ * @param {String} name
+ * The name for the new property
+ * @param {String} value
+ * The value for the new property
+ * @param {Object=} options
+ * @param {String=} options.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=} options.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 = async function (
+ view,
+ ruleIndex,
+ name,
+ value,
+ { commitValueWith = "VK_RETURN", blurNewProperty = true } = {}
+) {
+ info("Adding new property " + name + ":" + value + " to rule " + ruleIndex);
+
+ const ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
+ let editor = await focusNewRuleViewProperty(ruleEditor);
+ const numOfProps = ruleEditor.rule.textProps.length;
+
+ const onMutations = new Promise(r => {
+ // If the rule index is 0, then we are updating the rule for the "element"
+ // selector in the rule view.
+ // This rule is actually updating the style attribute of the element, and
+ // therefore we can expect mutations.
+ // For any other rule index, no mutation should be created, we can resolve
+ // immediately.
+ if (ruleIndex !== 0) {
+ r();
+ }
+
+ // Use CSS.escape for the name in order to match the logic at
+ // devtools/client/fronts/inspector/rule-rewriter.js
+ // This leads to odd values in the style attribute and might change in the
+ // future. See https://bugzilla.mozilla.org/show_bug.cgi?id=1765943
+ const expectedAttributeValue = `${CSS.escape(name)}: ${value}`;
+ view.inspector.walker.on(
+ "mutations",
+ function onWalkerMutations(mutations) {
+ // Wait until we receive a mutation which updates the style attribute
+ // with the expected value.
+ const receivedLastMutation = mutations.some(
+ mut =>
+ mut.attributeName === "style" &&
+ mut.newValue.includes(expectedAttributeValue)
+ );
+ if (receivedLastMutation) {
+ view.inspector.walker.off("mutations", onWalkerMutations);
+ r();
+ }
+ }
+ );
+ });
+
+ info("Adding name " + name);
+ editor.input.value = name;
+ const onNameAdded = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ await onNameAdded;
+
+ // Focus has moved to the value inplace-editor automatically.
+ editor = inplaceEditor(view.styleDocument.activeElement);
+ const textProps = ruleEditor.rule.textProps;
+ const 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).
+ const onPreview = view.once("ruleview-changed");
+ editor.input.value = value;
+ view.debounce.flush();
+ await onPreview;
+
+ const onValueAdded = view.once("ruleview-changed");
+ EventUtils.synthesizeKey(commitValueWith, {}, view.styleWindow);
+ await onValueAdded;
+
+ info(
+ "Waiting for DOM mutations in case the property was added to the element style"
+ );
+ await onMutations;
+
+ 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 {Object} options
+ * @param {Boolean} options.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.
+ * @param {number} options.flushCount
+ * The ruleview uses a manual flush for tests only, and some properties are
+ * only updated after several flush. Allow tests to trigger several flushes
+ * if necessary. Defaults to 1.
+ */
+var setProperty = async function (
+ view,
+ textProp,
+ value,
+ { blurNewProperty = true, flushCount = 1 } = {}
+) {
+ info("Set property to: " + value);
+ await focusEditableField(view, textProp.editor.valueSpan);
+
+ // Because of the manual flush approach used for tests, we might have an
+ // unknown number of debounced "preview" requests . Each preview should
+ // synchronously emit "start-preview-property-value".
+ // Listen to both this event and "ruleview-changed" which is emitted at the
+ // end of a preview and make sure each preview completes successfully.
+ let previewStartedCounter = 0;
+ const onStartPreview = () => previewStartedCounter++;
+ view.on("start-preview-property-value", onStartPreview);
+
+ let previewCounter = 0;
+ const onPreviewApplied = () => previewCounter++;
+ view.on("ruleview-changed", onPreviewApplied);
+
+ if (value === null) {
+ const onPopupOpened = once(view.popup, "popup-opened");
+ EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow);
+ await onPopupOpened;
+ } else {
+ EventUtils.sendString(value, view.styleWindow);
+ }
+
+ info(`Flush debounced ruleview methods (remaining: ${flushCount})`);
+ view.debounce.flush();
+ await waitFor(() => previewCounter >= previewStartedCounter);
+
+ flushCount--;
+
+ while (flushCount > 0) {
+ // Wait for some time before triggering a new flush to let new debounced
+ // functions queue in-between.
+ await wait(100);
+
+ info(`Flush debounced ruleview methods (remaining: ${flushCount})`);
+ view.debounce.flush();
+ await waitFor(() => previewCounter >= previewStartedCounter);
+
+ flushCount--;
+ }
+
+ view.off("start-preview-property-value", onStartPreview);
+ view.off("ruleview-changed", onPreviewApplied);
+
+ const onValueDone = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+
+ info("Waiting for another ruleview-changed after setting property");
+ await onValueDone;
+
+ if (blurNewProperty) {
+ info("Force blur on the active element");
+ view.styleDocument.activeElement.blur();
+ }
+};
+
+/**
+ * Change the name 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} name
+ * The new property name.
+ */
+var renameProperty = async function (view, textProp, name) {
+ await focusEditableField(view, textProp.editor.nameSpan);
+
+ const onNameDone = view.once("ruleview-changed");
+ info(`Rename the property to ${name}`);
+ EventUtils.sendString(name, view.styleWindow);
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ info("Wait for property name.");
+ await onNameDone;
+ // Renaming the property auto-advances the focus to the value input. Exiting without
+ // committing will still fire a change event. @see TextPropertyEditor._onValueDone().
+ // Wait for that event too before proceeding.
+ const onValueDone = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+ info("Wait for property value.");
+ await onValueDone;
+};
+
+/**
+ * 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 = async function (view, textProp, blurNewProperty = true) {
+ await focusEditableField(view, textProp.editor.nameSpan);
+
+ const onModifications = view.once("ruleview-changed");
+ info("Deleting the property name now");
+ EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow);
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ await 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 = async function (view, textProp) {
+ const onRuleViewRefreshed = view.once("ruleview-changed");
+ textProp.editor.enable.click();
+ await onRuleViewRefreshed;
+};
+
+/**
+ * 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
+ */
+async function addNewRule(inspector, view) {
+ info("Adding the new rule using the button");
+ view.addRuleButton.click();
+
+ info("Waiting for rule view to change");
+ await 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
+ */
+async function addNewRuleAndDismissEditor(
+ inspector,
+ view,
+ expectedSelector,
+ expectedIndex
+) {
+ await addNewRule(inspector, view);
+
+ info("Getting the new rule at index " + expectedIndex);
+ const ruleEditor = getRuleViewRuleEditor(view, expectedIndex);
+ const 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("KEY_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
+ */
+async function sendKeysAndWaitForFocus(view, element, keys) {
+ const onFocus = once(element, "focus", true);
+ for (const key of keys) {
+ EventUtils.sendKey(key, view.styleWindow);
+ }
+ await onFocus;
+}
+
+/**
+ * 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(mutations) {
+ for (const mutation of mutations) {
+ if (
+ mutation.type === "attributes" &&
+ mutation.attributeName === "style"
+ ) {
+ inspector.off("markupmutation", checkForStyleModification);
+ resolve();
+ return;
+ }
+ }
+ }
+ inspector.on("markupmutation", checkForStyleModification);
+ });
+}
+
+/**
+ * Click on the icon next to the selector of a CSS rule in the Rules view
+ * to toggle the selector highlighter. If a selector highlighter is not already visible
+ * for the given selector, wait for it to be shown. Otherwise, wait for it to be hidden.
+ *
+ * @param {CssRuleView} view
+ * The instance of the Rules view
+ * @param {String} selectorText
+ * The selector of the CSS rule to look for
+ * @param {Number} index
+ * If there are more CSS rules with the same selector, use this index
+ * to determine which one should be retrieved. Defaults to 0 (first)
+ */
+async function clickSelectorIcon(view, selectorText, index = 0) {
+ const { inspector } = view;
+ const rule = getRuleViewRule(view, selectorText, index);
+
+ info(`Waiting for icon to be available for selector: ${selectorText}`);
+ const icon = await waitFor(() => {
+ return rule.querySelector(".js-toggle-selector-highlighter");
+ });
+
+ // Grab the actual selector associated with the matched icon.
+ // For inline styles, the CSS rule with the "element" selector actually points to
+ // a generated unique selector, for example: "div:nth-child(1)".
+ // The selector highlighter is invoked with this unique selector.
+ // Continuing to use selectorText ("element") would fail some of the checks below.
+ const selector = icon.dataset.selector;
+
+ const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } =
+ getHighlighterTestHelpers(inspector);
+
+ // If there is an active selector highlighter, get its configuration options.
+ // Will be undefined if there isn't an active selector highlighter.
+ const options = inspector.highlighters.getOptionsForActiveHighlighter(
+ inspector.highlighters.TYPES.SELECTOR
+ );
+
+ // If there is already a highlighter visible for this selector,
+ // wait for hidden event. Otherwise, wait for shown event.
+ const waitForEvent =
+ options?.selector === selector
+ ? waitForHighlighterTypeHidden(inspector.highlighters.TYPES.SELECTOR)
+ : waitForHighlighterTypeShown(inspector.highlighters.TYPES.SELECTOR);
+
+ // Boolean flag whether we waited for a highlighter shown event
+ const waitedForShown = options?.selector !== selector;
+
+ info(`Click the icon for selector: ${selectorText}`);
+ EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow);
+
+ // Promise resolves with event data from either highlighter shown or hidden event.
+ const data = await waitForEvent;
+ return { ...data, isShown: waitedForShown };
+}
+/**
+ * Toggle one of the checkboxes inside the class-panel. Resolved after the DOM mutation
+ * has been recorded.
+ * @param {CssRuleView} view The rule-view instance.
+ * @param {String} name The class name to find the checkbox.
+ */
+async function toggleClassPanelCheckBox(view, name) {
+ info(`Clicking on checkbox for class ${name}`);
+ const checkBox = [
+ ...view.classPanel.querySelectorAll("[type=checkbox]"),
+ ].find(box => {
+ return box.dataset.name === name;
+ });
+
+ const onMutation = view.inspector.once("markupmutation");
+ checkBox.click();
+ info("Waiting for a markupmutation as a result of toggling this class");
+ await onMutation;
+}
+
+/**
+ * Verify the content of the class-panel.
+ * @param {CssRuleView} view The rule-view instance
+ * @param {Array} classes The list of expected classes. Each item in this array is an
+ * object with the following properties: {name: {String}, state: {Boolean}}
+ */
+function checkClassPanelContent(view, classes) {
+ const checkBoxNodeList = view.classPanel.querySelectorAll("[type=checkbox]");
+ is(
+ checkBoxNodeList.length,
+ classes.length,
+ "The panel contains the expected number of checkboxes"
+ );
+
+ for (let i = 0; i < classes.length; i++) {
+ is(
+ checkBoxNodeList[i].dataset.name,
+ classes[i].name,
+ `Checkbox ${i} has the right class name`
+ );
+ is(
+ checkBoxNodeList[i].checked,
+ classes[i].state,
+ `Checkbox ${i} has the right state`
+ );
+ }
+}
+
+/**
+ * Opens the eyedropper from the colorpicker tooltip
+ * by selecting the colorpicker and then selecting the eyedropper icon
+ * @param {view} ruleView
+ * @param {swatch} color swatch of a particular property
+ */
+async function openEyedropper(view, swatch) {
+ const tooltip = view.tooltips.getTooltip("colorPicker").tooltip;
+
+ info("Click on the swatch");
+ const onColorPickerReady = view.tooltips
+ .getTooltip("colorPicker")
+ .once("ready");
+ EventUtils.synthesizeMouseAtCenter(swatch, {}, swatch.ownerGlobal);
+ await onColorPickerReady;
+
+ const dropperButton = tooltip.container.querySelector("#eyedropper-button");
+
+ info("Click on the eyedropper icon");
+ const onOpened = tooltip.once("eyedropper-opened");
+ dropperButton.click();
+ await onOpened;
+}
+
+/**
+ * Gets a set of declarations for a rule index.
+ *
+ * @param {ruleView} view
+ * The rule-view instance.
+ * @param {Number} ruleIndex
+ * The index we expect the rule to have in the rule-view.
+ * @param {boolean} addCompatibilityData
+ * Optional argument to add compatibility dat with the property data
+ *
+ * @returns A Promise that resolves with a Map containing stringified property declarations e.g.
+ * [
+ * {
+ * "color:red":
+ * {
+ * propertyName: "color",
+ * propertyValue: "red",
+ * warning: "This won't work",
+ * used: true,
+ * compatibilityData: {
+ * isCompatible: true,
+ * },
+ * }
+ * },
+ * ...
+ * ]
+ */
+async function getPropertiesForRuleIndex(
+ view,
+ ruleIndex,
+ addCompatibilityData = false
+) {
+ const declaration = new Map();
+ const ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
+ for (const currProp of ruleEditor.rule.textProps) {
+ const icon = currProp.editor.unusedState;
+ const unused = currProp.editor.element.classList.contains("unused");
+
+ let compatibilityData;
+ let compatibilityIcon;
+ if (addCompatibilityData) {
+ compatibilityData = await currProp.isCompatible();
+ compatibilityIcon = currProp.editor.compatibilityState;
+ }
+
+ declaration.set(`${currProp.name}:${currProp.value}`, {
+ propertyName: currProp.name,
+ propertyValue: currProp.value,
+ icon,
+ data: currProp.isUsed(),
+ warning: unused,
+ used: !unused,
+ ...(addCompatibilityData
+ ? {
+ compatibilityData,
+ compatibilityIcon,
+ }
+ : {}),
+ });
+ }
+
+ return declaration;
+}
+
+/**
+ * Toggle a declaration disabled or enabled.
+ *
+ * @param {ruleView} view
+ * The rule-view instance
+ * @param {Number} ruleIndex
+ * The index of the CSS rule where we can find the declaration to be
+ * toggled.
+ * @param {Object} declaration
+ * An object representing the declaration e.g. { color: "red" }.
+ */
+async function toggleDeclaration(view, ruleIndex, declaration) {
+ const textProp = getTextProperty(view, ruleIndex, declaration);
+ const [[name, value]] = Object.entries(declaration);
+ const dec = `${name}:${value}`;
+ ok(textProp, `Declaration "${dec}" found`);
+
+ const newStatus = textProp.enabled ? "disabled" : "enabled";
+ info(`Toggling declaration "${dec}" of rule ${ruleIndex} to ${newStatus}`);
+
+ await togglePropStatus(view, textProp);
+ info("Toggled successfully.");
+}
+
+/**
+ * Update a declaration from a CSS rule in the Rules view
+ * by changing its property name, property value or both.
+ *
+ * @param {RuleView} view
+ * Instance of RuleView.
+ * @param {Number} ruleIndex
+ * The index of the CSS rule where to find the declaration.
+ * @param {Object} declaration
+ * An object representing the target declaration e.g. { color: red }.
+ * @param {Object} newDeclaration
+ * An object representing the desired updated declaration e.g. { display: none }.
+ */
+async function updateDeclaration(
+ view,
+ ruleIndex,
+ declaration,
+ newDeclaration = {}
+) {
+ const textProp = getTextProperty(view, ruleIndex, declaration);
+ const [[name, value]] = Object.entries(declaration);
+ const [[newName, newValue]] = Object.entries(newDeclaration);
+
+ if (newName && name !== newName) {
+ info(
+ `Updating declaration ${name}:${value};
+ Changing ${name} to ${newName}`
+ );
+ await renameProperty(view, textProp, newName);
+ }
+
+ if (newValue && value !== newValue) {
+ info(
+ `Updating declaration ${name}:${value};
+ Changing ${value} to ${newValue}`
+ );
+ await setProperty(view, textProp, newValue);
+ }
+}
+
+/**
+ * Get the TextProperty instance corresponding to a CSS declaration
+ * from a CSS rule in the Rules view.
+ *
+ * @param {RuleView} view
+ * Instance of RuleView.
+ * @param {Number} ruleIndex
+ * The index of the CSS rule where to find the declaration.
+ * @param {Object} declaration
+ * An object representing the target declaration e.g. { color: red }.
+ * The first TextProperty instance which matches will be returned.
+ * @return {TextProperty}
+ */
+function getTextProperty(view, ruleIndex, declaration) {
+ const ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
+ const [[name, value]] = Object.entries(declaration);
+ const textProp = ruleEditor.rule.textProps.find(prop => {
+ return prop.name === name && prop.value === value;
+ });
+
+ if (!textProp) {
+ throw Error(
+ `Declaration ${name}:${value} not found on rule at index ${ruleIndex}`
+ );
+ }
+
+ return textProp;
+}
+
+/**
+ * Check whether the given CSS declaration is compatible or not
+ *
+ * @param {ruleView} view
+ * The rule-view instance.
+ * @param {Number} ruleIndex
+ * The index we expect the rule to have in the rule-view.
+ * @param {Object} declaration
+ * An object representing the declaration e.g. { color: "red" }.
+ * @param {Object} options
+ * @param {string | undefined} options.expected
+ * Expected message ID for the given incompatible property.
+ * If the expected message is not specified (undefined), the given declaration
+ * is inferred as cross-browser compatible and is tested for same.
+ * @param {string | null | undefined} options.expectedLearnMoreUrl
+ * Expected learn more link. Pass `null` to check that no "Learn more" link is displayed.
+ */
+async function checkDeclarationCompatibility(
+ view,
+ ruleIndex,
+ declaration,
+ { expected, expectedLearnMoreUrl }
+) {
+ const declarations = await getPropertiesForRuleIndex(view, ruleIndex, true);
+ const [[name, value]] = Object.entries(declaration);
+ const dec = `${name}:${value}`;
+ const { compatibilityData } = declarations.get(dec);
+
+ is(
+ !expected,
+ compatibilityData.isCompatible,
+ `"${dec}" has the correct compatibility status in the payload`
+ );
+
+ is(compatibilityData.msgId, expected, `"${dec}" has expected message ID`);
+
+ if (expected) {
+ await checkInteractiveTooltip(
+ view,
+ "compatibility-tooltip",
+ ruleIndex,
+ declaration
+ );
+ }
+
+ if (expectedLearnMoreUrl !== undefined) {
+ // Show the tooltip
+ const tooltip = view.tooltips.getTooltip("interactiveTooltip");
+ const onTooltipReady = tooltip.once("shown");
+ const { compatibilityIcon } = declarations.get(dec);
+ await view.tooltips.onInteractiveTooltipTargetHover(compatibilityIcon);
+ tooltip.show(compatibilityIcon);
+ await onTooltipReady;
+
+ const learnMoreEl = tooltip.panel.querySelector(".link");
+ if (expectedLearnMoreUrl === null) {
+ ok(!learnMoreEl, `"${dec}" has no "Learn more" link`);
+ } else {
+ ok(learnMoreEl, `"${dec}" has a "Learn more" link`);
+
+ const { link } = await simulateLinkClick(learnMoreEl);
+ is(
+ link,
+ expectedLearnMoreUrl,
+ `Click on ${dec} "Learn more" link navigates user to expected url`
+ );
+ }
+
+ // Hide the tooltip.
+ const onTooltipHidden = tooltip.once("hidden");
+ tooltip.hide();
+ await onTooltipHidden;
+ }
+}
+
+/**
+ * Check that a declaration is marked inactive and that it has the expected
+ * warning.
+ *
+ * @param {ruleView} view
+ * The rule-view instance.
+ * @param {Number} ruleIndex
+ * The index we expect the rule to have in the rule-view.
+ * @param {Object} declaration
+ * An object representing the declaration e.g. { color: "red" }.
+ */
+async function checkDeclarationIsInactive(view, ruleIndex, declaration) {
+ const declarations = await getPropertiesForRuleIndex(view, ruleIndex);
+ const [[name, value]] = Object.entries(declaration);
+ const dec = `${name}:${value}`;
+ const { used, warning } = declarations.get(dec);
+
+ ok(!used, `"${dec}" is inactive`);
+ ok(warning, `"${dec}" has a warning`);
+
+ await checkInteractiveTooltip(
+ view,
+ "inactive-css-tooltip",
+ ruleIndex,
+ declaration
+ );
+}
+
+/**
+ * Check that a declaration is marked active.
+ *
+ * @param {ruleView} view
+ * The rule-view instance.
+ * @param {Number} ruleIndex
+ * The index we expect the rule to have in the rule-view.
+ * @param {Object} declaration
+ * An object representing the declaration e.g. { color: "red" }.
+ */
+async function checkDeclarationIsActive(view, ruleIndex, declaration) {
+ const declarations = await getPropertiesForRuleIndex(view, ruleIndex);
+ const [[name, value]] = Object.entries(declaration);
+ const dec = `${name}:${value}`;
+ const { used, warning } = declarations.get(dec);
+
+ ok(used, `${dec} is active`);
+ ok(!warning, `${dec} has no warning`);
+}
+
+/**
+ * Check that a tooltip contains the correct value.
+ *
+ * @param {ruleView} view
+ * The rule-view instance.
+ * @param {string} type
+ * The interactive tooltip type being tested.
+ * @param {Number} ruleIndex
+ * The index we expect the rule to have in the rule-view.
+ * @param {Object} declaration
+ * An object representing the declaration e.g. { color: "red" }.
+ */
+async function checkInteractiveTooltip(view, type, ruleIndex, declaration) {
+ // Get the declaration
+ const declarations = await getPropertiesForRuleIndex(
+ view,
+ ruleIndex,
+ type === "compatibility-tooltip"
+ );
+ const [[name, value]] = Object.entries(declaration);
+ const dec = `${name}:${value}`;
+
+ // Get the relevant icon and tooltip payload data
+ let icon;
+ let data;
+ if (type === "inactive-css-tooltip") {
+ ({ icon, data } = declarations.get(dec));
+ } else {
+ const { compatibilityIcon, compatibilityData } = declarations.get(dec);
+ icon = compatibilityIcon;
+ data = compatibilityData;
+ }
+
+ // Get the tooltip.
+ const tooltip = view.tooltips.getTooltip("interactiveTooltip");
+
+ // Get the necessary tooltip helper to fetch the Fluent template.
+ let tooltipHelper;
+ if (type === "inactive-css-tooltip") {
+ tooltipHelper = view.tooltips.inactiveCssTooltipHelper;
+ } else {
+ tooltipHelper = view.tooltips.compatibilityTooltipHelper;
+ }
+
+ // Get the HTML template.
+ const template = tooltipHelper.getTemplate(data, tooltip);
+
+ // Translate the template using Fluent.
+ const { doc } = tooltip;
+ await doc.l10n.translateFragment(template);
+
+ // Get the expected HTML content of the now translated template.
+ const expected = template.firstElementChild.outerHTML;
+
+ // Show the tooltip for the correct icon.
+ const onTooltipReady = tooltip.once("shown");
+ await view.tooltips.onInteractiveTooltipTargetHover(icon);
+ tooltip.show(icon);
+ await onTooltipReady;
+
+ // Get the tooltip's actual HTML content.
+ const actual = tooltip.panel.firstElementChild.outerHTML;
+
+ // Hide the tooltip.
+ const onTooltipHidden = tooltip.once("hidden");
+ tooltip.hide();
+ await onTooltipHidden;
+
+ // Finally, check the values.
+ is(actual, expected, "Tooltip contains the correct value.");
+}
+
+/**
+ * CSS compatibility test runner.
+ *
+ * @param {ruleView} view
+ * The rule-view instance.
+ * @param {InspectorPanel} inspector
+ * The instance of InspectorPanel currently loaded in the toolbox.
+ * @param {Array} tests
+ * An array of test object for this method to consume e.g.
+ * [
+ * {
+ * selector: "#flex-item",
+ * rules: [
+ * // Rule Index: 0
+ * {
+ * // If the object doesn't include the "expected"
+ * // key, we consider the declaration as
+ * // cross-browser compatible and test for same
+ * color: { value: "green" },
+ * },
+ * // Rule Index: 1
+ * {
+ * cursor:
+ * {
+ * value: "grab",
+ * expected: INCOMPATIBILITY_TOOLTIP_MESSAGE.default,
+ * expectedLearnMoreUrl: "https://developer.mozilla.org/en-US/docs/Web/CSS/cursor",
+ * },
+ * },
+ * ],
+ * },
+ * ...
+ * ]
+ */
+async function runCSSCompatibilityTests(view, inspector, tests) {
+ for (const test of tests) {
+ if (test.selector) {
+ await selectNode(test.selector, inspector);
+ }
+
+ for (const [ruleIndex, rules] of test.rules.entries()) {
+ for (const rule in rules) {
+ await checkDeclarationCompatibility(
+ view,
+ ruleIndex,
+ {
+ [rule]: rules[rule].value,
+ },
+ {
+ expected: rules[rule].expected,
+ expectedLearnMoreUrl: rules[rule].expectedLearnMoreUrl,
+ }
+ );
+ }
+ }
+ }
+}
+
+/**
+ * Inactive CSS test runner.
+ *
+ * @param {ruleView} view
+ * The rule-view instance.
+ * @param {InspectorPanel} inspector
+ * The instance of InspectorPanel currently loaded in the toolbox.
+ * @param {Array} tests
+ * An array of test object for this method to consume e.g.
+ * [
+ * {
+ * selector: "#flex-item",
+ * activeDeclarations: [
+ * {
+ * declarations: {
+ * "order": "2",
+ * },
+ * ruleIndex: 0,
+ * },
+ * {
+ * declarations: {
+ * "flex-basis": "auto",
+ * "flex-grow": "1",
+ * "flex-shrink": "1",
+ * },
+ * ruleIndex: 1,
+ * },
+ * ],
+ * inactiveDeclarations: [
+ * {
+ * declaration: {
+ * "flex-direction": "row",
+ * },
+ * ruleIndex: 1,
+ * },
+ * ],
+ * },
+ * ...
+ * ]
+ */
+async function runInactiveCSSTests(view, inspector, tests) {
+ for (const test of tests) {
+ if (test.selector) {
+ await selectNode(test.selector, inspector);
+ }
+
+ if (test.activeDeclarations) {
+ info("Checking whether declarations are marked as used.");
+
+ for (const activeDeclarations of test.activeDeclarations) {
+ for (const [name, value] of Object.entries(
+ activeDeclarations.declarations
+ )) {
+ await checkDeclarationIsActive(view, activeDeclarations.ruleIndex, {
+ [name]: value,
+ });
+ }
+ }
+ }
+
+ if (test.inactiveDeclarations) {
+ info("Checking that declarations are unused and have a warning.");
+
+ for (const inactiveDeclaration of test.inactiveDeclarations) {
+ await checkDeclarationIsInactive(
+ view,
+ inactiveDeclaration.ruleIndex,
+ inactiveDeclaration.declaration
+ );
+ }
+ }
+ }
+}
+
+/**
+ * Return the checkbox element from the Rules view corresponding
+ * to the given pseudo-class.
+ *
+ * @param {Object} view
+ * Instance of RuleView.
+ * @param {String} pseudo
+ * Pseudo-class, like :hover, :active, :focus, etc.
+ * @return {HTMLElement}
+ */
+function getPseudoClassCheckbox(view, pseudo) {
+ return view.pseudoClassCheckboxes.filter(
+ checkbox => checkbox.value === pseudo
+ )[0];
+}
+
+/**
+ * Check that the CSS variable output has the expected class name and data attribute.
+ *
+ * @param {RulesView} view
+ * The RulesView instance.
+ * @param {String} selector
+ * Selector name for a rule. (e.g. "div", "div::before" and ".sample" etc);
+ * @param {String} propertyName
+ * Property name (e.g. "color" and "padding-top" etc);
+ * @param {String} expectedClassName
+ * The class name the variable should have.
+ * @param {String} expectedDatasetValue
+ * The variable data attribute value.
+ */
+function checkCSSVariableOutput(
+ view,
+ selector,
+ propertyName,
+ expectedClassName,
+ expectedDatasetValue
+) {
+ const target = getRuleViewProperty(
+ view,
+ selector,
+ propertyName
+ ).valueSpan.querySelector(`.${expectedClassName}`);
+
+ ok(target, "The target element should exist");
+ is(target.dataset.variable, expectedDatasetValue);
+}
+
+/**
+ * Return specific rule ancestor data element (i.e. the one containing @layer / @media
+ * information) from the Rules view
+ *
+ * @param {RulesView} view
+ * The RulesView instance.
+ * @param {Number} ruleIndex
+ * @returns {HTMLElement}
+ */
+function getRuleViewAncestorRulesDataElementByIndex(view, ruleIndex) {
+ return view.styleDocument
+ .querySelectorAll(`.ruleview-rule`)
+ [ruleIndex]?.querySelector(`.ruleview-rule-ancestor-data`);
+}
+
+/**
+ * Return specific rule ancestor data text from the Rules view.
+ * Will return something like "@layer topLayer\n@media screen\n@layer".
+ *
+ * @param {RulesView} view
+ * The RulesView instance.
+ * @param {Number} ruleIndex
+ * @returns {String}
+ */
+function getRuleViewAncestorRulesDataTextByIndex(view, ruleIndex) {
+ return getRuleViewAncestorRulesDataElementByIndex(view, ruleIndex)?.innerText;
+}
diff --git a/devtools/client/inspector/rules/test/sjs_imported_stylesheet_edit.sjs b/devtools/client/inspector/rules/test/sjs_imported_stylesheet_edit.sjs
new file mode 100644
index 0000000000..5a68c5571c
--- /dev/null
+++ b/devtools/client/inspector/rules/test/sjs_imported_stylesheet_edit.sjs
@@ -0,0 +1,53 @@
+/* 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 INITIAL_CONTENT = `
+div {
+ color: red
+}
+`;
+
+const UPDATED_CONTENT = `
+span {
+ color: green;
+}
+
+a {
+ color: blue;
+}
+
+div {
+ color: gold;
+}
+`;
+
+/**
+ * This sjs file supports three endpoint:
+ * - "sjs_imported_stylesheet_edit.sjs" -> will return a text/css content which
+ * will be either INITIAL_CONTENT or UPDATED_CONTENT. Initially will return
+ * INITIAL_CONTENT.
+ * - "sjs_imported_stylesheet_edit.sjs?update-stylesheet" -> will update an
+ * internal flag. After calling this URL, the regular endpoint will return
+ * UPDATED_CONTENT instead of INITIAL_CONTENT
+ * - "sjs_imported_stylesheet_edit.sjs?setup" -> set the internal flag to its
+ * default value. Should be called at the beginning of every test to avoid
+ * side effects.
+ */
+function handleRequest(request, response) {
+ const { queryString } = request;
+ if (queryString === "setup") {
+ setState("serve-updated-content", "false");
+ response.setHeader("Content-Type", "text/html");
+ response.write("OK");
+ } else if (queryString === "update-stylesheet") {
+ setState("serve-updated-content", "true");
+ response.setHeader("Content-Type", "text/html");
+ response.write("OK");
+ } else {
+ response.setHeader("Content-Type", "text/css");
+ const shouldServeUpdatedCSS = getState("serve-updated-content") == "true";
+ response.write(shouldServeUpdatedCSS ? UPDATED_CONTENT : INITIAL_CONTENT);
+ }
+}
diff --git a/devtools/client/inspector/rules/test/square_svg.sjs b/devtools/client/inspector/rules/test/square_svg.sjs
new file mode 100644
index 0000000000..7f42dfdae1
--- /dev/null
+++ b/devtools/client/inspector/rules/test/square_svg.sjs
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Access-Control-Allow-Headers", "content-type", false);
+ response.setHeader("Content-Type", "image/svg+xml", false);
+ response.write(
+ `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="16" /></svg>`
+ );
+}
diff --git a/devtools/client/inspector/rules/types.js b/devtools/client/inspector/rules/types.js
new file mode 100644
index 0000000000..62c0db7fd4
--- /dev/null
+++ b/devtools/client/inspector/rules/types.js
@@ -0,0 +1,165 @@
+/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+/**
+ * A CSS class.
+ */
+exports.classes = {
+ // The CSS class name.
+ name: PropTypes.string,
+
+ // Whether or not the CSS class is applied.
+ isApplied: PropTypes.bool,
+};
+
+/**
+ * A CSS declaration.
+ */
+const declaration = (exports.declaration = {
+ // Array of the computed properties for a CSS declaration.
+ computedProperties: PropTypes.arrayOf(
+ PropTypes.shape({
+ // Whether or not the computed property is overridden.
+ isOverridden: PropTypes.bool,
+ // The computed property name.
+ name: PropTypes.string,
+ // The computed priority (either "important" or an empty string).
+ priority: PropTypes.string,
+ // The computed property value.
+ value: PropTypes.string,
+ })
+ ),
+
+ // An unique CSS declaration id.
+ id: PropTypes.string,
+
+ // Whether or not the declaration is valid. (Does it make sense for this value
+ // to be assigned to this property name?)
+ isDeclarationValid: PropTypes.bool,
+
+ // Whether or not the declaration is enabled.
+ isEnabled: PropTypes.bool,
+
+ // Whether or not the declaration is invisible. In an inherited rule, only the
+ // inherited declarations are shown and the rest are considered invisible.
+ isInvisible: PropTypes.bool,
+
+ // Whether or not the declaration's property name is known.
+ isKnownProperty: PropTypes.bool,
+
+ // Whether or not the property name is valid.
+ isNameValid: PropTypes.bool,
+
+ // Whether or not the the declaration is overridden.
+ isOverridden: PropTypes.bool,
+
+ // Whether or not the declaration is changed by the user.
+ isPropertyChanged: PropTypes.bool,
+
+ // The declaration's property name.
+ name: PropTypes.string,
+
+ // The declaration's priority (either "important" or an empty string).
+ priority: PropTypes.string,
+
+ // The declaration's property value.
+ value: PropTypes.string,
+});
+
+/**
+ * The pseudo classes redux structure.
+ */
+exports.pseudoClasses = {
+ // An object containing the :active pseudo class toggle state.
+ ":active": PropTypes.shape({
+ // Whether or not the :active pseudo class is checked.
+ isChecked: PropTypes.bool,
+ // Whether or not the :active pseudo class is disabled.
+ isDisabled: PropTypes.bool,
+ }),
+
+ // An object containing the :focus pseudo class toggle state.
+ ":focus": PropTypes.shape({
+ // Whether or not the :focus pseudo class is checked
+ isChecked: PropTypes.bool,
+ // Whether or not the :focus pseudo class is disabled.
+ isDisabled: PropTypes.bool,
+ }),
+
+ // An object containing the :focus-within pseudo class toggle state.
+ ":focus-within": PropTypes.shape({
+ // Whether or not the :focus-within pseudo class is checked
+ isChecked: PropTypes.bool,
+ // Whether or not the :focus-within pseudo class is disabled.
+ isDisabled: PropTypes.bool,
+ }),
+
+ // An object containing the :hover pseudo class toggle state.
+ ":hover": PropTypes.shape({
+ // Whether or not the :hover pseudo class is checked.
+ isChecked: PropTypes.bool,
+ // Whether or not the :hover pseudo class is disabled.
+ isDisabled: PropTypes.bool,
+ }),
+};
+
+/**
+ * A CSS selector.
+ */
+const selector = (exports.selector = {
+ // Function that returns a Promise containing an unique CSS selector.
+ getUniqueSelector: PropTypes.func,
+ // Array of the selectors that match the selected element.
+ matchedSelectors: PropTypes.arrayOf(PropTypes.string),
+ // The CSS rule's selector text content.
+ selectorText: PropTypes.string,
+ // Array of the CSS rule's selectors.
+ selectors: PropTypes.arrayOf(PropTypes.string),
+});
+
+/**
+ * A CSS Rule.
+ */
+exports.rule = {
+ // Array of CSS declarations.
+ declarations: PropTypes.arrayOf(PropTypes.shape(declaration)),
+
+ // An unique CSS rule id.
+ id: PropTypes.string,
+
+ // An object containing information about the CSS rule's inheritance.
+ inheritance: PropTypes.shape({
+ // The NodeFront of the element this rule was inherited from.
+ inherited: PropTypes.object,
+ // A header label for where the element this rule was inherited from.
+ inheritedSource: PropTypes.string,
+ }),
+
+ // Whether or not the rule does not match the current selected element.
+ isUnmatched: PropTypes.bool,
+
+ // Whether or not the rule is an user agent style.
+ isUserAgentStyle: PropTypes.bool,
+
+ // An object containing information about the CSS keyframes rules.
+ keyframesRule: PropTypes.shape({
+ // The actor ID of the keyframes rule.
+ id: PropTypes.string,
+ // The keyframes rule name.
+ keyframesName: PropTypes.string,
+ }),
+
+ // The pseudo-element keyword used in the rule.
+ pseudoElement: PropTypes.string,
+
+ // An object containing information about the CSS rule's selector.
+ selector: PropTypes.shape(selector),
+
+ // The CSS rule type.
+ type: PropTypes.number,
+};
diff --git a/devtools/client/inspector/rules/utils/l10n.js b/devtools/client/inspector/rules/utils/l10n.js
new file mode 100644
index 0000000000..b90748ae22
--- /dev/null
+++ b/devtools/client/inspector/rules/utils/l10n.js
@@ -0,0 +1,15 @@
+/* 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 { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const L10N = new LocalizationHelper(
+ "devtools/shared/locales/styleinspector.properties"
+);
+
+module.exports = {
+ getStr: (...args) => L10N.getStr(...args),
+ getFormatStr: (...args) => L10N.getFormatStr(...args),
+};
diff --git a/devtools/client/inspector/rules/utils/moz.build b/devtools/client/inspector/rules/utils/moz.build
new file mode 100644
index 0000000000..cc3fa4dfbd
--- /dev/null
+++ b/devtools/client/inspector/rules/utils/moz.build
@@ -0,0 +1,10 @@
+# -*- 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(
+ "l10n.js",
+ "utils.js",
+)
diff --git a/devtools/client/inspector/rules/utils/utils.js b/devtools/client/inspector/rules/utils/utils.js
new file mode 100644
index 0000000000..c6a23ac59b
--- /dev/null
+++ b/devtools/client/inspector/rules/utils/utils.js
@@ -0,0 +1,357 @@
+/* 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 {
+ VIEW_NODE_CSS_QUERY_CONTAINER,
+ VIEW_NODE_FONT_TYPE,
+ VIEW_NODE_IMAGE_URL_TYPE,
+ VIEW_NODE_INACTIVE_CSS,
+ VIEW_NODE_LOCATION_TYPE,
+ VIEW_NODE_PROPERTY_TYPE,
+ VIEW_NODE_SELECTOR_TYPE,
+ VIEW_NODE_SHAPE_POINT_TYPE,
+ VIEW_NODE_SHAPE_SWATCH,
+ VIEW_NODE_VALUE_TYPE,
+ VIEW_NODE_VARIABLE_TYPE,
+} = require("resource://devtools/client/inspector/shared/node-types.js");
+const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
+
+/**
+ * Returns the [Rule] object associated with the given node.
+ *
+ * @param {DOMNode} node
+ * The node which we want to find the [Rule] object for
+ * @param {ElementStyle} elementStyle
+ * The [ElementStyle] associated with the selected element
+ * @return {Rule|null} associated with the given node
+ */
+function getRuleFromNode(node, elementStyle) {
+ const ruleEl = node.closest(".ruleview-rule[data-rule-id]");
+ const ruleId = ruleEl ? ruleEl.dataset.ruleId : null;
+ return ruleId ? elementStyle.getRule(ruleId) : null;
+}
+
+/**
+ * Returns the [TextProperty] object associated with the given node.
+ *
+ * @param {DOMNode} node
+ * The node which we want to find the [TextProperty] object for
+ * @param {Rule|null} rule
+ * The [Rule] associated with the given node
+ * @return {TextProperty|null} associated with the given node
+ */
+function getDeclarationFromNode(node, rule) {
+ if (!rule) {
+ return null;
+ }
+
+ const declarationEl = node.closest(".ruleview-property[data-declaration-id]");
+ const declarationId = declarationEl
+ ? declarationEl.dataset.declarationId
+ : null;
+ return rule ? rule.getDeclaration(declarationId) : null;
+}
+
+/**
+ * Get the type of a given node in the Rules view.
+ *
+ * @param {DOMNode} node
+ * The node which we want information about
+ * @param {ElementStyle} elementStyle
+ * The ElementStyle to which this rule belongs
+ * @return {Object|null} containing the following props:
+ * - rule {Rule} The Rule object.
+ * - 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.
+ * - view {String} Always "rule" to indicate the rule view.
+ * Otherwise, returns null if the node isn't anything we care about.
+ */
+// eslint-disable-next-line complexity
+function getNodeInfo(node, elementStyle) {
+ if (!node) {
+ return null;
+ }
+
+ const rule = getRuleFromNode(node, elementStyle);
+ const declaration = getDeclarationFromNode(node, rule);
+ const classList = node.classList;
+
+ let type, value;
+
+ if (declaration && classList.contains("ruleview-propertyname")) {
+ type = VIEW_NODE_PROPERTY_TYPE;
+ value = {
+ property: node.textContent,
+ value: getPropertyNameAndValue(node).value,
+ enabled: declaration.enabled,
+ overridden: declaration.overridden,
+ pseudoElement: rule.pseudoElement,
+ sheetHref: rule.domRule.href,
+ textProperty: declaration,
+ };
+ } else if (declaration && classList.contains("ruleview-propertyvalue")) {
+ type = VIEW_NODE_VALUE_TYPE;
+ value = {
+ property: getPropertyNameAndValue(node).name,
+ value: node.textContent,
+ enabled: declaration.enabled,
+ overridden: declaration.overridden,
+ pseudoElement: rule.pseudoElement,
+ sheetHref: rule.domRule.href,
+ textProperty: declaration,
+ };
+ } else if (declaration && classList.contains("ruleview-font-family")) {
+ const { name: propertyName, value: propertyValue } =
+ getPropertyNameAndValue(node);
+ type = VIEW_NODE_FONT_TYPE;
+ value = {
+ property: propertyName,
+ value: propertyValue,
+ enabled: declaration.enabled,
+ overridden: declaration.overridden,
+ pseudoElement: rule.pseudoElement,
+ sheetHref: rule.domRule.href,
+ textProperty: declaration,
+ };
+ } else if (declaration && classList.contains("ruleview-shape-point")) {
+ type = VIEW_NODE_SHAPE_POINT_TYPE;
+ value = {
+ property: getPropertyNameAndValue(node).name,
+ value: node.textContent,
+ enabled: declaration.enabled,
+ overridden: declaration.overridden,
+ pseudoElement: rule.pseudoElement,
+ sheetHref: rule.domRule.href,
+ textProperty: declaration,
+ toggleActive: getShapeToggleActive(node),
+ point: getShapePoint(node),
+ };
+ } else if (declaration && classList.contains("ruleview-unused-warning")) {
+ type = VIEW_NODE_INACTIVE_CSS;
+ value = declaration.isUsed();
+ } else if (node.closest(".container-query-declaration")) {
+ type = VIEW_NODE_CSS_QUERY_CONTAINER;
+ const li = node.closest("li.container-query");
+ value = {
+ ancestorIndex: li.getAttribute("data-ancestor-index"),
+ rule,
+ };
+ } else if (declaration && classList.contains("ruleview-shapeswatch")) {
+ type = VIEW_NODE_SHAPE_SWATCH;
+ value = {
+ enabled: declaration.enabled,
+ overridden: declaration.overridden,
+ textProperty: declaration,
+ };
+ } else if (
+ declaration &&
+ (classList.contains("ruleview-variable") ||
+ classList.contains("ruleview-unmatched-variable"))
+ ) {
+ type = VIEW_NODE_VARIABLE_TYPE;
+ value = {
+ property: getPropertyNameAndValue(node).name,
+ value: node.textContent.trim(),
+ enabled: declaration.enabled,
+ overridden: declaration.overridden,
+ pseudoElement: rule.pseudoElement,
+ sheetHref: rule.domRule.href,
+ textProperty: declaration,
+ variable: node.dataset.variable,
+ };
+ } else if (
+ declaration &&
+ classList.contains("theme-link") &&
+ !classList.contains("ruleview-rule-source")
+ ) {
+ type = VIEW_NODE_IMAGE_URL_TYPE;
+ value = {
+ property: getPropertyNameAndValue(node).name,
+ value: node.parentNode.textContent,
+ url: node.href,
+ enabled: declaration.enabled,
+ overridden: declaration.overridden,
+ pseudoElement: rule.pseudoElement,
+ sheetHref: rule.domRule.href,
+ textProperty: declaration,
+ };
+ } else if (
+ classList.contains("ruleview-selector-unmatched") ||
+ classList.contains("ruleview-selector-matched") ||
+ classList.contains("ruleview-selectorcontainer") ||
+ classList.contains("ruleview-selector") ||
+ classList.contains("ruleview-selector-attribute") ||
+ classList.contains("ruleview-selector-pseudo-class") ||
+ classList.contains("ruleview-selector-pseudo-class-lock")
+ ) {
+ type = VIEW_NODE_SELECTOR_TYPE;
+ value = rule.selectorText;
+ } else if (
+ classList.contains("ruleview-rule-source") ||
+ classList.contains("ruleview-rule-source-label")
+ ) {
+ type = VIEW_NODE_LOCATION_TYPE;
+ value = rule.sheet?.href ? rule.sheet.href : rule.title;
+ } else {
+ return null;
+ }
+
+ return {
+ rule,
+ type,
+ value,
+ view: "rule",
+ };
+}
+
+/**
+ * Walk 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 (node?.classList) {
+ // 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;
+ }
+
+ return null;
+}
+
+/**
+ * Walk up the DOM from a given node until a parent property holder is found,
+ * and return an active shape toggle if one exists.
+ *
+ * @param {DOMNode} node
+ * The node to start from
+ * @returns {DOMNode} The active shape toggle node, if one exists.
+ */
+function getShapeToggleActive(node) {
+ while (node?.classList) {
+ // Check first for ruleview-computed since it's the deepest
+ if (
+ node.classList.contains("ruleview-computed") ||
+ node.classList.contains("ruleview-property")
+ ) {
+ return node.querySelector(".ruleview-shapeswatch.active");
+ }
+
+ node = node.parentNode;
+ }
+
+ return null;
+}
+
+/**
+ * Get the point associated with a shape point node.
+ *
+ * @param {DOMNode} node
+ * A shape point node
+ * @returns {String} The point associated with the given node.
+ */
+function getShapePoint(node) {
+ const classList = node.classList;
+ let point = node.dataset.point;
+ // Inset points use classes instead of data because a single span can represent
+ // multiple points.
+ const insetClasses = [];
+ classList.forEach(className => {
+ if (INSET_POINT_TYPES.includes(className)) {
+ insetClasses.push(className);
+ }
+ });
+ if (insetClasses.length) {
+ point = insetClasses.join(",");
+ }
+ return point;
+}
+
+/**
+ * Returns an array of CSS variables used in a CSS property value.
+ * If no CSS variables are used, returns an empty array.
+ *
+ * @param {String} propertyValue
+ * CSS property value (e.g. "1px solid var(--color, blue)")
+ * @return {Array}
+ * List of variable names (e.g. ["--color"])
+ *
+ */
+function getCSSVariables(propertyValue = "") {
+ const variables = [];
+ const parts = propertyValue.split(/var\(\s*--/);
+
+ if (parts.length) {
+ // Skip first part. It is the substring before the first occurence of "var(--"
+ for (let i = 1; i < parts.length; i++) {
+ // Split the part by any of the following characters expected after a variable name:
+ // comma, closing parenthesis or whitespace.
+ // Take just the first match. Anything else is either:
+ // - the fallback value, ex: ", blue" from "var(--color, blue)"
+ // - the closing parenthesis, ex: ")" from "var(--color)"
+ const variable = parts[i].split(/[,)\s+]/).shift();
+
+ if (variable) {
+ // Add back the double-dash. The initial string was split by "var(--"
+ variables.push(`--${variable}`);
+ }
+ }
+ }
+
+ return variables;
+}
+
+/**
+ * Get the CSS compatibility issue information for a given node.
+ *
+ * @param {DOMNode} node
+ * The node which we want compatibility information about
+ * @param {ElementStyle} elementStyle
+ * The ElementStyle to which this rule belongs
+ */
+async function getNodeCompatibilityInfo(node, elementStyle) {
+ const rule = getRuleFromNode(node, elementStyle);
+ const declaration = getDeclarationFromNode(node, rule);
+ const issue = await declaration.isCompatible();
+
+ return issue;
+}
+
+/**
+ * Returns true if the given CSS property value contains the given variable name.
+ *
+ * @param {String} propertyValue
+ * CSS property value (e.g. "var(--color)")
+ * @param {String} variableName
+ * CSS variable name (e.g. "--color")
+ * @return {Boolean}
+ */
+function hasCSSVariable(propertyValue, variableName) {
+ return getCSSVariables(propertyValue).includes(variableName);
+}
+
+module.exports = {
+ getCSSVariables,
+ getNodeInfo,
+ getRuleFromNode,
+ hasCSSVariable,
+ getNodeCompatibilityInfo,
+};
diff --git a/devtools/client/inspector/rules/views/class-list-previewer.js b/devtools/client/inspector/rules/views/class-list-previewer.js
new file mode 100644
index 0000000000..e4e99bedde
--- /dev/null
+++ b/devtools/client/inspector/rules/views/class-list-previewer.js
@@ -0,0 +1,310 @@
+/* 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 ClassList = require("resource://devtools/client/inspector/rules/models/class-list.js");
+
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/inspector.properties"
+);
+const AutocompletePopup = require("resource://devtools/client/shared/autocomplete-popup.js");
+const { debounce } = require("resource://devtools/shared/debounce.js");
+
+/**
+ * This UI widget shows a textfield and a series of checkboxes in the rule-view. It is
+ * used to toggle classes on the current node selection, and add new classes.
+ */
+class ClassListPreviewer {
+ /*
+ * @param {Inspector} inspector
+ * The current inspector instance.
+ * @param {DomNode} containerEl
+ * The element in the rule-view where the widget should go.
+ */
+ constructor(inspector, containerEl) {
+ this.inspector = inspector;
+ this.containerEl = containerEl;
+ this.model = new ClassList(inspector);
+
+ this.onNewSelection = this.onNewSelection.bind(this);
+ this.onCheckBoxChanged = this.onCheckBoxChanged.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onAddElementInputModified = debounce(
+ this.onAddElementInputModified,
+ 75,
+ this
+ );
+ this.onCurrentNodeClassChanged = this.onCurrentNodeClassChanged.bind(this);
+ this.onNodeFrontWillUnset = this.onNodeFrontWillUnset.bind(this);
+ this.onAutocompleteClassHovered = debounce(
+ this.onAutocompleteClassHovered,
+ 75,
+ this
+ );
+ this.onAutocompleteClosed = this.onAutocompleteClosed.bind(this);
+
+ // Create the add class text field.
+ this.addEl = this.doc.createElement("input");
+ this.addEl.classList.add("devtools-textinput");
+ this.addEl.classList.add("add-class");
+ this.addEl.setAttribute(
+ "placeholder",
+ L10N.getStr("inspector.classPanel.newClass.placeholder")
+ );
+ this.addEl.addEventListener("keydown", this.onKeyDown);
+ this.addEl.addEventListener("input", this.onAddElementInputModified);
+ this.containerEl.appendChild(this.addEl);
+
+ // Create the class checkboxes container.
+ this.classesEl = this.doc.createElement("div");
+ this.classesEl.classList.add("classes");
+ this.containerEl.appendChild(this.classesEl);
+
+ // Create the autocomplete popup
+ this.autocompletePopup = new AutocompletePopup(this.inspector.toolbox.doc, {
+ listId: "inspector_classListPreviewer_autocompletePopupListBox",
+ position: "bottom",
+ autoSelect: true,
+ useXulWrapper: true,
+ input: this.addEl,
+ onClick: (e, item) => {
+ if (item) {
+ this.addEl.value = item.label;
+ this.autocompletePopup.hidePopup();
+ this.autocompletePopup.clearItems();
+ this.model.previewClass(item.label);
+ }
+ },
+ onSelect: item => {
+ if (item) {
+ this.onAutocompleteClassHovered(item?.label);
+ }
+ },
+ });
+
+ // Start listening for interesting events.
+ this.inspector.selection.on("new-node-front", this.onNewSelection);
+ this.inspector.selection.on(
+ "node-front-will-unset",
+ this.onNodeFrontWillUnset
+ );
+ this.containerEl.addEventListener("input", this.onCheckBoxChanged);
+ this.model.on("current-node-class-changed", this.onCurrentNodeClassChanged);
+ this.autocompletePopup.on("popup-closed", this.onAutocompleteClosed);
+
+ this.onNewSelection();
+ }
+
+ destroy() {
+ this.inspector.selection.off("new-node-front", this.onNewSelection);
+ this.inspector.selection.off(
+ "node-front-will-unset",
+ this.onNodeFrontWillUnset
+ );
+ this.autocompletePopup.off("popup-closed", this.onAutocompleteClosed);
+ this.addEl.removeEventListener("keydown", this.onKeyDown);
+ this.addEl.removeEventListener("input", this.onAddElementInputModified);
+ this.containerEl.removeEventListener("input", this.onCheckBoxChanged);
+
+ this.autocompletePopup.destroy();
+
+ this.containerEl.innerHTML = "";
+
+ this.model.destroy();
+ this.containerEl = null;
+ this.inspector = null;
+ this.addEl = null;
+ this.classesEl = null;
+ }
+
+ get doc() {
+ return this.containerEl.ownerDocument;
+ }
+
+ /**
+ * Render the content of the panel. You typically don't need to call this as the panel
+ * renders itself on inspector selection changes.
+ */
+ render() {
+ this.classesEl.innerHTML = "";
+
+ for (const { name, isApplied } of this.model.currentClasses) {
+ const checkBox = this.renderCheckBox(name, isApplied);
+ this.classesEl.appendChild(checkBox);
+ }
+
+ if (!this.model.currentClasses.length) {
+ this.classesEl.appendChild(this.renderNoClassesMessage());
+ }
+ }
+
+ /**
+ * Render a single checkbox for a given classname.
+ *
+ * @param {String} name
+ * The name of this class.
+ * @param {Boolean} isApplied
+ * Is this class currently applied on the DOM node.
+ * @return {DOMNode} The DOM element for this checkbox.
+ */
+ renderCheckBox(name, isApplied) {
+ const box = this.doc.createElement("input");
+ box.setAttribute("type", "checkbox");
+ if (isApplied) {
+ box.setAttribute("checked", "checked");
+ }
+ box.dataset.name = name;
+
+ const labelWrapper = this.doc.createElement("label");
+ labelWrapper.setAttribute("title", name);
+ labelWrapper.appendChild(box);
+
+ // A child element is required to do the ellipsis.
+ const label = this.doc.createElement("span");
+ label.textContent = name;
+ labelWrapper.appendChild(label);
+
+ return labelWrapper;
+ }
+
+ /**
+ * Render the message displayed in the panel when the current element has no classes.
+ *
+ * @return {DOMNode} The DOM element for the message.
+ */
+ renderNoClassesMessage() {
+ const msg = this.doc.createElement("p");
+ msg.classList.add("no-classes");
+ msg.textContent = L10N.getStr("inspector.classPanel.noClasses");
+ return msg;
+ }
+
+ /**
+ * Focus the add-class text field.
+ */
+ focusAddClassField() {
+ if (this.addEl) {
+ this.addEl.focus();
+ }
+ }
+
+ onCheckBoxChanged({ target }) {
+ if (!target.dataset.name) {
+ return;
+ }
+
+ this.model.setClassState(target.dataset.name, target.checked).catch(e => {
+ // Only log the error if the panel wasn't destroyed in the meantime.
+ if (this.containerEl) {
+ console.error(e);
+ }
+ });
+ }
+
+ onKeyDown(event) {
+ // If the popup is already open, all the keyboard interaction are handled
+ // directly by the popup component.
+ if (this.autocompletePopup.isOpen) {
+ return;
+ }
+
+ // Open the autocomplete popup on Ctrl+Space / ArrowDown (when the input isn't empty)
+ if (
+ (this.addEl.value && event.key === " " && event.ctrlKey) ||
+ event.key === "ArrowDown"
+ ) {
+ this.onAddElementInputModified();
+ return;
+ }
+
+ if (this.addEl.value !== "" && event.key === "Enter") {
+ this.addClassName(this.addEl.value);
+ }
+ }
+
+ async onAddElementInputModified() {
+ const newValue = this.addEl.value;
+
+ // if the input is empty, let's close the popup, if it was open.
+ if (newValue === "") {
+ if (this.autocompletePopup.isOpen) {
+ this.autocompletePopup.hidePopup();
+ this.autocompletePopup.clearItems();
+ } else {
+ this.model.previewClass("");
+ }
+ return;
+ }
+
+ // Otherwise, we need to update the popup items to match the new input.
+ let items = [];
+ try {
+ const classNames = await this.model.getClassNames(newValue);
+ if (!this.autocompletePopup.isOpen) {
+ this._previewClassesBeforeAutocompletion =
+ this.model.previewClasses.map(previewClass => previewClass.className);
+ }
+ items = classNames.map(className => {
+ return {
+ preLabel: className.substring(0, newValue.length),
+ label: className,
+ };
+ });
+ } catch (e) {
+ // If there was an error while retrieving the classNames, we'll simply NOT show the
+ // popup, which is okay.
+ console.warn("Error when calling getClassNames", e);
+ }
+
+ if (!items.length || (items.length == 1 && items[0].label === newValue)) {
+ this.autocompletePopup.clearItems();
+ await this.autocompletePopup.hidePopup();
+ this.model.previewClass(newValue);
+ } else {
+ this.autocompletePopup.setItems(items);
+ this.autocompletePopup.openPopup();
+ }
+ }
+
+ async addClassName(className) {
+ try {
+ await this.model.addClassName(className);
+ this.render();
+ this.addEl.value = "";
+ } catch (e) {
+ // Only log the error if the panel wasn't destroyed in the meantime.
+ if (this.containerEl) {
+ console.error(e);
+ }
+ }
+ }
+
+ onNewSelection() {
+ this.render();
+ }
+
+ onCurrentNodeClassChanged() {
+ this.render();
+ }
+
+ onNodeFrontWillUnset() {
+ this.model.eraseClassPreview();
+ this.addEl.value = "";
+ }
+
+ onAutocompleteClassHovered(autocompleteItemLabel = "") {
+ if (this.autocompletePopup.isOpen) {
+ this.model.previewClass(autocompleteItemLabel);
+ }
+ }
+
+ onAutocompleteClosed() {
+ const inputValue = this.addEl.value;
+ this.model.previewClass(inputValue);
+ }
+}
+
+module.exports = ClassListPreviewer;
diff --git a/devtools/client/inspector/rules/views/moz.build b/devtools/client/inspector/rules/views/moz.build
new file mode 100644
index 0000000000..d5058bdbc2
--- /dev/null
+++ b/devtools/client/inspector/rules/views/moz.build
@@ -0,0 +1,9 @@
+# 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(
+ "class-list-previewer.js",
+ "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 0000000000..35c4b41e37
--- /dev/null
+++ b/devtools/client/inspector/rules/views/rule-editor.js
@@ -0,0 +1,842 @@
+/* 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("resource://devtools/shared/inspector/css-logic.js");
+const {
+ PSEUDO_CLASSES,
+} = require("resource://devtools/shared/css/constants.js");
+const {
+ style: { ELEMENT_STYLE },
+} = require("resource://devtools/shared/constants.js");
+const Rule = require("resource://devtools/client/inspector/rules/models/rule.js");
+const {
+ InplaceEditor,
+ editableField,
+ editableItem,
+} = require("resource://devtools/client/shared/inplace-editor.js");
+const TextPropertyEditor = require("resource://devtools/client/inspector/rules/views/text-property-editor.js");
+const {
+ createChild,
+ blurOnMultipleProperties,
+ promiseWarn,
+} = require("resource://devtools/client/inspector/shared/utils.js");
+const {
+ parseNamedDeclarations,
+ parsePseudoClassesAndAttributes,
+ SELECTOR_ATTRIBUTE,
+ SELECTOR_ELEMENT,
+ SELECTOR_PSEUDO_CLASS,
+} = require("resource://devtools/shared/css/parsing-utils.js");
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+const CssLogic = require("resource://devtools/shared/inspector/css-logic.js");
+
+loader.lazyRequireGetter(
+ this,
+ "Tools",
+ "resource://devtools/client/definitions.js",
+ true
+);
+
+const STYLE_INSPECTOR_PROPERTIES =
+ "devtools/shared/locales/styleinspector.properties";
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+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.
+ *
+ * @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.telemetry = this.toolbox.telemetry;
+ 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._onToolChanged = this._onToolChanged.bind(this);
+ this._updateLocation = this._updateLocation.bind(this);
+ this._onSourceClick = this._onSourceClick.bind(this);
+
+ this.rule.domRule.on("location-changed", this._locationChanged);
+ this.toolbox.on("tool-registered", this._onToolChanged);
+ this.toolbox.on("tool-unregistered", this._onToolChanged);
+
+ this._create();
+}
+
+RuleEditor.prototype = {
+ destroy() {
+ this.rule.domRule.off("location-changed");
+ this.toolbox.off("tool-registered", this._onToolChanged);
+ this.toolbox.off("tool-unregistered", this._onToolChanged);
+
+ if (this._unsubscribeSourceMap) {
+ this._unsubscribeSourceMap();
+ }
+ },
+
+ get sourceMapURLService() {
+ if (!this._sourceMapURLService) {
+ // sourceMapURLService is a lazy getter in the toolbox.
+ this._sourceMapURLService = this.toolbox.sourceMapURLService;
+ }
+
+ return this._sourceMapURLService;
+ },
+
+ get isSelectorEditable() {
+ const trait =
+ this.isEditable &&
+ 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() {
+ this.element = this.doc.createElement("div");
+ this.element.className = "ruleview-rule devtools-monospace";
+ this.element.dataset.ruleId = this.rule.domRule.actorID;
+ 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", this._onSourceClick);
+
+ const sourceLabel = this.doc.createElement("span");
+ sourceLabel.classList.add("ruleview-rule-source-label");
+ this.source.appendChild(sourceLabel);
+
+ this.updateSourceLink();
+
+ if (this.rule.domRule.ancestorData.length) {
+ const parts = this.rule.domRule.ancestorData.map(
+ (ancestorData, index) => {
+ if (ancestorData.type == "container") {
+ const container = this.doc.createElement("li");
+ container.classList.add("container-query");
+ container.setAttribute("data-ancestor-index", index);
+
+ createChild(container, "span", {
+ class: "container-query-declaration",
+ textContent: `@container${
+ ancestorData.containerName
+ ? " " + ancestorData.containerName
+ : ""
+ }`,
+ });
+
+ container.classList.add("has-tooltip");
+
+ const jumpToNodeButton = createChild(container, "button", {
+ class: "open-inspector",
+ title: l10n("rule.containerQuery.selectContainerButton.tooltip"),
+ });
+
+ let containerNodeFront;
+ const getNodeFront = async () => {
+ if (!containerNodeFront) {
+ const res = await this.rule.domRule.getQueryContainerForNode(
+ index,
+ this.rule.inherited ||
+ this.ruleView.inspector.selection.nodeFront
+ );
+ containerNodeFront = res.node;
+ }
+ return containerNodeFront;
+ };
+
+ jumpToNodeButton.addEventListener("click", async () => {
+ const front = await getNodeFront();
+ if (!front) {
+ return;
+ }
+ this.ruleView.inspector.selection.setNodeFront(front);
+ await this.ruleView.inspector.highlighters.hideHighlighterType(
+ this.ruleView.inspector.highlighters.TYPES.BOXMODEL
+ );
+ });
+ container.append(jumpToNodeButton);
+
+ container.addEventListener("mouseenter", async () => {
+ const front = await getNodeFront();
+ if (!front) {
+ return;
+ }
+
+ await this.ruleView.inspector.highlighters.showHighlighterTypeForNode(
+ this.ruleView.inspector.highlighters.TYPES.BOXMODEL,
+ front
+ );
+ });
+ container.addEventListener("mouseleave", async () => {
+ await this.ruleView.inspector.highlighters.hideHighlighterType(
+ this.ruleView.inspector.highlighters.TYPES.BOXMODEL
+ );
+ });
+
+ createChild(container, "span", {
+ // Add a space between the container name (or @container if there's no name)
+ // and the query so the title, which is computed from the DOM, displays correctly.
+ textContent: " " + ancestorData.containerQuery,
+ });
+ return container;
+ }
+ if (ancestorData.type == "layer") {
+ return `@layer${
+ ancestorData.value ? " " + ancestorData.value : ""
+ }`;
+ }
+ if (ancestorData.type == "media") {
+ return `@media ${ancestorData.value}`;
+ }
+
+ if (ancestorData.type == "supports") {
+ return `@supports ${ancestorData.conditionText}`;
+ }
+
+ if (ancestorData.type == "import") {
+ return `@import ${ancestorData.value}`;
+ }
+
+ // We shouldn't get here as `type` should only match to what can be set in
+ // the StyleRuleActor form, but just in case, let's return an empty string.
+ console.warn("Unknown ancestor data type:", ancestorData.type);
+ return ``;
+ }
+ );
+
+ this.ancestorDataEl = createChild(this.element, "ul", {
+ class: "ruleview-rule-ancestor-data theme-link",
+ });
+
+ for (const part of parts) {
+ if (typeof part == "string") {
+ createChild(this.ancestorDataEl, "li", {
+ textContent: part,
+ });
+ } else {
+ this.ancestorDataEl.append(part);
+ }
+ }
+ }
+
+ const code = createChild(this.element, "div", {
+ class: "ruleview-code",
+ });
+
+ const header = createChild(code, "div", {});
+
+ this.selectorText = createChild(header, "span", {
+ class: "ruleview-selectorcontainer",
+ tabindex: this.isSelectorEditable ? "0" : "-1",
+ });
+
+ if (this.isSelectorEditable) {
+ this.selectorText.addEventListener("click", event => {
+ // Clicks within the selector shouldn't propagate any further.
+ event.stopPropagation();
+ });
+
+ editableField({
+ element: this.selectorText,
+ done: this._onSelectorDone,
+ cssProperties: this.rule.cssProperties,
+ });
+ }
+
+ if (this.rule.domRule.type !== CSSRule.KEYFRAME_RULE) {
+ let selector = "";
+ if (this.rule.domRule.selectors) {
+ // This is a "normal" rule with a selector.
+ selector = this.rule.domRule.selectors.join(", ");
+ // Otherwise, the rule is either inherited or inline, and selectors will
+ // be computed on demand when the highlighter is requested.
+ }
+
+ const isHighlighted = this.ruleView.isSelectorHighlighted(selector);
+ // Handling of click events is delegated to CssRuleView.handleEvent()
+ createChild(header, "span", {
+ class:
+ "ruleview-selectorhighlighter js-toggle-selector-highlighter" +
+ (isHighlighted ? " highlighted" : ""),
+ "data-selector": selector,
+ title: l10n("rule.selectorHighlighter.tooltip"),
+ });
+ }
+
+ 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", event => {
+ const selection = this.doc.defaultView.getSelection();
+ if (selection.isCollapsed && !this._ruleViewIsEditing) {
+ this.newProperty();
+ }
+ // Cleanup the _ruleViewIsEditing flag
+ this._ruleViewIsEditing = false;
+ });
+
+ this.element.addEventListener("mousedown", () => {
+ this.doc.defaultView.focus();
+ });
+
+ // Create a property editor when the close brace is clicked.
+ editableItem({ element: this.closeBrace }, () => {
+ this.newProperty();
+ });
+ }
+ },
+
+ /**
+ * Called when a tool is registered or unregistered.
+ */
+ _onToolChanged() {
+ // When the source editor is registered, update the source links
+ // to be clickable; and if it is unregistered, update the links to
+ // be unclickable. However, some links are never clickable, so
+ // filter those out first.
+ if (this.source.getAttribute("unselectable") === "permanent") {
+ // Nothing.
+ } else if (this.toolbox.isToolRegistered("styleeditor")) {
+ this.source.removeAttribute("unselectable");
+ } else {
+ this.source.setAttribute("unselectable", "true");
+ }
+ },
+
+ /**
+ * Event handler called when a property changes on the
+ * StyleRuleActor.
+ */
+ _locationChanged() {
+ this.updateSourceLink();
+ },
+
+ _onSourceClick() {
+ if (this.source.hasAttribute("unselectable")) {
+ return;
+ }
+
+ const { inspector } = this.ruleView;
+ if (Tools.styleEditor.isToolSupported(inspector.toolbox)) {
+ inspector.toolbox.viewSourceInStyleEditorByResource(
+ this.rule.sheet,
+ this.rule.ruleLine,
+ this.rule.ruleColumn
+ );
+ }
+ },
+
+ /**
+ * Update the text of the source link to reflect whether we're showing
+ * original sources or not. This is a callback for
+ * SourceMapURLService.subscribeByID, which see.
+ *
+ * @param {Object | null} originalLocation
+ * The original position object (url/line/column) or null.
+ */
+ _updateLocation(originalLocation) {
+ let displayURL = this.rule.sheet?.href;
+ const constructed = this.rule.sheet?.constructed;
+ let line = this.rule.ruleLine;
+ if (originalLocation) {
+ displayURL = originalLocation.url;
+ line = originalLocation.line;
+ }
+
+ let sourceTextContent = CssLogic.shortSource({
+ constructed,
+ href: displayURL,
+ });
+ let title = displayURL ? displayURL : sourceTextContent;
+ if (line > 0) {
+ sourceTextContent += ":" + line;
+ title += ":" + line;
+ }
+
+ const sourceLabel = this.element.querySelector(
+ ".ruleview-rule-source-label"
+ );
+ sourceLabel.setAttribute("title", title);
+ sourceLabel.textContent = sourceTextContent;
+ },
+
+ updateSourceLink() {
+ if (this.rule.isSystem) {
+ const sourceLabel = this.element.querySelector(
+ ".ruleview-rule-source-label"
+ );
+ const title = this.rule.title;
+ const sourceHref = this.rule.sheet?.href || title;
+
+ const 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", "permanent");
+ sourceLabel.textContent = uaLabel;
+ sourceLabel.removeAttribute("title");
+ }
+ } else {
+ this._updateLocation(null);
+ }
+
+ if (
+ this.rule.sheet &&
+ !this.rule.isSystem &&
+ this.rule.domRule.type !== ELEMENT_STYLE
+ ) {
+ // Only get the original source link if the rule isn't a system
+ // rule and if it isn't an inline rule.
+ if (this._unsubscribeSourceMap) {
+ this._unsubscribeSourceMap();
+ }
+ this._unsubscribeSourceMap = this.sourceMapURLService.subscribeByID(
+ this.rule.sheet.resourceId,
+ this.rule.ruleLine,
+ this.rule.ruleColumn,
+ this._updateLocation
+ );
+ // Set "unselectable" appropriately.
+ this._onToolChanged();
+ } else if (this.rule.domRule.type === ELEMENT_STYLE) {
+ this.source.setAttribute("unselectable", "permanent");
+ } else {
+ // Set "unselectable" appropriately.
+ this._onToolChanged();
+ }
+
+ Promise.resolve().then(() => {
+ this.emit("source-link-updated");
+ });
+ },
+
+ /**
+ * Update the rule editor with the contents of the rule.
+ *
+ * @param {Boolean} reset
+ * True to completely reset the rule editor before populating.
+ */
+ populate(reset) {
+ // 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: ", ",
+ });
+ }
+
+ const containerClass =
+ this.rule.matchedSelectors.indexOf(selector) > -1
+ ? "ruleview-selector-matched"
+ : "ruleview-selector-unmatched";
+ const selectorContainer = createChild(this.selectorText, "span", {
+ class: containerClass,
+ });
+
+ const parsedSelector = parsePseudoClassesAndAttributes(selector);
+
+ for (const 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 = PSEUDO_CLASSES.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,
+ });
+ }
+ });
+ }
+
+ if (reset) {
+ while (this.propertyList.hasChildNodes()) {
+ this.propertyList.removeChild(this.propertyList.lastChild);
+ }
+ }
+
+ for (const prop of this.rule.textProps) {
+ if (!prop.editor && !prop.invisible) {
+ const editor = new TextPropertyEditor(this, prop);
+ this.propertyList.appendChild(editor.element);
+ } else if (prop.editor) {
+ // If an editor already existed, append it to the bottom now to make sure the
+ // order of editors in the DOM follow the order of the rule's properties.
+ this.propertyList.appendChild(prop.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(name, value, priority, enabled, siblingProp) {
+ const prop = this.rule.createProperty(
+ name,
+ value,
+ priority,
+ enabled,
+ siblingProp
+ );
+ const index = this.rule.textProps.indexOf(prop);
+ const 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(properties, siblingProp) {
+ if (!properties || !properties.length) {
+ return;
+ }
+
+ let lastProp = siblingProp;
+ for (const p of properties) {
+ const isCommented = Boolean(p.commentOffsets);
+ const 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() {
+ // 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,
+ });
+
+ // Auto-close the input if multiple rules get pasted into new property.
+ this.editor.input.addEventListener(
+ "paste",
+ blurOnMultipleProperties(this.rule.cssProperties)
+ );
+ },
+
+ /**
+ * 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(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 = parseNamedDeclarations(
+ this.rule.cssProperties.isKnown,
+ value,
+ true
+ );
+
+ // Blur the editor field now and deal with adding declarations later when
+ // the field gets destroyed (see _newPropertyDestroy)
+ this.editor.input.blur();
+
+ this.telemetry.recordEvent("edit_rule", "ruleview");
+ },
+
+ /**
+ * 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() {
+ // 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.
+ */
+ async _onSelectorDone(value, commit, direction) {
+ if (
+ !commit ||
+ this.isEditing ||
+ value === "" ||
+ value === this.rule.selectorText
+ ) {
+ return;
+ }
+
+ const ruleView = this.ruleView;
+ const elementStyle = ruleView._elementStyle;
+ const element = elementStyle.element;
+
+ this.isEditing = true;
+
+ // Remove highlighter for the previous selector.
+ if (this.ruleView.isSelectorHighlighted(this.rule.selectorText)) {
+ await this.ruleView.toggleSelectorHighlighter(this.rule.selectorText);
+ }
+
+ try {
+ const response = await this.rule.domRule.modifySelector(element, value);
+
+ // We recompute the list of applied styles, because editing a
+ // selector might cause this rule's position to change.
+ const applied = await elementStyle.pageStyle.getApplied(element, {
+ inherited: true,
+ matchedSelectors: true,
+ filter: elementStyle.showUserAgentStyles ? "ua" : undefined,
+ });
+
+ this.isEditing = false;
+
+ const { 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;
+ const newRule = new Rule(elementStyle, ruleProps);
+ const editor = new RuleEditor(ruleView, newRule);
+ const rules = elementStyle.rules;
+
+ let newRuleIndex = applied.findIndex(r => r.rule == ruleProps.rule);
+ const 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.onRuleUpdated();
+
+ // 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);
+
+ 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(direction) {
+ if (!direction || direction === Services.focus.MOVEFOCUS_BACKWARD) {
+ return;
+ }
+
+ if (this.rule.textProps.length) {
+ this.rule.textProps[0].editor.nameSpan.click();
+ } else {
+ this.propertyList.click();
+ }
+ },
+};
+
+module.exports = 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 0000000000..28340c8a14
--- /dev/null
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -0,0 +1,1598 @@
+/* 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("resource://devtools/shared/inspector/css-logic.js");
+const {
+ InplaceEditor,
+ editableField,
+} = require("resource://devtools/client/shared/inplace-editor.js");
+const {
+ createChild,
+ appendText,
+ advanceValidate,
+ blurOnMultipleProperties,
+} = require("resource://devtools/client/inspector/shared/utils.js");
+const { throttle } = require("resource://devtools/shared/throttle.js");
+const {
+ style: { ELEMENT_STYLE },
+} = require("resource://devtools/shared/constants.js");
+
+loader.lazyRequireGetter(
+ this,
+ "openContentLink",
+ "resource://devtools/client/shared/link.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ ["parseDeclarations", "parseSingleValue"],
+ "resource://devtools/shared/css/parsing-utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "findCssSelector",
+ "resource://devtools/shared/inspector/css-logic.js",
+ true
+);
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
+});
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const inlineCompatibilityWarningEnabled = Services.prefs.getBoolPref(
+ "devtools.inspector.ruleview.inline-compatibility-warning.enabled"
+);
+
+const SHARED_SWATCH_CLASS = "ruleview-swatch";
+const COLOR_SWATCH_CLASS = "ruleview-colorswatch";
+const BEZIER_SWATCH_CLASS = "ruleview-bezierswatch";
+const LINEAR_EASING_SWATCH_CLASS = "ruleview-lineareasingswatch";
+const FILTER_SWATCH_CLASS = "ruleview-filterswatch";
+const ANGLE_SWATCH_CLASS = "ruleview-angleswatch";
+const FONT_FAMILY_CLASS = "ruleview-font-family";
+const SHAPE_SWATCH_CLASS = "ruleview-shapeswatch";
+
+/*
+ * 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}`,
+ `.${LINEAR_EASING_SWATCH_CLASS}`,
+ `.${FILTER_SWATCH_CLASS}`,
+ `.${ANGLE_SWATCH_CLASS}`,
+ "a",
+];
+
+/*
+ * Speeds at which we update the value when the user is dragging its mouse
+ * over a value.
+ */
+const SLOW_DRAGGING_SPEED = 0.1;
+const DEFAULT_DRAGGING_SPEED = 1;
+const FAST_DRAGGING_SPEED = 10;
+
+// Deadzone in pixels where dragging should not update the value.
+const DRAGGING_DEADZONE_DISTANCE = 5;
+
+const DRAGGABLE_VALUE_CLASSNAME = "ruleview-propertyvalue-draggable";
+const IS_DRAGGING_CLASSNAME = "ruleview-propertyvalue-dragging";
+
+// In order to highlight the used fonts in font-family properties, we
+// retrieve the list of used fonts from the server. That always
+// returns the actually used font family name(s). If the property's
+// authored value is sans-serif for instance, the used font might be
+// arial instead. So we need the list of all generic font family
+// names to underline those when we find them.
+const GENERIC_FONT_FAMILIES = [
+ "serif",
+ "sans-serif",
+ "cursive",
+ "fantasy",
+ "monospace",
+ "system-ui",
+];
+
+/**
+ * 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.cssProperties = this.ruleView.cssProperties;
+ 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;
+
+ this.toolbox = this.ruleView.inspector.toolbox;
+ this.telemetry = this.toolbox.telemetry;
+
+ this._isDragging = false;
+ this._hasDragged = false;
+ this._draggingController = null;
+ this._draggingValueCache = null;
+
+ this.getGridlineNames = this.getGridlineNames.bind(this);
+ this.update = this.update.bind(this);
+ this.updatePropertyState = this.updatePropertyState.bind(this);
+ this._onDraggablePreferenceChanged =
+ this._onDraggablePreferenceChanged.bind(this);
+ this._onEnableChanged = this._onEnableChanged.bind(this);
+ this._onEnableClicked = this._onEnableClicked.bind(this);
+ this._onExpandClicked = this._onExpandClicked.bind(this);
+ this._onNameDone = this._onNameDone.bind(this);
+ this._onStartEditing = this._onStartEditing.bind(this);
+ this._onSwatchCommit = this._onSwatchCommit.bind(this);
+ this._onSwatchPreview = this._onSwatchPreview.bind(this);
+ this._onSwatchRevert = this._onSwatchRevert.bind(this);
+ this._onValidate = this.ruleView.debounce(this._previewValue, 10, this);
+ this._onValueDone = this._onValueDone.bind(this);
+
+ this._draggingOnMouseDown = this._draggingOnMouseDown.bind(this);
+ this._draggingOnMouseMove = throttle(this._draggingOnMouseMove, 30, this);
+ this._draggingOnMouseUp = this._draggingOnMouseUp.bind(this);
+ this._draggingOnKeydown = this._draggingOnKeydown.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;
+ },
+
+ // Exposed for tests.
+ get _DRAGGING_DEADZONE_DISTANCE() {
+ return DRAGGING_DEADZONE_DISTANCE;
+ },
+
+ /**
+ * Create the property editor's DOM.
+ */
+ _create() {
+ this.element = this.doc.createElementNS(HTML_NS, "li");
+ this.element.classList.add("ruleview-property");
+ this.element.dataset.declarationId = this.prop.id;
+ 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, "input", {
+ type: "checkbox",
+ class: "ruleview-enableproperty",
+ "aria-labelledby": this.prop.id,
+ tabindex: "-1",
+ });
+
+ 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-color3",
+ tabindex: this.ruleEditor.isEditable ? "0" : "-1",
+ id: this.prop.id,
+ });
+
+ appendText(this.nameContainer, ": ");
+
+ // 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);
+
+ // 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;
+
+ appendText(this.valueContainer, ";");
+
+ this.warning = createChild(this.container, "div", {
+ class: "ruleview-warning",
+ hidden: "",
+ title: l10n("rule.warning.title"),
+ });
+
+ this.unusedState = createChild(this.container, "div", {
+ class: "ruleview-unused-warning",
+ hidden: "",
+ });
+
+ if (inlineCompatibilityWarningEnabled) {
+ this.compatibilityState = createChild(this.container, "div", {
+ class: "ruleview-compatibility-warning",
+ hidden: "",
+ });
+ }
+
+ // 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();
+ });
+
+ // Holds the viewers for the computed properties.
+ // will be populated in |_updateComputed|.
+ this.computed = createChild(this.element, "ul", {
+ class: "ruleview-computedlist",
+ });
+
+ // Holds the viewers for the overridden shorthand properties.
+ // will be populated in |_updateShorthandOverridden|.
+ this.shorthandOverridden = createChild(this.element, "ul", {
+ class: "ruleview-overridden-items",
+ });
+
+ // Only bind event handlers if the rule is editable.
+ if (this.ruleEditor.isEditable) {
+ this.enable.addEventListener("click", this._onEnableClicked, true);
+ this.enable.addEventListener("change", this._onEnableChanged, 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();
+ }
+ });
+
+ 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,
+ });
+
+ // Auto blur name field on multiple CSS rules get pasted in.
+ this.nameContainer.addEventListener(
+ "paste",
+ blurOnMultipleProperties(this.cssProperties)
+ );
+
+ 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();
+ }
+ });
+
+ // 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 => {
+ const clickedEl = event.target;
+ if (clickedEl === this.valueSpan) {
+ return;
+ }
+ this._hasPendingClick = true;
+
+ const matchedSelector = ACTIONABLE_ELEMENTS_SELECTORS.find(selector =>
+ clickedEl.matches(selector)
+ );
+ if (matchedSelector) {
+ const similarElements = [
+ ...this.valueSpan.querySelectorAll(matchedSelector),
+ ];
+ this._clickedElementOptions = {
+ selector: matchedSelector,
+ index: similarElements.indexOf(clickedEl),
+ };
+ }
+ });
+
+ this.valueSpan.addEventListener("mouseup", event => {
+ // if we have dragged, we will handle the pending click in _draggingOnMouseUp instead
+ if (this._hasDragged) {
+ return;
+ }
+ this._clickedElementOptions = null;
+ this._hasPendingClick = false;
+ });
+
+ this.valueSpan.addEventListener("click", event => {
+ const target = event.target;
+
+ if (target.nodeName === "a") {
+ event.stopPropagation();
+ event.preventDefault();
+ openContentLink(target.href);
+ }
+ });
+
+ this.ruleView.on(
+ "draggable-preference-updated",
+ this._onDraggablePreferenceChanged
+ );
+ if (this._isDraggableProperty(this.prop)) {
+ this._addDraggingCapability();
+ }
+
+ 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,
+ defaultIncrement: this.prop.name === "opacity" ? 0.1 : 1,
+ popup: this.popup,
+ multiline: true,
+ maxWidth: () => this.container.getBoundingClientRect().width,
+ cssProperties: this.cssProperties,
+ cssVariables:
+ this.rule.elementStyle.variablesMap.get(this.rule.pseudoElement) ||
+ [],
+ getGridLineNames: this.getGridlineNames,
+ showSuggestCompletionOnEmpty: true,
+ });
+ }
+ },
+
+ /**
+ * Get the grid line names of the grid that the currently selected element is
+ * contained in.
+ *
+ * @return {Object} Contains the names of the cols and rows as arrays
+ * {cols: [], rows: []}.
+ */
+ async getGridlineNames() {
+ const gridLineNames = { cols: [], rows: [] };
+ const layoutInspector =
+ await this.ruleView.inspector.walker.getLayoutInspector();
+ const gridFront = await layoutInspector.getCurrentGrid(
+ this.ruleView.inspector.selection.nodeFront
+ );
+
+ if (gridFront) {
+ const gridFragments = gridFront.gridFragments;
+
+ for (const gridFragment of gridFragments) {
+ for (const rowLine of gridFragment.rows.lines) {
+ // We specifically ignore implicit line names created from implicitly named
+ // areas. This is because showing implicit line names can be confusing for
+ // designers who may have used a line name with "-start" or "-end" and created
+ // an implicitly named grid area without meaning to.
+ let gridArea;
+
+ for (const name of rowLine.names) {
+ const rowLineName =
+ name.substring(0, name.lastIndexOf("-start")) ||
+ name.substring(0, name.lastIndexOf("-end"));
+ gridArea = gridFragment.areas.find(
+ area => area.name === rowLineName
+ );
+
+ if (
+ rowLine.type === "implicit" &&
+ gridArea &&
+ gridArea.type === "implicit"
+ ) {
+ continue;
+ }
+ gridLineNames.rows.push(name);
+ }
+ }
+
+ for (const colLine of gridFragment.cols.lines) {
+ let gridArea;
+
+ for (const name of colLine.names) {
+ const colLineName =
+ name.substring(0, name.lastIndexOf("-start")) ||
+ name.substring(0, name.lastIndexOf("-end"));
+ gridArea = gridFragment.areas.find(
+ area => area.name === colLineName
+ );
+
+ if (
+ colLine.type === "implicit" &&
+ gridArea &&
+ gridArea.type === "implicit"
+ ) {
+ continue;
+ }
+ gridLineNames.cols.push(name);
+ }
+ }
+ }
+ }
+
+ // Emit message for test files
+ this.ruleView.inspector.emit("grid-line-names-updated");
+ return gridLineNames;
+ },
+
+ /**
+ * Get the path from which to resolve requests for this
+ * rule's stylesheet.
+ *
+ * @return {String} the stylesheet's href.
+ */
+ get sheetHref() {
+ const domRule = this.rule.domRule;
+ if (domRule) {
+ return domRule.href || domRule.nodeHref;
+ }
+ return undefined;
+ },
+
+ /**
+ * Populate the span based on changes to the TextProperty.
+ */
+ // eslint-disable-next-line complexity
+ update() {
+ if (this.ruleView.isDestroyed) {
+ return;
+ }
+
+ this.updatePropertyState();
+
+ const name = this.prop.name;
+ this.nameSpan.textContent = name;
+
+ // Combine the property's value and priority into one string for
+ // the value.
+ const store = this.rule.elementStyle.store;
+ let val = store.userProperties.getProperty(
+ this.rule.domRule,
+ name,
+ this.prop.value
+ );
+ if (this.prop.priority) {
+ val += " !" + this.prop.priority;
+ }
+
+ const propDirty = store.userProperties.contains(this.rule.domRule, name);
+
+ if (propDirty) {
+ this.element.setAttribute("dirty", "");
+ } else {
+ this.element.removeAttribute("dirty");
+ }
+
+ const outputParser = this.ruleView._outputParser;
+ const 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,
+ flexClass: "ruleview-flex js-toggle-flexbox-highlighter",
+ gridClass: "ruleview-grid js-toggle-grid-highlighter",
+ linearEasingClass: "ruleview-lineareasing",
+ linearEasingSwatchClass:
+ SHARED_SWATCH_CLASS + " " + LINEAR_EASING_SWATCH_CLASS,
+ shapeClass: "ruleview-shape",
+ shapeSwatchClass: SHAPE_SWATCH_CLASS,
+ // Only ask the parser to convert colors to the default color type specified by the
+ // user if the property hasn't been changed yet.
+ defaultColorType: !propDirty,
+ urlClass: "theme-link",
+ fontFamilyClass: FONT_FAMILY_CLASS,
+ baseURI: this.sheetHref,
+ unmatchedVariableClass: "ruleview-unmatched-variable",
+ matchedVariableClass: "ruleview-variable",
+ getVariableValue: varName =>
+ this.rule.elementStyle.getVariable(varName, this.rule.pseudoElement),
+ };
+ const frag = outputParser.parseCssProperty(name, val, parserOptions);
+
+ // Save the initial value as the last committed value,
+ // for restoring after pressing escape.
+ if (!this.committed) {
+ this.committed = {
+ name,
+ value: frag.textContent,
+ priority: this.prop.priority,
+ };
+ }
+
+ // Save focused element inside value span if one exists before wiping the innerHTML
+ let focusedElSelector = null;
+ if (this.valueSpan.contains(this.doc.activeElement)) {
+ focusedElSelector = findCssSelector(this.doc.activeElement);
+ }
+
+ this.valueSpan.innerHTML = "";
+ this.valueSpan.appendChild(frag);
+ if (
+ this.valueSpan.textProperty?.name === "grid-template-areas" &&
+ this.isValid() &&
+ (this.valueSpan.innerText.includes(`"`) ||
+ this.valueSpan.innerText.includes(`'`))
+ ) {
+ this._formatGridTemplateAreasValue();
+ }
+
+ this.ruleView.emit("property-value-updated", {
+ rule: this.prop.rule,
+ property: name,
+ value: val,
+ });
+
+ // Highlight the currently used font in font-family properties.
+ // If we cannot find a match, highlight the first generic family instead.
+ const fontFamilySpans = this.valueSpan.querySelectorAll(
+ "." + FONT_FAMILY_CLASS
+ );
+ if (fontFamilySpans.length && this.prop.enabled && !this.prop.overridden) {
+ this.rule.elementStyle
+ .getUsedFontFamilies()
+ .then(families => {
+ const usedFontFamilies = families.map(font => font.toLowerCase());
+ let foundMatchingFamily = false;
+ let firstGenericSpan = null;
+
+ for (const span of fontFamilySpans) {
+ const authoredFont = span.textContent.toLowerCase();
+
+ if (
+ !firstGenericSpan &&
+ GENERIC_FONT_FAMILIES.includes(authoredFont)
+ ) {
+ firstGenericSpan = span;
+ }
+
+ if (usedFontFamilies.includes(authoredFont)) {
+ span.classList.add("used-font");
+ foundMatchingFamily = true;
+ }
+ }
+
+ if (!foundMatchingFamily && firstGenericSpan) {
+ firstGenericSpan.classList.add("used-font");
+ }
+
+ this.ruleView.emit("font-highlighted", this.valueSpan);
+ })
+ .catch(e =>
+ console.error("Could not get the list of font families", e)
+ );
+ }
+
+ // Attach the color picker tooltip to the color swatches
+ this._colorSwatchSpans = this.valueSpan.querySelectorAll(
+ "." + COLOR_SWATCH_CLASS
+ );
+ if (this.ruleEditor.isEditable) {
+ for (const span of this._colorSwatchSpans) {
+ // Adding this swatch to the list of swatches our colorpicker
+ // knows about
+ this.ruleView.tooltips.getTooltip("colorPicker").addSwatch(span, {
+ onShow: this._onStartEditing,
+ onPreview: this._onSwatchPreview,
+ onCommit: this._onSwatchCommit,
+ onRevert: this._onSwatchRevert,
+ });
+ const title = l10n("rule.colorSwatch.tooltip");
+ span.setAttribute("title", title);
+ span.dataset.propertyName = this.nameSpan.textContent;
+ }
+ }
+
+ // Attach the cubic-bezier tooltip to the bezier swatches
+ this._bezierSwatchSpans = this.valueSpan.querySelectorAll(
+ "." + BEZIER_SWATCH_CLASS
+ );
+ if (this.ruleEditor.isEditable) {
+ for (const span of this._bezierSwatchSpans) {
+ // Adding this swatch to the list of swatches our colorpicker
+ // knows about
+ this.ruleView.tooltips.getTooltip("cubicBezier").addSwatch(span, {
+ onShow: this._onStartEditing,
+ onPreview: this._onSwatchPreview,
+ onCommit: this._onSwatchCommit,
+ onRevert: this._onSwatchRevert,
+ });
+ const title = l10n("rule.bezierSwatch.tooltip");
+ span.setAttribute("title", title);
+ }
+ }
+
+ // Attach the linear easing tooltip to the linear easing swatches
+ this._linearEasingSwatchSpans = this.valueSpan.querySelectorAll(
+ "." + LINEAR_EASING_SWATCH_CLASS
+ );
+ if (this.ruleEditor.isEditable) {
+ for (const span of this._linearEasingSwatchSpans) {
+ // Adding this swatch to the list of swatches our colorpicker
+ // knows about
+ this.ruleView.tooltips
+ .getTooltip("linearEaseFunction")
+ .addSwatch(span, {
+ onShow: this._onStartEditing,
+ onPreview: this._onSwatchPreview,
+ onCommit: this._onSwatchCommit,
+ onRevert: this._onSwatchRevert,
+ });
+ span.setAttribute("title", l10n("rule.bezierSwatch.tooltip"));
+ }
+ }
+
+ // Attach the filter editor tooltip to the filter swatch
+ const span = this.valueSpan.querySelector("." + FILTER_SWATCH_CLASS);
+ if (this.ruleEditor.isEditable) {
+ if (span) {
+ parserOptions.filterSwatch = true;
+
+ this.ruleView.tooltips.getTooltip("filterEditor").addSwatch(
+ span,
+ {
+ onShow: this._onStartEditing,
+ onPreview: this._onSwatchPreview,
+ onCommit: this._onSwatchCommit,
+ onRevert: this._onSwatchRevert,
+ },
+ outputParser,
+ parserOptions
+ );
+ const title = l10n("rule.filterSwatch.tooltip");
+ span.setAttribute("title", title);
+ }
+ }
+
+ this.angleSwatchSpans = this.valueSpan.querySelectorAll(
+ "." + ANGLE_SWATCH_CLASS
+ );
+ if (this.ruleEditor.isEditable) {
+ for (const angleSpan of this.angleSwatchSpans) {
+ angleSpan.on("unit-change", this._onSwatchCommit);
+ const title = l10n("rule.angleSwatch.tooltip");
+ angleSpan.setAttribute("title", title);
+ }
+ }
+
+ const nodeFront = this.ruleView.inspector.selection.nodeFront;
+
+ const flexToggle = this.valueSpan.querySelector(".ruleview-flex");
+ if (flexToggle) {
+ flexToggle.setAttribute("title", l10n("rule.flexToggle.tooltip"));
+ flexToggle.classList.toggle(
+ "active",
+ this.ruleView.inspector.highlighters.getNodeForActiveHighlighter(
+ this.ruleView.inspector.highlighters.TYPES.FLEXBOX
+ ) === nodeFront
+ );
+ }
+
+ const gridToggle = this.valueSpan.querySelector(".ruleview-grid");
+ if (gridToggle) {
+ gridToggle.setAttribute("title", l10n("rule.gridToggle.tooltip"));
+ gridToggle.classList.toggle(
+ "active",
+ this.ruleView.highlighters.gridHighlighters.has(nodeFront)
+ );
+ gridToggle.toggleAttribute(
+ "disabled",
+ !this.ruleView.highlighters.canGridHighlighterToggle(nodeFront)
+ );
+ }
+
+ const shapeToggle = this.valueSpan.querySelector(".ruleview-shapeswatch");
+ if (shapeToggle) {
+ const mode =
+ "css" +
+ name
+ .split("-")
+ .map(s => {
+ return s[0].toUpperCase() + s.slice(1);
+ })
+ .join("");
+ shapeToggle.setAttribute("data-mode", mode);
+ }
+
+ // 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 we are dragging, we don't need to handle the pending click
+ if (this._hasPendingClick && !this._isDragging) {
+ this._hasPendingClick = false;
+ let elToClick;
+
+ if (this._clickedElementOptions !== null) {
+ const { selector, index } = this._clickedElementOptions;
+ elToClick = this.valueSpan.querySelectorAll(selector)[index];
+
+ this._clickedElementOptions = null;
+ }
+
+ if (!elToClick) {
+ elToClick = this.valueSpan;
+ }
+ elToClick.click();
+ }
+
+ // Populate the computed styles and shorthand overridden styles.
+ this._updateComputed();
+ this._updateShorthandOverridden();
+
+ // Update the rule property highlight.
+ this.ruleView._updatePropertyHighlight(this);
+
+ // Restore focus back to the element whose markup was recreated above.
+ if (focusedElSelector) {
+ const elementToFocus = this.doc.querySelector(focusedElSelector);
+ if (elementToFocus) {
+ elementToFocus.focus();
+ }
+ }
+ },
+
+ _onStartEditing() {
+ this.element.classList.remove("ruleview-overridden");
+ this.filterProperty.hidden = true;
+ this.enable.style.visibility = "hidden";
+ this.expander.style.display = "none";
+ },
+
+ get shouldShowComputedExpander() {
+ // Only show the expander to reveal computed properties if:
+ // - the computed properties are actually different from the current property (i.e
+ // these are longhands while the current property is the shorthand)
+ // - all of the computed properties have defined values. In case the current property
+ // value contains CSS variables, then the computed properties will be missing and we
+ // want to avoid showing them.
+ return (
+ this.prop.computed.some(c => c.name !== this.prop.name) &&
+ !this.prop.computed.every(c => !c.value)
+ );
+ },
+
+ /**
+ * Update the visibility of the enable checkbox, the warning indicator, the used
+ * indicator and the filter property, as well as the overridden state of the property.
+ */
+ updatePropertyState() {
+ if (this.prop.enabled) {
+ this.enable.style.removeProperty("visibility");
+ } else {
+ this.enable.style.visibility = "visible";
+ }
+
+ this.enable.checked = this.prop.enabled;
+
+ this.warning.title = !this.isNameValid()
+ ? l10n("rule.warningName.title")
+ : l10n("rule.warning.title");
+
+ this.warning.hidden = this.editing || this.isValid();
+ this.filterProperty.hidden =
+ this.editing ||
+ !this.isValid() ||
+ !this.prop.overridden ||
+ this.ruleEditor.rule.isUnmatched;
+
+ this.expander.style.display = this.shouldShowComputedExpander
+ ? "inline-block"
+ : "none";
+
+ 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");
+ }
+
+ this.updatePropertyUsedIndicator();
+
+ if (inlineCompatibilityWarningEnabled) {
+ this.updatePropertyCompatibilityIndicator();
+ }
+ },
+
+ updatePropertyUsedIndicator() {
+ const { used } = this.prop.isUsed();
+
+ if (this.editing || this.prop.overridden || !this.prop.enabled || used) {
+ this.element.classList.remove("unused");
+ this.unusedState.hidden = true;
+ } else {
+ this.element.classList.add("unused");
+ this.unusedState.hidden = false;
+ }
+ },
+
+ async updatePropertyCompatibilityIndicator() {
+ const { isCompatible } = await this.prop.isCompatible();
+
+ if (this.editing || isCompatible) {
+ this.compatibilityState.hidden = true;
+ } else {
+ this.compatibilityState.hidden = false;
+ }
+ },
+
+ /**
+ * Update the indicator for computed styles. The computed styles themselves
+ * are populated on demand, when they become visible.
+ */
+ _updateComputed() {
+ this.computed.innerHTML = "";
+
+ this.expander.style.display =
+ !this.editing && this.shouldShowComputedExpander
+ ? "inline-block"
+ : "none";
+
+ this._populatedComputed = false;
+ if (this.expander.hasAttribute("open")) {
+ this._populateComputed();
+ }
+ },
+
+ /**
+ * Populate the list of computed styles.
+ */
+ _populateComputed() {
+ if (this._populatedComputed) {
+ return;
+ }
+ this._populatedComputed = true;
+
+ for (const computed of this.prop.computed) {
+ // Don't bother to duplicate information already
+ // shown in the text property.
+ if (computed.name === this.prop.name) {
+ continue;
+ }
+
+ // Store the computed style element for easy access when highlighting
+ // styles
+ computed.element = this._createComputedListItem(
+ this.computed,
+ computed,
+ "ruleview-computed"
+ );
+ }
+ },
+
+ /**
+ * Update the indicator for overridden shorthand styles. The shorthand
+ * overridden styles themselves are populated on demand, when they
+ * become visible.
+ */
+ _updateShorthandOverridden() {
+ this.shorthandOverridden.innerHTML = "";
+
+ this._populatedShorthandOverridden = false;
+ this._populateShorthandOverridden();
+ },
+
+ /**
+ * Populate the list of overridden shorthand styles.
+ */
+ _populateShorthandOverridden() {
+ if (
+ this._populatedShorthandOverridden ||
+ this.prop.overridden ||
+ !this.shouldShowComputedExpander
+ ) {
+ return;
+ }
+ this._populatedShorthandOverridden = true;
+
+ for (const computed of this.prop.computed) {
+ // Don't display duplicate information or show properties
+ // that are completely overridden.
+ if (computed.name === this.prop.name || !computed.overridden) {
+ continue;
+ }
+
+ this._createComputedListItem(
+ this.shorthandOverridden,
+ computed,
+ "ruleview-overridden-item"
+ );
+ }
+ },
+
+ /**
+ * Creates and populates a list item with the computed CSS property.
+ */
+ _createComputedListItem(parentEl, computed, className) {
+ const li = createChild(parentEl, "li", {
+ class: className,
+ });
+
+ if (computed.overridden) {
+ li.classList.add("ruleview-overridden");
+ }
+
+ const nameContainer = createChild(li, "span", {
+ class: "ruleview-namecontainer",
+ });
+
+ createChild(nameContainer, "span", {
+ class: "ruleview-propertyname theme-fg-color3",
+ textContent: computed.name,
+ });
+ appendText(nameContainer, ": ");
+
+ const outputParser = this.ruleView._outputParser;
+ const frag = outputParser.parseCssProperty(computed.name, computed.value, {
+ colorSwatchClass: "ruleview-swatch ruleview-colorswatch",
+ urlClass: "theme-link",
+ baseURI: this.sheetHref,
+ fontFamilyClass: "ruleview-font-family",
+ });
+
+ // Store the computed property value that was parsed for output
+ computed.parsedValue = frag.textContent;
+
+ const propertyContainer = createChild(li, "span", {
+ class: "ruleview-propertyvaluecontainer",
+ });
+
+ createChild(propertyContainer, "span", {
+ class: "ruleview-propertyvalue theme-fg-color1",
+ child: frag,
+ });
+ appendText(propertyContainer, ";");
+
+ return li;
+ },
+
+ /**
+ * Handle updates to the preference which disables/enables the feature to
+ * edit size properties on drag.
+ */
+ _onDraggablePreferenceChanged() {
+ if (this._isDraggableProperty(this.prop)) {
+ this._addDraggingCapability();
+ } else {
+ this._removeDraggingCapacity();
+ }
+ },
+
+ /**
+ * Stop clicks propogating down the tree from the enable / disable checkbox.
+ */
+ _onEnableClicked(event) {
+ event.stopPropagation();
+ },
+
+ /**
+ * Handles clicks on the disabled property.
+ */
+ _onEnableChanged(event) {
+ this.prop.setEnabled(this.enable.checked);
+ event.stopPropagation();
+ this.telemetry.recordEvent("edit_rule", "ruleview");
+ },
+
+ /**
+ * 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(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");
+ this.shorthandOverridden.hidden = false;
+ this._populateShorthandOverridden();
+ } else {
+ this.expander.setAttribute("open", "true");
+ this.computed.setAttribute("user-open", "");
+ this.shorthandOverridden.hidden = true;
+ 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() {
+ 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() {
+ 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(value, commit, direction) {
+ const isNameUnchanged =
+ (!commit && !this.ruleEditor.isEditing) || this.committed.name === value;
+ if (this.prop.value && isNameUnchanged) {
+ return;
+ }
+
+ this.telemetry.recordEvent("edit_rule", "ruleview");
+
+ // 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.
+ const 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(direction) {
+ if (this._colorSwatchSpans && this._colorSwatchSpans.length) {
+ for (const span of this._colorSwatchSpans) {
+ this.ruleView.tooltips.getTooltip("colorPicker").removeSwatch(span);
+ span.off("unit-change", this._onSwatchCommit);
+ }
+ }
+
+ if (this.angleSwatchSpans && this.angleSwatchSpans.length) {
+ for (const span of this.angleSwatchSpans) {
+ span.off("unit-change", this._onSwatchCommit);
+ }
+ }
+
+ this.ruleView.off(
+ "draggable-preference-updated",
+ this._onDraggablePreferenceChanged
+ );
+
+ this.element.remove();
+ 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(value = "", commit, direction) {
+ const parsedProperties = this._getValueAndExtraProperties(value);
+ const val = parseSingleValue(
+ this.cssProperties.isKnown,
+ parsedProperties.firstValue
+ );
+ const 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;
+ }
+
+ // Check if unit of value changed to add dragging feature
+ if (this._isDraggableProperty(val)) {
+ this._addDraggingCapability();
+ } else {
+ this._removeDraggingCapacity();
+ }
+
+ this.telemetry.recordEvent("edit_rule", "ruleview");
+
+ // 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() {
+ this._onValueDone(this.valueSpan.textContent, true);
+ this.update();
+ },
+
+ /**
+ * Called when the swatch editor wants to preview a value change.
+ */
+ _onSwatchPreview() {
+ 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() {
+ 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(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 = [];
+
+ const 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,
+ 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(value, reverting = false) {
+ // Since function call is debounced, we need to make sure we are still
+ // editing, and any selector modifications have been completed
+ if (!reverting && (!this.editing || this.ruleEditor.isEditing)) {
+ return;
+ }
+
+ const val = parseSingleValue(this.cssProperties.isKnown, value);
+ this.ruleEditor.rule.previewPropertyValue(
+ this.prop,
+ val.value,
+ val.priority
+ );
+ },
+
+ /**
+ * Check if the event passed has a "small increment" modifier
+ * Alt on macosx and ctrl on other OSs
+ *
+ * @param {KeyboardEvent} event
+ * @returns {Boolean}
+ */
+ _hasSmallIncrementModifier(event) {
+ const modifier =
+ lazy.AppConstants.platform === "macosx" ? "altKey" : "ctrlKey";
+ return event[modifier] === true;
+ },
+
+ /**
+ * Parses the value to check if it is a dimension
+ * e.g. if the input is "128px" it will return an object like
+ * { groups: { value: "128", unit: "px"}}
+ *
+ * @param {String} value
+ * @returns {Object|null}
+ */
+ _parseDimension(value) {
+ // The regex handles values like +1, -1, 1e4, .4, 1.3e-4, 1.567
+ const cssDimensionRegex =
+ /^(?<value>[+-]?(\d*\.)?\d+(e[+-]?\d+)?)(?<unit>(%|[a-zA-Z]+))$/;
+ return value.match(cssDimensionRegex);
+ },
+
+ /**
+ * Check if a textProperty value is supported to add the dragging feature
+ *
+ * @param {TextProperty} textProperty
+ * @returns {Boolean}
+ */
+ _isDraggableProperty(textProperty) {
+ // Check if the feature is explicitly disabled.
+ if (!this.ruleView.draggablePropertiesEnabled) {
+ return false;
+ }
+ // temporary way of fixing the bug when editing inline styles
+ // otherwise the textPropertyEditor object is destroyed on each value edit
+ // See Bug 1755024
+ if (this.rule.domRule.type == ELEMENT_STYLE) {
+ return false;
+ }
+
+ const nbValues = textProperty.value.split(" ").length;
+ if (nbValues > 1) {
+ // we do not support values like "1px solid red" yet
+ // See 1755025
+ return false;
+ }
+
+ const dimensionMatchObj = this._parseDimension(textProperty.value);
+ return !!dimensionMatchObj;
+ },
+
+ _draggingOnMouseDown(event) {
+ this._isDragging = true;
+ this.valueSpan.setPointerCapture(event.pointerId);
+ this._draggingController = new AbortController();
+ const { signal } = this._draggingController;
+
+ // turn off user-select in CSS when we drag
+ this.valueSpan.classList.add(IS_DRAGGING_CLASSNAME);
+
+ const dimensionObj = this._parseDimension(this.prop.value);
+ const { value, unit } = dimensionObj.groups;
+ this._draggingValueCache = {
+ isInDeadzone: true,
+ previousScreenX: event.screenX,
+ value: parseFloat(value),
+ unit,
+ };
+
+ this.valueSpan.addEventListener("mousemove", this._draggingOnMouseMove, {
+ signal,
+ });
+ this.valueSpan.addEventListener("mouseup", this._draggingOnMouseUp, {
+ signal,
+ });
+ this.valueSpan.addEventListener("keydown", this._draggingOnKeydown, {
+ signal,
+ });
+ },
+
+ _draggingOnMouseMove(event) {
+ if (!this._isDragging) {
+ return;
+ }
+
+ const { isInDeadzone, previousScreenX } = this._draggingValueCache;
+ let deltaX = event.screenX - previousScreenX;
+
+ // If `isInDeadzone` is still true, the user has not previously left the deadzone.
+ if (isInDeadzone) {
+ // If the mouse is still in the deadzone, bail out immediately.
+ if (Math.abs(deltaX) < DRAGGING_DEADZONE_DISTANCE) {
+ return;
+ }
+
+ // Otherwise, remove the DRAGGING_DEADZONE_DISTANCE from the current deltaX, so that
+ // the value does not update too abruptly.
+ deltaX =
+ Math.sign(deltaX) * (Math.abs(deltaX) - DRAGGING_DEADZONE_DISTANCE);
+
+ // Update the state to remember the user is out of the deadzone.
+ this._draggingValueCache.isInDeadzone = false;
+ }
+
+ let draggingSpeed = DEFAULT_DRAGGING_SPEED;
+ if (event.shiftKey) {
+ draggingSpeed = FAST_DRAGGING_SPEED;
+ } else if (this._hasSmallIncrementModifier(event)) {
+ draggingSpeed = SLOW_DRAGGING_SPEED;
+ }
+
+ const delta = deltaX * draggingSpeed;
+ this._draggingValueCache.previousScreenX = event.screenX;
+ this._draggingValueCache.value += delta;
+
+ if (delta == 0) {
+ return;
+ }
+
+ const { value, unit } = this._draggingValueCache;
+ // We use toFixed to avoid the case where value is too long, 9.00001px for example
+ const roundedValue = Number.isInteger(value) ? value : value.toFixed(1);
+ this.prop.setValue(roundedValue + unit, this.prop.priority);
+ this.ruleView.emitForTests("property-updated-by-dragging");
+ this._hasDragged = true;
+ },
+
+ _draggingOnMouseUp(event) {
+ if (!this._isDragging) {
+ return;
+ }
+ if (this._hasDragged) {
+ this.committed.value = this.prop.value;
+ this.prop.setEnabled(true);
+ }
+ this._onStopDragging(event);
+ },
+
+ _draggingOnKeydown(event) {
+ if (event.key == "Escape") {
+ this.prop.setValue(this.committed.value, this.committed.priority);
+ this._onStopDragging(event);
+ event.preventDefault();
+ }
+ },
+
+ _onStopDragging(event) {
+ // childHasDragged is used to stop the propagation of a click event when we
+ // release the mouse in the ruleview.
+ // The click event is not emitted when we have a pending click on the text property.
+ if (this._hasDragged && !this._hasPendingClick) {
+ this.ruleView.childHasDragged = true;
+ }
+ this._isDragging = false;
+ this._hasDragged = false;
+ this._draggingValueCache = null;
+ this.valueSpan.releasePointerCapture(event.pointerId);
+ this.valueSpan.classList.remove(IS_DRAGGING_CLASSNAME);
+ this._draggingController.abort();
+ },
+
+ /**
+ * add event listeners to add the ability to modify any size value
+ * by dragging the mouse horizontally
+ */
+ _addDraggingCapability() {
+ if (this.valueSpan.classList.contains(DRAGGABLE_VALUE_CLASSNAME)) {
+ return;
+ }
+ this.valueSpan.classList.add(DRAGGABLE_VALUE_CLASSNAME);
+ this.valueSpan.addEventListener("mousedown", this._draggingOnMouseDown);
+ },
+
+ _removeDraggingCapacity() {
+ if (!this.valueSpan.classList.contains(DRAGGABLE_VALUE_CLASSNAME)) {
+ return;
+ }
+ this._draggingController = null;
+ this.valueSpan.classList.remove(DRAGGABLE_VALUE_CLASSNAME);
+ this.valueSpan.removeEventListener("mousedown", this._draggingOnMouseDown);
+ },
+
+ /**
+ * 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 name + value pair is valid, false otherwise.
+ */
+ isValid() {
+ return this.prop.isValid();
+ },
+
+ /**
+ * Validate the name of this property.
+ * @return {Boolean} true if the property name is valid, false otherwise.
+ */
+ isNameValid() {
+ return this.prop.isNameValid();
+ },
+
+ /**
+ * Display grid-template-area value strings each on their own line
+ * to display it in an ascii-art style matrix
+ */
+ _formatGridTemplateAreasValue() {
+ this.valueSpan.classList.add("ruleview-propertyvalue-break-spaces");
+
+ let quoteSymbolsUsed = [];
+
+ const getQuoteSymbolsUsed = cssValue => {
+ const regex = /\"|\'/g;
+ const found = cssValue.match(regex);
+ quoteSymbolsUsed = found.filter((_, i) => i % 2 === 0);
+ };
+
+ getQuoteSymbolsUsed(this.valueSpan.innerText);
+
+ this.valueSpan.innerText = this.valueSpan.innerText
+ .split('"')
+ .filter(s => s !== "")
+ .map(s => s.split("'"))
+ .flat()
+ .map(s => s.trim().replace(/\s+/g, " "))
+ .filter(s => s.length)
+ .map(line => line.split(" "))
+ .map((line, i, lines) =>
+ line.map((col, j) =>
+ col.padEnd(Math.max(...lines.map(l => l[j].length)), " ")
+ )
+ )
+ .map(
+ (line, i) =>
+ `\n${quoteSymbolsUsed[i]}` + line.join(" ") + quoteSymbolsUsed[i]
+ )
+ .join(" ");
+ },
+};
+
+module.exports = TextPropertyEditor;