summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/rules/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/inspector/rules/test
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/inspector/rules/test')
-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
332 files changed, 27461 insertions, 0 deletions
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>`
+ );
+}