diff options
Diffstat (limited to 'devtools/client/inspector/rules/test')
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 Ɋ</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 Binary files differnew file mode 100644 index 0000000000..769c636340 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_test_image.png 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>` + ); +} |