From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../client/inspector/markup/components/TextNode.js | 88 + .../client/inspector/markup/components/moz.build | 9 + .../client/inspector/markup/markup-context-menu.js | 951 +++++++ devtools/client/inspector/markup/markup.js | 2682 ++++++++++++++++++++ devtools/client/inspector/markup/markup.xhtml | 43 + devtools/client/inspector/markup/moz.build | 19 + devtools/client/inspector/markup/test/browser.ini | 249 ++ .../browser_markup_accessibility_focus_blur.js | 77 + .../browser_markup_accessibility_navigation.js | 277 ++ ...r_markup_accessibility_navigation_after_edit.js | 126 + .../browser_markup_accessibility_new_selection.js | 34 + .../test/browser_markup_accessibility_semantics.js | 146 ++ .../markup/test/browser_markup_anonymous_01.js | 46 + .../markup/test/browser_markup_anonymous_03.js | 41 + .../markup/test/browser_markup_anonymous_04.js | 38 + .../markup/test/browser_markup_copy_html.js | 93 + .../markup/test/browser_markup_copy_image_data.js | 81 + ...ser_markup_css_completion_style_attribute_01.js | 130 + ...ser_markup_css_completion_style_attribute_02.js | 103 + ...ser_markup_css_completion_style_attribute_03.js | 52 + .../markup/test/browser_markup_display_node_01.js | 91 + .../markup/test/browser_markup_display_node_02.js | 165 ++ .../browser_markup_dom_mutation_breakpoints.js | 196 ++ .../test/browser_markup_dragdrop_autoscroll_01.js | 48 + .../test/browser_markup_dragdrop_autoscroll_02.js | 46 + ...browser_markup_dragdrop_before_marker_pseudo.js | 78 + .../test/browser_markup_dragdrop_distance.js | 48 + .../test/browser_markup_dragdrop_dragRootNode.js | 21 + .../test/browser_markup_dragdrop_draggable.js | 62 + .../test/browser_markup_dragdrop_escapeKeyPress.js | 38 + .../test/browser_markup_dragdrop_invalidNodes.js | 67 + .../markup/test/browser_markup_dragdrop_reorder.js | 111 + .../markup/test/browser_markup_dragdrop_tooltip.js | 36 + .../markup/test/browser_markup_events-overflow.js | 104 + .../test/browser_markup_events-windowed-host.js | 80 + .../markup/test/browser_markup_events_01.js | 132 + .../markup/test/browser_markup_events_02.js | 123 + .../markup/test/browser_markup_events_03.js | 88 + .../markup/test/browser_markup_events_04.js | 124 + .../test/browser_markup_events_chrome_blocked.js | 46 + .../browser_markup_events_chrome_not_blocked.js | 53 + .../test/browser_markup_events_click_to_close.js | 102 + .../test/browser_markup_events_jquery_1.0.js | 224 ++ .../test/browser_markup_events_jquery_1.1.js | 231 ++ .../test/browser_markup_events_jquery_1.11.1.js | 160 ++ .../test/browser_markup_events_jquery_1.2.js | 141 + .../test/browser_markup_events_jquery_1.3.js | 150 ++ .../test/browser_markup_events_jquery_1.4.js | 185 ++ .../test/browser_markup_events_jquery_1.6.js | 351 +++ .../test/browser_markup_events_jquery_1.7.js | 201 ++ .../test/browser_markup_events_jquery_2.1.1.js | 160 ++ .../test/browser_markup_events_object_listener.js | 43 + ...owser_markup_events_react_development_15.4.1.js | 113 + ...r_markup_events_react_development_15.4.1_jsx.js | 116 + ...rowser_markup_events_react_production_15.3.1.js | 113 + ...er_markup_events_react_production_15.3.1_jsx.js | 116 + ...rowser_markup_events_react_production_16.2.0.js | 133 + ...er_markup_events_react_production_16.2.0_jsx.js | 114 + .../test/browser_markup_events_source_map.js | 54 + .../markup/test/browser_markup_events_toggle.js | 295 +++ .../test/browser_markup_flex_display_badge.js | 107 + .../browser_markup_flex_display_badge_telemetry.js | 53 + .../test/browser_markup_grid_display_badge_01.js | 91 + .../test/browser_markup_grid_display_badge_02.js | 256 ++ .../test/browser_markup_grid_display_badge_03.js | 82 + .../browser_markup_grid_display_badge_telemetry.js | 45 + .../markup/test/browser_markup_html_edit_01.js | 111 + .../markup/test/browser_markup_html_edit_02.js | 157 ++ .../markup/test/browser_markup_html_edit_03.js | 305 +++ .../markup/test/browser_markup_html_edit_04.js | 101 + .../test/browser_markup_html_edit_undo-redo.js | 88 + .../test/browser_markup_iframe_blocked_by_csp.js | 53 + .../markup/test/browser_markup_image_tooltip.js | 63 + .../test/browser_markup_image_tooltip_mutations.js | 95 + .../markup/test/browser_markup_keybindings_01.js | 48 + .../markup/test/browser_markup_keybindings_02.js | 31 + .../markup/test/browser_markup_keybindings_03.js | 64 + .../markup/test/browser_markup_keybindings_04.js | 71 + ...browser_markup_keybindings_delete_attributes.js | 73 + .../browser_markup_keybindings_scrolltonode.js | 100 + .../markup/test/browser_markup_links_01.js | 178 ++ .../markup/test/browser_markup_links_02.js | 40 + .../markup/test/browser_markup_links_03.js | 39 + .../markup/test/browser_markup_links_04.js | 150 ++ .../markup/test/browser_markup_links_05.js | 74 + .../markup/test/browser_markup_links_06.js | 60 + .../markup/test/browser_markup_links_07.js | 134 + .../markup/test/browser_markup_load_01.js | 74 + .../markup/test/browser_markup_mutation_01.js | 419 +++ .../markup/test/browser_markup_mutation_02.js | 191 ++ .../markup/test/browser_markup_navigation.js | 128 + .../markup/test/browser_markup_node_names.js | 34 + .../test/browser_markup_node_names_namespaced.js | 52 + .../test/browser_markup_node_not_displayed_01.js | 37 + .../test/browser_markup_node_not_displayed_02.js | 147 ++ .../markup/test/browser_markup_overflow_badge.js | 101 + .../markup/test/browser_markup_pagesize_01.js | 91 + .../markup/test/browser_markup_pagesize_02.js | 48 + .../markup/test/browser_markup_pseudo_on_reload.js | 44 + .../test/browser_markup_remove_xul_attributes.js | 36 + .../markup/test/browser_markup_screenshot_node.js | 25 + .../browser_markup_screenshot_node_about_page.js | 46 + .../test/browser_markup_screenshot_node_iframe.js | 46 + .../browser_markup_screenshot_node_shadowdom.js | 41 + .../test/browser_markup_screenshot_node_warning.js | 38 + .../markup/test/browser_markup_scrollable_badge.js | 67 + .../test/browser_markup_scrollable_badge_click.js | 155 ++ .../markup/test/browser_markup_search_01.js | 56 + .../markup/test/browser_markup_shadowdom.js | 290 +++ .../test/browser_markup_shadowdom_clickreveal.js | 108 + .../browser_markup_shadowdom_clickreveal_scroll.js | 88 + .../test/browser_markup_shadowdom_copy_paths.js | 80 + .../markup/test/browser_markup_shadowdom_delete.js | 105 + .../test/browser_markup_shadowdom_dynamic.js | 155 ++ .../markup/test/browser_markup_shadowdom_hover.js | 78 + ...r_markup_shadowdom_marker_and_before_pseudos.js | 117 + .../test/browser_markup_shadowdom_maxchildren.js | 122 + .../browser_markup_shadowdom_mutations_shadow.js | 85 + .../test/browser_markup_shadowdom_navigation.js | 97 + ...browser_markup_shadowdom_nested_pick_inspect.js | 132 + .../markup/test/browser_markup_shadowdom_noslot.js | 107 + .../test/browser_markup_shadowdom_open_debugger.js | 133 + ...arkup_shadowdom_open_debugger_pretty_printed.js | 53 + .../browser_markup_shadowdom_shadowroot_mode.js | 52 + .../browser_markup_shadowdom_show_nodes_button.js | 51 + ...wser_markup_shadowdom_slotted_keyboard_focus.js | 71 + .../test/browser_markup_shadowdom_slotupdate.js | 68 + .../test/browser_markup_shadowdom_ua_widgets.js | 104 + ...browser_markup_shadowdom_ua_widgets_with_nac.js | 70 + .../test/browser_markup_subgrid_display_badge.js | 78 + .../browser_markup_tag_delete_whitespace_node.js | 79 + .../markup/test/browser_markup_tag_edit_01.js | 73 + .../markup/test/browser_markup_tag_edit_02.js | 46 + .../markup/test/browser_markup_tag_edit_03.js | 68 + .../test/browser_markup_tag_edit_04-backspace.js | 64 + .../test/browser_markup_tag_edit_04-delete.js | 64 + .../markup/test/browser_markup_tag_edit_05.js | 85 + .../markup/test/browser_markup_tag_edit_06.js | 95 + .../markup/test/browser_markup_tag_edit_07.js | 152 ++ .../markup/test/browser_markup_tag_edit_08.js | 140 + .../markup/test/browser_markup_tag_edit_09.js | 74 + .../markup/test/browser_markup_tag_edit_10.js | 39 + .../markup/test/browser_markup_tag_edit_11.js | 37 + .../markup/test/browser_markup_tag_edit_12.js | 103 + .../test/browser_markup_tag_edit_13-other.js | 39 + .../test/browser_markup_tag_edit_avoid_refocus.js | 51 + .../test/browser_markup_tag_edit_long-classname.js | 51 + .../markup/test/browser_markup_template.js | 55 + .../test/browser_markup_textcontent_display.js | 123 + .../test/browser_markup_textcontent_edit_01.js | 110 + .../test/browser_markup_textcontent_edit_02.js | 121 + .../markup/test/browser_markup_toggle_01.js | 59 + .../markup/test/browser_markup_toggle_02.js | 61 + .../markup/test/browser_markup_toggle_03.js | 51 + .../markup/test/browser_markup_toggle_04.js | 40 + .../test/browser_markup_toggle_closing_tag_line.js | 54 + .../test/browser_markup_update-on-navigtion.js | 57 + .../test/browser_markup_view-original-source.js | 55 + .../markup/test/browser_markup_view-source.js | 126 + .../test/browser_markup_void_elements_html.js | 51 + .../test/browser_markup_void_elements_xhtml.js | 33 + .../markup/test/browser_markup_whitespace.js | 106 + .../markup/test/doc_markup_anonymous.html | 32 + .../inspector/markup/test/doc_markup_dragdrop.html | 45 + .../test/doc_markup_dragdrop_autoscroll_01.html | 87 + .../test/doc_markup_dragdrop_autoscroll_02.html | 40 + .../inspector/markup/test/doc_markup_edit.html | 48 + .../markup/test/doc_markup_events-overflow.html | 19 + .../markup/test/doc_markup_events-source_map.html | 10 + .../markup/test/doc_markup_events_01.html | 118 + .../markup/test/doc_markup_events_02.html | 115 + .../markup/test/doc_markup_events_03.html | 103 + .../markup/test/doc_markup_events_04.html | 101 + .../test/doc_markup_events_chrome_listeners.html | 9 + .../markup/test/doc_markup_events_jquery.html | 69 + .../test/doc_markup_events_object_listener.html | 40 + ...doc_markup_events_react_development_15.4.1.html | 73 + ...markup_events_react_development_15.4.1_jsx.html | 51 + .../doc_markup_events_react_production_15.3.1.html | 73 + ..._markup_events_react_production_15.3.1_jsx.html | 51 + .../doc_markup_events_react_production_16.2.0.html | 80 + ..._markup_events_react_production_16.2.0_jsx.html | 50 + .../markup/test/doc_markup_events_toggle.html | 25 + .../inspector/markup/test/doc_markup_flashing.html | 15 + .../markup/test/doc_markup_html_mixed_case.html | 12 + .../markup/test/doc_markup_image_and_canvas.html | 24 + .../markup/test/doc_markup_image_and_canvas_2.html | 25 + .../inspector/markup/test/doc_markup_links.html | 42 + .../inspector/markup/test/doc_markup_mutation.html | 42 + .../markup/test/doc_markup_navigation.html | 28 + .../markup/test/doc_markup_not_displayed.html | 18 + .../markup/test/doc_markup_pagesize_01.html | 32 + .../markup/test/doc_markup_pagesize_02.html | 33 + .../inspector/markup/test/doc_markup_pseudo.html | 11 + .../inspector/markup/test/doc_markup_search.html | 11 + ...kup_shadowdom_open_debugger_pretty_printed.html | 11 + .../inspector/markup/test/doc_markup_subgrid.html | 53 + .../markup/test/doc_markup_svg_attributes.html | 8 + .../inspector/markup/test/doc_markup_toggle.html | 28 + .../inspector/markup/test/doc_markup_tooltip.png | Bin 0 -> 1095 bytes .../test/doc_markup_update-on-navigtion_1.html | 1 + .../test/doc_markup_update-on-navigtion_2.html | 1 + .../test/doc_markup_view-original-source.html | 9 + .../markup/test/doc_markup_void_elements.html | 18 + .../markup/test/doc_markup_void_elements.xhtml | 21 + .../markup/test/doc_markup_whitespace.html | 25 + .../inspector/markup/test/doc_markup_xul.xhtml | 9 + .../client/inspector/markup/test/events_bundle.js | 94 + .../inspector/markup/test/events_bundle.js.map | 1 + .../inspector/markup/test/events_original.js | 15 + devtools/client/inspector/markup/test/head.js | 670 +++++ .../markup/test/helper_attributes_test_runner.js | 161 ++ .../client/inspector/markup/test/helper_diff.js | 286 +++ .../markup/test/helper_events_test_runner.js | 229 ++ .../test/helper_markup_accessibility_navigation.js | 92 + .../markup/test/helper_outerhtml_test_runner.js | 99 + .../markup/test/helper_style_attr_test_runner.js | 151 ++ .../inspector/markup/test/lib_babel_6.21.0_min.js | 24 + .../client/inspector/markup/test/lib_jquery_1.0.js | 1814 +++++++++++++ .../client/inspector/markup/test/lib_jquery_1.1.js | 2172 ++++++++++++++++ .../inspector/markup/test/lib_jquery_1.11.1_min.js | 4 + .../inspector/markup/test/lib_jquery_1.2_min.js | 32 + .../inspector/markup/test/lib_jquery_1.3_min.js | 19 + .../inspector/markup/test/lib_jquery_1.4_min.js | 151 ++ .../inspector/markup/test/lib_jquery_1.6_min.js | 16 + .../inspector/markup/test/lib_jquery_1.7_min.js | 4 + .../inspector/markup/test/lib_jquery_2.1.1_min.js | 4 + .../inspector/markup/test/lib_react_16.2.0_min.js | 21 + .../markup/test/lib_react_dom_15.3.1_min.js | 12 + .../markup/test/lib_react_dom_16.2.0_min.js | 193 ++ .../test/lib_react_with_addons_15.3.1_min.js | 16 + .../markup/test/react_external_listeners.js | 10 + .../markup/test/shadowdom_open_debugger.min.js | 1 + devtools/client/inspector/markup/utils.js | 136 + devtools/client/inspector/markup/utils/l10n.js | 15 + devtools/client/inspector/markup/utils/moz.build | 9 + .../inspector/markup/views/element-container.js | 257 ++ .../inspector/markup/views/element-editor.js | 1149 +++++++++ .../client/inspector/markup/views/html-editor.js | 177 ++ .../inspector/markup/views/markup-container.js | 868 +++++++ devtools/client/inspector/markup/views/moz.build | 19 + .../inspector/markup/views/read-only-container.js | 36 + .../inspector/markup/views/read-only-editor.js | 82 + .../inspector/markup/views/root-container.js | 60 + .../markup/views/slotted-node-container.js | 76 + .../inspector/markup/views/slotted-node-editor.js | 63 + .../inspector/markup/views/text-container.js | 44 + .../client/inspector/markup/views/text-editor.js | 143 ++ 248 files changed, 30336 insertions(+) create mode 100644 devtools/client/inspector/markup/components/TextNode.js create mode 100644 devtools/client/inspector/markup/components/moz.build create mode 100644 devtools/client/inspector/markup/markup-context-menu.js create mode 100644 devtools/client/inspector/markup/markup.js create mode 100644 devtools/client/inspector/markup/markup.xhtml create mode 100644 devtools/client/inspector/markup/moz.build create mode 100644 devtools/client/inspector/markup/test/browser.ini create mode 100644 devtools/client/inspector/markup/test/browser_markup_accessibility_focus_blur.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_accessibility_navigation_after_edit.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_accessibility_new_selection.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_accessibility_semantics.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_anonymous_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_anonymous_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_anonymous_04.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_copy_html.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_copy_image_data.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_display_node_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_display_node_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dom_mutation_breakpoints.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_before_marker_pseudo.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_distance.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_dragRootNode.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_draggable.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_escapeKeyPress.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_reorder.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_tooltip.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events-overflow.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events-windowed-host.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_04.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_chrome_blocked.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_chrome_not_blocked.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_click_to_close.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.0.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.1.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.11.1.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.2.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.3.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.4.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.6.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.7.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_2.1.1.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_object_listener.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_react_development_15.4.1.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_react_development_15.4.1_jsx.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_react_production_15.3.1.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_react_production_15.3.1_jsx.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_react_production_16.2.0.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_react_production_16.2.0_jsx.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_source_map.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_toggle.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_flex_display_badge.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_flex_display_badge_telemetry.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_grid_display_badge_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_grid_display_badge_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_grid_display_badge_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_grid_display_badge_telemetry.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_html_edit_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_html_edit_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_html_edit_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_html_edit_04.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_html_edit_undo-redo.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_iframe_blocked_by_csp.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_image_tooltip.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_04.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_delete_attributes.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_scrolltonode.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_04.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_05.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_06.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_07.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_load_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_mutation_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_mutation_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_navigation.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_node_names.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_node_names_namespaced.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_node_not_displayed_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_node_not_displayed_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_overflow_badge.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_pagesize_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_pagesize_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_pseudo_on_reload.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_remove_xul_attributes.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_screenshot_node.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_screenshot_node_about_page.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_screenshot_node_iframe.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_screenshot_node_shadowdom.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_screenshot_node_warning.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_scrollable_badge.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_scrollable_badge_click.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_search_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal_scroll.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_copy_paths.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_delete.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_dynamic.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_hover.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_marker_and_before_pseudos.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_maxchildren.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_mutations_shadow.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_navigation.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_nested_pick_inspect.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_noslot.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_open_debugger.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_open_debugger_pretty_printed.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_shadowroot_mode.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_show_nodes_button.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_slotted_keyboard_focus.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_slotupdate.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_ua_widgets.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_ua_widgets_with_nac.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_subgrid_display_badge.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_delete_whitespace_node.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_04-backspace.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_04-delete.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_05.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_06.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_07.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_08.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_09.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_10.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_11.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_12.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_13-other.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_avoid_refocus.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_long-classname.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_template.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_textcontent_display.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_textcontent_edit_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_textcontent_edit_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_toggle_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_toggle_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_toggle_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_toggle_04.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_toggle_closing_tag_line.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_update-on-navigtion.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_view-original-source.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_view-source.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_void_elements_html.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_void_elements_xhtml.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_whitespace.js create mode 100644 devtools/client/inspector/markup/test/doc_markup_anonymous.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_dragdrop.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_01.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_02.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_edit.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events-overflow.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events-source_map.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_01.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_02.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_03.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_04.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_chrome_listeners.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_jquery.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_object_listener.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_react_development_15.4.1.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_react_development_15.4.1_jsx.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_react_production_15.3.1.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_react_production_15.3.1_jsx.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_react_production_16.2.0.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_react_production_16.2.0_jsx.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_toggle.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_flashing.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_html_mixed_case.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_image_and_canvas.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_image_and_canvas_2.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_links.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_mutation.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_navigation.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_not_displayed.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_pagesize_01.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_pagesize_02.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_pseudo.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_search.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_shadowdom_open_debugger_pretty_printed.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_subgrid.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_svg_attributes.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_toggle.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_tooltip.png create mode 100644 devtools/client/inspector/markup/test/doc_markup_update-on-navigtion_1.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_update-on-navigtion_2.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_view-original-source.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_void_elements.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_void_elements.xhtml create mode 100644 devtools/client/inspector/markup/test/doc_markup_whitespace.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_xul.xhtml create mode 100644 devtools/client/inspector/markup/test/events_bundle.js create mode 100644 devtools/client/inspector/markup/test/events_bundle.js.map create mode 100644 devtools/client/inspector/markup/test/events_original.js create mode 100644 devtools/client/inspector/markup/test/head.js create mode 100644 devtools/client/inspector/markup/test/helper_attributes_test_runner.js create mode 100644 devtools/client/inspector/markup/test/helper_diff.js create mode 100644 devtools/client/inspector/markup/test/helper_events_test_runner.js create mode 100644 devtools/client/inspector/markup/test/helper_markup_accessibility_navigation.js create mode 100644 devtools/client/inspector/markup/test/helper_outerhtml_test_runner.js create mode 100644 devtools/client/inspector/markup/test/helper_style_attr_test_runner.js create mode 100644 devtools/client/inspector/markup/test/lib_babel_6.21.0_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.0.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.1.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.11.1_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.2_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.3_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.4_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.6_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.7_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_2.1.1_min.js create mode 100644 devtools/client/inspector/markup/test/lib_react_16.2.0_min.js create mode 100644 devtools/client/inspector/markup/test/lib_react_dom_15.3.1_min.js create mode 100644 devtools/client/inspector/markup/test/lib_react_dom_16.2.0_min.js create mode 100644 devtools/client/inspector/markup/test/lib_react_with_addons_15.3.1_min.js create mode 100644 devtools/client/inspector/markup/test/react_external_listeners.js create mode 100644 devtools/client/inspector/markup/test/shadowdom_open_debugger.min.js create mode 100644 devtools/client/inspector/markup/utils.js create mode 100644 devtools/client/inspector/markup/utils/l10n.js create mode 100644 devtools/client/inspector/markup/utils/moz.build create mode 100644 devtools/client/inspector/markup/views/element-container.js create mode 100644 devtools/client/inspector/markup/views/element-editor.js create mode 100644 devtools/client/inspector/markup/views/html-editor.js create mode 100644 devtools/client/inspector/markup/views/markup-container.js create mode 100644 devtools/client/inspector/markup/views/moz.build create mode 100644 devtools/client/inspector/markup/views/read-only-container.js create mode 100644 devtools/client/inspector/markup/views/read-only-editor.js create mode 100644 devtools/client/inspector/markup/views/root-container.js create mode 100644 devtools/client/inspector/markup/views/slotted-node-container.js create mode 100644 devtools/client/inspector/markup/views/slotted-node-editor.js create mode 100644 devtools/client/inspector/markup/views/text-container.js create mode 100644 devtools/client/inspector/markup/views/text-editor.js (limited to 'devtools/client/inspector/markup') diff --git a/devtools/client/inspector/markup/components/TextNode.js b/devtools/client/inspector/markup/components/TextNode.js new file mode 100644 index 0000000000..1cb88f2538 --- /dev/null +++ b/devtools/client/inspector/markup/components/TextNode.js @@ -0,0 +1,88 @@ +/* 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 { + createElement, + createRef, + Fragment, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + editableItem, +} = require("resource://devtools/client/shared/inplace-editor.js"); + +const { + getStr, + getFormatStr, +} = require("resource://devtools/client/inspector/markup/utils/l10n.js"); + +class TextNode extends PureComponent { + static get propTypes() { + return { + showTextEditor: PropTypes.func.isRequired, + type: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + }; + } + + constructor(props) { + super(props); + + this.state = { + value: this.props.value, + }; + + this.valuePreRef = createRef(); + } + + componentDidMount() { + editableItem( + { + element: this.valuePreRef.current, + trigger: "dblclick", + }, + element => { + this.props.showTextEditor(element); + } + ); + } + + render() { + const { value } = this.state; + const isComment = this.props.type === "comment"; + const isWhiteSpace = !/[^\s]/.exec(value); + + return createElement( + Fragment, + null, + isComment ? dom.span({}, "") : null + ); + } +} + +module.exports = TextNode; diff --git a/devtools/client/inspector/markup/components/moz.build b/devtools/client/inspector/markup/components/moz.build new file mode 100644 index 0000000000..13e4ee411d --- /dev/null +++ b/devtools/client/inspector/markup/components/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "TextNode.js", +) diff --git a/devtools/client/inspector/markup/markup-context-menu.js b/devtools/client/inspector/markup/markup-context-menu.js new file mode 100644 index 0000000000..38ee79d6ba --- /dev/null +++ b/devtools/client/inspector/markup/markup-context-menu.js @@ -0,0 +1,951 @@ +/* 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 { + PSEUDO_CLASSES, +} = require("resource://devtools/shared/css/constants.js"); +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); + +loader.lazyRequireGetter( + this, + "Menu", + "resource://devtools/client/framework/menu.js" +); +loader.lazyRequireGetter( + this, + "MenuItem", + "resource://devtools/client/framework/menu-item.js" +); +loader.lazyRequireGetter( + this, + "clipboardHelper", + "resource://devtools/shared/platform/clipboard.js" +); + +loader.lazyGetter(this, "TOOLBOX_L10N", function () { + return new LocalizationHelper("devtools/client/locales/toolbox.properties"); +}); + +const INSPECTOR_L10N = new LocalizationHelper( + "devtools/client/locales/inspector.properties" +); + +/** + * Context menu for the Markup view. + */ +class MarkupContextMenu { + constructor(markup) { + this.markup = markup; + this.inspector = markup.inspector; + this.selection = this.inspector.selection; + this.target = this.inspector.currentTarget; + this.telemetry = this.inspector.telemetry; + this.toolbox = this.inspector.toolbox; + this.walker = this.inspector.walker; + } + + destroy() { + this.markup = null; + this.inspector = null; + this.selection = null; + this.target = null; + this.telemetry = null; + this.toolbox = null; + this.walker = null; + } + + show(event) { + if ( + !Element.isInstance(event.originalTarget) || + event.originalTarget.closest("input[type=text]") || + event.originalTarget.closest("input:not([type])") || + event.originalTarget.closest("textarea") + ) { + return; + } + + event.stopPropagation(); + event.preventDefault(); + + this._openMenu({ + screenX: event.screenX, + screenY: event.screenY, + target: event.target, + }); + } + + /** + * This method is here for the benefit of copying links. + */ + _copyAttributeLink(link) { + this.inspector.inspectorFront + .resolveRelativeURL(link, this.selection.nodeFront) + .then(url => { + clipboardHelper.copyString(url); + }, console.error); + } + + /** + * Copy the full CSS Path of the selected Node to the clipboard. + */ + _copyCssPath() { + if (!this.selection.isNode()) { + return; + } + + this.telemetry.scalarSet("devtools.copy.full.css.selector.opened", 1); + this.selection.nodeFront + .getCssPath() + .then(path => { + clipboardHelper.copyString(path); + }) + .catch(console.error); + } + + /** + * Copy the data-uri for the currently selected image in the clipboard. + */ + _copyImageDataUri() { + const container = this.markup.getContainer(this.selection.nodeFront); + if (container && container.isPreviewable()) { + container.copyImageDataUri(); + } + } + + /** + * Copy the innerHTML of the selected Node to the clipboard. + */ + _copyInnerHTML() { + this.markup.copyInnerHTML(); + } + + /** + * Copy the outerHTML of the selected Node to the clipboard. + */ + _copyOuterHTML() { + this.markup.copyOuterHTML(); + } + + /** + * Copy a unique selector of the selected Node to the clipboard. + */ + _copyUniqueSelector() { + if (!this.selection.isNode()) { + return; + } + + this.telemetry.scalarSet("devtools.copy.unique.css.selector.opened", 1); + this.selection.nodeFront + .getUniqueSelector() + .then(selector => { + clipboardHelper.copyString(selector); + }) + .catch(console.error); + } + + /** + * Copy the XPath of the selected Node to the clipboard. + */ + _copyXPath() { + if (!this.selection.isNode()) { + return; + } + + this.telemetry.scalarSet("devtools.copy.xpath.opened", 1); + this.selection.nodeFront + .getXPath() + .then(path => { + clipboardHelper.copyString(path); + }) + .catch(console.error); + } + + /** + * Delete the selected node. + */ + _deleteNode() { + if (!this.selection.isNode() || this.selection.isRoot()) { + return; + } + + const nodeFront = this.selection.nodeFront; + + // If the markup panel is active, use the markup panel to delete + // the node, making this an undoable action. + if (this.markup) { + this.markup.deleteNode(nodeFront); + } else { + // remove the node from content + nodeFront.walkerFront.removeNode(nodeFront); + } + } + + /** + * Duplicate the selected node + */ + _duplicateNode() { + if ( + !this.selection.isElementNode() || + this.selection.isRoot() || + this.selection.isAnonymousNode() || + this.selection.isPseudoElementNode() + ) { + return; + } + + const nodeFront = this.selection.nodeFront; + nodeFront.walkerFront.duplicateNode(nodeFront).catch(console.error); + } + + /** + * Edit the outerHTML of the selected Node. + */ + _editHTML() { + if (!this.selection.isNode()) { + return; + } + this.markup.beginEditingHTML(this.selection.nodeFront); + } + + /** + * Jumps to the custom element definition in the debugger. + */ + _jumpToCustomElementDefinition() { + const { url, line, column } = + this.selection.nodeFront.customElementLocation; + this.toolbox.viewSourceInDebugger( + url, + line, + column, + null, + "show_custom_element" + ); + } + + /** + * Add attribute to node. + * Used for node context menu and shouldn't be called directly. + */ + _onAddAttribute() { + const container = this.markup.getContainer(this.selection.nodeFront); + container.addAttribute(); + } + + /** + * Copy attribute value for node. + * Used for node context menu and shouldn't be called directly. + */ + _onCopyAttributeValue() { + clipboardHelper.copyString(this.nodeMenuTriggerInfo.value); + } + + /** + * This method is here for the benefit of the node-menu-link-copy menu item + * in the inspector contextual-menu. + */ + _onCopyLink() { + this._copyAttributeLink(this.contextMenuTarget.dataset.link); + } + + /** + * Edit attribute for node. + * Used for node context menu and shouldn't be called directly. + */ + _onEditAttribute() { + const container = this.markup.getContainer(this.selection.nodeFront); + container.editAttribute(this.nodeMenuTriggerInfo.name); + } + + /** + * This method is here for the benefit of the node-menu-link-follow menu item + * in the inspector contextual-menu. + */ + _onFollowLink() { + const type = this.contextMenuTarget.dataset.type; + const link = this.contextMenuTarget.dataset.link; + this.markup.followAttributeLink(type, link); + } + + /** + * Remove attribute from node. + * Used for node context menu and shouldn't be called directly. + */ + _onRemoveAttribute() { + const container = this.markup.getContainer(this.selection.nodeFront); + container.removeAttribute(this.nodeMenuTriggerInfo.name); + } + + /** + * Paste the contents of the clipboard as adjacent HTML to the selected Node. + * + * @param {String} position + * The position as specified for Element.insertAdjacentHTML + * (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd"). + */ + _pasteAdjacentHTML(position) { + const content = this._getClipboardContentForPaste(); + if (!content) { + return Promise.reject("No clipboard content for paste"); + } + + const node = this.selection.nodeFront; + return this.markup.insertAdjacentHTMLToNode(node, position, content); + } + + /** + * Paste the contents of the clipboard into the selected Node's inner HTML. + */ + _pasteInnerHTML() { + const content = this._getClipboardContentForPaste(); + if (!content) { + return Promise.reject("No clipboard content for paste"); + } + + const node = this.selection.nodeFront; + return this.markup.getNodeInnerHTML(node).then(oldContent => { + this.markup.updateNodeInnerHTML(node, content, oldContent); + }); + } + + /** + * Paste the contents of the clipboard into the selected Node's outer HTML. + */ + _pasteOuterHTML() { + const content = this._getClipboardContentForPaste(); + if (!content) { + return Promise.reject("No clipboard content for paste"); + } + + const node = this.selection.nodeFront; + return this.markup.getNodeOuterHTML(node).then(oldContent => { + this.markup.updateNodeOuterHTML(node, content, oldContent); + }); + } + + /** + * Show Accessibility properties for currently selected node + */ + async _showAccessibilityProperties() { + const a11yPanel = await this.toolbox.selectTool("accessibility"); + // Select the accessible object in the panel and wait for the event that + // tells us it has been done. + const onSelected = a11yPanel.once("new-accessible-front-selected"); + a11yPanel.selectAccessibleForNode( + this.selection.nodeFront, + "inspector-context-menu" + ); + await onSelected; + } + + /** + * Show DOM properties + */ + _showDOMProperties() { + this.toolbox.openSplitConsole().then(() => { + const { hud } = this.toolbox.getPanel("webconsole"); + hud.ui.wrapper.dispatchEvaluateExpression("inspect($0, true)"); + }); + } + + /** + * Use in Console. + * + * Takes the currently selected node in the inspector and assigns it to a + * temp variable on the content window. Also opens the split console and + * autofills it with the temp variable. + */ + async _useInConsole() { + await this.toolbox.openSplitConsole(); + const { hud } = this.toolbox.getPanel("webconsole"); + + const evalString = `{ let i = 0; + while (window.hasOwnProperty("temp" + i) && i < 1000) { + i++; + } + window["temp" + i] = $0; + "temp" + i; + }`; + + const res = await this.toolbox.commands.scriptCommand.execute(evalString, { + selectedNodeActor: this.selection.nodeFront.actorID, + }); + hud.setInputValue(res.result); + this.inspector.emit("console-var-ready"); + } + + _getAttributesSubmenu(isEditableElement) { + const attributesSubmenu = new Menu(); + const nodeInfo = this.nodeMenuTriggerInfo; + const isAttributeClicked = + isEditableElement && nodeInfo && nodeInfo.type === "attribute"; + + attributesSubmenu.append( + new MenuItem({ + id: "node-menu-add-attribute", + label: INSPECTOR_L10N.getStr("inspectorAddAttribute.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorAddAttribute.accesskey"), + disabled: !isEditableElement, + click: () => this._onAddAttribute(), + }) + ); + attributesSubmenu.append( + new MenuItem({ + id: "node-menu-copy-attribute", + label: INSPECTOR_L10N.getFormatStr( + "inspectorCopyAttributeValue.label", + isAttributeClicked ? `${nodeInfo.value}` : "" + ), + accesskey: INSPECTOR_L10N.getStr( + "inspectorCopyAttributeValue.accesskey" + ), + disabled: !isAttributeClicked, + click: () => this._onCopyAttributeValue(), + }) + ); + attributesSubmenu.append( + new MenuItem({ + id: "node-menu-edit-attribute", + label: INSPECTOR_L10N.getFormatStr( + "inspectorEditAttribute.label", + isAttributeClicked ? `${nodeInfo.name}` : "" + ), + accesskey: INSPECTOR_L10N.getStr("inspectorEditAttribute.accesskey"), + disabled: !isAttributeClicked, + click: () => this._onEditAttribute(), + }) + ); + attributesSubmenu.append( + new MenuItem({ + id: "node-menu-remove-attribute", + label: INSPECTOR_L10N.getFormatStr( + "inspectorRemoveAttribute.label", + isAttributeClicked ? `${nodeInfo.name}` : "" + ), + accesskey: INSPECTOR_L10N.getStr("inspectorRemoveAttribute.accesskey"), + disabled: !isAttributeClicked, + click: () => this._onRemoveAttribute(), + }) + ); + + return attributesSubmenu; + } + + /** + * Returns the clipboard content if it is appropriate for pasting + * into the current node's outer HTML, otherwise returns null. + */ + _getClipboardContentForPaste() { + const content = clipboardHelper.getText(); + if (content && content.trim().length) { + return content; + } + return null; + } + + _getCopySubmenu(markupContainer, isElement, isFragment) { + const copySubmenu = new Menu(); + copySubmenu.append( + new MenuItem({ + id: "node-menu-copyinner", + label: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.accesskey"), + disabled: !isElement && !isFragment, + click: () => this._copyInnerHTML(), + }) + ); + copySubmenu.append( + new MenuItem({ + id: "node-menu-copyouter", + label: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.accesskey"), + disabled: !isElement, + click: () => this._copyOuterHTML(), + }) + ); + copySubmenu.append( + new MenuItem({ + id: "node-menu-copyuniqueselector", + label: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.accesskey"), + disabled: !isElement, + click: () => this._copyUniqueSelector(), + }) + ); + copySubmenu.append( + new MenuItem({ + id: "node-menu-copycsspath", + label: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.accesskey"), + disabled: !isElement, + click: () => this._copyCssPath(), + }) + ); + copySubmenu.append( + new MenuItem({ + id: "node-menu-copyxpath", + label: INSPECTOR_L10N.getStr("inspectorCopyXPath.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorCopyXPath.accesskey"), + disabled: !isElement, + click: () => this._copyXPath(), + }) + ); + copySubmenu.append( + new MenuItem({ + id: "node-menu-copyimagedatauri", + label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"), + disabled: + !isElement || !markupContainer || !markupContainer.isPreviewable(), + click: () => this._copyImageDataUri(), + }) + ); + + return copySubmenu; + } + + _getDOMBreakpointSubmenu(isElement) { + const menu = new Menu(); + const mutationBreakpoints = this.selection.nodeFront.mutationBreakpoints; + + menu.append( + new MenuItem({ + id: "node-menu-mutation-breakpoint-subtree", + checked: mutationBreakpoints.subtree, + click: () => this.markup.toggleMutationBreakpoint("subtree"), + disabled: !isElement, + label: INSPECTOR_L10N.getStr("inspectorSubtreeModification.label"), + type: "checkbox", + }) + ); + + menu.append( + new MenuItem({ + id: "node-menu-mutation-breakpoint-attribute", + checked: mutationBreakpoints.attribute, + click: () => this.markup.toggleMutationBreakpoint("attribute"), + disabled: !isElement, + label: INSPECTOR_L10N.getStr("inspectorAttributeModification.label"), + type: "checkbox", + }) + ); + + menu.append( + new MenuItem({ + checked: mutationBreakpoints.removal, + click: () => this.markup.toggleMutationBreakpoint("removal"), + disabled: !isElement, + label: INSPECTOR_L10N.getStr("inspectorNodeRemoval.label"), + type: "checkbox", + }) + ); + + return menu; + } + + /** + * Link menu items can be shown or hidden depending on the context and + * selected node, and their labels can vary. + * + * @return {Array} list of visible menu items related to links. + */ + _getNodeLinkMenuItems() { + const linkFollow = new MenuItem({ + id: "node-menu-link-follow", + visible: false, + click: () => this._onFollowLink(), + }); + const linkCopy = new MenuItem({ + id: "node-menu-link-copy", + visible: false, + click: () => this._onCopyLink(), + }); + + // Get information about the right-clicked node. + const popupNode = this.contextMenuTarget; + if (!popupNode || !popupNode.classList.contains("link")) { + return [linkFollow, linkCopy]; + } + + const type = popupNode.dataset.type; + if (type === "uri" || type === "cssresource" || type === "jsresource") { + // Links can't be opened in new tabs in the browser toolbox. + if (type === "uri" && !this.toolbox.isBrowserToolbox) { + linkFollow.visible = true; + linkFollow.label = INSPECTOR_L10N.getStr( + "inspector.menu.openUrlInNewTab.label" + ); + } else if (type === "cssresource") { + linkFollow.visible = true; + linkFollow.label = TOOLBOX_L10N.getStr( + "toolbox.viewCssSourceInStyleEditor.label" + ); + } else if (type === "jsresource") { + linkFollow.visible = true; + linkFollow.label = TOOLBOX_L10N.getStr( + "toolbox.viewJsSourceInDebugger.label" + ); + } + + linkCopy.visible = true; + linkCopy.label = INSPECTOR_L10N.getStr( + "inspector.menu.copyUrlToClipboard.label" + ); + } else if (type === "idref") { + linkFollow.visible = true; + linkFollow.label = INSPECTOR_L10N.getFormatStr( + "inspector.menu.selectElement.label", + popupNode.dataset.link + ); + } + + return [linkFollow, linkCopy]; + } + + _getPasteSubmenu(isElement, isFragment, isAnonymous) { + const isPasteable = + !isAnonymous && + (isFragment || isElement) && + this._getClipboardContentForPaste(); + const disableAdjacentPaste = + !isPasteable || + !isElement || + this.selection.isRoot() || + this.selection.isBodyNode() || + this.selection.isHeadNode(); + const disableFirstLastPaste = + !isPasteable || + !isElement || + (this.selection.isHTMLNode() && this.selection.isRoot()); + + const pasteSubmenu = new Menu(); + pasteSubmenu.append( + new MenuItem({ + id: "node-menu-pasteinnerhtml", + label: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.accesskey"), + disabled: !isPasteable, + click: () => this._pasteInnerHTML(), + }) + ); + pasteSubmenu.append( + new MenuItem({ + id: "node-menu-pasteouterhtml", + label: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.accesskey"), + disabled: !isPasteable || !isElement, + click: () => this._pasteOuterHTML(), + }) + ); + pasteSubmenu.append( + new MenuItem({ + id: "node-menu-pastebefore", + label: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.accesskey"), + disabled: disableAdjacentPaste, + click: () => this._pasteAdjacentHTML("beforeBegin"), + }) + ); + pasteSubmenu.append( + new MenuItem({ + id: "node-menu-pasteafter", + label: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.accesskey"), + disabled: disableAdjacentPaste, + click: () => this._pasteAdjacentHTML("afterEnd"), + }) + ); + pasteSubmenu.append( + new MenuItem({ + id: "node-menu-pastefirstchild", + label: INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.label"), + accesskey: INSPECTOR_L10N.getStr( + "inspectorHTMLPasteFirstChild.accesskey" + ), + disabled: disableFirstLastPaste, + click: () => this._pasteAdjacentHTML("afterBegin"), + }) + ); + pasteSubmenu.append( + new MenuItem({ + id: "node-menu-pastelastchild", + label: INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.label"), + accesskey: INSPECTOR_L10N.getStr( + "inspectorHTMLPasteLastChild.accesskey" + ), + disabled: disableFirstLastPaste, + click: () => this._pasteAdjacentHTML("beforeEnd"), + }) + ); + + return pasteSubmenu; + } + + _getPseudoClassSubmenu(isElement) { + const menu = new Menu(); + + // Set the pseudo classes + for (const name of PSEUDO_CLASSES) { + const menuitem = new MenuItem({ + id: "node-menu-pseudo-" + name.substr(1), + label: name.substr(1), + type: "checkbox", + click: () => this.inspector.togglePseudoClass(name), + }); + + if (isElement) { + const checked = this.selection.nodeFront.hasPseudoClassLock(name); + menuitem.checked = checked; + } else { + menuitem.disabled = true; + } + + menu.append(menuitem); + } + + return menu; + } + + _getEditMarkupString() { + if (this.selection.isHTMLNode()) { + return "inspectorHTMLEdit"; + } else if (this.selection.isSVGNode()) { + return "inspectorSVGEdit"; + } else if (this.selection.isMathMLNode()) { + return "inspectorMathMLEdit"; + } + return "inspectorXMLEdit"; + } + + _openMenu({ target, screenX = 0, screenY = 0 } = {}) { + if (this.selection.isSlotted()) { + // Slotted elements should not show any context menu. + return null; + } + + const markupContainer = this.markup.getContainer(this.selection.nodeFront); + + this.contextMenuTarget = target; + this.nodeMenuTriggerInfo = + markupContainer && markupContainer.editor.getInfoAtNode(target); + + const isFragment = this.selection.isDocumentFragmentNode(); + const isAnonymous = this.selection.isAnonymousNode(); + const isElement = + this.selection.isElementNode() && !this.selection.isPseudoElementNode(); + const isDuplicatableElement = + isElement && !isAnonymous && !this.selection.isRoot(); + const isScreenshotable = + isElement && this.selection.nodeFront.isTreeDisplayed; + + const menu = new Menu(); + menu.append( + new MenuItem({ + id: "node-menu-edithtml", + label: INSPECTOR_L10N.getStr(`${this._getEditMarkupString()}.label`), + accesskey: INSPECTOR_L10N.getStr("inspectorHTMLEdit.accesskey"), + disabled: isAnonymous || (!isElement && !isFragment), + click: () => this._editHTML(), + }) + ); + menu.append( + new MenuItem({ + id: "node-menu-add", + label: INSPECTOR_L10N.getStr("inspectorAddNode.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorAddNode.accesskey"), + disabled: !this.inspector.canAddHTMLChild(), + click: () => this.inspector.addNode(), + }) + ); + menu.append( + new MenuItem({ + id: "node-menu-duplicatenode", + label: INSPECTOR_L10N.getStr("inspectorDuplicateNode.label"), + disabled: !isDuplicatableElement, + click: () => this._duplicateNode(), + }) + ); + menu.append( + new MenuItem({ + id: "node-menu-delete", + label: INSPECTOR_L10N.getStr("inspectorHTMLDelete.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorHTMLDelete.accesskey"), + disabled: !this.markup.isDeletable(this.selection.nodeFront), + click: () => this._deleteNode(), + }) + ); + + menu.append( + new MenuItem({ + label: INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.label"), + accesskey: INSPECTOR_L10N.getStr( + "inspectorAttributesSubmenu.accesskey" + ), + submenu: this._getAttributesSubmenu(isElement && !isAnonymous), + }) + ); + + menu.append( + new MenuItem({ + type: "separator", + }) + ); + + if ( + Services.prefs.getBoolPref( + "devtools.markup.mutationBreakpoints.enabled" + ) && + this.selection.nodeFront.mutationBreakpoints + ) { + menu.append( + new MenuItem({ + label: INSPECTOR_L10N.getStr("inspectorBreakpointSubmenu.label"), + // FIXME(bug 1598952): This doesn't work in shadow trees at all, but + // we still display the active menu. Also, this should probably be + // enabled for ShadowRoot, at least the non-attribute breakpoints. + submenu: this._getDOMBreakpointSubmenu(isElement), + id: "node-menu-mutation-breakpoint", + }) + ); + } + + menu.append( + new MenuItem({ + id: "node-menu-useinconsole", + label: INSPECTOR_L10N.getStr("inspectorUseInConsole.label"), + click: () => this._useInConsole(), + }) + ); + + menu.append( + new MenuItem({ + id: "node-menu-showdomproperties", + label: INSPECTOR_L10N.getStr("inspectorShowDOMProperties.label"), + click: () => this._showDOMProperties(), + }) + ); + + if (this.selection.isElementNode() || this.selection.isTextNode()) { + menu.append( + new MenuItem({ + id: "node-menu-showaccessibilityproperties", + label: INSPECTOR_L10N.getStr( + "inspectorShowAccessibilityProperties.label" + ), + click: () => this._showAccessibilityProperties(), + }) + ); + } + + if (this.selection.nodeFront.customElementLocation) { + menu.append( + new MenuItem({ + id: "node-menu-jumptodefinition", + label: INSPECTOR_L10N.getStr( + "inspectorCustomElementDefinition.label" + ), + click: () => this._jumpToCustomElementDefinition(), + }) + ); + } + + menu.append( + new MenuItem({ + type: "separator", + }) + ); + + menu.append( + new MenuItem({ + label: INSPECTOR_L10N.getStr("inspectorPseudoClassSubmenu.label"), + submenu: this._getPseudoClassSubmenu(isElement), + }) + ); + + menu.append( + new MenuItem({ + id: "node-menu-screenshotnode", + label: INSPECTOR_L10N.getStr("inspectorScreenshotNode.label"), + disabled: !isScreenshotable, + click: () => this.inspector.screenshotNode().catch(console.error), + }) + ); + + menu.append( + new MenuItem({ + id: "node-menu-scrollnodeintoview", + label: INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.label"), + accesskey: INSPECTOR_L10N.getStr( + "inspectorScrollNodeIntoView.accesskey" + ), + disabled: !isElement, + click: () => this.markup.scrollNodeIntoView(), + }) + ); + + menu.append( + new MenuItem({ + type: "separator", + }) + ); + + menu.append( + new MenuItem({ + label: INSPECTOR_L10N.getStr("inspectorCopyHTMLSubmenu.label"), + submenu: this._getCopySubmenu(markupContainer, isElement, isFragment), + }) + ); + + menu.append( + new MenuItem({ + label: INSPECTOR_L10N.getStr("inspectorPasteHTMLSubmenu.label"), + submenu: this._getPasteSubmenu(isElement, isFragment, isAnonymous), + }) + ); + + menu.append( + new MenuItem({ + type: "separator", + }) + ); + + const isNodeWithChildren = + this.selection.isNode() && markupContainer.hasChildren; + menu.append( + new MenuItem({ + id: "node-menu-expand", + label: INSPECTOR_L10N.getStr("inspectorExpandNode.label"), + disabled: !isNodeWithChildren, + click: () => this.markup.expandAll(this.selection.nodeFront), + }) + ); + menu.append( + new MenuItem({ + id: "node-menu-collapse", + label: INSPECTOR_L10N.getStr("inspectorCollapseAll.label"), + disabled: !isNodeWithChildren || !markupContainer.expanded, + click: () => this.markup.collapseAll(this.selection.nodeFront), + }) + ); + + const nodeLinkMenuItems = this._getNodeLinkMenuItems(); + if (nodeLinkMenuItems.filter(item => item.visible).length) { + menu.append( + new MenuItem({ + id: "node-menu-link-separator", + type: "separator", + }) + ); + } + + for (const menuitem of nodeLinkMenuItems) { + menu.append(menuitem); + } + + menu.popup(screenX, screenY, this.toolbox.doc); + return menu; + } +} + +module.exports = MarkupContextMenu; diff --git a/devtools/client/inspector/markup/markup.js b/devtools/client/inspector/markup/markup.js new file mode 100644 index 0000000000..849af6c155 --- /dev/null +++ b/devtools/client/inspector/markup/markup.js @@ -0,0 +1,2682 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const flags = require("resource://devtools/shared/flags.js"); +const nodeConstants = require("resource://devtools/shared/dom-node-constants.js"); +const nodeFilterConstants = require("resource://devtools/shared/dom-node-filter-constants.js"); +const EventEmitter = require("resource://devtools/shared/event-emitter.js"); +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); +const { PluralForm } = require("resource://devtools/shared/plural-form.js"); +const AutocompletePopup = require("resource://devtools/client/shared/autocomplete-popup.js"); +const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js"); +const { + scrollIntoViewIfNeeded, +} = require("resource://devtools/client/shared/scroll.js"); +const { PrefObserver } = require("resource://devtools/client/shared/prefs.js"); +const MarkupElementContainer = require("resource://devtools/client/inspector/markup/views/element-container.js"); +const MarkupReadOnlyContainer = require("resource://devtools/client/inspector/markup/views/read-only-container.js"); +const MarkupTextContainer = require("resource://devtools/client/inspector/markup/views/text-container.js"); +const RootContainer = require("resource://devtools/client/inspector/markup/views/root-container.js"); +const WalkerEventListener = require("resource://devtools/client/inspector/shared/walker-event-listener.js"); + +loader.lazyRequireGetter( + this, + ["createDOMMutationBreakpoint", "deleteDOMMutationBreakpoint"], + "resource://devtools/client/framework/actions/index.js", + true +); +loader.lazyRequireGetter( + this, + "MarkupContextMenu", + "resource://devtools/client/inspector/markup/markup-context-menu.js" +); +loader.lazyRequireGetter( + this, + "SlottedNodeContainer", + "resource://devtools/client/inspector/markup/views/slotted-node-container.js" +); +loader.lazyRequireGetter( + this, + "getLongString", + "resource://devtools/client/inspector/shared/utils.js", + true +); +loader.lazyRequireGetter( + this, + "openContentLink", + "resource://devtools/client/shared/link.js", + true +); +loader.lazyRequireGetter( + this, + "HTMLTooltip", + "resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js", + true +); +loader.lazyRequireGetter( + this, + "UndoStack", + "resource://devtools/client/shared/undo.js", + true +); +loader.lazyRequireGetter( + this, + "clipboardHelper", + "resource://devtools/shared/platform/clipboard.js" +); +loader.lazyRequireGetter( + this, + "beautify", + "resource://devtools/shared/jsbeautify/beautify.js" +); +loader.lazyRequireGetter( + this, + "getTabPrefs", + "resource://devtools/shared/indentation.js", + true +); + +const INSPECTOR_L10N = new LocalizationHelper( + "devtools/client/locales/inspector.properties" +); + +// Page size for pageup/pagedown +const PAGE_SIZE = 10; +const DEFAULT_MAX_CHILDREN = 100; +const DRAG_DROP_AUTOSCROLL_EDGE_MAX_DISTANCE = 50; +const DRAG_DROP_AUTOSCROLL_EDGE_RATIO = 0.1; +const DRAG_DROP_MIN_AUTOSCROLL_SPEED = 2; +const DRAG_DROP_MAX_AUTOSCROLL_SPEED = 8; +const DRAG_DROP_HEIGHT_TO_SPEED = 500; +const DRAG_DROP_HEIGHT_TO_SPEED_MIN = 0.5; +const DRAG_DROP_HEIGHT_TO_SPEED_MAX = 1; +const ATTR_COLLAPSE_ENABLED_PREF = "devtools.markup.collapseAttributes"; +const ATTR_COLLAPSE_LENGTH_PREF = "devtools.markup.collapseAttributeLength"; +const BEAUTIFY_HTML_ON_COPY_PREF = "devtools.markup.beautifyOnCopy"; + +/** + * These functions are called when a shortcut (as defined in `_initShortcuts`) occurs. + * Each property in the following object corresponds to one of the shortcut that is + * handled by the markup-view. + * Each property value is a function that takes the markup-view instance as only + * argument, and returns a boolean that signifies whether the event should be consumed. + * By default, the event gets consumed after the shortcut handler returns, + * this means its propagation is stopped. If you do want the shortcut event + * to continue propagating through DevTools, then return true from the handler. + */ +const shortcutHandlers = { + // Localizable keys + "markupView.hide.key": markupView => { + const node = markupView._selectedContainer.node; + const walkerFront = node.walkerFront; + + if (node.hidden) { + walkerFront.unhideNode(node); + } else { + walkerFront.hideNode(node); + } + }, + "markupView.edit.key": markupView => { + markupView.beginEditingHTML(markupView._selectedContainer.node); + }, + "markupView.scrollInto.key": markupView => { + markupView.scrollNodeIntoView(); + }, + // Generic keys + Delete: markupView => { + markupView.deleteNodeOrAttribute(); + }, + Backspace: markupView => { + markupView.deleteNodeOrAttribute(true); + }, + Home: markupView => { + const rootContainer = markupView.getContainer(markupView._rootNode); + markupView.navigate(rootContainer.children.firstChild.container); + }, + Left: markupView => { + if (markupView._selectedContainer.expanded) { + markupView.collapseNode(markupView._selectedContainer.node); + } else { + const parent = markupView._selectionWalker().parentNode(); + if (parent) { + markupView.navigate(parent.container); + } + } + }, + Right: markupView => { + if ( + !markupView._selectedContainer.expanded && + markupView._selectedContainer.hasChildren + ) { + markupView._expandContainer(markupView._selectedContainer); + } else { + const next = markupView._selectionWalker().nextNode(); + if (next) { + markupView.navigate(next.container); + } + } + }, + Up: markupView => { + const previousNode = markupView._selectionWalker().previousNode(); + if (previousNode) { + markupView.navigate(previousNode.container); + } + }, + Down: markupView => { + const nextNode = markupView._selectionWalker().nextNode(); + if (nextNode) { + markupView.navigate(nextNode.container); + } + }, + PageUp: markupView => { + const walker = markupView._selectionWalker(); + let selection = markupView._selectedContainer; + for (let i = 0; i < PAGE_SIZE; i++) { + const previousNode = walker.previousNode(); + if (!previousNode) { + break; + } + selection = previousNode.container; + } + markupView.navigate(selection); + }, + PageDown: markupView => { + const walker = markupView._selectionWalker(); + let selection = markupView._selectedContainer; + for (let i = 0; i < PAGE_SIZE; i++) { + const nextNode = walker.nextNode(); + if (!nextNode) { + break; + } + selection = nextNode.container; + } + markupView.navigate(selection); + }, + Enter: markupView => { + if (!markupView._selectedContainer.canFocus) { + markupView._selectedContainer.canFocus = true; + markupView._selectedContainer.focus(); + return false; + } + return true; + }, + Space: markupView => { + if (!markupView._selectedContainer.canFocus) { + markupView._selectedContainer.canFocus = true; + markupView._selectedContainer.focus(); + return false; + } + return true; + }, + Esc: markupView => { + if (markupView.isDragging) { + markupView.cancelDragging(); + return false; + } + // Prevent cancelling the event when not + // dragging, to allow the split console to be toggled. + return true; + }, +}; + +/** + * Vocabulary for the purposes of this file: + * + * MarkupContainer - the structure that holds an editor and its + * immediate children in the markup panel. + * - MarkupElementContainer: markup container for element nodes + * - MarkupTextContainer: markup container for text / comment nodes + * - MarkupReadonlyContainer: markup container for other nodes + * Node - A content node. + * object.elt - A UI element in the markup panel. + */ + +/** + * The markup tree. Manages the mapping of nodes to MarkupContainers, + * updating based on mutations, and the undo/redo bindings. + * + * @param {Inspector} inspector + * The inspector we're watching. + * @param {iframe} frame + * An iframe in which the caller has kindly loaded markup.xhtml. + * @param {XULWindow} controllerWindow + * Will enable the undo/redo feature from devtools/client/shared/undo. + * Should be a XUL window, will typically point to the toolbox window. + */ +function MarkupView(inspector, frame, controllerWindow) { + EventEmitter.decorate(this); + + this.controllerWindow = controllerWindow; + this.inspector = inspector; + this.highlighters = inspector.highlighters; + this.walker = this.inspector.walker; + this._frame = frame; + this.win = this._frame.contentWindow; + this.doc = this._frame.contentDocument; + this._elt = this.doc.getElementById("root"); + this.telemetry = this.inspector.telemetry; + this._breakpointIDsInLocalState = new Map(); + this._containersToUpdate = new Map(); + + this.maxChildren = Services.prefs.getIntPref( + "devtools.markup.pagesize", + DEFAULT_MAX_CHILDREN + ); + + this.collapseAttributes = Services.prefs.getBoolPref( + ATTR_COLLAPSE_ENABLED_PREF + ); + this.collapseAttributeLength = Services.prefs.getIntPref( + ATTR_COLLAPSE_LENGTH_PREF + ); + + // Creating the popup to be used to show CSS suggestions. + // The popup will be attached to the toolbox document. + this.popup = new AutocompletePopup(inspector.toolbox.doc, { + autoSelect: true, + }); + + this._containers = new Map(); + // This weakmap will hold keys used with the _containers map, in order to retrieve the + // slotted container for a given node front. + this._slottedContainerKeys = new WeakMap(); + + // Binding functions that need to be called in scope. + this._handleRejectionIfNotDestroyed = + this._handleRejectionIfNotDestroyed.bind(this); + this._isImagePreviewTarget = this._isImagePreviewTarget.bind(this); + this._onWalkerMutations = this._onWalkerMutations.bind(this); + this._onBlur = this._onBlur.bind(this); + this._onContextMenu = this._onContextMenu.bind(this); + this._onCopy = this._onCopy.bind(this); + this._onCollapseAttributesPrefChange = + this._onCollapseAttributesPrefChange.bind(this); + this._onWalkerNodeStatesChanged = this._onWalkerNodeStatesChanged.bind(this); + this._onFocus = this._onFocus.bind(this); + this._onResourceAvailable = this._onResourceAvailable.bind(this); + this._onTargetAvailable = this._onTargetAvailable.bind(this); + this._onTargetDestroyed = this._onTargetDestroyed.bind(this); + this._onMouseClick = this._onMouseClick.bind(this); + this._onMouseMove = this._onMouseMove.bind(this); + this._onMouseOut = this._onMouseOut.bind(this); + this._onMouseUp = this._onMouseUp.bind(this); + this._onNewSelection = this._onNewSelection.bind(this); + this._onToolboxPickerCanceled = this._onToolboxPickerCanceled.bind(this); + this._onToolboxPickerHover = this._onToolboxPickerHover.bind(this); + this._onDomMutation = this._onDomMutation.bind(this); + + // Listening to various events. + this._elt.addEventListener("blur", this._onBlur, true); + this._elt.addEventListener("click", this._onMouseClick); + this._elt.addEventListener("contextmenu", this._onContextMenu); + this._elt.addEventListener("mousemove", this._onMouseMove); + this._elt.addEventListener("mouseout", this._onMouseOut); + this._frame.addEventListener("focus", this._onFocus); + this.inspector.selection.on("new-node-front", this._onNewSelection); + this._unsubscribeFromToolboxStore = this.inspector.toolbox.store.subscribe( + this._onDomMutation + ); + + if (flags.testing) { + // In tests, we start listening immediately to avoid having to simulate a mousemove. + this._initTooltips(); + } + + this.win.addEventListener("copy", this._onCopy); + this.win.addEventListener("mouseup", this._onMouseUp); + this.inspector.toolbox.nodePicker.on( + "picker-node-canceled", + this._onToolboxPickerCanceled + ); + this.inspector.toolbox.nodePicker.on( + "picker-node-hovered", + this._onToolboxPickerHover + ); + + // Event listeners for highlighter events + this.onHighlighterShown = data => + this.handleHighlighterEvent("highlighter-shown", data); + this.onHighlighterHidden = data => + this.handleHighlighterEvent("highlighter-hidden", data); + this.inspector.highlighters.on("highlighter-shown", this.onHighlighterShown); + this.inspector.highlighters.on( + "highlighter-hidden", + this.onHighlighterHidden + ); + + this._onNewSelection(); + if (this.inspector.selection.nodeFront) { + this.expandNode(this.inspector.selection.nodeFront); + } + + this._prefObserver = new PrefObserver("devtools.markup"); + this._prefObserver.on( + ATTR_COLLAPSE_ENABLED_PREF, + this._onCollapseAttributesPrefChange + ); + this._prefObserver.on( + ATTR_COLLAPSE_LENGTH_PREF, + this._onCollapseAttributesPrefChange + ); + + this._initShortcuts(); + + this._walkerEventListener = new WalkerEventListener(this.inspector, { + "display-change": this._onWalkerNodeStatesChanged, + "scrollable-change": this._onWalkerNodeStatesChanged, + "overflow-change": this._onWalkerNodeStatesChanged, + mutations: this._onWalkerMutations, + }); + + this.resourceCommand = this.inspector.toolbox.resourceCommand; + this.resourceCommand.watchResources([this.resourceCommand.TYPES.ROOT_NODE], { + onAvailable: this._onResourceAvailable, + }); + + this.targetCommand = this.inspector.commands.targetCommand; + this.targetCommand.watchTargets({ + types: [this.targetCommand.TYPES.FRAME], + onAvailable: this._onTargetAvailable, + onDestroyed: this._onTargetDestroyed, + }); +} + +MarkupView.prototype = { + /** + * How long does a node flash when it mutates (in ms). + */ + CONTAINER_FLASHING_DURATION: 500, + + _selectedContainer: null, + + get contextMenu() { + if (!this._contextMenu) { + this._contextMenu = new MarkupContextMenu(this); + } + + return this._contextMenu; + }, + + get eventDetailsTooltip() { + if (!this._eventDetailsTooltip) { + // This tooltip will be attached to the toolbox document. + this._eventDetailsTooltip = new HTMLTooltip(this.toolbox.doc, { + type: "arrow", + consumeOutsideClicks: false, + }); + } + + return this._eventDetailsTooltip; + }, + + get toolbox() { + return this.inspector.toolbox; + }, + + get undo() { + if (!this._undo) { + this._undo = new UndoStack(); + this._undo.installController(this.controllerWindow); + } + + return this._undo; + }, + + _onDomMutation() { + const domMutationBreakpoints = + this.inspector.toolbox.store.getState().domMutationBreakpoints + .breakpoints; + const breakpointIDsInCurrentState = []; + for (const breakpoint of domMutationBreakpoints) { + const nodeFront = breakpoint.nodeFront; + const mutationType = breakpoint.mutationType; + const enabledStatus = breakpoint.enabled; + breakpointIDsInCurrentState.push(breakpoint.id); + // If breakpoint is not in local state + if (!this._breakpointIDsInLocalState.has(breakpoint.id)) { + this._breakpointIDsInLocalState.set(breakpoint.id, breakpoint); + if (!this._containersToUpdate.has(nodeFront)) { + this._containersToUpdate.set(nodeFront, new Map()); + } + } + this._containersToUpdate.get(nodeFront).set(mutationType, enabledStatus); + } + // If a breakpoint is in local state but not current state, it has been + // removed by the user. + for (const id of this._breakpointIDsInLocalState.keys()) { + if (breakpointIDsInCurrentState.includes(id) === false) { + const nodeFront = this._breakpointIDsInLocalState.get(id).nodeFront; + const mutationType = + this._breakpointIDsInLocalState.get(id).mutationType; + this._containersToUpdate.get(nodeFront).delete(mutationType); + this._breakpointIDsInLocalState.delete(id); + } + } + // Update each container + for (const nodeFront of this._containersToUpdate.keys()) { + const mutationBreakpoints = this._containersToUpdate.get(nodeFront); + const container = this.getContainer(nodeFront); + container.update(mutationBreakpoints); + if (this._containersToUpdate.get(nodeFront).size === 0) { + this._containersToUpdate.delete(nodeFront); + } + } + }, + + /** + * Handle promise rejections for various asynchronous actions, and only log errors if + * the markup view still exists. + * This is useful to silence useless errors that happen when the markup view is + * destroyed while still initializing (and making protocol requests). + */ + _handleRejectionIfNotDestroyed(e) { + if (!this._destroyed) { + console.error(e); + } + }, + + _initTooltips() { + if (this.imagePreviewTooltip) { + return; + } + // The tooltips will be attached to the toolbox document. + this.imagePreviewTooltip = new HTMLTooltip(this.toolbox.doc, { + type: "arrow", + useXulWrapper: true, + }); + this._enableImagePreviewTooltip(); + }, + + _enableImagePreviewTooltip() { + this.imagePreviewTooltip.startTogglingOnHover( + this._elt, + this._isImagePreviewTarget + ); + }, + + _disableImagePreviewTooltip() { + this.imagePreviewTooltip.stopTogglingOnHover(); + }, + + _onToolboxPickerHover(nodeFront) { + this.showNode(nodeFront).then(() => { + this._showNodeAsHovered(nodeFront); + }, console.error); + }, + + /** + * If the element picker gets canceled, make sure and re-center the view on the + * current selected element. + */ + _onToolboxPickerCanceled() { + if (this._selectedContainer) { + scrollIntoViewIfNeeded(this._selectedContainer.editor.elt); + } + }, + + isDragging: false, + _draggedContainer: null, + + _onMouseMove(event) { + // Note that in tests, we start listening immediately from the constructor to avoid having to simulate a mousemove. + // Also note that initTooltips bails out if it is called many times, so it isn't an issue to call it a second + // time from here in case tests are doing a mousemove. + this._initTooltips(); + + let target = event.target; + + if (this._draggedContainer) { + this._draggedContainer.onMouseMove(event); + } + // Auto-scroll if we're dragging. + if (this.isDragging) { + event.preventDefault(); + this._autoScroll(event); + return; + } + + // Show the current container as hovered and highlight it. + // This requires finding the current MarkupContainer (walking up the DOM). + while (!target.container) { + if (target.tagName.toLowerCase() === "body") { + return; + } + target = target.parentNode; + } + + const container = target.container; + if (this._hoveredContainer !== container) { + this._showBoxModel(container.node); + } + this._showContainerAsHovered(container); + + this.emit("node-hover"); + }, + + /** + * If focus is moved outside of the markup view document and there is a + * selected container, make its contents not focusable by a keyboard. + */ + _onBlur(event) { + if (!this._selectedContainer) { + return; + } + + const { relatedTarget } = event; + if (relatedTarget && relatedTarget.ownerDocument === this.doc) { + return; + } + + if (this._selectedContainer) { + this._selectedContainer.clearFocus(); + } + }, + + _onContextMenu(event) { + this.contextMenu.show(event); + }, + + /** + * Executed on each mouse-move while a node is being dragged in the view. + * Auto-scrolls the view to reveal nodes below the fold to drop the dragged + * node in. + */ + _autoScroll(event) { + const docEl = this.doc.documentElement; + + if (this._autoScrollAnimationFrame) { + this.win.cancelAnimationFrame(this._autoScrollAnimationFrame); + } + + // Auto-scroll when the mouse approaches top/bottom edge. + const fromBottom = docEl.clientHeight - event.pageY + this.win.scrollY; + const fromTop = event.pageY - this.win.scrollY; + const edgeDistance = Math.min( + DRAG_DROP_AUTOSCROLL_EDGE_MAX_DISTANCE, + docEl.clientHeight * DRAG_DROP_AUTOSCROLL_EDGE_RATIO + ); + + // The smaller the screen, the slower the movement. + const heightToSpeedRatio = Math.max( + DRAG_DROP_HEIGHT_TO_SPEED_MIN, + Math.min( + DRAG_DROP_HEIGHT_TO_SPEED_MAX, + docEl.clientHeight / DRAG_DROP_HEIGHT_TO_SPEED + ) + ); + + if (fromBottom <= edgeDistance) { + // Map our distance range to a speed range so that the speed is not too + // fast or too slow. + const speed = map( + fromBottom, + 0, + edgeDistance, + DRAG_DROP_MIN_AUTOSCROLL_SPEED, + DRAG_DROP_MAX_AUTOSCROLL_SPEED + ); + + this._runUpdateLoop(() => { + docEl.scrollTop -= + heightToSpeedRatio * (speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED); + }); + } + + if (fromTop <= edgeDistance) { + const speed = map( + fromTop, + 0, + edgeDistance, + DRAG_DROP_MIN_AUTOSCROLL_SPEED, + DRAG_DROP_MAX_AUTOSCROLL_SPEED + ); + + this._runUpdateLoop(() => { + docEl.scrollTop += + heightToSpeedRatio * (speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED); + }); + } + }, + + /** + * Run a loop on the requestAnimationFrame. + */ + _runUpdateLoop(update) { + const loop = () => { + update(); + this._autoScrollAnimationFrame = this.win.requestAnimationFrame(loop); + }; + loop(); + }, + + _onMouseClick(event) { + // From the target passed here, let's find the parent MarkupContainer + // and forward the event if needed. + let parentNode = event.target; + let container; + while (parentNode !== this.doc.body) { + if (parentNode.container) { + container = parentNode.container; + break; + } + parentNode = parentNode.parentNode; + } + + if (typeof container.onContainerClick === "function") { + // Forward the event to the container if it implements onContainerClick. + container.onContainerClick(event); + } + }, + + _onMouseUp(event) { + if (this._draggedContainer) { + this._draggedContainer.onMouseUp(event); + } + + this.indicateDropTarget(null); + this.indicateDragTarget(null); + if (this._autoScrollAnimationFrame) { + this.win.cancelAnimationFrame(this._autoScrollAnimationFrame); + } + }, + + _onCollapseAttributesPrefChange() { + this.collapseAttributes = Services.prefs.getBoolPref( + ATTR_COLLAPSE_ENABLED_PREF + ); + this.collapseAttributeLength = Services.prefs.getIntPref( + ATTR_COLLAPSE_LENGTH_PREF + ); + this.update(); + }, + + cancelDragging() { + if (!this.isDragging) { + return; + } + + for (const [, container] of this._containers) { + if (container.isDragging) { + container.cancelDragging(); + break; + } + } + + this.indicateDropTarget(null); + this.indicateDragTarget(null); + if (this._autoScrollAnimationFrame) { + this.win.cancelAnimationFrame(this._autoScrollAnimationFrame); + } + }, + + _hoveredContainer: null, + + /** + * Show a NodeFront's container as being hovered + * + * @param {NodeFront} nodeFront + * The node to show as hovered + */ + _showNodeAsHovered(nodeFront) { + const container = this.getContainer(nodeFront); + this._showContainerAsHovered(container); + }, + + _showContainerAsHovered(container) { + if (this._hoveredContainer === container) { + return; + } + + if (this._hoveredContainer) { + this._hoveredContainer.hovered = false; + } + + container.hovered = true; + this._hoveredContainer = container; + }, + + async _onMouseOut(event) { + // Emulate mouseleave by skipping any relatedTarget inside the markup-view. + if (this._elt.contains(event.relatedTarget)) { + return; + } + + if (this._autoScrollAnimationFrame) { + this.win.cancelAnimationFrame(this._autoScrollAnimationFrame); + } + if (this.isDragging) { + return; + } + + await this._hideBoxModel(); + if (this._hoveredContainer) { + this._hoveredContainer.hovered = false; + } + this._hoveredContainer = null; + + this.emit("leave"); + }, + + /** + * Show the Box Model Highlighter on a given node front + * + * @param {NodeFront} nodeFront + * The node for which to show the highlighter. + * @param {Object} options + * Configuration object with options for the Box Model Highlighter. + * @return {Promise} Resolves after the highlighter for this nodeFront is shown. + */ + _showBoxModel(nodeFront, options) { + return this.inspector.highlighters.showHighlighterTypeForNode( + this.inspector.highlighters.TYPES.BOXMODEL, + nodeFront, + options + ); + }, + + /** + * Hide the Box Model Highlighter for any node that may be highlighted. + * + * @return {Promise} Resolves when the highlighter is hidden. + */ + _hideBoxModel() { + return this.inspector.highlighters.hideHighlighterType( + this.inspector.highlighters.TYPES.BOXMODEL + ); + }, + + /** + * Delegate handler for highlighter events. + * + * This is the place to observe for highlighter events, check the highlighter type and + * event name, then react for example by modifying the DOM. + * + * @param {String} eventName + * Highlighter event name. One of: "highlighter-hidden", "highlighter-shown" + * @param {Object} data + * Object with data associated with the highlighter event. + * {String} data.type + * Highlighter type + * {NodeFront} data.nodeFront + * NodeFront of the node associated with the highlighter event + * {Object} data.options + * Optional configuration passed to the highlighter when shown + * {CustomHighlighterFront} data.highlighter + * Highlighter instance + * + */ + handleHighlighterEvent(eventName, data) { + switch (data.type) { + // Toggle the "active" CSS class name on flex and grid display badges next to + // elements in the Markup view when a coresponding flex or grid highlighter is + // shown or hidden for a node. + case this.inspector.highlighters.TYPES.FLEXBOX: + case this.inspector.highlighters.TYPES.GRID: + const { nodeFront } = data; + if (!nodeFront) { + return; + } + + // Find the badge corresponding to the node from the highlighter event payload. + const container = this.getContainer(nodeFront); + const badge = container?.editor?.displayBadge; + if (badge) { + badge.classList.toggle("active", eventName == "highlighter-shown"); + } + + // There is a limit to how many grid highlighters can be active at the same time. + // If the limit was reached, disable all non-active grid badges. + if (data.type === this.inspector.highlighters.TYPES.GRID) { + // Matches badges for "grid", "inline-grid" and "subgrid" + const selector = "[data-display*='grid']:not(.active)"; + const isLimited = + this.inspector.highlighters.isGridHighlighterLimitReached(); + Array.from(this._elt.querySelectorAll(selector)).map(el => { + el.classList.toggle("interactive", !isLimited); + }); + } + break; + } + }, + + /** + * Used by tests + */ + getSelectedContainer() { + return this._selectedContainer; + }, + + /** + * Get the MarkupContainer object for a given node, or undefined if + * none exists. + * + * @param {NodeFront} nodeFront + * The node to get the container for. + * @param {Boolean} slotted + * true to get the slotted version of the container. + * @return {MarkupContainer} The container for the provided node. + */ + getContainer(node, slotted) { + const key = this._getContainerKey(node, slotted); + return this._containers.get(key); + }, + + /** + * Register a given container for a given node/slotted node. + * + * @param {NodeFront} nodeFront + * The node to set the container for. + * @param {Boolean} slotted + * true if the container represents the slotted version of the node. + */ + setContainer(node, container, slotted) { + const key = this._getContainerKey(node, slotted); + return this._containers.set(key, container); + }, + + /** + * Check if a MarkupContainer object exists for a given node/slotted node + * + * @param {NodeFront} nodeFront + * The node to check. + * @param {Boolean} slotted + * true to check for a container matching the slotted version of the node. + * @return {Boolean} True if a container exists, false otherwise. + */ + hasContainer(node, slotted) { + const key = this._getContainerKey(node, slotted); + return this._containers.has(key); + }, + + _getContainerKey(node, slotted) { + if (!slotted) { + return node; + } + + if (!this._slottedContainerKeys.has(node)) { + this._slottedContainerKeys.set(node, { node }); + } + return this._slottedContainerKeys.get(node); + }, + + _isContainerSelected(container) { + if (!container) { + return false; + } + + const selection = this.inspector.selection; + return ( + container.node == selection.nodeFront && + container.isSlotted() == selection.isSlotted() + ); + }, + + update() { + const updateChildren = node => { + this.getContainer(node).update(); + for (const child of node.treeChildren()) { + updateChildren(child); + } + }; + + // Start with the documentElement + let documentElement; + for (const node of this._rootNode.treeChildren()) { + if (node.isDocumentElement === true) { + documentElement = node; + break; + } + } + + // Recursively update each node starting with documentElement. + updateChildren(documentElement); + }, + + /** + * Executed when the mouse hovers over a target in the markup-view and is used + * to decide whether this target should be used to display an image preview + * tooltip. + * Delegates the actual decision to the corresponding MarkupContainer instance + * if one is found. + * + * @return {Promise} the promise returned by + * MarkupElementContainer._isImagePreviewTarget + */ + async _isImagePreviewTarget(target) { + // From the target passed here, let's find the parent MarkupContainer + // and ask it if the tooltip should be shown + if (this.isDragging) { + return false; + } + + let parent = target, + container; + while (parent) { + if (parent.container) { + container = parent.container; + break; + } + parent = parent.parentNode; + } + + if (container instanceof MarkupElementContainer) { + return container.isImagePreviewTarget(target, this.imagePreviewTooltip); + } + + return false; + }, + + /** + * Given the known reason, should the current selection be briefly highlighted + * In a few cases, we don't want to highlight the node: + * - If the reason is null (used to reset the selection), + * - if it's "inspector-default-selection" (initial node selected, either when + * opening the inspector or after a navigation/reload) + * - if it's "picker-node-picked" or "picker-node-previewed" (node selected with the + * node picker. Note that this does not include the "Inspect element" context menu, + * which has a dedicated reason, "browser-context-menu"). + * - if it's "test" (this is a special case for mochitest. In tests, we often + * need to select elements but don't necessarily want the highlighter to come + * and go after a delay as this might break test scenarios) + * We also do not want to start a brief highlight timeout if the node is + * already being hovered over, since in that case it will already be + * highlighted. + */ + _shouldNewSelectionBeHighlighted() { + const reason = this.inspector.selection.reason; + const unwantedReasons = [ + "inspector-default-selection", + "nodeselected", + "picker-node-picked", + "picker-node-previewed", + "test", + ]; + + const isHighlight = this._isContainerSelected(this._hoveredContainer); + return !isHighlight && reason && !unwantedReasons.includes(reason); + }, + + /** + * React to new-node-front selection events. + * Highlights the node if needed, and make sure it is shown and selected in + * the view. + */ + _onNewSelection(nodeFront, reason) { + const selection = this.inspector.selection; + // this will probably leak. + // TODO: use resource api listeners? + if (nodeFront) { + nodeFront.walkerFront.on( + "display-change", + this._onWalkerNodeStatesChanged + ); + nodeFront.walkerFront.on( + "scrollable-change", + this._onWalkerNodeStatesChanged + ); + nodeFront.walkerFront.on( + "overflow-change", + this._onWalkerNodeStatesChanged + ); + nodeFront.walkerFront.on("mutations", this._onWalkerMutations); + } + + if (this.htmlEditor) { + this.htmlEditor.hide(); + } + if (this._isContainerSelected(this._hoveredContainer)) { + this._hoveredContainer.hovered = false; + this._hoveredContainer = null; + } + + if (!selection.isNode()) { + this.unmarkSelectedNode(); + return; + } + + const done = this.inspector.updating("markup-view"); + let onShowBoxModel; + + // Highlight the element briefly if needed. + if (this._shouldNewSelectionBeHighlighted()) { + onShowBoxModel = this._showBoxModel(nodeFront, { + duration: this.inspector.HIGHLIGHTER_AUTOHIDE_TIMER, + }); + } + + const slotted = selection.isSlotted(); + const smoothScroll = reason === "reveal-from-slot"; + const onShow = this.showNode(selection.nodeFront, { slotted, smoothScroll }) + .then(() => { + // We could be destroyed by now. + if (this._destroyed) { + return Promise.reject("markupview destroyed"); + } + + // Mark the node as selected. + const container = this.getContainer(selection.nodeFront, slotted); + this._markContainerAsSelected(container); + + // Make sure the new selection is navigated to. + this.maybeNavigateToNewSelection(); + return undefined; + }) + .catch(this._handleRejectionIfNotDestroyed); + + Promise.all([onShowBoxModel, onShow]).then(done); + }, + + /** + * Maybe make selected the current node selection's MarkupContainer depending + * on why the current node got selected. + */ + async maybeNavigateToNewSelection() { + const { reason, nodeFront } = this.inspector.selection; + + // The list of reasons that should lead to navigating to the node. + const reasonsToNavigate = [ + // If the user picked an element with the element picker. + "picker-node-picked", + // If the user shift-clicked (previewed) an element. + "picker-node-previewed", + // If the user selected an element with the browser context menu. + "browser-context-menu", + // If the user added a new node by clicking in the inspector toolbar. + "node-inserted", + ]; + + // If the user performed an action with a keyboard, move keyboard focus to + // the markup tree container. + if (reason && reason.endsWith("-keyboard")) { + this.getContainer(this._rootNode).elt.focus(); + } + + if (reasonsToNavigate.includes(reason)) { + // not sure this is necessary + const root = await nodeFront.walkerFront.getRootNode(); + this.getContainer(root).elt.focus(); + this.navigate(this.getContainer(nodeFront)); + } + }, + + /** + * Create a TreeWalker to find the next/previous + * node for selection. + */ + _selectionWalker(start) { + const walker = this.doc.createTreeWalker( + start || this._elt, + nodeFilterConstants.SHOW_ELEMENT, + function (element) { + if ( + element.container && + element.container.elt === element && + element.container.visible + ) { + return nodeFilterConstants.FILTER_ACCEPT; + } + return nodeFilterConstants.FILTER_SKIP; + } + ); + walker.currentNode = this._selectedContainer.elt; + return walker; + }, + + _onCopy(evt) { + // Ignore copy events from editors + if (this._isInputOrTextarea(evt.target)) { + return; + } + + const selection = this.inspector.selection; + if (selection.isNode()) { + this.copyOuterHTML(); + } + evt.stopPropagation(); + evt.preventDefault(); + }, + + /** + * Copy the outerHTML of the selected Node to the clipboard. + */ + copyOuterHTML() { + if (!this.inspector.selection.isNode()) { + return; + } + const node = this.inspector.selection.nodeFront; + + switch (node.nodeType) { + case nodeConstants.ELEMENT_NODE: + copyLongHTMLString(node.walkerFront.outerHTML(node)); + break; + case nodeConstants.COMMENT_NODE: + getLongString(node.getNodeValue()).then(comment => { + clipboardHelper.copyString(""); + }); + break; + case nodeConstants.DOCUMENT_TYPE_NODE: + clipboardHelper.copyString(node.doctypeString); + break; + } + }, + + /** + * Copy the innerHTML of the selected Node to the clipboard. + */ + copyInnerHTML() { + const nodeFront = this.inspector.selection.nodeFront; + if (!this.inspector.selection.isNode()) { + return; + } + + copyLongHTMLString(nodeFront.walkerFront.innerHTML(nodeFront)); + }, + + /** + * Given a type and link found in a node's attribute in the markup-view, + * attempt to follow that link (which may result in opening a new tab, the + * style editor or debugger). + */ + followAttributeLink(type, link) { + if (!type || !link) { + return; + } + + const nodeFront = this.inspector.selection.nodeFront; + if (type === "uri" || type === "cssresource" || type === "jsresource") { + // Open link in a new tab. + nodeFront.inspectorFront + .resolveRelativeURL(link, this.inspector.selection.nodeFront) + .then(url => { + if (type === "uri") { + openContentLink(url); + } else if (type === "cssresource") { + return this.toolbox.viewGeneratedSourceInStyleEditor(url); + } else if (type === "jsresource") { + return this.toolbox.viewGeneratedSourceInDebugger(url); + } + return null; + }) + .catch(console.error); + } else if (type == "idref") { + // Select the node in the same document. + nodeFront.walkerFront + .document(nodeFront) + .then(doc => { + return nodeFront.walkerFront + .querySelector(doc, "#" + CSS.escape(link)) + .then(node => { + if (!node) { + this.emit("idref-attribute-link-failed"); + return; + } + this.inspector.selection.setNodeFront(node); + }); + }) + .catch(console.error); + } + }, + + /** + * Register all key shortcuts. + */ + _initShortcuts() { + const shortcuts = new KeyShortcuts({ + window: this.win, + }); + + // Keep a pointer on shortcuts to destroy them when destroying the markup + // view. + this._shortcuts = shortcuts; + + this._onShortcut = this._onShortcut.bind(this); + + // Process localizable keys + [ + "markupView.hide.key", + "markupView.edit.key", + "markupView.scrollInto.key", + ].forEach(name => { + const key = INSPECTOR_L10N.getStr(name); + shortcuts.on(key, event => this._onShortcut(name, event)); + }); + + // Process generic keys: + [ + "Delete", + "Backspace", + "Home", + "Left", + "Right", + "Up", + "Down", + "PageUp", + "PageDown", + "Esc", + "Enter", + "Space", + ].forEach(key => { + shortcuts.on(key, event => this._onShortcut(key, event)); + }); + }, + + /** + * Key shortcut listener. + */ + _onShortcut(name, event) { + if (this._isInputOrTextarea(event.target)) { + return; + } + + const handler = shortcutHandlers[name]; + const shouldPropagate = handler(this); + if (shouldPropagate) { + return; + } + + event.stopPropagation(); + event.preventDefault(); + }, + + /** + * Check if a node is an input or textarea + */ + _isInputOrTextarea(element) { + const name = element.tagName.toLowerCase(); + return name === "input" || name === "textarea"; + }, + + /** + * If there's an attribute on the current node that's currently focused, then + * delete this attribute, otherwise delete the node itself. + * + * @param {Boolean} moveBackward + * If set to true and if we're deleting the node, focus the previous + * sibling after deletion, otherwise the next one. + */ + deleteNodeOrAttribute(moveBackward) { + const focusedAttribute = this.doc.activeElement + ? this.doc.activeElement.closest(".attreditor") + : null; + if (focusedAttribute) { + // The focused attribute might not be in the current selected container. + const container = focusedAttribute.closest("li.child").container; + container.removeAttribute(focusedAttribute.dataset.attr); + } else { + this.deleteNode(this._selectedContainer.node, moveBackward); + } + }, + + /** + * Returns a value indicating whether a node can be deleted. + * + * @param {NodeFront} nodeFront + * The node to test for deletion + */ + isDeletable(nodeFront) { + return !( + nodeFront.isDocumentElement || + nodeFront.nodeType == nodeConstants.DOCUMENT_NODE || + nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE || + nodeFront.nodeType == nodeConstants.DOCUMENT_FRAGMENT_NODE || + nodeFront.isAnonymous + ); + }, + + /** + * Delete a node from the DOM. + * This is an undoable action. + * + * @param {NodeFront} node + * The node to remove. + * @param {Boolean} moveBackward + * If set to true, focus the previous sibling, otherwise the next one. + */ + deleteNode(node, moveBackward) { + if (!this.isDeletable(node)) { + return; + } + + const container = this.getContainer(node); + + // Retain the node so we can undo this... + node.walkerFront + .retainNode(node) + .then(() => { + const parent = node.parentNode(); + let nextSibling = null; + this.undo.do( + () => { + node.walkerFront.removeNode(node).then(siblings => { + nextSibling = siblings.nextSibling; + const prevSibling = siblings.previousSibling; + let focusNode = moveBackward ? prevSibling : nextSibling; + + // If we can't move as the user wants, we move to the other direction. + // If there is no sibling elements anymore, move to the parent node. + if (!focusNode) { + focusNode = nextSibling || prevSibling || parent; + } + + const isNextSiblingText = nextSibling + ? nextSibling.nodeType === nodeConstants.TEXT_NODE + : false; + const isPrevSiblingText = prevSibling + ? prevSibling.nodeType === nodeConstants.TEXT_NODE + : false; + + // If the parent had two children and the next or previous sibling + // is a text node, then it now has only a single text node, is about + // to be in-lined; and focus should move to the parent. + if ( + parent.numChildren === 2 && + (isNextSiblingText || isPrevSiblingText) + ) { + focusNode = parent; + } + + if (container.selected) { + this.navigate(this.getContainer(focusNode)); + } + }); + }, + () => { + const isValidSibling = nextSibling && !nextSibling.isPseudoElement; + nextSibling = isValidSibling ? nextSibling : null; + node.walkerFront.insertBefore(node, parent, nextSibling); + } + ); + }) + .catch(console.error); + }, + + /** + * Scroll the node into view. + */ + scrollNodeIntoView() { + if (!this.inspector.selection.isNode()) { + return; + } + + this.inspector.selection.nodeFront.scrollIntoView(); + }, + + async toggleMutationBreakpoint(name) { + if (!this.inspector.selection.isElementNode()) { + return; + } + + const toolboxStore = this.inspector.toolbox.store; + const nodeFront = this.inspector.selection.nodeFront; + + if (nodeFront.mutationBreakpoints[name]) { + toolboxStore.dispatch(deleteDOMMutationBreakpoint(nodeFront, name)); + } else { + toolboxStore.dispatch(createDOMMutationBreakpoint(nodeFront, name)); + } + }, + + /** + * If an editable item is focused, select its container. + */ + _onFocus(event) { + let parent = event.target; + while (!parent.container) { + parent = parent.parentNode; + } + if (parent) { + this.navigate(parent.container); + } + }, + + /** + * Handle a user-requested navigation to a given MarkupContainer, + * updating the inspector's currently-selected node. + * + * @param {MarkupContainer} container + * The container we're navigating to. + */ + navigate(container) { + if (!container) { + return; + } + + this._markContainerAsSelected(container, "treepanel"); + }, + + /** + * Make sure a node is included in the markup tool. + * + * @param {NodeFront} node + * The node in the content document. + * @param {Boolean} flashNode + * Whether the newly imported node should be flashed + * @param {Boolean} slotted + * Whether we are importing the slotted version of the node. + * @return {MarkupContainer} The MarkupContainer object for this element. + */ + importNode(node, flashNode, slotted) { + if (!node) { + return null; + } + + if (this.hasContainer(node, slotted)) { + return this.getContainer(node, slotted); + } + + let container; + const { nodeType, isPseudoElement } = node; + if (node === node.walkerFront.rootNode) { + container = new RootContainer(this, node); + this._elt.appendChild(container.elt); + } + if (node === this.walker.rootNode) { + this._rootNode = node; + } else if (slotted) { + container = new SlottedNodeContainer(this, node, this.inspector); + } else if (nodeType == nodeConstants.ELEMENT_NODE && !isPseudoElement) { + container = new MarkupElementContainer(this, node, this.inspector); + } else if ( + nodeType == nodeConstants.COMMENT_NODE || + nodeType == nodeConstants.TEXT_NODE + ) { + container = new MarkupTextContainer(this, node, this.inspector); + } else { + container = new MarkupReadOnlyContainer(this, node, this.inspector); + } + + if (flashNode) { + container.flashMutation(); + } + + this.setContainer(node, container, slotted); + this._forceUpdateChildren(container); + + this.inspector.emit("container-created", container); + + return container; + }, + + async _onResourceAvailable(resources) { + for (const resource of resources) { + if ( + !this.resourceCommand || + resource.resourceType !== this.resourceCommand.TYPES.ROOT_NODE || + resource.isDestroyed() + ) { + // Only handle alive root-node resources + continue; + } + + if (resource.targetFront.isTopLevel && resource.isTopLevelDocument) { + // The topmost root node will lead to the destruction and recreation of + // the MarkupView. This is handled by the inspector. + continue; + } + + const parentNodeFront = resource.parentNode(); + const container = this.getContainer(parentNodeFront); + if (container) { + // If there is no container for the parentNodeFront, the markup view is + // currently not watching this part of the tree. + this._forceUpdateChildren(container, { + flash: true, + updateLevel: true, + }); + } + } + }, + + _onTargetAvailable({ targetFront }) {}, + + _onTargetDestroyed({ targetFront, isModeSwitching }) { + // Bug 1776250: We only watch targets in order to update containers which + // might no longer be able to display children hosted in remote processes, + // which corresponds to a Browser Toolbox mode switch. + if (isModeSwitching) { + const container = this.getContainer(targetFront.getParentNodeFront()); + if (container) { + this._forceUpdateChildren(container, { + updateLevel: true, + }); + } + } + }, + + /** + * Mutation observer used for included nodes. + */ + _onWalkerMutations(mutations) { + for (const mutation of mutations) { + const type = mutation.type; + const target = mutation.target; + + const container = this.getContainer(target); + if (!container) { + // Container might not exist if this came from a load event for a node + // we're not viewing. + continue; + } + + if ( + type === "attributes" || + type === "characterData" || + type === "customElementDefined" || + type === "events" || + type === "pseudoClassLock" + ) { + container.update(); + } else if ( + type === "childList" || + type === "slotchange" || + type === "shadowRootAttached" + ) { + this._forceUpdateChildren(container, { + flash: true, + updateLevel: true, + }); + } else if (type === "inlineTextChild") { + this._forceUpdateChildren(container, { flash: true }); + container.update(); + } + } + + this._waitForChildren().then(() => { + if (this._destroyed) { + // Could not fully update after markup mutations, the markup-view was destroyed + // while waiting for children. Bail out silently. + return; + } + this._flashMutatedNodes(mutations); + this.inspector.emit("markupmutation", mutations); + + // Since the htmlEditor is absolutely positioned, a mutation may change + // the location in which it should be shown. + if (this.htmlEditor) { + this.htmlEditor.refresh(); + } + }); + }, + + /** + * React to display-change and scrollable-change events from the walker. These are + * events that tell us when something of interest changed on a collection of nodes: + * whether their display type changed, or whether they became scrollable. + * + * @param {Array} nodes + * An array of nodeFronts + */ + _onWalkerNodeStatesChanged(nodes) { + for (const node of nodes) { + const container = this.getContainer(node); + if (container) { + container.update(); + } + } + }, + + /** + * Given a list of mutations returned by the mutation observer, flash the + * corresponding containers to attract attention. + */ + _flashMutatedNodes(mutations) { + const addedOrEditedContainers = new Set(); + const removedContainers = new Set(); + + for (const { type, target, added, removed, newValue } of mutations) { + const container = this.getContainer(target); + + if (container) { + if (type === "characterData") { + addedOrEditedContainers.add(container); + } else if (type === "attributes" && newValue === null) { + // Removed attributes should flash the entire node. + // New or changed attributes will flash the attribute itself + // in ElementEditor.flashAttribute. + addedOrEditedContainers.add(container); + } else if (type === "childList") { + // If there has been removals, flash the parent + if (removed.length) { + removedContainers.add(container); + } + + // If there has been additions, flash the nodes if their associated + // container exist (so if their parent is expanded in the inspector). + added.forEach(node => { + const addedContainer = this.getContainer(node); + if (addedContainer) { + addedOrEditedContainers.add(addedContainer); + + // The node may be added as a result of an append, in which case + // it will have been removed from another container first, but in + // these cases we don't want to flash both the removal and the + // addition + removedContainers.delete(container); + } + }); + } + } + } + + for (const container of removedContainers) { + container.flashMutation(); + } + for (const container of addedOrEditedContainers) { + container.flashMutation(); + } + }, + + /** + * Make sure the given node's parents are expanded and the + * node is scrolled on to screen. + */ + showNode(node, { centered = true, slotted, smoothScroll = false } = {}) { + if (slotted && !this.hasContainer(node, slotted)) { + throw new Error("Tried to show a slotted node not previously imported"); + } else { + this._ensureNodeImported(node); + } + + return this._waitForChildren() + .then(() => { + if (this._destroyed) { + return Promise.reject("markupview destroyed"); + } + return this._ensureVisible(node); + }) + .then(() => { + const container = this.getContainer(node, slotted); + scrollIntoViewIfNeeded(container.editor.elt, centered, smoothScroll); + }, this._handleRejectionIfNotDestroyed); + }, + + _ensureNodeImported(node) { + let parent = node; + + this.importNode(node); + + while ((parent = this._getParentInTree(parent))) { + this.importNode(parent); + this.expandNode(parent); + } + }, + + /** + * Expand the container's children. + */ + _expandContainer(container) { + return this._updateChildren(container, { expand: true }).then(() => { + if (this._destroyed) { + // Could not expand the node, the markup-view was destroyed in the meantime. Just + // silently give up. + return; + } + container.setExpanded(true); + }); + }, + + /** + * Expand the node's children. + */ + expandNode(node) { + const container = this.getContainer(node); + return this._expandContainer(container); + }, + + /** + * Expand the entire tree beneath a container. + * + * @param {MarkupContainer} container + * The container to expand. + */ + _expandAll(container) { + return this._expandContainer(container) + .then(() => { + let child = container.children.firstChild; + const promises = []; + while (child) { + promises.push(this._expandAll(child.container)); + child = child.nextSibling; + } + return Promise.all(promises); + }) + .catch(console.error); + }, + + /** + * Expand the entire tree beneath a node. + * + * @param {DOMNode} node + * The node to expand, or null to start from the top. + * @return {Promise} promise that resolves once all children are expanded. + */ + expandAll(node) { + node = node || this._rootNode; + return this._expandAll(this.getContainer(node)); + }, + + /** + * Collapse the node's children. + */ + collapseNode(node) { + const container = this.getContainer(node); + container.setExpanded(false); + }, + + _collapseAll(container) { + container.setExpanded(false); + const children = container.getChildContainers() || []; + children.forEach(child => this._collapseAll(child)); + }, + + /** + * Collapse the entire tree beneath a node. + * + * @param {DOMNode} node + * The node to collapse. + * @return {Promise} promise that resolves once all children are collapsed. + */ + collapseAll(node) { + this._collapseAll(this.getContainer(node)); + + // collapseAll is synchronous, return a promise for consistency with expandAll. + return Promise.resolve(); + }, + + /** + * Returns either the innerHTML or the outerHTML for a remote node. + * + * @param {NodeFront} node + * The NodeFront to get the outerHTML / innerHTML for. + * @param {Boolean} isOuter + * If true, makes the function return the outerHTML, + * otherwise the innerHTML. + * @return {Promise} that will be resolved with the outerHTML / innerHTML. + */ + _getNodeHTML(node, isOuter) { + let walkerPromise = null; + + if (isOuter) { + walkerPromise = node.walkerFront.outerHTML(node); + } else { + walkerPromise = node.walkerFront.innerHTML(node); + } + + return getLongString(walkerPromise); + }, + + /** + * Retrieve the outerHTML for a remote node. + * + * @param {NodeFront} node + * The NodeFront to get the outerHTML for. + * @return {Promise} that will be resolved with the outerHTML. + */ + getNodeOuterHTML(node) { + return this._getNodeHTML(node, true); + }, + + /** + * Retrieve the innerHTML for a remote node. + * + * @param {NodeFront} node + * The NodeFront to get the innerHTML for. + * @return {Promise} that will be resolved with the innerHTML. + */ + getNodeInnerHTML(node) { + return this._getNodeHTML(node); + }, + + /** + * Listen to mutations, expect a given node to be removed and try and select + * the node that sits at the same place instead. + * This is useful when changing the outerHTML or the tag name so that the + * newly inserted node gets selected instead of the one that just got removed. + */ + reselectOnRemoved(removedNode, reason) { + // Only allow one removed node reselection at a time, so that when there are + // more than 1 request in parallel, the last one wins. + this.cancelReselectOnRemoved(); + + // Get the removedNode index in its parent node to reselect the right node. + const isRootElement = ["html", "svg"].includes( + removedNode.tagName.toLowerCase() + ); + const oldContainer = this.getContainer(removedNode); + const parentContainer = this.getContainer(removedNode.parentNode()); + const childIndex = parentContainer + .getChildContainers() + .indexOf(oldContainer); + + const onMutations = (this._removedNodeObserver = mutations => { + let isNodeRemovalMutation = false; + for (const mutation of mutations) { + const containsRemovedNode = + mutation.removed && mutation.removed.some(n => n === removedNode); + if ( + mutation.type === "childList" && + (containsRemovedNode || isRootElement) + ) { + isNodeRemovalMutation = true; + break; + } + } + if (!isNodeRemovalMutation) { + return; + } + + this.inspector.off("markupmutation", onMutations); + this._removedNodeObserver = null; + + // Don't select the new node if the user has already changed the current + // selection. + if ( + this.inspector.selection.nodeFront === parentContainer.node || + (this.inspector.selection.nodeFront === removedNode && isRootElement) + ) { + const childContainers = parentContainer.getChildContainers(); + if (childContainers?.[childIndex]) { + const childContainer = childContainers[childIndex]; + this._markContainerAsSelected(childContainer, reason); + if (childContainer.hasChildren) { + this.expandNode(childContainer.node); + } + this.emit("reselectedonremoved"); + } + } + }); + + // Start listening for mutations until we find a childList change that has + // removedNode removed. + this.inspector.on("markupmutation", onMutations); + }, + + /** + * Make sure to stop listening for node removal markupmutations and not + * reselect the corresponding node when that happens. + * Useful when the outerHTML/tagname edition failed. + */ + cancelReselectOnRemoved() { + if (this._removedNodeObserver) { + this.inspector.off("markupmutation", this._removedNodeObserver); + this._removedNodeObserver = null; + this.emit("canceledreselectonremoved"); + } + }, + + /** + * Replace the outerHTML of any node displayed in the inspector with + * some other HTML code + * + * @param {NodeFront} node + * Node which outerHTML will be replaced. + * @param {String} newValue + * The new outerHTML to set on the node. + * @param {String} oldValue + * The old outerHTML that will be used if the user undoes the update. + * @return {Promise} that will resolve when the outer HTML has been updated. + */ + updateNodeOuterHTML(node, newValue) { + const container = this.getContainer(node); + if (!container) { + return Promise.reject(); + } + + // Changing the outerHTML removes the node which outerHTML was changed. + // Listen to this removal to reselect the right node afterwards. + this.reselectOnRemoved(node, "outerhtml"); + return node.walkerFront.setOuterHTML(node, newValue).catch(() => { + this.cancelReselectOnRemoved(); + }); + }, + + /** + * Replace the innerHTML of any node displayed in the inspector with + * some other HTML code + * @param {Node} node + * node which innerHTML will be replaced. + * @param {String} newValue + * The new innerHTML to set on the node. + * @param {String} oldValue + * The old innerHTML that will be used if the user undoes the update. + * @return {Promise} that will resolve when the inner HTML has been updated. + */ + updateNodeInnerHTML(node, newValue, oldValue) { + const container = this.getContainer(node); + if (!container) { + return Promise.reject(); + } + + return new Promise((resolve, reject) => { + container.undo.do( + () => { + node.walkerFront.setInnerHTML(node, newValue).then(resolve, reject); + }, + () => { + node.walkerFront.setInnerHTML(node, oldValue); + } + ); + }); + }, + + /** + * Insert adjacent HTML to any node displayed in the inspector. + * + * @param {NodeFront} node + * The reference node. + * @param {String} position + * The position as specified for Element.insertAdjacentHTML + * (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd"). + * @param {String} newValue + * The adjacent HTML. + * @return {Promise} that will resolve when the adjacent HTML has + * been inserted. + */ + insertAdjacentHTMLToNode(node, position, value) { + const container = this.getContainer(node); + if (!container) { + return Promise.reject(); + } + + let injectedNodes = []; + + return new Promise((resolve, reject) => { + container.undo.do( + () => { + // eslint-disable-next-line no-unsanitized/method + node.walkerFront + .insertAdjacentHTML(node, position, value) + .then(nodeArray => { + injectedNodes = nodeArray.nodes; + return nodeArray; + }) + .then(resolve, reject); + }, + () => { + node.walkerFront.removeNodes(injectedNodes); + } + ); + }); + }, + + /** + * Open an editor in the UI to allow editing of a node's html. + * + * @param {NodeFront} node + * The NodeFront to edit. + */ + beginEditingHTML(node) { + // We use outer html for elements, but inner html for fragments. + const isOuter = node.nodeType == nodeConstants.ELEMENT_NODE; + const html = isOuter + ? this.getNodeOuterHTML(node) + : this.getNodeInnerHTML(node); + html.then(oldValue => { + const container = this.getContainer(node); + if (!container) { + return; + } + // Load load and create HTML Editor as it is rarely used and fetch complex deps + if (!this.htmlEditor) { + const HTMLEditor = require("resource://devtools/client/inspector/markup/views/html-editor.js"); + this.htmlEditor = new HTMLEditor(this.doc); + } + this.htmlEditor.show(container.tagLine, oldValue); + const start = this.telemetry.msSystemNow(); + this.htmlEditor.once("popuphidden", (commit, value) => { + // Need to focus the element instead of the frame / window + // in order to give keyboard focus back to doc (from editor). + this.doc.documentElement.focus(); + + if (commit) { + if (isOuter) { + this.updateNodeOuterHTML(node, value, oldValue); + } else { + this.updateNodeInnerHTML(node, value, oldValue); + } + } + + const end = this.telemetry.msSystemNow(); + this.telemetry.recordEvent("edit_html", "inspector", null, { + made_changes: commit, + time_open: end - start, + }); + }); + + this.emit("begin-editing"); + }); + }, + + /** + * Expand or collapse the given node. + * + * @param {NodeFront} node + * The NodeFront to update. + * @param {Boolean} expanded + * Whether the node should be expanded/collapsed. + * @param {Boolean} applyToDescendants + * Whether all descendants should also be expanded/collapsed + */ + setNodeExpanded(node, expanded, applyToDescendants) { + if (expanded) { + if (applyToDescendants) { + this.expandAll(node); + } else { + this.expandNode(node); + } + } else if (applyToDescendants) { + this.collapseAll(node); + } else { + this.collapseNode(node); + } + }, + + /** + * Mark the given node selected, and update the inspector.selection + * object's NodeFront to keep consistent state between UI and selection. + * + * @param {NodeFront} aNode + * The NodeFront to mark as selected. + * @param {String} reason + * The reason for marking the node as selected. + * @return {Boolean} False if the node is already marked as selected, true + * otherwise. + */ + markNodeAsSelected(node, reason = "nodeselected") { + const container = this.getContainer(node); + return this._markContainerAsSelected(container); + }, + + _markContainerAsSelected(container, reason) { + if (!container || this._selectedContainer === container) { + return false; + } + + const { node } = container; + + // Un-select and remove focus from the previous container. + if (this._selectedContainer) { + this._selectedContainer.selected = false; + this._selectedContainer.clearFocus(); + } + + // Select the new container. + this._selectedContainer = container; + if (node) { + this._selectedContainer.selected = true; + } + + // Change the current selection if needed. + if (!this._isContainerSelected(this._selectedContainer)) { + const isSlotted = container.isSlotted(); + this.inspector.selection.setNodeFront(node, { reason, isSlotted }); + } + + return true; + }, + + /** + * Make sure that every ancestor of the selection are updated + * and included in the list of visible children. + */ + _ensureVisible(node) { + while (node) { + const container = this.getContainer(node); + const parent = this._getParentInTree(node); + if (!container.elt.parentNode) { + const parentContainer = this.getContainer(parent); + if (parentContainer) { + this._forceUpdateChildren(parentContainer, { expand: true }); + } + } + + node = parent; + } + return this._waitForChildren(); + }, + + /** + * Unmark selected node (no node selected). + */ + unmarkSelectedNode() { + if (this._selectedContainer) { + this._selectedContainer.selected = false; + this._selectedContainer = null; + } + }, + + /** + * Check if the current selection is a descendent of the container. + * if so, make sure it's among the visible set for the container, + * and set the dirty flag if needed. + * + * @return The node that should be made visible, if any. + */ + _checkSelectionVisible(container) { + let centered = null; + let node = this.inspector.selection.nodeFront; + while (node) { + if (this._getParentInTree(node) === container.node) { + centered = node; + break; + } + node = this._getParentInTree(node); + } + + return centered; + }, + + async _forceUpdateChildren(container, options = {}) { + const { flash, updateLevel, expand } = options; + + // Set childrenDirty to true to force fetching new children. + container.childrenDirty = true; + + // Update the children to take care of changes in the markup view DOM + await this._updateChildren(container, { expand, flash }); + + // The markup view may have been destroyed in the meantime + if (this._destroyed) { + return; + } + + if (updateLevel) { + // Update container (and its subtree) DOM tree depth level for + // accessibility where necessary. + container.updateLevel(); + } + }, + + /** + * Make sure all children of the given container's node are + * imported and attached to the container in the right order. + * + * Children need to be updated only in the following circumstances: + * a) We just imported this node and have never seen its children. + * container.childrenDirty will be set by importNode in this case. + * b) We received a childList mutation on the node. + * container.childrenDirty will be set in that case too. + * c) We have changed the selection, and the path to that selection + * wasn't loaded in a previous children request (because we only + * grab a subset). + * container.childrenDirty should be set in that case too! + * + * @param {MarkupContainer} container + * The markup container whose children need updating + * @param {Object} options + * Options are {expand:boolean,flash:boolean} + * @return {Promise} that will be resolved when the children are ready + * (which may be immediately). + */ + _updateChildren(container, options) { + // Slotted containers do not display any children. + if (container.isSlotted()) { + return Promise.resolve(container); + } + + const expand = options?.expand; + const flash = options?.flash; + + container.hasChildren = container.node.hasChildren; + // Accessibility should either ignore empty children or semantically + // consider them a group. + container.setChildrenRole(); + + if (!this._queuedChildUpdates) { + this._queuedChildUpdates = new Map(); + } + + if (this._queuedChildUpdates.has(container)) { + return this._queuedChildUpdates.get(container); + } + + if (!container.childrenDirty) { + return Promise.resolve(container); + } + + // Before bailing out for other conditions, check if the unavailable + // children badge needs updating (Bug 1776250). + if ( + typeof container?.editor?.hasUnavailableChildren == "function" && + container.editor.hasUnavailableChildren() != + container.node.childrenUnavailable + ) { + container.update(); + } + + if ( + container.inlineTextChild && + container.inlineTextChild != container.node.inlineTextChild + ) { + // This container was doing double duty as a container for a single + // text child, back that out. + this._containers.delete(container.inlineTextChild); + container.clearInlineTextChild(); + + if (container.hasChildren && container.selected) { + container.setExpanded(true); + } + } + + if (container.node.inlineTextChild) { + container.setExpanded(false); + // this container will do double duty as the container for the single + // text child. + while (container.children.firstChild) { + container.children.firstChild.remove(); + } + + container.setInlineTextChild(container.node.inlineTextChild); + + this.setContainer(container.node.inlineTextChild, container); + container.childrenDirty = false; + return Promise.resolve(container); + } + + if (!container.hasChildren) { + while (container.children.firstChild) { + container.children.firstChild.remove(); + } + container.childrenDirty = false; + container.setExpanded(false); + return Promise.resolve(container); + } + + // If we're not expanded (or asked to update anyway), we're done for + // now. Note that this will leave the childrenDirty flag set, so when + // expanded we'll refresh the child list. + if (!(container.expanded || expand)) { + return Promise.resolve(container); + } + + // We're going to issue a children request, make sure it includes the + // centered node. + const centered = this._checkSelectionVisible(container); + + // Children aren't updated yet, but clear the childrenDirty flag anyway. + // If the dirty flag is re-set while we're fetching we'll need to fetch + // again. + container.childrenDirty = false; + + const isShadowHost = container.node.isShadowHost; + const updatePromise = this._getVisibleChildren(container, centered) + .then(children => { + if (!this._containers) { + return Promise.reject("markup view destroyed"); + } + this._queuedChildUpdates.delete(container); + + // If children are dirty, we got a change notification for this node + // while the request was in progress, we need to do it again. + if (container.childrenDirty) { + return this._updateChildren(container, { + expand: centered || expand, + }); + } + + const fragment = this.doc.createDocumentFragment(); + + for (const child of children.nodes) { + const slotted = !isShadowHost && child.isDirectShadowHostChild; + const childContainer = this.importNode(child, flash, slotted); + fragment.appendChild(childContainer.elt); + } + + while (container.children.firstChild) { + container.children.firstChild.remove(); + } + + if (!children.hasFirst) { + const topItem = this.buildMoreNodesButtonMarkup(container); + fragment.insertBefore(topItem, fragment.firstChild); + } + if (!children.hasLast) { + const bottomItem = this.buildMoreNodesButtonMarkup(container); + fragment.appendChild(bottomItem); + } + + container.children.appendChild(fragment); + return container; + }) + .catch(this._handleRejectionIfNotDestroyed); + this._queuedChildUpdates.set(container, updatePromise); + return updatePromise; + }, + + buildMoreNodesButtonMarkup(container) { + const elt = this.doc.createElement("li"); + elt.classList.add("more-nodes", "devtools-class-comment"); + + const label = this.doc.createElement("span"); + label.textContent = INSPECTOR_L10N.getStr("markupView.more.showing"); + elt.appendChild(label); + + const button = this.doc.createElement("button"); + button.setAttribute("href", "#"); + const showAllString = PluralForm.get( + container.node.numChildren, + INSPECTOR_L10N.getStr("markupView.more.showAll2") + ); + button.textContent = showAllString.replace( + "#1", + container.node.numChildren + ); + elt.appendChild(button); + + button.addEventListener("click", () => { + container.maxChildren = -1; + this._forceUpdateChildren(container); + }); + + return elt; + }, + + _waitForChildren() { + if (!this._queuedChildUpdates) { + return Promise.resolve(undefined); + } + + return Promise.all([...this._queuedChildUpdates.values()]); + }, + + /** + * Return a list of the children to display for this container. + */ + async _getVisibleChildren(container, centered) { + let maxChildren = container.maxChildren || this.maxChildren; + if (maxChildren == -1) { + maxChildren = undefined; + } + + // We have to use node's walker and not a top level walker + // as for fission frames, we are going to have multiple walkers + const inspectorFront = await container.node.targetFront.getFront( + "inspector" + ); + return inspectorFront.walker.children(container.node, { + maxNodes: maxChildren, + center: centered, + }); + }, + + /** + * The parent of a given node as rendered in the markup view is not necessarily + * node.parentNode(). For instance, shadow roots don't have a parentNode, but a host + * element. However they are represented as parent and children in the markup view. + * + * Use this method when you are interested in the parent of a node from the perspective + * of the markup-view tree, and not from the perspective of the actual DOM. + */ + _getParentInTree(node) { + const parent = node.parentOrHost(); + if (!parent) { + return null; + } + + // If the parent node belongs to a different target while the node's target is the + // one selected by the user in the iframe picker, we don't want to go further up. + if ( + node.targetFront !== parent.targetFront && + node.targetFront == + this.inspector.commands.targetCommand.selectedTargetFront + ) { + return null; + } + + return parent; + }, + + /** + * Tear down the markup panel. + */ + destroy() { + if (this._destroyed) { + return; + } + + this._destroyed = true; + + this._hoveredContainer = null; + + if (this._contextMenu) { + this._contextMenu.destroy(); + this._contextMenu = null; + } + + if (this._eventDetailsTooltip) { + this._eventDetailsTooltip.destroy(); + this._eventDetailsTooltip = null; + } + + if (this.htmlEditor) { + this.htmlEditor.destroy(); + this.htmlEditor = null; + } + + if (this.imagePreviewTooltip) { + this.imagePreviewTooltip.destroy(); + this.imagePreviewTooltip = null; + } + + if (this._undo) { + this._undo.destroy(); + this._undo = null; + } + + if (this._shortcuts) { + this._shortcuts.destroy(); + this._shortcuts = null; + } + + this.popup.destroy(); + this.popup = null; + this._selectedContainer = null; + + this._elt.removeEventListener("blur", this._onBlur, true); + this._elt.removeEventListener("click", this._onMouseClick); + this._elt.removeEventListener("contextmenu", this._onContextMenu); + this._elt.removeEventListener("mousemove", this._onMouseMove); + this._elt.removeEventListener("mouseout", this._onMouseOut); + this._frame.removeEventListener("focus", this._onFocus); + this._unsubscribeFromToolboxStore(); + this.inspector.selection.off("new-node-front", this._onNewSelection); + this.resourceCommand.unwatchResources( + [this.resourceCommand.TYPES.ROOT_NODE], + { onAvailable: this._onResourceAvailable } + ); + this.targetCommand.unwatchTargets({ + types: [this.targetCommand.TYPES.FRAME], + onAvailable: this._onTargetAvailable, + onDestroyed: this._onTargetDestroyed, + }); + this.inspector.toolbox.nodePicker.off( + "picker-node-hovered", + this._onToolboxPickerHover + ); + this.inspector.toolbox.nodePicker.off( + "picker-node-canceled", + this._onToolboxPickerCanceled + ); + this.inspector.highlighters.off( + "highlighter-shown", + this.onHighlighterShown + ); + this.inspector.highlighters.off( + "highlighter-hidden", + this.onHighlighterHidden + ); + this.win.removeEventListener("copy", this._onCopy); + this.win.removeEventListener("mouseup", this._onMouseUp); + + this._walkerEventListener.destroy(); + this._walkerEventListener = null; + + this._prefObserver.off( + ATTR_COLLAPSE_ENABLED_PREF, + this._onCollapseAttributesPrefChange + ); + this._prefObserver.off( + ATTR_COLLAPSE_LENGTH_PREF, + this._onCollapseAttributesPrefChange + ); + this._prefObserver.destroy(); + + for (const [, container] of this._containers) { + container.destroy(); + } + this._containers = null; + + this._elt.innerHTML = ""; + this._elt = null; + + this.controllerWindow = null; + this.doc = null; + this.highlighters = null; + this.walker = null; + this.resourceCommand = null; + this.win = null; + + this._lastDropTarget = null; + this._lastDragTarget = null; + }, + + /** + * Find the closest element with class tag-line. These are used to indicate + * drag and drop targets. + * + * @param {DOMNode} el + * @return {DOMNode} + */ + findClosestDragDropTarget(el) { + return el.classList.contains("tag-line") + ? el + : el.querySelector(".tag-line") || el.closest(".tag-line"); + }, + + /** + * Takes an element as it's only argument and marks the element + * as the drop target + */ + indicateDropTarget(el) { + if (this._lastDropTarget) { + this._lastDropTarget.classList.remove("drop-target"); + } + + if (!el) { + return; + } + + const target = this.findClosestDragDropTarget(el); + if (target) { + target.classList.add("drop-target"); + this._lastDropTarget = target; + } + }, + + /** + * Takes an element to mark it as indicator of dragging target's initial place + */ + indicateDragTarget(el) { + if (this._lastDragTarget) { + this._lastDragTarget.classList.remove("drag-target"); + } + + if (!el) { + return; + } + + const target = this.findClosestDragDropTarget(el); + if (target) { + target.classList.add("drag-target"); + this._lastDragTarget = target; + } + }, + + /** + * Used to get the nodes required to modify the markup after dragging the + * element (parent/nextSibling). + */ + get dropTargetNodes() { + const target = this._lastDropTarget; + + if (!target) { + return null; + } + + let parent, nextSibling; + + if ( + target.previousElementSibling && + target.previousElementSibling.nodeName.toLowerCase() === "ul" + ) { + parent = target.parentNode.container.node; + nextSibling = null; + } else { + parent = target.parentNode.container.node.parentNode(); + nextSibling = target.parentNode.container.node; + } + + if (nextSibling) { + while ( + nextSibling.isMarkerPseudoElement || + nextSibling.isBeforePseudoElement + ) { + nextSibling = + this.getContainer(nextSibling).elt.nextSibling.container.node; + } + if (nextSibling.isAfterPseudoElement) { + parent = target.parentNode.container.node.parentNode(); + nextSibling = null; + } + } + + if (parent.nodeType !== nodeConstants.ELEMENT_NODE) { + return null; + } + + return { parent, nextSibling }; + }, +}; + +/** + * Copy the content of a longString containing HTML code to the clipboard. + * The string is retrieved, and possibly beautified if the user has the right pref set and + * then placed in the clipboard. + * + * @param {Promise} longStringActorPromise + * The promise expected to resolve a LongStringActor instance + */ +async function copyLongHTMLString(longStringActorPromise) { + let string = await getLongString(longStringActorPromise); + + if (Services.prefs.getBoolPref(BEAUTIFY_HTML_ON_COPY_PREF)) { + const { indentUnit, indentWithTabs } = getTabPrefs(); + string = beautify.html(string, { + // eslint-disable-next-line camelcase + preserve_newlines: false, + // eslint-disable-next-line camelcase + indent_size: indentWithTabs ? 1 : indentUnit, + // eslint-disable-next-line camelcase + indent_char: indentWithTabs ? "\t" : " ", + unformatted: [], + }); + } + + clipboardHelper.copyString(string); +} + +/** + * Map a number from one range to another. + */ +function map(value, oldMin, oldMax, newMin, newMax) { + const ratio = oldMax - oldMin; + if (ratio == 0) { + return value; + } + return newMin + (newMax - newMin) * ((value - oldMin) / ratio); +} + +module.exports = MarkupView; diff --git a/devtools/client/inspector/markup/markup.xhtml b/devtools/client/inspector/markup/markup.xhtml new file mode 100644 index 0000000000..dd8115bdc1 --- /dev/null +++ b/devtools/client/inspector/markup/markup.xhtml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + diff --git a/devtools/client/inspector/markup/moz.build b/devtools/client/inspector/markup/moz.build new file mode 100644 index 0000000000..1fcba80d9b --- /dev/null +++ b/devtools/client/inspector/markup/moz.build @@ -0,0 +1,19 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + "components", + "utils", + "views", +] + +DevToolsModules( + "markup-context-menu.js", + "markup.js", + "utils.js", +) + +BROWSER_CHROME_MANIFESTS += ["test/browser.ini"] diff --git a/devtools/client/inspector/markup/test/browser.ini b/devtools/client/inspector/markup/test/browser.ini new file mode 100644 index 0000000000..6ebc393626 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser.ini @@ -0,0 +1,249 @@ +[DEFAULT] +prefs = + # Bug 1520383 - Force devtools.chrome.enabled to false regardless of whether + # we're in an official build so we don't show event bubbles from chrome event + # listeners in the inspector on unprivileged test pages. + devtools.chrome.enabled=false +tags = devtools +subsuite = devtools +support-files = + doc_markup_anonymous.html + doc_markup_dragdrop.html + doc_markup_dragdrop_autoscroll_01.html + doc_markup_dragdrop_autoscroll_02.html + doc_markup_edit.html + doc_markup_events_01.html + doc_markup_events_02.html + doc_markup_events_03.html + doc_markup_events_04.html + doc_markup_events_chrome_listeners.html + doc_markup_events_jquery.html + doc_markup_events_object_listener.html + doc_markup_events-overflow.html + doc_markup_events_react_development_15.4.1.html + doc_markup_events_react_development_15.4.1_jsx.html + doc_markup_events_react_production_15.3.1.html + doc_markup_events_react_production_15.3.1_jsx.html + doc_markup_events_react_production_16.2.0.html + doc_markup_events_react_production_16.2.0_jsx.html + doc_markup_events-source_map.html + doc_markup_events_toggle.html + doc_markup_flashing.html + doc_markup_html_mixed_case.html + doc_markup_image_and_canvas.html + doc_markup_image_and_canvas_2.html + doc_markup_links.html + doc_markup_mutation.html + doc_markup_navigation.html + doc_markup_not_displayed.html + doc_markup_pagesize_01.html + doc_markup_pagesize_02.html + doc_markup_pseudo.html + doc_markup_search.html + doc_markup_subgrid.html + doc_markup_svg_attributes.html + doc_markup_toggle.html + doc_markup_tooltip.png + doc_markup_void_elements.html + doc_markup_void_elements.xhtml + doc_markup_whitespace.html + doc_markup_xul.xhtml + doc_markup_update-on-navigtion_1.html + doc_markup_update-on-navigtion_2.html + doc_markup_view-original-source.html + doc_markup_shadowdom_open_debugger_pretty_printed.html + events_bundle.js + events_bundle.js.map + events_original.js + head.js + helper_attributes_test_runner.js + helper_diff.js + helper_events_test_runner.js + helper_markup_accessibility_navigation.js + helper_outerhtml_test_runner.js + helper_style_attr_test_runner.js + lib_babel_6.21.0_min.js + lib_jquery_1.0.js + lib_jquery_1.1.js + lib_jquery_1.2_min.js + lib_jquery_1.3_min.js + lib_jquery_1.4_min.js + lib_jquery_1.6_min.js + lib_jquery_1.7_min.js + lib_jquery_1.11.1_min.js + lib_jquery_2.1.1_min.js + lib_react_16.2.0_min.js + lib_react_dom_15.3.1_min.js + lib_react_dom_15.4.1.js + lib_react_dom_16.2.0_min.js + lib_react_with_addons_15.3.1_min.js + lib_react_with_addons_15.4.1.js + react_external_listeners.js + shadowdom_open_debugger.min.js + !/devtools/client/debugger/test/mochitest/shared-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_markup_accessibility_focus_blur.js] +skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences +[browser_markup_accessibility_navigation.js] +skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences +[browser_markup_accessibility_new_selection.js] +[browser_markup_accessibility_navigation_after_edit.js] +skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences +[browser_markup_accessibility_semantics.js] +[browser_markup_anonymous_01.js] +[browser_markup_anonymous_03.js] +[browser_markup_anonymous_04.js] +[browser_markup_copy_html.js] +[browser_markup_copy_image_data.js] +[browser_markup_css_completion_style_attribute_01.js] +[browser_markup_css_completion_style_attribute_02.js] +[browser_markup_css_completion_style_attribute_03.js] +[browser_markup_display_node_01.js] +[browser_markup_display_node_02.js] +[browser_markup_dom_mutation_breakpoints.js] +[browser_markup_dragdrop_autoscroll_01.js] +[browser_markup_dragdrop_autoscroll_02.js] +[browser_markup_dragdrop_before_marker_pseudo.js] +[browser_markup_dragdrop_distance.js] +[browser_markup_dragdrop_draggable.js] +[browser_markup_dragdrop_dragRootNode.js] +[browser_markup_dragdrop_escapeKeyPress.js] +[browser_markup_dragdrop_invalidNodes.js] +[browser_markup_dragdrop_reorder.js] +[browser_markup_dragdrop_tooltip.js] +[browser_markup_events_01.js] +[browser_markup_events_02.js] +[browser_markup_events_03.js] +[browser_markup_events_04.js] +[browser_markup_events_chrome_blocked.js] +[browser_markup_events_chrome_not_blocked.js] +[browser_markup_events_click_to_close.js] +[browser_markup_events_jquery_1.0.js] +[browser_markup_events_jquery_1.1.js] +[browser_markup_events_jquery_1.2.js] +[browser_markup_events_jquery_1.3.js] +[browser_markup_events_jquery_1.4.js] +[browser_markup_events_jquery_1.6.js] +[browser_markup_events_jquery_1.7.js] +[browser_markup_events_jquery_1.11.1.js] +[browser_markup_events_jquery_2.1.1.js] +[browser_markup_events_object_listener.js] +[browser_markup_events-overflow.js] +skip-if = true # Bug 1177550 +[browser_markup_events_react_development_15.4.1.js] +[browser_markup_events_react_development_15.4.1_jsx.js] +[browser_markup_events_react_production_15.3.1.js] +[browser_markup_events_react_production_15.3.1_jsx.js] +[browser_markup_events_react_production_16.2.0.js] +[browser_markup_events_react_production_16.2.0_jsx.js] +[browser_markup_events_source_map.js] +[browser_markup_events_toggle.js] +[browser_markup_events-windowed-host.js] +[browser_markup_flex_display_badge.js] +[browser_markup_flex_display_badge_telemetry.js] +[browser_markup_grid_display_badge_01.js] +[browser_markup_grid_display_badge_02.js] +[browser_markup_grid_display_badge_03.js] +[browser_markup_grid_display_badge_telemetry.js] +[browser_markup_iframe_blocked_by_csp.js] +[browser_markup_links_01.js] +[browser_markup_links_02.js] +[browser_markup_links_03.js] +[browser_markup_links_04.js] +[browser_markup_links_05.js] +[browser_markup_links_06.js] +[browser_markup_links_07.js] +[browser_markup_load_01.js] +skip-if = true # Bug 1706833, times out waiting for context menu to open +[browser_markup_html_edit_01.js] +[browser_markup_html_edit_02.js] +[browser_markup_html_edit_03.js] +[browser_markup_html_edit_04.js] +[browser_markup_html_edit_undo-redo.js] +[browser_markup_image_tooltip.js] +[browser_markup_image_tooltip_mutations.js] +[browser_markup_keybindings_01.js] +[browser_markup_keybindings_02.js] +[browser_markup_keybindings_03.js] +[browser_markup_keybindings_04.js] +[browser_markup_keybindings_delete_attributes.js] +[browser_markup_keybindings_scrolltonode.js] +[browser_markup_mutation_01.js] +[browser_markup_mutation_02.js] +[browser_markup_navigation.js] +[browser_markup_node_names.js] +[browser_markup_node_names_namespaced.js] +[browser_markup_node_not_displayed_01.js] +[browser_markup_node_not_displayed_02.js] +[browser_markup_overflow_badge.js] +[browser_markup_pagesize_01.js] +[browser_markup_pagesize_02.js] +[browser_markup_pseudo_on_reload.js] +[browser_markup_remove_xul_attributes.js] +[browser_markup_screenshot_node.js] +[browser_markup_screenshot_node_about_page.js] +[browser_markup_screenshot_node_iframe.js] +[browser_markup_screenshot_node_shadowdom.js] +[browser_markup_screenshot_node_warning.js] +[browser_markup_scrollable_badge.js] +[browser_markup_scrollable_badge_click.js] +[browser_markup_search_01.js] +[browser_markup_shadowdom.js] +[browser_markup_shadowdom_clickreveal.js] +[browser_markup_shadowdom_clickreveal_scroll.js] +[browser_markup_shadowdom_copy_paths.js] +[browser_markup_shadowdom_delete.js] +[browser_markup_shadowdom_dynamic.js] +[browser_markup_shadowdom_hover.js] +[browser_markup_shadowdom_marker_and_before_pseudos.js] +[browser_markup_shadowdom_maxchildren.js] +[browser_markup_shadowdom_mutations_shadow.js] +[browser_markup_shadowdom_navigation.js] +[browser_markup_shadowdom_nested_pick_inspect.js] +[browser_markup_shadowdom_noslot.js] +[browser_markup_shadowdom_open_debugger.js] +[browser_markup_shadowdom_open_debugger_pretty_printed.js] +[browser_markup_shadowdom_shadowroot_mode.js] +[browser_markup_shadowdom_show_nodes_button.js] +[browser_markup_shadowdom_slotted_keyboard_focus.js] +[browser_markup_shadowdom_slotupdate.js] +[browser_markup_shadowdom_ua_widgets.js] +[browser_markup_shadowdom_ua_widgets_with_nac.js] +[browser_markup_subgrid_display_badge.js] +[browser_markup_tag_delete_whitespace_node.js] +[browser_markup_tag_edit_01.js] +[browser_markup_tag_edit_02.js] +[browser_markup_tag_edit_03.js] +[browser_markup_tag_edit_04-backspace.js] +[browser_markup_tag_edit_04-delete.js] +[browser_markup_tag_edit_05.js] +[browser_markup_tag_edit_06.js] +[browser_markup_tag_edit_07.js] +[browser_markup_tag_edit_08.js] +[browser_markup_tag_edit_09.js] +[browser_markup_tag_edit_10.js] +[browser_markup_tag_edit_11.js] +[browser_markup_tag_edit_12.js] +[browser_markup_tag_edit_13-other.js] +[browser_markup_tag_edit_avoid_refocus.js] +[browser_markup_tag_edit_long-classname.js] +[browser_markup_template.js] +[browser_markup_textcontent_display.js] +[browser_markup_textcontent_edit_01.js] +[browser_markup_textcontent_edit_02.js] +[browser_markup_toggle_01.js] +[browser_markup_toggle_02.js] +[browser_markup_toggle_03.js] +[browser_markup_toggle_04.js] +[browser_markup_toggle_closing_tag_line.js] +[browser_markup_update-on-navigtion.js] +[browser_markup_view-source.js] +[browser_markup_view-original-source.js] +[browser_markup_void_elements_html.js] +[browser_markup_void_elements_xhtml.js] +[browser_markup_whitespace.js] diff --git a/devtools/client/inspector/markup/test/browser_markup_accessibility_focus_blur.js b/devtools/client/inspector/markup/test/browser_markup_accessibility_focus_blur.js new file mode 100644 index 0000000000..6be70144d8 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_focus_blur.js @@ -0,0 +1,77 @@ +/* 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"; + +// Test inspector markup view handling focus and blur when moving between markup +// view, its root and other containers, and other parts of inspector. + +add_task(async function () { + const { inspector } = await openInspectorForURL( + "data:text/html;charset=utf-8,

foo

bar" + ); + const markup = inspector.markup; + const doc = markup.doc; + const win = doc.defaultView; + + const spanContainer = await getContainerForSelector("span", inspector); + const rootContainer = markup.getContainer(markup._rootNode); + + is( + doc.activeElement, + doc.body, + "Keyboard focus by default is on document body" + ); + + await selectNode("span", inspector); + + is(doc.activeElement, doc.body, "Keyboard focus is still on document body"); + + info("Focusing on the test span node using 'Return' key"); + // Focus on the tree element. + rootContainer.elt.focus(); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + + is( + doc.activeElement, + spanContainer.editor.tag, + "Keyboard focus should be on tag element of focused container" + ); + + info("Focusing on search box, external to markup view document"); + await focusSearchBoxUsingShortcut(inspector.panelWin); + + is( + doc.activeElement, + doc.body, + "Keyboard focus should be removed from focused container" + ); + + info("Selecting the test span node again"); + await selectNode("span", inspector); + + is( + doc.activeElement, + doc.body, + "Keyboard focus should again be on document body" + ); + + info("Focusing on the test span node using 'Space' key"); + // Focus on the tree element. + rootContainer.elt.focus(); + EventUtils.synthesizeKey("VK_SPACE", {}, win); + + is( + doc.activeElement, + spanContainer.editor.tag, + "Keyboard focus should again be on tag element of focused container" + ); + + await clickOnInspectMenuItem("h1"); + is( + doc.activeElement, + rootContainer.elt, + "When inspect menu item is used keyboard focus should move to tree." + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.js b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.js new file mode 100644 index 0000000000..d7eaffae82 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.js @@ -0,0 +1,277 @@ +/* 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/. */ +/* import-globals-from helper_markup_accessibility_navigation.js */ + +"use strict"; + +// Test keyboard navigation accessibility of inspector's markup view. + +loadHelperScript("helper_markup_accessibility_navigation.js"); + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * key {String} key event's key + * options {?Object} optional event data such as shiftKey, etc + * focused {String} path to expected focused element relative to + * its container + * activedescendant {String} path to expected aria-activedescendant element + * relative to its container + * waitFor {String} optional event to wait for if keyboard actions + * result in asynchronous updates + * } + */ +const TESTS = [ + { + desc: "Collapse body container", + focused: "root.elt", + activedescendant: "body.tagLine", + key: "VK_LEFT", + options: {}, + waitFor: "collapsed", + }, + { + desc: "Expand body container", + focused: "root.elt", + activedescendant: "body.tagLine", + key: "VK_RIGHT", + options: {}, + waitFor: "expanded", + }, + { + desc: "Select header container", + focused: "root.elt", + activedescendant: "header.tagLine", + key: "VK_DOWN", + options: {}, + waitFor: "inspector-updated", + }, + { + desc: "Expand header container", + focused: "root.elt", + activedescendant: "header.tagLine", + key: "VK_RIGHT", + options: {}, + waitFor: "expanded", + }, + { + desc: "Select text container", + focused: "root.elt", + activedescendant: "container-0.tagLine", + key: "VK_DOWN", + options: {}, + waitFor: "inspector-updated", + }, + { + desc: "Select header container again", + focused: "root.elt", + activedescendant: "header.tagLine", + key: "VK_UP", + options: {}, + waitFor: "inspector-updated", + }, + { + desc: "Collapse header container", + focused: "root.elt", + activedescendant: "header.tagLine", + key: "VK_LEFT", + options: {}, + waitFor: "collapsed", + }, + { + desc: "Focus on header container tag", + focused: "header.focusableElms.0", + activedescendant: "header.tagLine", + key: "VK_RETURN", + options: {}, + }, + { + desc: "Remove focus from header container tag", + focused: "root.elt", + activedescendant: "header.tagLine", + key: "VK_ESCAPE", + options: {}, + }, + { + desc: "Focus on header container tag again", + focused: "header.focusableElms.0", + activedescendant: "header.tagLine", + key: "VK_SPACE", + options: {}, + }, + { + desc: "Focus on header id attribute", + focused: "header.focusableElms.1", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: {}, + }, + { + desc: "Focus on header class attribute", + focused: "header.focusableElms.2", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: {}, + }, + { + desc: "Focus on header new attribute", + focused: "header.focusableElms.3", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: {}, + }, + { + desc: "Circle back and focus on header tag again", + focused: "header.focusableElms.0", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: {}, + }, + { + desc: "Circle back and focus on header new attribute again", + focused: "header.focusableElms.3", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { shiftKey: true }, + }, + { + desc: "Tab back and focus on header class attribute", + focused: "header.focusableElms.2", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { shiftKey: true }, + }, + { + desc: "Tab back and focus on header id attribute", + focused: "header.focusableElms.1", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { shiftKey: true }, + }, + { + desc: "Tab back and focus on header tag", + focused: "header.focusableElms.0", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { shiftKey: true }, + }, + { + desc: "Expand header container, ensure that focus is still on header tag", + focused: "header.focusableElms.0", + activedescendant: "header.tagLine", + key: "VK_RIGHT", + options: {}, + waitFor: "expanded", + }, + { + desc: "Activate header tag editor", + focused: "header.editor.tag.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_RETURN", + options: {}, + }, + { + desc: "Activate header id attribute editor", + focused: "header.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: {}, + }, + { + desc: "Deselect text in header id attribute editor", + focused: "header.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: {}, + }, + { + desc: "Activate header class attribute editor", + focused: "header.editor.attrList.children.1.children.1.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: {}, + }, + { + desc: "Deselect text in header class attribute editor", + focused: "header.editor.attrList.children.1.children.1.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: {}, + }, + { + desc: "Activate header new attribute editor", + focused: "header.editor.newAttr.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: {}, + }, + { + desc: "Circle back and activate header tag editor again", + focused: "header.editor.tag.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: {}, + }, + { + desc: "Circle back and activate header new attribute editor again", + focused: "header.editor.newAttr.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { shiftKey: true }, + }, + { + desc: "Exit edit mode and keep focus on header new attribute", + focused: "header.focusableElms.3", + activedescendant: "header.tagLine", + key: "VK_ESCAPE", + options: {}, + }, + { + desc: "Move the selection to body and reset focus to container tree", + focused: "docBody", + activedescendant: "body.tagLine", + key: "VK_UP", + options: {}, + waitFor: "inspector-updated", + }, +]; + +let containerID = 0; +let elms = {}; + +add_task(async function () { + const { inspector } = await openInspectorForURL(`data:text/html;charset=utf-8, +

fooChild span

`); + + // Record containers that are created after inspector is initialized to be + // useful in testing. + inspector.on("container-created", memorizeContainer); + registerCleanupFunction(() => { + inspector.off("container-created", memorizeContainer); + }); + + elms.docBody = inspector.markup.doc.body; + elms.root = inspector.markup.getContainer(inspector.markup._rootNode); + elms.header = await getContainerForSelector("h1", inspector); + elms.body = await getContainerForSelector("body", inspector); + + // Initial focus is on root element and active descendant should be set on + // body tag line. + testNavigationState(inspector, elms, elms.docBody, elms.body.tagLine); + + // Focus on the tree element. + elms.root.elt.focus(); + + for (const testData of TESTS) { + await runAccessibilityNavigationTest(inspector, elms, testData); + } + + elms = null; +}); + +// Record all containers that are created dynamically into elms object. +function memorizeContainer(container) { + elms[`container-${containerID++}`] = container; +} diff --git a/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation_after_edit.js b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation_after_edit.js new file mode 100644 index 0000000000..7f3782fab0 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation_after_edit.js @@ -0,0 +1,126 @@ +/* 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/. */ +/* import-globals-from helper_markup_accessibility_navigation.js */ + +"use strict"; + +// Test keyboard navigation accessibility is preserved after editing attributes. + +loadHelperScript("helper_markup_accessibility_navigation.js"); + +const TEST_URI = '
'; + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * key {String} key event's key + * options {?Object} optional event data such as shiftKey, etc + * focused {String} path to expected focused element relative to + * its container + * activedescendant {String} path to expected aria-activedescendant element + * relative to its container + * waitFor {String} optional event to wait for if keyboard actions + * result in asynchronous updates + * } + */ +const TESTS = [ + { + desc: "Select header container", + focused: "root.elt", + activedescendant: "div.tagLine", + key: "VK_DOWN", + options: {}, + waitFor: "inspector-updated", + }, + { + desc: "Focus on header tag", + focused: "div.focusableElms.0", + activedescendant: "div.tagLine", + key: "VK_RETURN", + options: {}, + }, + { + desc: "Activate header tag editor", + focused: "div.editor.tag.inplaceEditor.input", + activedescendant: "div.tagLine", + key: "VK_RETURN", + options: {}, + }, + { + desc: "Activate header id attribute editor", + focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "div.tagLine", + key: "VK_TAB", + options: {}, + }, + { + desc: "Deselect text in header id attribute editor", + focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "div.tagLine", + key: "VK_TAB", + options: {}, + }, + { + desc: "Move the cursor to the left", + focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "div.tagLine", + key: "VK_LEFT", + options: {}, + }, + { + desc: "Modify the attribute", + focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "div.tagLine", + key: "A", + options: {}, + }, + { + desc: "Commit the attribute change", + focused: "div.focusableElms.1", + activedescendant: "div.tagLine", + key: "VK_RETURN", + options: {}, + waitFor: "inspector-updated", + }, + { + desc: "Tab and focus on header class attribute", + focused: "div.focusableElms.2", + activedescendant: "div.tagLine", + key: "VK_TAB", + options: {}, + }, + { + desc: "Tab and focus on header new attribute node", + focused: "div.focusableElms.3", + activedescendant: "div.tagLine", + key: "VK_TAB", + options: {}, + }, +]; + +let elms = {}; + +add_task(async function () { + const url = `data:text/html;charset=utf-8,${TEST_URI}`; + const { inspector } = await openInspectorForURL(url); + + elms.docBody = inspector.markup.doc.body; + elms.root = inspector.markup.getContainer(inspector.markup._rootNode); + elms.div = await getContainerForSelector("div", inspector); + elms.body = await getContainerForSelector("body", inspector); + + // Initial focus is on root element and active descendant should be set on + // body tag line. + testNavigationState(inspector, elms, elms.docBody, elms.body.tagLine); + + // Focus on the tree element. + elms.root.elt.focus(); + + for (const testData of TESTS) { + await runAccessibilityNavigationTest(inspector, elms, testData); + } + + elms = null; +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_accessibility_new_selection.js b/devtools/client/inspector/markup/test/browser_markup_accessibility_new_selection.js new file mode 100644 index 0000000000..f1a6d3d769 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_new_selection.js @@ -0,0 +1,34 @@ +/* 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"; + +// Test inspector markup view handling new node selection that is triggered by +// the user keyboard action. In this case markup tree container must receive +// keyboard focus so that further interactions continue within the markup tree. + +add_task(async function () { + const { inspector } = await openInspectorForURL( + "data:text/html;charset=utf-8,

foo

bar" + ); + const markup = inspector.markup; + const doc = markup.doc; + const rootContainer = markup.getContainer(markup._rootNode); + + is( + doc.activeElement, + doc.body, + "Keyboard focus by default is on document body" + ); + + await selectNode("span", inspector, "test"); + is(doc.activeElement, doc.body, "Keyboard focus remains on document body."); + + await selectNode("h1", inspector, "test-keyboard"); + is( + doc.activeElement, + rootContainer.elt, + "Keyboard focus must be on the markup tree conainer." + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_accessibility_semantics.js b/devtools/client/inspector/markup/test/browser_markup_accessibility_semantics.js new file mode 100644 index 0000000000..8aa5a2e335 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_semantics.js @@ -0,0 +1,146 @@ +/* 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"; + +// Test that inspector markup view has all expected ARIA properties set and +// updated. + +const TOP_CONTAINER_LEVEL = 3; + +add_task(async function () { + const { inspector } = await openInspectorForURL(` + data:text/html;charset=utf-8, +

foo

+ bar +
+
+
`); + const markup = inspector.markup; + const doc = markup.doc; + const win = doc.defaultView; + + const rootElt = markup.getContainer(markup._rootNode).elt; + const bodyContainer = await getContainerForSelector("body", inspector); + const spanContainer = await getContainerForSelector("span", inspector); + const headerContainer = await getContainerForSelector("h1", inspector); + const listContainer = await getContainerForSelector("dl", inspector); + + // Focus on the tree element. + rootElt.focus(); + + // Test tree related semantics + is( + rootElt.getAttribute("role"), + "tree", + "Root container should have tree semantics" + ); + is( + rootElt.getAttribute("aria-dropeffect"), + "none", + "By default root container's drop effect should be set to none" + ); + is( + rootElt.getAttribute("aria-activedescendant"), + bodyContainer.tagLine.getAttribute("id"), + "Default active descendant should be set to body" + ); + is( + parseInt(bodyContainer.tagLine.getAttribute("aria-level"), 10), + TOP_CONTAINER_LEVEL - 1, + "Body container tagLine should have nested level up to date" + ); + [spanContainer, headerContainer, listContainer].forEach(container => { + const treeitem = container.tagLine; + is( + treeitem.getAttribute("role"), + "treeitem", + "Child container tagLine elements should have tree item semantics" + ); + is( + parseInt(treeitem.getAttribute("aria-level"), 10), + TOP_CONTAINER_LEVEL, + "Child container tagLine should have nested level up to date" + ); + is( + treeitem.getAttribute("aria-grabbed"), + "false", + "Child container should be draggable but not grabbed by default" + ); + is( + container.children.getAttribute("role"), + "group", + "Container with children should have its children element have group " + + "semantics" + ); + ok(treeitem.id, "Tree item should have id assigned"); + if (container.closeTagLine) { + is( + container.closeTagLine.getAttribute("role"), + "presentation", + "Ignore closing tag" + ); + } + if (container.expander) { + is( + container.expander.getAttribute("role"), + "presentation", + "Ignore expander" + ); + } + }); + + // Test expanding/expandable semantics + ok( + !spanContainer.tagLine.hasAttribute("aria-expanded"), + "Non expandable tree items should not have aria-expanded attribute" + ); + ok( + !headerContainer.tagLine.hasAttribute("aria-expanded"), + "Non expandable tree items should not have aria-expanded attribute" + ); + is( + listContainer.tagLine.getAttribute("aria-expanded"), + "false", + "Closed tree item should have aria-expanded unset" + ); + + info("Selecting and expanding list container"); + await selectNode("dl", inspector); + EventUtils.synthesizeKey("VK_RIGHT", {}, win); + await waitForMultipleChildrenUpdates(inspector); + + is( + rootElt.getAttribute("aria-activedescendant"), + listContainer.tagLine.getAttribute("id"), + "Active descendant should not be set to list container tagLine" + ); + is( + listContainer.tagLine.getAttribute("aria-expanded"), + "true", + "Open tree item should have aria-expanded set" + ); + const listItemContainer = await getContainerForSelector("dt", inspector); + is( + parseInt(listItemContainer.tagLine.getAttribute("aria-level"), 10), + TOP_CONTAINER_LEVEL + 1, + "Grand child container tagLine should have nested level up to date" + ); + is( + listItemContainer.children.getAttribute("role"), + "presentation", + "Container with no children should have its children element ignored by " + + "accessibility" + ); + + info("Collapsing list container"); + EventUtils.synthesizeKey("VK_LEFT", {}, win); + await waitForMultipleChildrenUpdates(inspector); + + is( + listContainer.tagLine.getAttribute("aria-expanded"), + "false", + "Closed tree item should have aria-expanded unset" + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_anonymous_01.js b/devtools/client/inspector/markup/test/browser_markup_anonymous_01.js new file mode 100644 index 0000000000..4cf50de11e --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_anonymous_01.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test native anonymous content in the markupview. +const TEST_URL = URL_ROOT + "doc_markup_anonymous.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + const pseudo = await getNodeFront("#pseudo", inspector); + + // Markup looks like:
<::before /><::after />
+ const children = await inspector.walker.children(pseudo); + is(children.nodes.length, 3, "Children returned from walker"); + + info("Checking the ::before pseudo element"); + const before = children.nodes[0]; + await isEditingMenuDisabled(before, inspector); + + info("Checking the normal child element"); + const span = children.nodes[1]; + await isEditingMenuEnabled(span, inspector); + + info("Checking the ::after pseudo element"); + const after = children.nodes[2]; + await isEditingMenuDisabled(after, inspector); + + const native = await getNodeFront("#native", inspector); + + // Markup looks like:
+ const nativeChildren = await inspector.walker.children(native); + is(nativeChildren.nodes.length, 1, "Children returned from walker"); + + info("Checking the input element"); + const child = nativeChildren.nodes[0]; + ok(!child.isAnonymous, " is not anonymous"); + + const grandchildren = await inspector.walker.children(child); + is( + grandchildren.nodes.length, + 0, + "No native children returned from walker for by default" + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_anonymous_03.js b/devtools/client/inspector/markup/test/browser_markup_anonymous_03.js new file mode 100644 index 0000000000..2d251a884a --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_anonymous_03.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test shadow DOM content in the markupview. +// Note that many features are not yet enabled, but basic listing +// of elements should be working. +const TEST_URL = URL_ROOT + "doc_markup_anonymous.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + const shadowHostFront = await getNodeFront("#shadow", inspector.markup); + is(shadowHostFront.numChildren, 3, "Children of the shadow host are correct"); + + await inspector.markup.expandNode(shadowHostFront); + await waitForMultipleChildrenUpdates(inspector); + + const shadowContainer = inspector.markup.getContainer(shadowHostFront); + const containers = shadowContainer.getChildContainers(); + + info("Checking the ::before pseudo element"); + const before = containers[1].node; + await isEditingMenuDisabled(before, inspector); + + info("Checking shadow dom children"); + const shadowRootFront = containers[0].node; + const children = await inspector.walker.children(shadowRootFront); + + is(shadowRootFront.numChildren, 2, "Children of the shadow root are counted"); + is(children.nodes.length, 2, "Children returned from walker"); + + info("Checking the

shadow element"); + const shadowChild1 = children.nodes[0]; + await isEditingMenuEnabled(shadowChild1, inspector); + + info("Checking the + const nativeChildren = await inspector.walker.children(native); + is(nativeChildren.nodes.length, 1, "Children returned from walker"); + + info("Checking the input element"); + const child = nativeChildren.nodes[0]; + ok(!child.isAnonymous, " is not anonymous"); + + const grandchildren = await inspector.walker.children(child); + is( + grandchildren.nodes.length, + 2, + " has native anonymous children" + ); + + for (const node of grandchildren.nodes) { + ok(node.isAnonymous, "Child is anonymous"); + ok(node._form.isNativeAnonymous, "Child is native anonymous"); + await isEditingMenuDisabled(node, inspector); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_copy_html.js b/devtools/client/inspector/markup/test/browser_markup_copy_html.js new file mode 100644 index 0000000000..e1693a6366 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_copy_html.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the copy inner and outer html menu options. + +// The nicely formatted HTML code. +const FORMATTED_HTML = ` + +
Hello
+ +`; + +// The inner HTML of the body node from the code above. +const FORMATTED_INNER_HTML = FORMATTED_HTML.replace(/<\/*body>/g, "") + .trim() + .replace(/^ {2}/gm, ""); + +// The formatted outer HTML, using tabs rather than spaces. +const TABS_FORMATTED_HTML = FORMATTED_HTML.replace(/[ ]{2}/g, "\t"); + +// The formatted outer HTML, using 3 spaces instead of 2. +const THREE_SPACES_FORMATTED_HTML = FORMATTED_HTML.replace(/[ ]{2}/g, " "); + +// Uglify the formatted code by removing all spaces and line breaks. +const UGLY_HTML = FORMATTED_HTML.replace(/[\r\n\s]+/g, ""); + +// And here is the inner html of the body node from the ugly code above. +const UGLY_INNER_HTML = UGLY_HTML.replace(/<\/*body>/g, ""); + +add_task(async function () { + // Load the ugly code in a new tab and open the inspector. + const { inspector } = await openInspectorForURL( + "data:text/html;charset=utf-8," + encodeURIComponent(UGLY_HTML) + ); + + info("Get the inner and outer html copy menu items"); + const allMenuItems = openContextMenuAndGetAllItems(inspector); + const outerHtmlMenu = allMenuItems.find( + ({ id }) => id === "node-menu-copyouter" + ); + const innerHtmlMenu = allMenuItems.find( + ({ id }) => id === "node-menu-copyinner" + ); + + info("Try to copy the outer html"); + await waitForClipboardPromise(() => outerHtmlMenu.click(), UGLY_HTML); + + info("Try to copy the inner html"); + await waitForClipboardPromise(() => innerHtmlMenu.click(), UGLY_INNER_HTML); + + info("Set the pref for beautifying html on copy"); + await pushPref("devtools.markup.beautifyOnCopy", true); + + info("Try to copy the beautified outer html"); + await waitForClipboardPromise(() => outerHtmlMenu.click(), FORMATTED_HTML); + + info("Try to copy the beautified inner html"); + await waitForClipboardPromise( + () => innerHtmlMenu.click(), + FORMATTED_INNER_HTML + ); + + info("Set the pref to stop expanding tabs into spaces"); + await pushPref("devtools.editor.expandtab", false); + + info("Check that the beautified outer html uses tabs"); + await waitForClipboardPromise( + () => outerHtmlMenu.click(), + TABS_FORMATTED_HTML + ); + + info("Set the pref to expand tabs to 3 spaces"); + await pushPref("devtools.editor.expandtab", true); + await pushPref("devtools.editor.tabsize", 3); + + info("Try to copy the beautified outer html"); + await waitForClipboardPromise( + () => outerHtmlMenu.click(), + THREE_SPACES_FORMATTED_HTML + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_copy_image_data.js b/devtools/client/inspector/markup/test/browser_markup_copy_image_data.js new file mode 100644 index 0000000000..0386551c41 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_copy_image_data.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that image nodes have the "copy data-uri" contextual menu item enabled +// and that clicking it puts the image data into the clipboard + +add_task(async function () { + await addTab(URL_ROOT + "doc_markup_image_and_canvas.html"); + const { inspector } = await openInspector(); + + await selectNode("div", inspector); + await assertCopyImageDataNotAvailable(inspector); + + await selectNode("img", inspector); + await assertCopyImageDataAvailable(inspector); + const expectedSrc = await getContentPageElementAttribute("img", "src"); + await triggerCopyImageUrlAndWaitForClipboard(expectedSrc, inspector); + + await selectNode("canvas", inspector); + await assertCopyImageDataAvailable(inspector); + const expectedURL = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => content.document.querySelector(".canvas").toDataURL() + ); + await triggerCopyImageUrlAndWaitForClipboard(expectedURL, inspector); + + // Check again that the menu isn't available on the DIV (to make sure our + // menu updating mechanism works) + await selectNode("div", inspector); + await assertCopyImageDataNotAvailable(inspector); +}); + +function assertCopyImageDataNotAvailable(inspector) { + const allMenuItems = openContextMenuAndGetAllItems(inspector); + const item = allMenuItems.find(i => i.id === "node-menu-copyimagedatauri"); + + ok(item, "The menu item was found in the contextual menu"); + ok(item.disabled, "The menu item is disabled"); +} + +function assertCopyImageDataAvailable(inspector) { + const allMenuItems = openContextMenuAndGetAllItems(inspector); + const item = allMenuItems.find(i => i.id === "node-menu-copyimagedatauri"); + + ok(item, "The menu item was found in the contextual menu"); + ok(!item.disabled, "The menu item is enabled"); +} + +function triggerCopyImageUrlAndWaitForClipboard(expected, inspector) { + return new Promise(resolve => { + SimpleTest.waitForClipboard( + expected, + () => { + inspector.markup + .getContainer(inspector.selection.nodeFront) + .copyImageDataUri(); + }, + () => { + ok( + true, + "The clipboard contains the expected value " + + expected.substring(0, 50) + + "..." + ); + resolve(); + }, + () => { + ok( + false, + "The clipboard doesn't contain the expected value " + + expected.substring(0, 50) + + "..." + ); + resolve(); + } + ); + }); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_01.js b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_01.js new file mode 100644 index 0000000000..40879836ab --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_01.js @@ -0,0 +1,130 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_style_attr_test_runner.js */ + +"use strict"; + +// Test CSS state is correctly determined and the corresponding suggestions are +// displayed. i.e. CSS property suggestions are shown when cursor is like: +// ```style="di|"``` where | is the cursor; And CSS value suggestion is +// displayed when the cursor is like: ```style="display:n|"``` properly. No +// suggestions should ever appear when the attribute is not a style attribute. +// The correctness and cycling of the suggestions is covered in the ruleview +// tests. + +loadHelperScript("helper_style_attr_test_runner.js"); + +const TEST_URL = URL_ROOT + "doc_markup_edit.html"; + +const COLOR_MIX_ENABLED = SpecialPowers.getBoolPref( + "layout.css.color-mix.enabled" +); + +// test data format : +// [ +// what key to press, +// expected input box value after keypress, +// expected input.selectionStart, +// expected input.selectionEnd, +// is popup expected to be open ? +// ] +const TEST_DATA = [ + ["s", "s", 1, 1, false], + ["t", "st", 2, 2, false], + ["y", "sty", 3, 3, false], + ["l", "styl", 4, 4, false], + ["e", "style", 5, 5, false], + ["=", "style=", 6, 6, false], + ['"', 'style="', 7, 7, false], + ["d", 'style="display', 8, 14, true], + ["VK_TAB", 'style="display', 14, 14, true], + ["VK_TAB", 'style="dominant-baseline', 24, 24, true], + ["VK_TAB", 'style="d', 8, 8, true], + ["VK_TAB", 'style="direction', 16, 16, true], + ["click_2", 'style="display', 14, 14, false], + [":", 'style="display:block', 15, 20, true], + ["n", 'style="display:none', 16, 19, false], + ["VK_BACK_SPACE", 'style="display:n', 16, 16, false], + ["VK_BACK_SPACE", 'style="display:', 15, 15, false], + [" ", 'style="display: block', 16, 21, true], + [" ", 'style="display: block', 17, 22, true], + ["i", 'style="display: inherit', 18, 24, true], + ["VK_RIGHT", 'style="display: inherit', 24, 24, false], + [";", 'style="display: inherit;', 25, 25, false], + [" ", 'style="display: inherit; ', 26, 26, false], + [" ", 'style="display: inherit; ', 27, 27, false], + ["VK_LEFT", 'style="display: inherit; ', 26, 26, false], + ["c", 'style="display: inherit; color ', 27, 31, true], + ["VK_RIGHT", 'style="display: inherit; color ', 31, 31, false], + [" ", 'style="display: inherit; color ', 32, 32, false], + ["c", 'style="display: inherit; color c ', 33, 33, false], + ["VK_BACK_SPACE", 'style="display: inherit; color ', 32, 32, false], + [":", 'style="display: inherit; color :aliceblue ', 33, 42, true], + ["c", 'style="display: inherit; color :color ', 34, 38, true], + COLOR_MIX_ENABLED + ? ["VK_DOWN", 'style="display: inherit; color :color-mix ', 34, 42, true] + : ["VK_DOWN", 'style="display: inherit; color :coral ', 34, 38, true], + COLOR_MIX_ENABLED + ? ["VK_RIGHT", 'style="display: inherit; color :color-mix ', 42, 42, false] + : ["VK_RIGHT", 'style="display: inherit; color :coral ', 38, 38, false], + COLOR_MIX_ENABLED + ? [ + " ", + 'style="display: inherit; color :color-mix aliceblue ', + 43, + 52, + true, + ] + : [" ", 'style="display: inherit; color :coral aliceblue ', 39, 48, true], + COLOR_MIX_ENABLED + ? [ + "!", + 'style="display: inherit; color :color-mix !important; ', + 44, + 54, + false, + ] + : [ + "!", + 'style="display: inherit; color :coral !important; ', + 40, + 50, + false, + ], + COLOR_MIX_ENABLED + ? [ + "VK_RIGHT", + 'style="display: inherit; color :color-mix !important; ', + 54, + 54, + false, + ] + : [ + "VK_RIGHT", + 'style="display: inherit; color :coral !important; ', + 50, + 50, + false, + ], + COLOR_MIX_ENABLED + ? [ + "VK_RETURN", + 'style="display: inherit; color :color-mix !important;"', + -1, + -1, + false, + ] + : [ + "VK_RETURN", + 'style="display: inherit; color :coral !important;"', + -1, + -1, + false, + ], +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + await runStyleAttributeAutocompleteTests(inspector, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_02.js b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_02.js new file mode 100644 index 0000000000..d934fc4e2c --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_02.js @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_style_attr_test_runner.js */ + +"use strict"; + +// Test CSS autocompletion of the style attributes stops after closing the +// attribute using a matching quote. + +loadHelperScript("helper_style_attr_test_runner.js"); + +const TEST_URL = URL_ROOT + "doc_markup_edit.html"; + +// test data format : +// [ +// what key to press, +// expected input box value after keypress, +// expected input.selectionStart, +// expected input.selectionEnd, +// is popup expected to be open ? +// ] +const TEST_DATA_DOUBLE = [ + ["s", "s", 1, 1, false], + ["t", "st", 2, 2, false], + ["y", "sty", 3, 3, false], + ["l", "styl", 4, 4, false], + ["e", "style", 5, 5, false], + ["=", "style=", 6, 6, false], + ['"', 'style="', 7, 7, false], + ["c", 'style="color', 8, 12, true], + ["VK_RIGHT", 'style="color', 12, 12, false], + [":", 'style="color:aliceblue', 13, 22, true], + ["b", 'style="color:beige', 14, 18, true], + ["VK_RIGHT", 'style="color:beige', 18, 18, false], + ['"', 'style="color:beige"', 19, 19, false], + [" ", 'style="color:beige" ', 20, 20, false], + ["d", 'style="color:beige" d', 21, 21, false], + ["a", 'style="color:beige" da', 22, 22, false], + ["t", 'style="color:beige" dat', 23, 23, false], + ["a", 'style="color:beige" data', 24, 24, false], + ["VK_RETURN", 'style="color:beige"', -1, -1, false], +]; + +// Check that single quote attribute is also supported +const TEST_DATA_SINGLE = [ + ["s", "s", 1, 1, false], + ["t", "st", 2, 2, false], + ["y", "sty", 3, 3, false], + ["l", "styl", 4, 4, false], + ["e", "style", 5, 5, false], + ["=", "style=", 6, 6, false], + ["'", "style='", 7, 7, false], + ["c", "style='color", 8, 12, true], + ["VK_RIGHT", "style='color", 12, 12, false], + [":", "style='color:aliceblue", 13, 22, true], + ["b", "style='color:beige", 14, 18, true], + ["VK_RIGHT", "style='color:beige", 18, 18, false], + ["'", "style='color:beige'", 19, 19, false], + [" ", "style='color:beige' ", 20, 20, false], + ["d", "style='color:beige' d", 21, 21, false], + ["a", "style='color:beige' da", 22, 22, false], + ["t", "style='color:beige' dat", 23, 23, false], + ["a", "style='color:beige' data", 24, 24, false], + ["VK_RETURN", 'style="color:beige"', -1, -1, false], +]; + +// Check that autocompletion is still enabled after using url('1) +const TEST_DATA_INNER = [ + ["s", "s", 1, 1, false], + ["t", "st", 2, 2, false], + ["y", "sty", 3, 3, false], + ["l", "styl", 4, 4, false], + ["e", "style", 5, 5, false], + ["=", "style=", 6, 6, false], + ['"', 'style="', 7, 7, false], + ["b", 'style="border', 8, 13, true], + ["a", 'style="background', 9, 17, true], + ["VK_RIGHT", 'style="background', 17, 17, false], + [":", 'style="background:aliceblue', 18, 27, true], + ["u", 'style="background:unset', 19, 23, true], + ["r", 'style="background:url', 20, 21, false], + ["l", 'style="background:url', 21, 21, false], + ["(", 'style="background:url()', 22, 22, false], + ["'", "style=\"background:url(')", 23, 23, false], + ["1", "style=\"background:url('1)", 24, 24, false], + ["'", "style=\"background:url('1')", 25, 25, false], + [")", "style=\"background:url('1')", 26, 26, false], + [";", "style=\"background:url('1');", 27, 27, false], + [" ", "style=\"background:url('1'); ", 28, 28, false], + ["c", "style=\"background:url('1'); color", 29, 33, true], + ["VK_RIGHT", "style=\"background:url('1'); color", 33, 33, false], + [":", "style=\"background:url('1'); color:aliceblue", 34, 43, true], + ["b", "style=\"background:url('1'); color:beige", 35, 39, true], + ["VK_RETURN", "style=\"background:url('1'); color:beige\"", -1, -1, false], +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + await runStyleAttributeAutocompleteTests(inspector, TEST_DATA_DOUBLE); + await runStyleAttributeAutocompleteTests(inspector, TEST_DATA_SINGLE); + await runStyleAttributeAutocompleteTests(inspector, TEST_DATA_INNER); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_03.js b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_03.js new file mode 100644 index 0000000000..038bf33f8b --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_03.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_style_attr_test_runner.js */ + +"use strict"; + +// Test CSS autocompletion of the style attribute can be triggered when the +// caret is before a non-word character. + +loadHelperScript("helper_style_attr_test_runner.js"); + +const TEST_URL = URL_ROOT + "doc_markup_edit.html"; + +// test data format : +// [ +// what key to press, +// expected input box value after keypress, +// expected input.selectionStart, +// expected input.selectionEnd, +// is popup expected to be open ? +// ] +const TEST_DATA = [ + ["s", "s", 1, 1, false], + ["t", "st", 2, 2, false], + ["y", "sty", 3, 3, false], + ["l", "styl", 4, 4, false], + ["e", "style", 5, 5, false], + ["=", "style=", 6, 6, false], + ['"', 'style="', 7, 7, false], + ['"', 'style=""', 8, 8, false], + ["VK_LEFT", 'style=""', 7, 7, false], + ["c", 'style="color"', 8, 12, true], + ["o", 'style="color"', 9, 12, true], + ["VK_RIGHT", 'style="color"', 12, 12, false], + [":", 'style="color:aliceblue"', 13, 22, true], + ["b", 'style="color:beige"', 14, 18, true], + ["VK_RIGHT", 'style="color:beige"', 18, 18, false], + [";", 'style="color:beige;"', 19, 19, false], + [";", 'style="color:beige;;"', 20, 20, false], + ["VK_LEFT", 'style="color:beige;;"', 19, 19, false], + ["p", 'style="color:beige;padding;"', 20, 26, true], + ["VK_RIGHT", 'style="color:beige;padding;"', 26, 26, false], + [":", 'style="color:beige;padding:inherit;"', 27, 34, true], + ["0", 'style="color:beige;padding:0;"', 28, 28, false], + ["VK_RETURN", 'style="color:beige;padding:0;"', -1, -1, false], +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + await runStyleAttributeAutocompleteTests(inspector, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_display_node_01.js b/devtools/client/inspector/markup/test/browser_markup_display_node_01.js new file mode 100644 index 0000000000..6082d586c1 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_display_node_01.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that markup display node shows for only for grid and flex containers. + +const TEST_URI = ` + +
+
+
+
Flex
+
Block
+ HELLO WORLD +`; + +add_task(async function () { + const { inspector } = await openInspectorForURL( + "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI) + ); + + info("Check the display node is shown and the value of #grid."); + await selectNode("#grid", inspector); + const gridContainer = await getContainerForSelector("#grid", inspector); + const gridDisplayNode = gridContainer.elt.querySelector( + ".inspector-badge.interactive[data-display]" + ); + ok(gridDisplayNode, "#grid display node is shown."); + is( + gridDisplayNode.textContent, + "grid", + "Got the correct display type for #grid." + ); + + info("Check the display node is shown and the value of #subgrid."); + await selectNode("#subgrid", inspector); + const subgridContainer = await getContainerForSelector("#subgrid", inspector); + const subgridDisplayNode = subgridContainer.elt.querySelector( + ".inspector-badge[data-display]" + ); + ok(subgridDisplayNode, "#subgrid display node is shown"); + is( + subgridDisplayNode.textContent, + "subgrid", + "Got the correct display type for #subgrid" + ); + + info("Check the display node is shown and the value of #flex."); + await selectNode("#flex", inspector); + const flexContainer = await getContainerForSelector("#flex", inspector); + const flexDisplayNode = flexContainer.elt.querySelector( + ".inspector-badge.interactive[data-display]" + ); + ok(flexDisplayNode, "#flex display node is shown."); + is( + flexDisplayNode.textContent, + "flex", + "Got the correct display type for #flex" + ); + + info("Check the display node is hidden for #block."); + await selectNode("#block", inspector); + const blockContainer = await getContainerForSelector("#block", inspector); + const blockDisplayNode = blockContainer.elt.querySelector( + ".inspector-badge.interactive[data-display]" + ); + ok(!blockDisplayNode, "#block display node is hidden."); + + info("Check the display node is hidden for span."); + await selectNode("span", inspector); + const spanContainer = await getContainerForSelector("span", inspector); + const spanDisplayNode = spanContainer.elt.querySelector( + ".inspector-badge.interactive[data-display]" + ); + ok(!spanDisplayNode, "span display node is hidden."); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_display_node_02.js b/devtools/client/inspector/markup/test/browser_markup_display_node_02.js new file mode 100644 index 0000000000..365562410e --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_display_node_02.js @@ -0,0 +1,165 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that markup display node are updated when their display changes. + +const TEST_URI = ` + +
Grid
+ +
Block
+`; + +const TEST_DATA = [ + { + desc: "Hiding the #grid display node by changing its style property", + selector: "#grid", + before: { + textContent: "grid", + visible: true, + }, + async changeStyle() { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const node = content.document.getElementById("grid"); + node.style.display = "block"; + }); + }, + after: { + visible: false, + }, + }, + { + desc: "Reusing the 'grid' node, updating the display to 'grid again", + selector: "#grid", + before: { + visible: false, + }, + async changeStyle() { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const node = content.document.getElementById("grid"); + node.style.display = "grid"; + }); + }, + after: { + textContent: "grid", + visible: true, + }, + }, + { + desc: "Showing a 'grid' node by changing its style property", + selector: "#block", + before: { + visible: false, + }, + async changeStyle() { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const node = content.document.getElementById("block"); + node.style.display = "grid"; + }); + }, + after: { + textContent: "grid", + visible: true, + }, + }, + { + desc: "Showing a 'flex' node by removing its hidden attribute", + selector: "#flex", + before: { + visible: false, + }, + async changeStyle() { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => + content.document.getElementById("flex").removeAttribute("hidden") + ); + }, + after: { + textContent: "flex", + visible: true, + }, + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL( + "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI) + ); + + for (const data of TEST_DATA) { + info("Running test case: " + data.desc); + await runTestData(inspector, data); + } +}); + +async function runTestData( + inspector, + { selector, before, changeStyle, after } +) { + await selectNode(selector, inspector); + const container = await getContainerForSelector(selector, inspector); + + const beforeBadge = container.elt.querySelector( + ".inspector-badge.interactive[data-display]" + ); + is( + !!beforeBadge, + before.visible, + `Display badge is visible as expected for ${selector}: ${before.visible}` + ); + if (before.visible) { + is( + beforeBadge.textContent, + before.textContent, + `Got the correct before display type for ${selector}: ${beforeBadge.textContent}` + ); + } + + info("Listening for the display-change event"); + const onDisplayChanged = inspector.markup.walker.once("display-change"); + info("Making style changes"); + await changeStyle(); + const nodes = await onDisplayChanged; + + info("Verifying that the list of changed nodes include our container"); + ok(nodes.length, "The display-change event was received with a nodes"); + let foundContainer = false; + for (const node of nodes) { + if (getContainerForNodeFront(node, inspector) === container) { + foundContainer = true; + break; + } + } + ok(foundContainer, "Container is part of the list of changed nodes"); + + const afterBadge = container.elt.querySelector( + ".inspector-badge.interactive[data-display]" + ); + is( + !!afterBadge, + after.visible, + `Display badge is visible as expected for ${selector}: ${after.visible}` + ); + if (after.visible) { + is( + afterBadge.textContent, + after.textContent, + `Got the correct after display type for ${selector}: ${afterBadge.textContent}` + ); + } +} diff --git a/devtools/client/inspector/markup/test/browser_markup_dom_mutation_breakpoints.js b/devtools/client/inspector/markup/test/browser_markup_dom_mutation_breakpoints.js new file mode 100644 index 0000000000..b17864ea53 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dom_mutation_breakpoints.js @@ -0,0 +1,196 @@ +/* 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"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/shared-head.js", + this +); + +function toggleMutationBreakpoint(inspector) { + const allMenuItems = openContextMenuAndGetAllItems(inspector); + const attributeMenuItem = allMenuItems.find( + ({ id }) => id === "node-menu-mutation-breakpoint-attribute" + ); + attributeMenuItem.click(); +} + +// Test inspector markup view handling DOM mutation breakpoints icons +// The icon should display when a breakpoint exists for a given node +add_task(async function () { + const { inspector } = await openInspectorForURL( + "data:text/html;charset=utf-8,

foo

bar" + ); + + await selectNode("span", inspector); + toggleMutationBreakpoint(inspector); + + const span = await getContainerForSelector("span", inspector); + const mutationMarker = span.tagLine.querySelector( + ".markup-tag-mutation-marker" + ); + + ok( + mutationMarker.classList.contains("has-mutations"), + "has-mutations class is present" + ); + + toggleMutationBreakpoint(inspector); + await waitFor(() => !mutationMarker.classList.contains("has-mutations")); + + ok(true, "has-mutations class is not present"); +}); + +// Test that the inspector markup view dom mutation breakpoint icon behaves +// correctly when disabled +add_task(async function () { + await pushPref("devtools.debugger.dom-mutation-breakpoints-visible", true); + const { inspector, toolbox } = await openInspectorForURL( + "data:text/html;charset=utf-8,

foo

bar" + ); + + await selectNode("span", inspector); + toggleMutationBreakpoint(inspector); + + const span = await getContainerForSelector("span", inspector); + const mutationMarker = span.tagLine.querySelector( + ".markup-tag-mutation-marker" + ); + + ok( + mutationMarker.classList.contains("has-mutations"), + "has-mutations class is present" + ); + is( + mutationMarker.classList.contains("mutation-breakpoint-disabled"), + false, + "mutation-breakpoint-disabled class is not present" + ); + + info("Switch over to the debugger pane"); + await toolbox.selectTool("jsdebugger"); + + const dbg = createDebuggerContext(toolbox); + + const mutationItem = await waitForElement(dbg, "domMutationItem"); + mutationItem.scrollIntoView(); + + info("Disable the DOM mutation breakpoint"); + const checkbox = mutationItem.querySelector("input"); + checkbox.click(); + await waitFor(() => !checkbox.checked); + + await waitFor( + () => + mutationMarker.classList.contains("has-mutations") && + mutationMarker.classList.contains("mutation-breakpoint-disabled") + ); + + ok( + true, + "has-mutations and mutation-breakpoint-disabled classes are both present" + ); + + info("Re-enable the DOM mutation breakpoint"); + checkbox.click(); + await waitFor(() => checkbox.checked); + + await waitFor( + () => + mutationMarker.classList.contains("has-mutations") && + !mutationMarker.classList.contains("mutation-breakpoint-disabled") + ); + + ok( + true, + "has-mutation class is present, mutation-breakpoint-disabled is not present" + ); + + // Test re-enabling disabled dom mutation breakpoint from inspector + info("Disable the DOM mutation breakpoint"); + checkbox.click(); + await waitFor(() => !checkbox.checked); + + await waitFor( + () => + mutationMarker.classList.contains("has-mutations") && + mutationMarker.classList.contains("mutation-breakpoint-disabled") + ); + + ok( + true, + "has-mutations and mutation-breakpoint-disabled classes are both present" + ); + + info("Switch over to the inspector pane"); + await toolbox.selectTool("inspector"); + + toggleMutationBreakpoint(inspector); + await waitFor( + () => + mutationMarker.classList.contains("has-mutations") && + !mutationMarker.classList.contains("mutation-breakpoint-disabled") + ); + + ok( + true, + "has-mutation class is present, mutation-breakpoint-disabled is not present" + ); +}); + +// Test icon behavior with multiple breakpoints on the same node. +add_task(async function () { + await pushPref("devtools.debugger.dom-mutation-breakpoints-visible", true); + const { inspector, toolbox } = await openInspectorForURL( + "data:text/html;charset=utf-8,

foo

bar" + ); + + await selectNode("span", inspector); + const span = await getContainerForSelector("span", inspector); + const mutationMarker = span.tagLine.querySelector( + ".markup-tag-mutation-marker" + ); + + info("Add 2 DOM mutation breakpoints"); + const allMenuItems = openContextMenuAndGetAllItems(inspector); + + const attributeMenuItem = allMenuItems.find( + item => item.id === "node-menu-mutation-breakpoint-attribute" + ); + attributeMenuItem.click(); + + const subtreeMenuItem = allMenuItems.find( + item => item.id === "node-menu-mutation-breakpoint-subtree" + ); + subtreeMenuItem.click(); + + info("Switch over to the debugger pane"); + await toolbox.selectTool("jsdebugger"); + + const dbg = createDebuggerContext(toolbox); + + info("Confirm that DOM mutation breakpoints exists"); + await waitForAllElements(dbg, "domMutationItem", 2, true); + + const mutationItem = await waitForElement(dbg, "domMutationItem"); + + mutationItem.scrollIntoView(); + + info("Disable 1 dom mutation breakpoint"); + const checkbox = mutationItem.querySelector("input"); + checkbox.click(); + await waitFor(() => !checkbox.checked); + + await waitFor( + () => + mutationMarker.classList.contains("has-mutations") && + !mutationMarker.classList.contains("mutation-breakpoint-disabled") + ); + + ok( + true, + "has-mutation class is present, mutation-breakpoint-disabled is not present" + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_01.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_01.js new file mode 100644 index 0000000000..d0ec2e2427 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_01.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that dragging a node near the top or bottom edge of the markup-view +// auto-scrolls the view on a large toolbox. + +const TEST_URL = URL_ROOT + "doc_markup_dragdrop_autoscroll_01.html"; + +add_task(async function () { + // Set the toolbox as large as it would get. The toolbox automatically shrinks + // to not overflow to window. + await pushPref("devtools.toolbox.footer.height", 10000); + + const { inspector } = await openInspectorForURL(TEST_URL); + const markup = inspector.markup; + const viewHeight = markup.doc.documentElement.clientHeight; + + info("Pretend the markup-view is dragging"); + markup.isDragging = true; + + info("Simulate a mousemove on the view, at the bottom, and expect scrolling"); + + markup._onMouseMove({ + preventDefault: () => {}, + target: markup.doc.body, + pageY: viewHeight + markup.doc.defaultView.scrollY, + }); + + const bottomScrollPos = await waitForScrollStop(markup.doc); + ok(bottomScrollPos > 0, "The view was scrolled down"); + + info("Simulate a mousemove at the top and expect more scrolling"); + + markup._onMouseMove({ + preventDefault: () => {}, + target: markup.doc.body, + pageY: markup.doc.defaultView.scrollY, + }); + + const topScrollPos = await waitForScrollStop(markup.doc); + ok(topScrollPos < bottomScrollPos, "The view was scrolled up"); + is(topScrollPos, 0, "The view was scrolled up to the top"); + + info("Simulate a mouseup to stop dragging"); + markup._onMouseUp(); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_02.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_02.js new file mode 100644 index 0000000000..1dc69a4ebb --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_02.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that dragging a node near the top or bottom edge of the markup-view +// auto-scrolls the view on a small toolbox. + +const TEST_URL = URL_ROOT + "doc_markup_dragdrop_autoscroll_02.html"; + +add_task(async function () { + // Set the toolbox to very small in size. + await pushPref("devtools.toolbox.footer.height", 150); + + const { inspector } = await openInspectorForURL(TEST_URL); + const markup = inspector.markup; + const viewHeight = markup.doc.documentElement.clientHeight; + + info("Pretend the markup-view is dragging"); + markup.isDragging = true; + + info("Simulate a mousemove on the view, at the bottom, and expect scrolling"); + + markup._onMouseMove({ + preventDefault: () => {}, + target: markup.doc.body, + pageY: viewHeight + markup.doc.defaultView.scrollY, + }); + + const bottomScrollPos = await waitForScrollStop(markup.doc); + ok(bottomScrollPos > 0, "The view was scrolled down"); + info("Simulate a mousemove at the top and expect more scrolling"); + + markup._onMouseMove({ + preventDefault: () => {}, + target: markup.doc.body, + pageY: markup.doc.defaultView.scrollY, + }); + + const topScrollPos = await waitForScrollStop(markup.doc); + ok(topScrollPos < bottomScrollPos, "The view was scrolled up"); + is(topScrollPos, 0, "The view was scrolled up to the top"); + + info("Simulate a mouseup to stop dragging"); + markup._onMouseUp(); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_before_marker_pseudo.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_before_marker_pseudo.js new file mode 100644 index 0000000000..7cca09ce94 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_before_marker_pseudo.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test drag and dropping a node before a ::marker pseudo. + +const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Expand #list node"); + const parentFront = await getNodeFront("#list", inspector); + await inspector.markup.expandNode(parentFront.parentNode()); + await inspector.markup.expandNode(parentFront); + await waitForMultipleChildrenUpdates(inspector); + + info("Scroll #list into view"); + const parentContainer = await getContainerForNodeFront( + parentFront, + inspector + ); + parentContainer.elt.scrollIntoView(true); + + info("Test placing an element before a ::marker psuedo"); + await moveElementBeforeMarker("#last-list-child", parentFront, inspector); + const childNodes = await getChildrenOf(parentFront, inspector); + is( + childNodes[0], + "_moz_generated_content_marker", + "::marker is still the first child of #list" + ); + is( + childNodes[1], + "last-list-child", + "#last-list-child is now the second child of #list" + ); + is( + childNodes[2], + "first-list-child", + "#first-list-child is now the last child of #list" + ); +}); + +async function moveElementBeforeMarker(selector, parentFront, inspector) { + info(`Placing ${selector} before its parent's ::marker`); + + const container = await getContainerForSelector(selector, inspector); + const parentContainer = await getContainerForNodeFront( + parentFront, + inspector + ); + const offsetY = + parentContainer.tagLine.offsetTop + + parentContainer.tagLine.offsetHeight - + container.tagLine.offsetTop; + + const onMutated = inspector.once("markupmutation"); + const uiUpdate = inspector.once("inspector-updated"); + + await simulateNodeDragAndDrop(inspector, selector, 0, offsetY); + + const mutations = await onMutated; + await uiUpdate; + + is(mutations.length, 2, "2 mutations were received"); +} + +async function getChildrenOf(parentFront, { walker }) { + const { nodes } = await walker.children(parentFront); + return nodes.map(node => { + if (node.isMarkerPseudoElement) { + return node.displayName; + } + return node.id; + }); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_distance.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_distance.js new file mode 100644 index 0000000000..6718d33317 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_distance.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that nodes don't start dragging before the mouse has moved by at least +// the minimum vertical distance defined in markup-view.js by +// DRAG_DROP_MIN_INITIAL_DISTANCE. + +const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html"; +const TEST_NODE = "#test"; + +// Keep this in sync with DRAG_DROP_MIN_INITIAL_DISTANCE in markup-view.js +const MIN_DISTANCE = 10; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Drag the test node by half of the minimum distance"); + await simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE / 2); + await checkIsDragging(inspector, TEST_NODE, false); + + info("Drag the test node by exactly the minimum distance"); + await simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE); + await checkIsDragging(inspector, TEST_NODE, true); + inspector.markup.cancelDragging(); + + info("Drag the test node by more than the minimum distance"); + await simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE * 2); + await checkIsDragging(inspector, TEST_NODE, true); + inspector.markup.cancelDragging(); + + info("Drag the test node by minus the minimum distance"); + await simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE * -1); + await checkIsDragging(inspector, TEST_NODE, true); + inspector.markup.cancelDragging(); +}); + +async function checkIsDragging(inspector, selector, isDragging) { + const container = await getContainerForSelector(selector, inspector); + if (isDragging) { + ok(container.isDragging, "The container is being dragged"); + ok(inspector.markup.isDragging, "And the markup-view knows it"); + } else { + ok(!container.isDragging, "The container hasn't been marked as dragging"); + ok(!inspector.markup.isDragging, "And the markup-view either"); + } +} diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_dragRootNode.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_dragRootNode.js new file mode 100644 index 0000000000..4c29fcc509 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_dragRootNode.js @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the root node isn't draggable (as well as head and body). + +const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html"; +const TEST_DATA = ["html", "head", "body"]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + for (const selector of TEST_DATA) { + info("Try to drag/drop node " + selector); + await simulateNodeDrag(inspector, selector); + + const container = await getContainerForSelector(selector, inspector); + ok(!container.isDragging, "The container hasn't been marked as dragging"); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_draggable.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_draggable.js new file mode 100644 index 0000000000..949161b01f --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_draggable.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test which nodes are consider draggable by the markup-view. + +const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html"; + +// Test cases should be objects with the following properties: +// - node {String|Function} A CSS selector that uniquely identifies the node to +// be tested. Or a generator function called in a Task that should return the +// corresponding MarkupContainer object to be tested. +// - draggable {Boolean} Whether or not the node should be draggable. +const TEST_DATA = [ + { node: "head", draggable: false }, + { node: "body", draggable: false }, + { node: "html", draggable: false }, + { node: "style", draggable: true }, + { node: "a", draggable: true }, + { node: "p", draggable: true }, + { node: "input", draggable: true }, + { node: "div", draggable: true }, + { + async node(inspector) { + const parentFront = await getNodeFront("#before", inspector); + const { nodes } = await inspector.walker.children(parentFront); + // Getting the comment node. + return getContainerForNodeFront(nodes[1], inspector); + }, + draggable: true, + }, + { + async node(inspector) { + const parentFront = await getNodeFront("#test", inspector); + const { nodes } = await inspector.walker.children(parentFront); + // Getting the ::before pseudo element. + return getContainerForNodeFront(nodes[0], inspector); + }, + draggable: false, + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + await inspector.markup.expandAll(); + + for (const { node, draggable } of TEST_DATA) { + let container; + let name; + if (typeof node === "string") { + container = await getContainerForSelector(node, inspector); + name = node; + } else { + container = await node(inspector); + name = container.toString(); + } + + const status = draggable ? "draggable" : "not draggable"; + info(`Testing ${name}, expecting it to be ${status}`); + is(container.isDraggable(), draggable, `The node is ${status}`); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_escapeKeyPress.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_escapeKeyPress.js new file mode 100644 index 0000000000..0c66fe4761 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_escapeKeyPress.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether ESCAPE keypress cancels dragging of an element. + +const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const { markup } = inspector; + + info("Get a test container"); + await selectNode("#test", inspector); + const container = await getContainerForSelector("#test", inspector); + + info("Simulate a drag/drop on this container"); + await simulateNodeDrag(inspector, "#test"); + + ok( + container.isDragging && markup.isDragging, + "The container is being dragged" + ); + ok( + markup.doc.body.classList.contains("dragging"), + "The dragging css class was added" + ); + + info("Simulate ESCAPE keypress"); + EventUtils.sendKey("escape", inspector.panelWin); + + ok(!container.isDragging && !markup.isDragging, "The dragging has stopped"); + ok( + !markup.doc.body.classList.contains("dragging"), + "The dragging css class was removed" + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js new file mode 100644 index 0000000000..e12299394a --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that pseudo-elements, anonymous nodes and slotted nodes are not draggable. + +const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html"; + +add_task(async function () { + await pushPref("devtools.inspector.showAllAnonymousContent", true); + + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Expanding nodes below #test"); + const parentFront = await getNodeFront("#test", inspector); + await inspector.markup.expandNode(parentFront); + await waitForMultipleChildrenUpdates(inspector); + + info("Getting the ::before pseudo element and selecting it"); + const parentContainer = await getContainerForNodeFront( + parentFront, + inspector + ); + const beforePseudo = parentContainer.elt.children[1].firstChild.container; + parentContainer.elt.scrollIntoView(true); + await selectNode(beforePseudo.node, inspector); + + info("Simulate dragging the ::before pseudo element"); + await simulateNodeDrag(inspector, beforePseudo); + + ok(!beforePseudo.isDragging, "::before pseudo element isn't dragging"); + + info("Expanding nodes below #anonymousParent"); + const inputFront = await getNodeFront("#anonymousParent", inspector); + await inspector.markup.expandNode(inputFront); + await waitForMultipleChildrenUpdates(inspector); + + info("Getting the anonymous node and selecting it"); + const inputContainer = await getContainerForNodeFront(inputFront, inspector); + const anonymousDiv = inputContainer.elt.children[1].firstChild.container; + inputContainer.elt.scrollIntoView(true); + await selectNode(anonymousDiv.node, inspector); + + info("Simulate dragging the anonymous node"); + await simulateNodeDrag(inspector, anonymousDiv); + + ok(!anonymousDiv.isDragging, "anonymous node isn't dragging"); + + info("Expanding all nodes below test-component"); + const testComponentFront = await getNodeFront("test-component", inspector); + await inspector.markup.expandAll(testComponentFront); + await waitForMultipleChildrenUpdates(inspector); + + info("Getting a slotted node and selecting it"); + // Directly use the markup getContainer API in order to retrieve the slotted container + // for a given node front. + const slotted1Front = await getNodeFront(".slotted1", inspector); + const slottedContainer = inspector.markup.getContainer(slotted1Front, true); + slottedContainer.elt.scrollIntoView(true); + await selectNode(slotted1Front, inspector, "no-reason", true); + + info("Simulate dragging the slotted node"); + await simulateNodeDrag(inspector, slottedContainer); + + ok(!slottedContainer.isDragging, "slotted node isn't dragging"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_reorder.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_reorder.js new file mode 100644 index 0000000000..cbcc86fa74 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_reorder.js @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Test different kinds of drag and drop node re-ordering. + +const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + let ids; + + info("Expand #test node"); + const parentFront = await getNodeFront("#test", inspector); + await inspector.markup.expandNode(parentFront); + await waitForMultipleChildrenUpdates(inspector); + + info("Scroll #test into view"); + const parentContainer = await getContainerForNodeFront( + parentFront, + inspector + ); + parentContainer.elt.scrollIntoView(true); + + info("Test putting an element back at its original place"); + await dragElementToOriginalLocation("#firstChild", inspector); + ids = await getChildrenIDsOf(parentFront, inspector); + is(ids[0], "firstChild", "#firstChild is still the first child of #test"); + is(ids[1], "middleChild", "#middleChild is still the second child of #test"); + + info("Testing switching elements inside their parent"); + await moveElementDown("#firstChild", "#middleChild", inspector); + ids = await getChildrenIDsOf(parentFront, inspector); + is(ids[0], "middleChild", "#firstChild is now the second child of #test"); + is(ids[1], "firstChild", "#middleChild is now the first child of #test"); + + info("Testing switching elements with a last child"); + await moveElementDown("#firstChild", "#lastChild", inspector); + ids = await getChildrenIDsOf(parentFront, inspector); + is(ids[1], "lastChild", "#lastChild is now the second child of #test"); + is(ids[2], "firstChild", "#firstChild is now the last child of #test"); + + info("Testing appending element to a parent"); + await moveElementDown("#before", "#test", inspector); + ids = await getChildrenIDsOf(parentFront, inspector); + is(ids.length, 4, "New element appended to #test"); + is( + ids[0], + "before", + "New element is appended at the right place (currently first child)" + ); + + info("Testing moving element to after it's parent"); + await moveElementDown("#firstChild", "#test", inspector); + ids = await getChildrenIDsOf(parentFront, inspector); + is(ids.length, 3, "#firstChild is no longer #test's child"); + const siblingFront = await inspector.walker.nextSibling(parentFront); + is( + siblingFront.id, + "firstChild", + "#firstChild is now #test's nextElementSibling" + ); +}); + +async function dragElementToOriginalLocation(selector, inspector) { + info("Picking up and putting back down " + selector); + + function onMutation() { + ok(false, "Mutation received from dragging a node back to its location"); + } + inspector.on("markupmutation", onMutation); + await simulateNodeDragAndDrop(inspector, selector, 0, 0); + + // Wait a bit to make sure the event never fires. + // This doesn't need to catch *all* cases, since the mutation + // will cause failure later in the test when it checks element ordering. + await wait(500); + inspector.off("markupmutation", onMutation); +} + +async function moveElementDown(selector, next, inspector) { + info("Switching " + selector + " with " + next); + + const container = await getContainerForSelector(next, inspector); + const height = container.tagLine.getBoundingClientRect().height; + + const onMutated = inspector.once("markupmutation"); + const uiUpdate = inspector.once("inspector-updated"); + + await simulateNodeDragAndDrop(inspector, selector, 0, Math.round(height) + 2); + + const mutations = await onMutated; + await uiUpdate; + + is(mutations.length, 2, "2 mutations were received"); +} + +async function getChildrenIDsOf(parentFront, { walker }) { + const { nodes } = await walker.children(parentFront); + // Filter out non-element nodes since children also returns pseudo-elements. + return nodes + .filter(node => { + return !node.isPseudoElement; + }) + .map(node => { + return node.id; + }); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_tooltip.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_tooltip.js new file mode 100644 index 0000000000..30e5567bc4 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_tooltip.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that tooltips don't appear when dragging over tooltip targets. + +const TEST_URL = 'data:text/html;charset=utf8,
'; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const { markup } = inspector; + + info("Get the tooltip target element for the image's src attribute"); + const img = await getContainerForSelector("img", inspector); + const target = img.editor.getAttributeElement("src").querySelector(".link"); + + info("Check that the src attribute of the image is a valid tooltip target"); + await assertTooltipShownOnHover(markup.imagePreviewTooltip, target); + await assertTooltipHiddenOnMouseOut(markup.imagePreviewTooltip, target); + + info("Start dragging the test div"); + await simulateNodeDrag(inspector, "div"); + + info("Now check that the src attribute of the image isn't a valid target"); + const isValid = await markup.imagePreviewTooltip._toggle.isValidHoverTarget( + target + ); + ok(!isValid, "The element is not a valid tooltip target"); + + info("Stop dragging the test div"); + await simulateNodeDrop(inspector, "div"); + + info("Check again the src attribute of the image"); + await assertTooltipShownOnHover(markup.imagePreviewTooltip, target); + await assertTooltipHiddenOnMouseOut(markup.imagePreviewTooltip, target); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events-overflow.js b/devtools/client/inspector/markup/test/browser_markup_events-overflow.js new file mode 100644 index 0000000000..ab7819f78b --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events-overflow.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const TEST_URL = URL_ROOT + "doc_markup_events-overflow.html"; +const TEST_DATA = [ + { + desc: "editor overflows container", + // scroll to bottom + initialScrollTop: -1, + // last header + headerToClick: 49, + alignBottom: true, + alignTop: false, + }, + { + desc: "header overflows the container", + initialScrollTop: 2, + headerToClick: 0, + alignBottom: false, + alignTop: true, + }, + { + desc: "neither header nor editor overflows the container", + initialScrollTop: 2, + headerToClick: 5, + alignBottom: false, + alignTop: false, + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + const markupContainer = await getContainerForSelector("#events", inspector); + const evHolder = markupContainer.elt.querySelector( + ".inspector-badge.interactive[data-event]" + ); + const tooltip = inspector.markup.eventDetailsTooltip; + + info("Clicking to open event tooltip."); + EventUtils.synthesizeMouseAtCenter( + evHolder, + {}, + inspector.markup.doc.defaultView + ); + await tooltip.once("shown"); + info("EventTooltip visible."); + + const container = tooltip.panel; + const containerRect = container.getBoundingClientRect(); + const headers = container.querySelectorAll(".event-header"); + + for (const data of TEST_DATA) { + info("Testing scrolling when " + data.desc); + + if (data.initialScrollTop < 0) { + info("Scrolling container to the bottom."); + const newScrollTop = container.scrollHeight - container.clientHeight; + data.initialScrollTop = container.scrollTop = newScrollTop; + } else { + info("Scrolling container by " + data.initialScrollTop + "px"); + container.scrollTop = data.initialScrollTop; + } + + is(container.scrollTop, data.initialScrollTop, "Container scrolled."); + + info("Clicking on header #" + data.headerToClick); + const header = headers[data.headerToClick]; + + const ready = tooltip.once("event-tooltip-ready"); + EventUtils.synthesizeMouseAtCenter(header, {}, header.ownerGlobal); + await ready; + + info("Event handler expanded."); + + // Wait for any scrolling to finish. + await promiseNextTick(); + + if (data.alignTop) { + const headerRect = header.getBoundingClientRect(); + + is( + Math.round(headerRect.top), + Math.round(containerRect.top), + "Clicked header is aligned with the container top." + ); + } else if (data.alignBottom) { + const editorRect = header.nextElementSibling.getBoundingClientRect(); + + is( + Math.round(editorRect.bottom), + Math.round(containerRect.bottom), + "Clicked event handler code is aligned with the container bottom." + ); + } else { + is( + container.scrollTop, + data.initialScrollTop, + "Container did not scroll, as expected." + ); + } + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events-windowed-host.js b/devtools/client/inspector/markup/test/browser_markup_events-windowed-host.js new file mode 100644 index 0000000000..40d404ae86 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events-windowed-host.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 event details tooltip can be hidden by clicking outside of the tooltip + * after switching hosts. + */ + +const TEST_URL = URL_ROOT + "doc_markup_events-overflow.html"; + +registerCleanupFunction(() => { + // Restore the default Toolbox host position after the test. + Services.prefs.clearUserPref("devtools.toolbox.host"); +}); + +add_task(async function () { + info( + "Switch to 2 pane inspector to avoid sidebar width issues with opening events" + ); + await pushPref("devtools.inspector.three-pane-enabled", false); + const { inspector, toolbox } = await openInspectorForURL(TEST_URL); + await runTests(inspector); + + await toolbox.switchHost("window"); + + // Switching hosts is not correctly waiting when DevTools run in content frame + // See Bug 1571421. + await wait(1000); + + await runTests(inspector); + + await toolbox.switchHost("bottom"); + + // Switching hosts is not correctly waiting when DevTools run in content frame + // See Bug 1571421. + await wait(1000); + + await runTests(inspector); + + await toolbox.destroy(); +}); + +async function runTests(inspector) { + const markupContainer = await getContainerForSelector("#events", inspector); + const evHolder = markupContainer.elt.querySelector( + ".inspector-badge.interactive[data-event]" + ); + const tooltip = inspector.markup.eventDetailsTooltip; + + info("Clicking to open event tooltip."); + + let onInspectorUpdated = inspector.once("inspector-updated"); + const onTooltipShown = tooltip.once("shown"); + EventUtils.synthesizeMouseAtCenter( + evHolder, + {}, + inspector.markup.doc.defaultView + ); + + await onTooltipShown; + // New node is selected when clicking on the events bubble, wait for inspector-updated. + await onInspectorUpdated; + + ok(tooltip.isVisible(), "EventTooltip visible."); + + onInspectorUpdated = inspector.once("inspector-updated"); + const onTooltipHidden = tooltip.once("hidden"); + + info("Click on another tag to hide the event tooltip"); + const script = await getContainerForSelector("script", inspector); + const tag = script.elt.querySelector(".tag"); + EventUtils.synthesizeMouseAtCenter(tag, {}, inspector.markup.doc.defaultView); + + await onTooltipHidden; + // New node is selected, wait for inspector-updated. + await onInspectorUpdated; + + ok(!tooltip.isVisible(), "EventTooltip hidden."); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_events_01.js b/devtools/client/inspector/markup/test/browser_markup_events_01.js new file mode 100644 index 0000000000..0fbd47b8f5 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_01.js @@ -0,0 +1,132 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ + +"use strict"; + +// Test that markup view event bubbles show the correct event info for DOM +// events. + +const TEST_URL = URL_ROOT_SSL + "doc_markup_events_01.html"; + +loadHelperScript("helper_events_test_runner.js"); + +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "load", + filename: TEST_URL, + attributes: ["Bubbling"], + handler: "function onload(event) {\n" + " init();\n" + "}", + }, + ], + }, + { + selector: "#container", + expected: [ + { + type: "mouseover", + filename: TEST_URL + ":48:31", + attributes: ["Capturing"], + handler: + "function mouseoverHandler(event) {\n" + + ' if (event.target.id !== "container") {\n' + + ' const output = document.getElementById("output");\n' + + " output.textContent = event.target.textContent;\n" + + " }\n" + + "}", + }, + ], + }, + { + selector: "#multiple", + expected: [ + { + type: "click", + filename: TEST_URL + ":55:27", + attributes: ["Bubbling"], + handler: + "function clickHandler(event) {\n" + + ' const output = document.getElementById("output");\n' + + ' output.textContent = "click";\n' + + "}", + }, + { + type: "mouseup", + filename: TEST_URL + ":60:29", + attributes: ["Bubbling"], + handler: + "function mouseupHandler(event) {\n" + + ' const output = document.getElementById("output");\n' + + ' output.textContent = "mouseup";\n' + + "}", + }, + ], + }, + // #noevents tests check that dynamically added events are properly displayed + // in the markupview + { + selector: "#noevents", + expected: [], + }, + { + selector: "#noevents", + async beforeTest(inspector) { + const nodeMutated = inspector.once("markupmutation"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => + content.wrappedJSObject.addNoeventsClickHandler() + ); + await nodeMutated; + }, + expected: [ + { + type: "click", + filename: TEST_URL + ":76:35", + attributes: ["Bubbling"], + handler: + "function noeventsClickHandler(event) {\n" + + ' alert("noevents has an event listener");\n' + + "}", + }, + ], + }, + { + selector: "#noevents", + async beforeTest(inspector) { + const nodeMutated = inspector.once("markupmutation"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => + content.wrappedJSObject.removeNoeventsClickHandler() + ); + await nodeMutated; + }, + expected: [], + }, + { + selector: "#DOM0", + expected: [ + { + type: "click", + filename: TEST_URL, + attributes: ["Bubbling"], + handler: "function onclick(event) {\n" + " alert('DOM0')\n" + "}", + }, + ], + }, + { + selector: "#handleevent", + expected: [ + { + type: "click", + filename: TEST_URL + ":71:29", + attributes: ["Bubbling"], + handler: "function(blah) {\n" + ' alert("handleEvent");\n' + "}", + }, + ], + }, +]; + +add_task(async function () { + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_02.js b/devtools/client/inspector/markup/test/browser_markup_events_02.js new file mode 100644 index 0000000000..44840179d2 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_02.js @@ -0,0 +1,123 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ + +"use strict"; + +// Test that markup view event bubbles show the correct event info for DOM +// events. + +const TEST_URL = URL_ROOT_SSL + "doc_markup_events_02.html"; + +loadHelperScript("helper_events_test_runner.js"); + +const TEST_DATA = [ + { + selector: "#fatarrow", + expected: [ + { + type: "click", + filename: TEST_URL + ":42:43", + attributes: ["Bubbling"], + handler: "() => {\n" + ' alert("Fat arrow without params!");\n' + "}", + }, + { + type: "click", + filename: TEST_URL + ":46:43", + attributes: ["Bubbling"], + handler: "event => {\n" + ' alert("Fat arrow with 1 param!");\n' + "}", + }, + { + type: "click", + filename: TEST_URL + ":50:43", + attributes: ["Bubbling"], + handler: + "(event, foo, bar) => {\n" + + ' alert("Fat arrow with 3 params!");\n' + + "}", + }, + { + type: "click", + filename: TEST_URL + ":54:43", + attributes: ["Bubbling"], + handler: "b => b", + }, + ], + }, + { + selector: "#bound", + expected: [ + { + type: "click", + filename: TEST_URL + ":65:32", + attributes: ["Bubbling"], + handler: "function(event) {\n" + ' alert("Bound event");\n' + "}", + }, + ], + }, + { + selector: "#boundhe", + expected: [ + { + type: "click", + filename: TEST_URL + ":89:19", + attributes: ["Bubbling"], + handler: "function() {\n" + ' alert("boundHandleEvent");\n' + "}", + }, + ], + }, + { + selector: "#comment-inline", + expected: [ + { + type: "click", + filename: TEST_URL + ":95:47", + attributes: ["Bubbling"], + handler: + "function functionProceededByInlineComment() {\n" + + ' alert("comment-inline");\n' + + "}", + }, + ], + }, + { + selector: "#comment-streaming", + expected: [ + { + type: "click", + filename: TEST_URL + ":100:50", + attributes: ["Bubbling"], + handler: + "function functionProceededByStreamingComment() {\n" + + ' alert("comment-streaming");\n' + + "}", + }, + ], + }, + { + selector: "#anon-object-method", + expected: [ + { + type: "click", + filename: TEST_URL + ":75:34", + attributes: ["Bubbling"], + handler: "function() {\n" + ' alert("obj.anonObjectMethod");\n' + "}", + }, + ], + }, + { + selector: "#object-method", + expected: [ + { + type: "click", + filename: TEST_URL + ":79:34", + attributes: ["Bubbling"], + handler: "function kay() {\n" + ' alert("obj.objectMethod");\n' + "}", + }, + ], + }, +]; + +add_task(async function () { + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_03.js b/devtools/client/inspector/markup/test/browser_markup_events_03.js new file mode 100644 index 0000000000..bebffec066 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_03.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ + +"use strict"; + +// Test that markup view event bubbles show the correct event info for DOM +// events. + +const TEST_URL = URL_ROOT_SSL + "doc_markup_events_03.html"; + +loadHelperScript("helper_events_test_runner.js"); + +const TEST_DATA = [ + { + selector: "#es6-method", + expected: [ + { + type: "click", + filename: TEST_URL + ":69:17", + attributes: ["Bubbling"], + handler: + "es6Method(foo, bar) {\n" + ' alert("obj.es6Method");\n' + "}", + }, + ], + }, + { + selector: "#generator", + expected: [ + { + type: "click", + filename: TEST_URL + ":89:25", + attributes: ["Bubbling"], + handler: "function* generator() {\n" + ' alert("generator");\n' + "}", + }, + ], + }, + { + selector: "#anon-generator", + expected: [ + { + type: "click", + filename: TEST_URL + ":46:58", + attributes: ["Bubbling"], + handler: "function*() {\n" + ' alert("anonGenerator");\n' + "}", + }, + ], + }, + { + selector: "#named-function-expression", + expected: [ + { + type: "click", + filename: TEST_URL + ":22:18", + attributes: ["Bubbling"], + handler: + "function foo() {\n" + ' alert("namedFunctionExpression");\n' + "}", + }, + ], + }, + { + selector: "#anon-function-expression", + expected: [ + { + type: "click", + filename: TEST_URL + ":26:45", + attributes: ["Bubbling"], + handler: + "function() {\n" + ' alert("anonFunctionExpression");\n' + "}", + }, + ], + }, + { + selector: "#returned-function", + expected: [ + { + type: "click", + filename: TEST_URL + ":31:27", + attributes: ["Bubbling"], + handler: "function bar() {\n" + ' alert("returnedFunction");\n' + "}", + }, + ], + }, +]; + +add_task(async function () { + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_04.js b/devtools/client/inspector/markup/test/browser_markup_events_04.js new file mode 100644 index 0000000000..b7dd546c29 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_04.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ + +"use strict"; + +// Test that markup view event bubbles show the correct event info for DOM +// events. + +const TEST_URL = URL_ROOT_SSL + "doc_markup_events_04.html"; + +loadHelperScript("helper_events_test_runner.js"); + +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "click", + filename: TEST_URL + ":59:67", + attributes: ["Bubbling"], + handler: + "function(foo2, bar2) {\n" + + ' alert("documentElement event listener clicked");\n' + + "}", + }, + { + type: "click", + filename: TEST_URL + ":55:51", + attributes: ["Bubbling"], + handler: + "function(foo, bar) {\n" + + ' alert("document event listener clicked");\n' + + "}", + }, + { + type: "load", + filename: TEST_URL, + attributes: ["Bubbling"], + handler: "function onload(event) {\n" + " init();\n" + "}", + }, + ], + }, + { + selector: "#constructed-function", + expected: [ + { + type: "click", + filename: TEST_URL + ":1:0", + attributes: ["Bubbling"], + handler: "function anonymous() {\n" + "\n" + "}", + }, + ], + }, + { + selector: "#constructed-function-with-body-string", + expected: [ + { + type: "click", + filename: TEST_URL + ":1:0", + attributes: ["Bubbling"], + handler: + "function anonymous(a, b, c) {\n" + + ' alert("constructedFuncWithBodyString");\n' + + "}", + }, + ], + }, + { + selector: "#multiple-assignment", + expected: [ + { + type: "click", + filename: TEST_URL + ":26:47", + attributes: ["Bubbling"], + handler: + "function multi() {\n" + ' alert("multipleAssignment");\n' + "}", + }, + ], + }, + { + selector: "#promise", + expected: [ + { + type: "click", + filename: "[native code]", + attributes: ["Bubbling"], + handler: "function() {\n" + " [native code]\n" + "}", + }, + ], + }, + { + selector: "#math-pow", + expected: [ + { + type: "click", + filename: "[native code]", + attributes: ["Bubbling"], + handler: "function pow(, ) {\n" + " [native code]\n" + "}", + }, + ], + }, + { + selector: "#handleEvent", + expected: [ + { + type: "click", + filename: TEST_URL + ":81:29", + attributes: ["Bubbling"], + handler: + "function(event) {\n" + + " switch (event.type) {\n" + + ' case "click":\n' + + ' alert("handleEvent click");\n' + + " }\n" + + "}", + }, + ], + }, +]; + +add_task(async function () { + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_chrome_blocked.js b/devtools/client/inspector/markup/test/browser_markup_events_chrome_blocked.js new file mode 100644 index 0000000000..395b89fd07 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_chrome_blocked.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ + +"use strict"; + +// Test that markup view chrome event bubbles are hidden when +// devtools.chrome.enabled = false. + +const TEST_URL = URL_ROOT + "doc_markup_events_chrome_listeners.html"; + +loadHelperScript("helper_events_test_runner.js"); + +const TEST_DATA = [ + { + selector: "div", + expected: [], + }, +]; + +add_task(async function () { + waitForExplicitFinish(); + await pushPref("devtools.chrome.enabled", false); + + const { tab, inspector } = await openInspectorForURL(TEST_URL); + const browser = tab.linkedBrowser; + + const badgeEventAdded = inspector.markup.once("badge-added-event"); + + info("Loading frame script"); + await SpecialPowers.spawn(browser, [], () => { + const div = content.document.querySelector("div"); + div.addEventListener("click", () => { + /* Do nothing */ + }); + }); + + // We need to check that the "badge-added-event" event is not triggered so we + // need to wait for 5 seconds here. + const result = await awaitWithTimeout(badgeEventAdded, 3000); + is(result, "timeout", "Ensure that no event badges were added"); + + for (const test of TEST_DATA) { + await checkEventsForNode(test, inspector); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_chrome_not_blocked.js b/devtools/client/inspector/markup/test/browser_markup_events_chrome_not_blocked.js new file mode 100644 index 0000000000..c04dd3c396 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_chrome_not_blocked.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ + +("use strict"); + +// Test that markup view chrome event bubbles are shown when +// devtools.chrome.enabled = true. + +const TEST_URL = URL_ROOT + "doc_markup_events_chrome_listeners.html"; + +loadHelperScript("helper_events_test_runner.js"); + +const TEST_DATA = [ + { + selector: "div", + expected: [ + { + type: "click", + filename: + getRootDirectory(gTestPath) + + "browser_markup_events_chrome_not_blocked.js:45:34", + attributes: ["Bubbling"], + handler: `() => { + /* Do nothing */ + }`, + }, + ], + }, +]; + +add_task(async function () { + waitForExplicitFinish(); + await pushPref("devtools.chrome.enabled", true); + + const { tab, inspector } = await openInspectorForURL(TEST_URL); + const browser = tab.linkedBrowser; + + const eventBadgeAdded = inspector.markup.once("badge-added-event"); + info("Loading frame script"); + + await SpecialPowers.spawn(browser, [], () => { + const div = content.document.querySelector("div"); + div.addEventListener("click", () => { + /* Do nothing */ + }); + }); + await eventBadgeAdded; + + for (const test of TEST_DATA) { + await checkEventsForNode(test, inspector); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_click_to_close.js b/devtools/client/inspector/markup/test/browser_markup_events_click_to_close.js new file mode 100644 index 0000000000..ed410fb85b --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_click_to_close.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ + +"use strict"; + +// Tests that click events that close the current event tooltip are still propagated to +// the target underneath. + +const TEST_URL = ` + +
test
+ + + + + +
test
+ +`; + +add_task(async function () { + // Make the toolbox tall enough to show the full markup without the need + // to manage scrolling event badges into view. + await pushPref("devtools.toolbox.footer.height", 400); + + const { inspector } = await openInspectorForURL( + "data:text/html;charset=utf-8," + encodeURI(TEST_URL) + ); + const { waitForHighlighterTypeHidden } = getHighlighterTestHelpers(inspector); + + await inspector.markup.expandAll(); + + const container1 = await getContainerForSelector("#d1", inspector); + const evHolder1 = container1.elt.querySelector( + ".inspector-badge.interactive[data-event]" + ); + + const container2 = await getContainerForSelector("#d2", inspector); + const evHolder2 = container2.elt.querySelector( + ".inspector-badge.interactive[data-event]" + ); + + const tooltip = inspector.markup.eventDetailsTooltip; + + info("Click the event icon for the first element"); + let onShown = tooltip.once("shown"); + EventUtils.synthesizeMouseAtCenter(evHolder1, {}, inspector.markup.win); + await onShown; + info("event tooltip for the first div is shown"); + + info("Click the event icon for the second element"); + let onHidden = tooltip.once("hidden"); + onShown = tooltip.once("shown"); + EventUtils.synthesizeMouseAtCenter(evHolder2, {}, inspector.markup.win); + + await onHidden; + info("previous tooltip hidden"); + + await onShown; + info("event tooltip for the second div is shown"); + + info("Check that clicking on evHolder2 again hides the tooltip"); + onHidden = tooltip.once("hidden"); + EventUtils.synthesizeMouseAtCenter(evHolder2, {}, inspector.markup.win); + await onHidden; + + info("Check that the tooltip does not reappear immediately after"); + await waitForTime(1000); + is( + tooltip.isVisible(), + false, + "The tooltip is still hidden after waiting for one second" + ); + + info("Open the tooltip on evHolder2 again"); + onShown = tooltip.once("shown"); + EventUtils.synthesizeMouseAtCenter(evHolder2, {}, inspector.markup.win); + await onShown; + + info("Click on the computed view tab"); + const onHighlighterHidden = waitForHighlighterTypeHidden( + inspector.highlighters.TYPES.BOXMODEL + ); + const onTabComputedViewSelected = inspector.sidebar.once( + "computedview-selected" + ); + const computedViewTab = inspector.panelDoc.querySelector("#computedview-tab"); + EventUtils.synthesizeMouseAtCenter( + computedViewTab, + {}, + inspector.panelDoc.defaultView + ); + + await onTabComputedViewSelected; + info("computed view was selected"); + + await onHighlighterHidden; + info( + "box model highlighter hidden after moving the mouse out of the markup view" + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.0.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.0.js new file mode 100644 index 0000000000..53a4805453 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.0.js @@ -0,0 +1,224 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 1.0). + +const TEST_LIB = "lib_jquery_1.0.js"; +const TEST_URL = URL_ROOT_SSL + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "DOMContentLoaded", + filename: URL_ROOT_SSL + TEST_LIB + ":1117:16", + attributes: ["Bubbling"], + handler: ` + function() { + // Make sure that the DOM is not already loaded + if (!jQuery.isReady) { + // Remember that the DOM is ready + jQuery.isReady = true; + + // If there are functions bound, to execute + if (jQuery.readyList) { + // Execute all of them + for (var i = 0; i < jQuery.readyList.length; i++) + jQuery.readyList[i].apply(document); + + // Reset the list of functions + jQuery.readyList = null; + } + } + }`, + }, + { + type: "load", + filename: TEST_URL + ":29:38", + attributes: ["Bubbling"], + handler: ` + () => { + const handler1 = function liveDivDblClick() { + alert(1); + }; + const handler2 = function liveDivDragStart() { + alert(2); + }; + const handler3 = function liveDivDragLeave() { + alert(3); + }; + const handler4 = function liveDivDragEnd() { + alert(4); + }; + const handler5 = function liveDivDrop() { + alert(5); + }; + const handler6 = function liveDivDragOver() { + alert(6); + }; + const handler7 = function divClick1() { + alert(7); + }; + const handler8 = function divClick2() { + alert(8); + }; + const handler9 = function divKeyDown() { + alert(9); + }; + const handler10 = function divDragOut() { + alert(10); + }; + + if ($("#livediv").live) { + $("#livediv").live("dblclick", handler1); + $("#livediv").live("dragstart", handler2); + } + + if ($("#livediv").delegate) { + $(document).delegate("#livediv", "dragleave", handler3); + $(document).delegate("#livediv", "dragend", handler4); + } + + if ($("#livediv").on) { + $(document).on("drop", "#livediv", handler5); + $(document).on("dragover", "#livediv", handler6); + $(document).on("dragout", "#livediv:xxxxx", handler10); + } + + const div = $("div")[0]; + $(div).click(handler7); + $(div).click(handler8); + $(div).keydown(handler9); + }`, + }, + { + type: "load", + filename: URL_ROOT_SSL + TEST_LIB + ":894:18", + attributes: ["Bubbling"], + handler: ` + function(event) { + if (typeof jQuery == "undefined") return; + + event = event || jQuery.event.fix(window.event); + + // If no correct event was found, fail + if (!event) return; + + var returnValue = true; + + var c = this.events[event.type]; + + for (var j in c) { + if (c[j].apply(this, [event]) === false) { + event.preventDefault(); + event.stopPropagation(); + returnValue = false; + } + } + + return returnValue; + }`, + }, + ], + }, + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":36:43", + attributes: ["jQuery"], + handler: ` + function divClick1() { + alert(7); + }`, + }, + { + type: "click", + filename: TEST_URL + ":37:43", + attributes: ["jQuery"], + handler: ` + function divClick2() { + alert(8); + }`, + }, + { + type: "click", + filename: URL_ROOT_SSL + TEST_LIB + ":894:18", + attributes: ["Bubbling"], + handler: ` + function(event) { + if (typeof jQuery == "undefined") return; + + event = event || jQuery.event.fix(window.event); + + // If no correct event was found, fail + if (!event) return; + + var returnValue = true; + + var c = this.events[event.type]; + + for (var j in c) { + if (c[j].apply(this, [event]) === false) { + event.preventDefault(); + event.stopPropagation(); + returnValue = false; + } + } + + return returnValue; + }`, + }, + { + type: "keydown", + filename: TEST_URL + ":38:44", + attributes: ["jQuery"], + handler: ` + function divKeyDown() { + alert(9); + }`, + }, + { + type: "keydown", + filename: URL_ROOT_SSL + TEST_LIB + ":894:18", + attributes: ["Bubbling"], + handler: ` + function(event) { + if (typeof jQuery == "undefined") return; + + event = event || jQuery.event.fix(window.event); + + // If no correct event was found, fail + if (!event) return; + + var returnValue = true; + + var c = this.events[event.type]; + + for (var j in c) { + if (c[j].apply(this, [event]) === false) { + event.preventDefault(); + event.stopPropagation(); + returnValue = false; + } + } + + return returnValue; + }`, + }, + ], + }, +]; +/* eslint-enable */ + +add_task(async function () { + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.1.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.1.js new file mode 100644 index 0000000000..6da88966a1 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.1.js @@ -0,0 +1,231 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 1.1). + +const TEST_LIB = "lib_jquery_1.1.js"; +const TEST_URL = URL_ROOT_SSL + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "load", + filename: TEST_URL + ":29:38", + attributes: ["Bubbling"], + handler: ` + () => { + const handler1 = function liveDivDblClick() { + alert(1); + }; + const handler2 = function liveDivDragStart() { + alert(2); + }; + const handler3 = function liveDivDragLeave() { + alert(3); + }; + const handler4 = function liveDivDragEnd() { + alert(4); + }; + const handler5 = function liveDivDrop() { + alert(5); + }; + const handler6 = function liveDivDragOver() { + alert(6); + }; + const handler7 = function divClick1() { + alert(7); + }; + const handler8 = function divClick2() { + alert(8); + }; + const handler9 = function divKeyDown() { + alert(9); + }; + const handler10 = function divDragOut() { + alert(10); + }; + + if ($("#livediv").live) { + $("#livediv").live("dblclick", handler1); + $("#livediv").live("dragstart", handler2); + } + + if ($("#livediv").delegate) { + $(document).delegate("#livediv", "dragleave", handler3); + $(document).delegate("#livediv", "dragend", handler4); + } + + if ($("#livediv").on) { + $(document).on("drop", "#livediv", handler5); + $(document).on("dragover", "#livediv", handler6); + $(document).on("dragout", "#livediv:xxxxx", handler10); + } + + const div = $("div")[0]; + $(div).click(handler7); + $(div).click(handler8); + $(div).keydown(handler9); + }`, + }, + { + type: "load", + filename: URL_ROOT_SSL + TEST_LIB + ":1224:17", + attributes: ["Bubbling"], + handler: ` + function(event) { + if (typeof jQuery == "undefined") return false; + + // Empty object is for triggered events with no data + event = jQuery.event.fix(event || window.event || {}); + + // returned undefined or false + var returnValue; + + var c = this.events[event.type]; + + var args = [].slice.call(arguments, 1); + args.unshift(event); + + for (var j in c) { + // Pass in a reference to the handler function itself + // So that we can later remove it + args[0].handler = c[j]; + args[0].data = c[j].data; + + if (c[j].apply(this, args) === false) { + event.preventDefault(); + event.stopPropagation(); + returnValue = false; + } + } + + // Clean up added properties in IE to prevent memory leak + if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null; + + return returnValue; + }`, + }, + ], + }, + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":36:43", + attributes: ["jQuery"], + handler: ` + function divClick1() { + alert(7); + }`, + }, + { + type: "click", + filename: TEST_URL + ":37:43", + attributes: ["jQuery"], + handler: ` + function divClick2() { + alert(8); + }`, + }, + { + type: "click", + filename: URL_ROOT_SSL + TEST_LIB + ":1224:17", + attributes: ["Bubbling"], + handler: ` + function(event) { + if (typeof jQuery == "undefined") return false; + + // Empty object is for triggered events with no data + event = jQuery.event.fix(event || window.event || {}); + + // returned undefined or false + var returnValue; + + var c = this.events[event.type]; + + var args = [].slice.call(arguments, 1); + args.unshift(event); + + for (var j in c) { + // Pass in a reference to the handler function itself + // So that we can later remove it + args[0].handler = c[j]; + args[0].data = c[j].data; + + if (c[j].apply(this, args) === false) { + event.preventDefault(); + event.stopPropagation(); + returnValue = false; + } + } + + // Clean up added properties in IE to prevent memory leak + if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null; + + return returnValue; + }`, + }, + { + type: "keydown", + filename: TEST_URL + ":38:44", + attributes: ["jQuery"], + handler: ` + function divKeyDown() { + alert(9); + }`, + }, + { + type: "keydown", + filename: URL_ROOT_SSL + TEST_LIB + ":1224:17", + attributes: ["Bubbling"], + handler: ` + function(event) { + if (typeof jQuery == "undefined") return false; + + // Empty object is for triggered events with no data + event = jQuery.event.fix(event || window.event || {}); + + // returned undefined or false + var returnValue; + + var c = this.events[event.type]; + + var args = [].slice.call(arguments, 1); + args.unshift(event); + + for (var j in c) { + // Pass in a reference to the handler function itself + // So that we can later remove it + args[0].handler = c[j]; + args[0].data = c[j].data; + + if (c[j].apply(this, args) === false) { + event.preventDefault(); + event.stopPropagation(); + returnValue = false; + } + } + + // Clean up added properties in IE to prevent memory leak + if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null; + + return returnValue; + }`, + }, + ], + }, +]; +/* eslint-enable */ + +add_task(async function () { + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.11.1.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.11.1.js new file mode 100644 index 0000000000..c8c273510d --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.11.1.js @@ -0,0 +1,160 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 1.11.1). + +const TEST_LIB = "lib_jquery_1.11.1_min.js"; +const TEST_URL = URL_ROOT_SSL + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "load", + filename: TEST_URL + ":29:38", + attributes: ["Bubbling"], + handler: ` + () => { + const handler1 = function liveDivDblClick() { + alert(1); + }; + const handler2 = function liveDivDragStart() { + alert(2); + }; + const handler3 = function liveDivDragLeave() { + alert(3); + }; + const handler4 = function liveDivDragEnd() { + alert(4); + }; + const handler5 = function liveDivDrop() { + alert(5); + }; + const handler6 = function liveDivDragOver() { + alert(6); + }; + const handler7 = function divClick1() { + alert(7); + }; + const handler8 = function divClick2() { + alert(8); + }; + const handler9 = function divKeyDown() { + alert(9); + }; + const handler10 = function divDragOut() { + alert(10); + }; + + if ($("#livediv").live) { + $("#livediv").live("dblclick", handler1); + $("#livediv").live("dragstart", handler2); + } + + if ($("#livediv").delegate) { + $(document).delegate("#livediv", "dragleave", handler3); + $(document).delegate("#livediv", "dragend", handler4); + } + + if ($("#livediv").on) { + $(document).on("drop", "#livediv", handler5); + $(document).on("dragover", "#livediv", handler6); + $(document).on("dragout", "#livediv:xxxxx", handler10); + } + + const div = $("div")[0]; + $(div).click(handler7); + $(div).click(handler8); + $(div).keydown(handler9); + }`, + }, + ], + }, + + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":36:43", + attributes: ["jQuery"], + handler: ` + function divClick1() { + alert(7); + }`, + }, + { + type: "click", + filename: TEST_URL + ":37:43", + attributes: ["jQuery"], + handler: ` + function divClick2() { + alert(8); + }`, + }, + { + type: "keydown", + filename: TEST_URL + ":38:44", + attributes: ["jQuery"], + handler: ` + function divKeyDown() { + alert(9); + }`, + }, + ], + }, + + { + selector: "#livediv", + expected: [ + { + type: "dragend", + filename: TEST_URL + ":33:48", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDragEnd() { + alert(4); + }`, + }, + { + type: "dragleave", + filename: TEST_URL + ":32:50", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDragLeave() { + alert(3); + }`, + }, + { + type: "dragover", + filename: TEST_URL + ":35:49", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDragOver() { + alert(6); + }`, + }, + { + type: "drop", + filename: TEST_URL + ":34:45", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDrop() { + alert(5); + }`, + }, + ], + }, +]; +/* eslint-enable */ + +add_task(async function () { + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.2.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.2.js new file mode 100644 index 0000000000..8739f6f025 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.2.js @@ -0,0 +1,141 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 1.2). + +const TEST_LIB = "lib_jquery_1.2_min.js"; +const TEST_URL = URL_ROOT_SSL + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "load", + filename: TEST_URL + ":29:38", + attributes: ["Bubbling"], + handler: ` + () => { + const handler1 = function liveDivDblClick() { + alert(1); + }; + const handler2 = function liveDivDragStart() { + alert(2); + }; + const handler3 = function liveDivDragLeave() { + alert(3); + }; + const handler4 = function liveDivDragEnd() { + alert(4); + }; + const handler5 = function liveDivDrop() { + alert(5); + }; + const handler6 = function liveDivDragOver() { + alert(6); + }; + const handler7 = function divClick1() { + alert(7); + }; + const handler8 = function divClick2() { + alert(8); + }; + const handler9 = function divKeyDown() { + alert(9); + }; + const handler10 = function divDragOut() { + alert(10); + }; + + if ($("#livediv").live) { + $("#livediv").live("dblclick", handler1); + $("#livediv").live("dragstart", handler2); + } + + if ($("#livediv").delegate) { + $(document).delegate("#livediv", "dragleave", handler3); + $(document).delegate("#livediv", "dragend", handler4); + } + + if ($("#livediv").on) { + $(document).on("drop", "#livediv", handler5); + $(document).on("dragover", "#livediv", handler6); + $(document).on("dragout", "#livediv:xxxxx", handler10); + } + + const div = $("div")[0]; + $(div).click(handler7); + $(div).click(handler8); + $(div).keydown(handler9); + }`, + }, + ], + }, + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":36:43", + attributes: ["jQuery"], + handler: ` + function divClick1() { + alert(7); + }`, + }, + { + type: "click", + filename: TEST_URL + ":37:43", + attributes: ["jQuery"], + handler: ` + function divClick2() { + alert(8); + }`, + }, + { + type: "click", + filename: URL_ROOT_SSL + TEST_LIB + ":24:10040", + attributes: ["Bubbling"], + handler: ` + function() { + var val; + if (typeof jQuery == "undefined" || jQuery.event.triggered) return val; + val = jQuery.event.handle.apply(element, arguments); + return val; + }`, + }, + { + type: "keydown", + filename: TEST_URL + ":38:44", + attributes: ["jQuery"], + handler: ` + function divKeyDown() { + alert(9); + }`, + }, + { + type: "keydown", + filename: URL_ROOT_SSL + TEST_LIB + ":24:10040", + attributes: ["Bubbling"], + handler: ` + function() { + var val; + if (typeof jQuery == "undefined" || jQuery.event.triggered) return val; + val = jQuery.event.handle.apply(element, arguments); + return val; + }`, + }, + ], + }, +]; +/* eslint-enable */ + +add_task(async function () { + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.3.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.3.js new file mode 100644 index 0000000000..79dc132878 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.3.js @@ -0,0 +1,150 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 1.3). + +const TEST_LIB = "lib_jquery_1.3_min.js"; +const TEST_URL = URL_ROOT_SSL + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "DOMContentLoaded", + filename: URL_ROOT_SSL + TEST_LIB + ":19:18937", + attributes: ["Bubbling"], + handler: ` + function() { + document.removeEventListener("DOMContentLoaded", arguments.callee, false); + n.ready() + }`, + }, + { + type: "load", + filename: TEST_URL + ":29:38", + attributes: ["Bubbling"], + handler: ` + () => { + const handler1 = function liveDivDblClick() { + alert(1); + }; + const handler2 = function liveDivDragStart() { + alert(2); + }; + const handler3 = function liveDivDragLeave() { + alert(3); + }; + const handler4 = function liveDivDragEnd() { + alert(4); + }; + const handler5 = function liveDivDrop() { + alert(5); + }; + const handler6 = function liveDivDragOver() { + alert(6); + }; + const handler7 = function divClick1() { + alert(7); + }; + const handler8 = function divClick2() { + alert(8); + }; + const handler9 = function divKeyDown() { + alert(9); + }; + const handler10 = function divDragOut() { + alert(10); + }; + + if ($("#livediv").live) { + $("#livediv").live("dblclick", handler1); + $("#livediv").live("dragstart", handler2); + } + + if ($("#livediv").delegate) { + $(document).delegate("#livediv", "dragleave", handler3); + $(document).delegate("#livediv", "dragend", handler4); + } + + if ($("#livediv").on) { + $(document).on("drop", "#livediv", handler5); + $(document).on("dragover", "#livediv", handler6); + $(document).on("dragout", "#livediv:xxxxx", handler10); + } + + const div = $("div")[0]; + $(div).click(handler7); + $(div).click(handler8); + $(div).keydown(handler9); + }`, + }, + ], + }, + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":36:43", + attributes: ["jQuery"], + handler: ` + function divClick1() { + alert(7); + }`, + }, + { + type: "click", + filename: TEST_URL + ":37:43", + attributes: ["jQuery"], + handler: ` + function divClick2() { + alert(8); + }`, + }, + { + type: "keydown", + filename: TEST_URL + ":38:44", + attributes: ["jQuery"], + handler: ` + function divKeyDown() { + alert(9); + }`, + }, + ], + }, + { + selector: "#livediv", + expected: [ + { + type: "dblclick", + filename: TEST_URL + ":30:49", + attributes: ["jQuery", "Live"], + handler: ` + function() { + return E.apply(this, arguments) + }`, + }, + { + type: "dragstart", + filename: TEST_URL + ":31:50", + attributes: ["jQuery", "Live"], + handler: ` + function() { + return E.apply(this, arguments) + }`, + }, + ], + }, +]; +/* eslint-enable */ + +add_task(async function () { + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.4.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.4.js new file mode 100644 index 0000000000..380b1770ff --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.4.js @@ -0,0 +1,185 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 1.4). + +const TEST_LIB = "lib_jquery_1.4_min.js"; +const TEST_URL = URL_ROOT_SSL + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "DOMContentLoaded", + filename: URL_ROOT_SSL + TEST_LIB + ":32:355", + attributes: ["Bubbling"], + handler: ` + function() { + s.removeEventListener(\"DOMContentLoaded\", M, false); + c.ready() + }`, + }, + { + type: "load", + filename: TEST_URL + ":29:38", + attributes: ["Bubbling"], + handler: ` + () => { + const handler1 = function liveDivDblClick() { + alert(1); + }; + const handler2 = function liveDivDragStart() { + alert(2); + }; + const handler3 = function liveDivDragLeave() { + alert(3); + }; + const handler4 = function liveDivDragEnd() { + alert(4); + }; + const handler5 = function liveDivDrop() { + alert(5); + }; + const handler6 = function liveDivDragOver() { + alert(6); + }; + const handler7 = function divClick1() { + alert(7); + }; + const handler8 = function divClick2() { + alert(8); + }; + const handler9 = function divKeyDown() { + alert(9); + }; + const handler10 = function divDragOut() { + alert(10); + }; + + if ($("#livediv").live) { + $("#livediv").live("dblclick", handler1); + $("#livediv").live("dragstart", handler2); + } + + if ($("#livediv").delegate) { + $(document).delegate("#livediv", "dragleave", handler3); + $(document).delegate("#livediv", "dragend", handler4); + } + + if ($("#livediv").on) { + $(document).on("drop", "#livediv", handler5); + $(document).on("dragover", "#livediv", handler6); + $(document).on("dragout", "#livediv:xxxxx", handler10); + } + + const div = $("div")[0]; + $(div).click(handler7); + $(div).click(handler8); + $(div).keydown(handler9); + }`, + }, + { + type: "load", + filename: URL_ROOT_SSL + TEST_LIB + ":26:107", + attributes: ["Bubbling"], + handler: ` + function() { + if (!c.isReady) { + if (!s.body) return setTimeout(c.ready, 13); + c.isReady = true; + if (Q) { + for (var a, b = 0; a = Q[b++];) a.call(s, c); + Q = null + } + c.fn.triggerHandler && c(s).triggerHandler("ready") + } + }`, + }, + ], + }, + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":36:43", + attributes: ["jQuery"], + handler: ` + function divClick1() { + alert(7); + }`, + }, + { + type: "click", + filename: TEST_URL + ":37:43", + attributes: ["jQuery"], + handler: ` + function divClick2() { + alert(8); + }`, + }, + { + type: "keydown", + filename: TEST_URL + ":38:44", + attributes: ["jQuery"], + handler: ` + function divKeyDown() { + alert(9); + }`, + }, + ], + }, + { + selector: "#livediv", + expected: [ + { + type: "dblclick", + filename: TEST_URL + ":30:49", + attributes: ["jQuery", "Live"], + handler: ` + function() { + return a.apply(d || this, arguments) + }`, + }, + { + type: "dblclick", + filename: URL_ROOT_SSL + TEST_LIB + ":17:183", + attributes: ["jQuery", "Live"], + handler: ` + function() { + return a.apply(d || this, arguments) + }`, + }, + { + type: "dragstart", + filename: TEST_URL + ":31:50", + attributes: ["jQuery", "Live"], + handler: ` + function() { + return a.apply(d || this, arguments) + }`, + }, + { + type: "dragstart", + filename: URL_ROOT_SSL + TEST_LIB + ":17:183", + attributes: ["jQuery", "Live"], + handler: ` + function() { + return a.apply(d || this, arguments) + }`, + }, + ], + }, +]; +/* eslint-enable */ + +add_task(async function () { + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.6.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.6.js new file mode 100644 index 0000000000..65ab16bb03 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.6.js @@ -0,0 +1,351 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +requestLongerTimeout(2); + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 1.6). + +const TEST_LIB = "lib_jquery_1.6_min.js"; +const TEST_URL = URL_ROOT_SSL + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "DOMContentLoaded", + filename: URL_ROOT_SSL + TEST_LIB + ":16:14483", + attributes: ["Bubbling"], + handler: ` + function() { + c.removeEventListener("DOMContentLoaded", z, !1), e.ready() + }`, + }, + { + type: "load", + filename: TEST_URL + ":29:38", + attributes: ["Bubbling"], + handler: ` + () => { + const handler1 = function liveDivDblClick() { + alert(1); + }; + const handler2 = function liveDivDragStart() { + alert(2); + }; + const handler3 = function liveDivDragLeave() { + alert(3); + }; + const handler4 = function liveDivDragEnd() { + alert(4); + }; + const handler5 = function liveDivDrop() { + alert(5); + }; + const handler6 = function liveDivDragOver() { + alert(6); + }; + const handler7 = function divClick1() { + alert(7); + }; + const handler8 = function divClick2() { + alert(8); + }; + const handler9 = function divKeyDown() { + alert(9); + }; + const handler10 = function divDragOut() { + alert(10); + }; + + if ($("#livediv").live) { + $("#livediv").live("dblclick", handler1); + $("#livediv").live("dragstart", handler2); + } + + if ($("#livediv").delegate) { + $(document).delegate("#livediv", "dragleave", handler3); + $(document).delegate("#livediv", "dragend", handler4); + } + + if ($("#livediv").on) { + $(document).on("drop", "#livediv", handler5); + $(document).on("dragover", "#livediv", handler6); + $(document).on("dragout", "#livediv:xxxxx", handler10); + } + + const div = $("div")[0]; + $(div).click(handler7); + $(div).click(handler8); + $(div).keydown(handler9); + }`, + }, + { + type: "load", + filename: URL_ROOT_SSL + TEST_LIB + ":16:10001", + attributes: ["Bubbling"], + handler: ` + function(a) { + if (a === !0 && !--e.readyWait || a !== !0 && !e.isReady) { + if (!c.body) return setTimeout(e.ready, 1); + e.isReady = !0; + if (a !== !0 && --e.readyWait > 0) return; + y.resolveWith(c, [e]), e.fn.trigger && e(c).trigger("ready").unbind("ready") + } + }`, + }, + ], + }, + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":36:43", + attributes: ["jQuery"], + handler: ` + function divClick1() { + alert(7); + }`, + }, + { + type: "click", + filename: TEST_URL + ":37:43", + attributes: ["jQuery"], + handler: ` + function divClick2() { + alert(8); + }`, + }, + { + type: "keydown", + filename: TEST_URL + ":38:44", + attributes: ["jQuery"], + handler: ` + function divKeyDown() { + alert(9); + }`, + }, + ], + }, + { + selector: "#livediv", + expected: [ + { + type: "dblclick", + filename: TEST_URL + ":30:49", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDblClick() { + alert(1); + }`, + }, + { + type: "dblclick", + filename: URL_ROOT_SSL + TEST_LIB + ":16:4732", + attributes: ["jQuery", "Live"], + handler: ` + function M(a) { + var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [], + q = [], + r = f._data(this, "events"); + if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === "click")) { + a.namespace && (n = new RegExp("(^|\\\\.)" + a.namespace.split(".").join("\\\\.(?:.*\\\\.)?") + "(\\\\.|$)")), a.liveFired = this; + var s = r.live.slice(0); + for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, "") === a.type ? q.push(g.selector) : s.splice(i--, 1); + e = f(a.target).closest(q, a.currentTarget); + for (j = 0, k = e.length; j < k; j++) { + m = e[j]; + for (i = 0; i < s.length; i++) { + g = s[i]; + if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) { + h = m.elem, d = null; + if (g.preType === "mouseenter" || g.preType === "mouseleave") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h); + (!d || d !== h) && p.push({ + elem: h, + handleObj: g, + level: m.level + }) + } + } + } + for (j = 0, k = p.length; j < k; j++) { + e = p[j]; + if (c && e.level > c) break; + a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments); + if (o === !1 || a.isPropagationStopped()) { + c = e.level, o === !1 && (b = !1); + if (a.isImmediatePropagationStopped()) break + } + } + return b + } + }`, + }, + { + type: "dragend", + filename: TEST_URL + ":33:48", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDragEnd() { + alert(4); + }`, + }, + { + type: "dragend", + filename: URL_ROOT_SSL + TEST_LIB + ":16:4732", + attributes: ["jQuery", "Live"], + handler: ` + function M(a) { + var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [], + q = [], + r = f._data(this, "events"); + if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === "click")) { + a.namespace && (n = new RegExp("(^|\\\\.)" + a.namespace.split(".").join("\\\\.(?:.*\\\\.)?") + "(\\\\.|$)")), a.liveFired = this; + var s = r.live.slice(0); + for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, "") === a.type ? q.push(g.selector) : s.splice(i--, 1); + e = f(a.target).closest(q, a.currentTarget); + for (j = 0, k = e.length; j < k; j++) { + m = e[j]; + for (i = 0; i < s.length; i++) { + g = s[i]; + if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) { + h = m.elem, d = null; + if (g.preType === "mouseenter" || g.preType === "mouseleave") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h); + (!d || d !== h) && p.push({ + elem: h, + handleObj: g, + level: m.level + }) + } + } + } + for (j = 0, k = p.length; j < k; j++) { + e = p[j]; + if (c && e.level > c) break; + a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments); + if (o === !1 || a.isPropagationStopped()) { + c = e.level, o === !1 && (b = !1); + if (a.isImmediatePropagationStopped()) break + } + } + return b + } + }`, + }, + { + type: "dragleave", + filename: TEST_URL + ":32:50", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDragLeave() { + alert(3); + }`, + }, + { + type: "dragleave", + filename: URL_ROOT_SSL + TEST_LIB + ":16:4732", + attributes: ["jQuery", "Live"], + handler: ` + function M(a) { + var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [], + q = [], + r = f._data(this, "events"); + if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === "click")) { + a.namespace && (n = new RegExp("(^|\\\\.)" + a.namespace.split(".").join("\\\\.(?:.*\\\\.)?") + "(\\\\.|$)")), a.liveFired = this; + var s = r.live.slice(0); + for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, "") === a.type ? q.push(g.selector) : s.splice(i--, 1); + e = f(a.target).closest(q, a.currentTarget); + for (j = 0, k = e.length; j < k; j++) { + m = e[j]; + for (i = 0; i < s.length; i++) { + g = s[i]; + if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) { + h = m.elem, d = null; + if (g.preType === "mouseenter" || g.preType === "mouseleave") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h); + (!d || d !== h) && p.push({ + elem: h, + handleObj: g, + level: m.level + }) + } + } + } + for (j = 0, k = p.length; j < k; j++) { + e = p[j]; + if (c && e.level > c) break; + a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments); + if (o === !1 || a.isPropagationStopped()) { + c = e.level, o === !1 && (b = !1); + if (a.isImmediatePropagationStopped()) break + } + } + return b + } + }`, + }, + { + type: "dragstart", + filename: TEST_URL + ":31:50", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDragStart() { + alert(2); + }`, + }, + { + type: "dragstart", + filename: URL_ROOT_SSL + TEST_LIB + ":16:4732", + attributes: ["jQuery", "Live"], + handler: ` + function M(a) { + var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [], + q = [], + r = f._data(this, "events"); + if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === "click")) { + a.namespace && (n = new RegExp("(^|\\\\.)" + a.namespace.split(".").join("\\\\.(?:.*\\\\.)?") + "(\\\\.|$)")), a.liveFired = this; + var s = r.live.slice(0); + for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, "") === a.type ? q.push(g.selector) : s.splice(i--, 1); + e = f(a.target).closest(q, a.currentTarget); + for (j = 0, k = e.length; j < k; j++) { + m = e[j]; + for (i = 0; i < s.length; i++) { + g = s[i]; + if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) { + h = m.elem, d = null; + if (g.preType === "mouseenter" || g.preType === "mouseleave") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h); + (!d || d !== h) && p.push({ + elem: h, + handleObj: g, + level: m.level + }) + } + } + } + for (j = 0, k = p.length; j < k; j++) { + e = p[j]; + if (c && e.level > c) break; + a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments); + if (o === !1 || a.isPropagationStopped()) { + c = e.level, o === !1 && (b = !1); + if (a.isImmediatePropagationStopped()) break + } + } + return b + } + }`, + }, + ], + }, +]; +/* eslint-enable */ + +add_task(async function () { + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.7.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.7.js new file mode 100644 index 0000000000..e45fd692c5 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.7.js @@ -0,0 +1,201 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +requestLongerTimeout(2); + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 1.7). + +const TEST_LIB = "lib_jquery_1.7_min.js"; +const TEST_URL = URL_ROOT_SSL + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "DOMContentLoaded", + filename: URL_ROOT_SSL + TEST_LIB + ":2:14177", + attributes: ["Bubbling"], + handler: ` + function() { + c.removeEventListener("DOMContentLoaded", C, !1), e.ready() + }`, + }, + { + type: "load", + filename: TEST_URL + ":29:38", + attributes: ["Bubbling"], + handler: ` + () => { + const handler1 = function liveDivDblClick() { + alert(1); + }; + const handler2 = function liveDivDragStart() { + alert(2); + }; + const handler3 = function liveDivDragLeave() { + alert(3); + }; + const handler4 = function liveDivDragEnd() { + alert(4); + }; + const handler5 = function liveDivDrop() { + alert(5); + }; + const handler6 = function liveDivDragOver() { + alert(6); + }; + const handler7 = function divClick1() { + alert(7); + }; + const handler8 = function divClick2() { + alert(8); + }; + const handler9 = function divKeyDown() { + alert(9); + }; + const handler10 = function divDragOut() { + alert(10); + }; + + if ($("#livediv").live) { + $("#livediv").live("dblclick", handler1); + $("#livediv").live("dragstart", handler2); + } + + if ($("#livediv").delegate) { + $(document).delegate("#livediv", "dragleave", handler3); + $(document).delegate("#livediv", "dragend", handler4); + } + + if ($("#livediv").on) { + $(document).on("drop", "#livediv", handler5); + $(document).on("dragover", "#livediv", handler6); + $(document).on("dragout", "#livediv:xxxxx", handler10); + } + + const div = $("div")[0]; + $(div).click(handler7); + $(div).click(handler8); + $(div).keydown(handler9); + }`, + }, + { + type: "load", + filename: URL_ROOT_SSL + TEST_LIB + ":2:9526", + attributes: ["Bubbling"], + handler: ` + function(a) { + if (a === !0 && !--e.readyWait || a !== !0 && !e.isReady) { + if (!c.body) return setTimeout(e.ready, 1); + e.isReady = !0; + if (a !== !0 && --e.readyWait > 0) return; + B.fireWith(c, [e]), e.fn.trigger && e(c).trigger("ready").unbind("ready") + } + }`, + }, + ], + }, + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":36:43", + attributes: ["jQuery"], + handler: ` + function divClick1() { + alert(7); + }`, + }, + { + type: "click", + filename: TEST_URL + ":37:43", + attributes: ["jQuery"], + handler: ` + function divClick2() { + alert(8); + }`, + }, + { + type: "keydown", + filename: TEST_URL + ":38:44", + attributes: ["jQuery"], + handler: ` + function divKeyDown() { + alert(9); + }`, + }, + ], + }, + { + selector: "#livediv", + expected: [ + { + type: "dblclick", + filename: TEST_URL + ":30:49", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDblClick() { + alert(1); + }`, + }, + { + type: "dragend", + filename: TEST_URL + ":33:48", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDragEnd() { + alert(4); + }`, + }, + { + type: "dragleave", + filename: TEST_URL + ":32:50", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDragLeave() { + alert(3); + }`, + }, + { + type: "dragover", + filename: TEST_URL + ":35:49", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDragOver() { + alert(6); + }`, + }, + { + type: "dragstart", + filename: TEST_URL + ":31:50", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDragStart() { + alert(2); + }`, + }, + { + type: "drop", + filename: TEST_URL + ":34:45", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDrop() { + alert(5); + }`, + }, + ], + }, +]; +/* eslint-enable */ + +add_task(async function () { + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_2.1.1.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_2.1.1.js new file mode 100644 index 0000000000..db038ee488 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_2.1.1.js @@ -0,0 +1,160 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +requestLongerTimeout(2); + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 2.1.1). + +const TEST_LIB = "lib_jquery_2.1.1_min.js"; +const TEST_URL = URL_ROOT_SSL + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "load", + filename: TEST_URL + ":29:38", + attributes: ["Bubbling"], + handler: ` + () => { + const handler1 = function liveDivDblClick() { + alert(1); + }; + const handler2 = function liveDivDragStart() { + alert(2); + }; + const handler3 = function liveDivDragLeave() { + alert(3); + }; + const handler4 = function liveDivDragEnd() { + alert(4); + }; + const handler5 = function liveDivDrop() { + alert(5); + }; + const handler6 = function liveDivDragOver() { + alert(6); + }; + const handler7 = function divClick1() { + alert(7); + }; + const handler8 = function divClick2() { + alert(8); + }; + const handler9 = function divKeyDown() { + alert(9); + }; + const handler10 = function divDragOut() { + alert(10); + }; + + if ($("#livediv").live) { + $("#livediv").live("dblclick", handler1); + $("#livediv").live("dragstart", handler2); + } + + if ($("#livediv").delegate) { + $(document).delegate("#livediv", "dragleave", handler3); + $(document).delegate("#livediv", "dragend", handler4); + } + + if ($("#livediv").on) { + $(document).on("drop", "#livediv", handler5); + $(document).on("dragover", "#livediv", handler6); + $(document).on("dragout", "#livediv:xxxxx", handler10); + } + + const div = $("div")[0]; + $(div).click(handler7); + $(div).click(handler8); + $(div).keydown(handler9); + }`, + }, + ], + }, + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":36:43", + attributes: ["jQuery"], + handler: ` + function divClick1() { + alert(7); + }`, + }, + { + type: "click", + filename: TEST_URL + ":37:43", + attributes: ["jQuery"], + handler: ` + function divClick2() { + alert(8); + }`, + }, + { + type: "keydown", + filename: TEST_URL + ":38:44", + attributes: ["jQuery"], + handler: ` + function divKeyDown() { + alert(9); + }`, + }, + ], + }, + { + selector: "#livediv", + expected: [ + { + type: "dragend", + filename: TEST_URL + ":33:48", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDragEnd() { + alert(4); + }`, + }, + { + type: "dragleave", + filename: TEST_URL + ":32:50", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDragLeave() { + alert(3); + }`, + }, + { + type: "dragover", + filename: TEST_URL + ":35:49", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDragOver() { + alert(6); + }`, + }, + { + type: "drop", + filename: TEST_URL + ":34:45", + attributes: ["jQuery", "Live"], + handler: ` + function liveDivDrop() { + alert(5); + }`, + }, + ], + }, +]; +/* eslint-enable */ + +add_task(async function () { + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_object_listener.js b/devtools/client/inspector/markup/test/browser_markup_events_object_listener.js new file mode 100644 index 0000000000..d139a5d9c3 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_object_listener.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ + +"use strict"; + +// Test that markup view event bubbles show the correct event info for object +// style event listeners and that no bubbles are shown for objects without any +// handleEvent method. + +const TEST_URL = URL_ROOT_SSL + "doc_markup_events_object_listener.html"; + +loadHelperScript("helper_events_test_runner.js"); + +const TEST_DATA = [ + // eslint-disable-line + { + selector: "#valid-object-listener", + expected: [ + { + type: "click", + filename: TEST_URL + ":20:23", + attributes: ["Bubbling"], + handler: `() => {\n` + ` console.log("handleEvent");\n` + `}`, + }, + ], + }, + { + selector: "#valid-invalid-object-listeners", + expected: [ + { + type: "click", + filename: TEST_URL + ":27:23", + attributes: ["Bubbling"], + handler: `() => {\n` + ` console.log("handleEvent");\n` + `}`, + }, + ], + }, +]; + +add_task(async function () { + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_react_development_15.4.1.js b/devtools/client/inspector/markup/test/browser_markup_events_react_development_15.4.1.js new file mode 100644 index 0000000000..a9eac7053b --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_react_development_15.4.1.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +requestLongerTimeout(4); + +// Test that markup view event bubbles show the correct event info for React +// events (React development version 15.4.1) without JSX. + +const TEST_LIB = URL_ROOT_SSL + "lib_react_dom_15.4.1.js"; +const TEST_EXTERNAL_LISTENERS = URL_ROOT_SSL + "react_external_listeners.js"; +const TEST_URL = + URL_ROOT_SSL + "doc_markup_events_react_development_15.4.1.html"; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "#inline", + expected: [ + { + type: "click", + filename: TEST_LIB + ":17530:42", + attributes: ["Bubbling"], + handler: `function emptyFunction() {}`, + }, + { + type: "onClick", + filename: TEST_URL + ":22:33", + attributes: ["React", "Bubbling"], + handler: ` + function() { + alert("inlineFunction"); + }`, + }, + ], + }, + { + selector: "#external", + expected: [ + { + type: "click", + filename: TEST_LIB + ":17530:42", + attributes: ["Bubbling"], + handler: `function emptyFunction() {}`, + }, + { + type: "onClick", + filename: TEST_EXTERNAL_LISTENERS + ":4:25", + attributes: ["React", "Bubbling"], + handler: ` + function externalFunction() { + alert("externalFunction"); + }`, + }, + ], + }, + { + selector: "#externalinline", + expected: [ + { + type: "click", + filename: TEST_LIB + ":17530:42", + attributes: ["Bubbling"], + handler: `function emptyFunction() {}`, + }, + { + type: "onClick", + filename: TEST_EXTERNAL_LISTENERS + ":4:25", + attributes: ["React", "Bubbling"], + handler: ` + function externalFunction() { + alert("externalFunction"); + }`, + }, + { + type: "onMouseUp", + filename: TEST_URL + ":22:33", + attributes: ["React", "Bubbling"], + handler: ` + function() { + alert("inlineFunction"); + }`, + }, + ], + }, + { + selector: "#externalcapturing", + expected: [ + { + type: "onClickCapture", + filename: TEST_EXTERNAL_LISTENERS + ":8:34", + attributes: ["React", "Capturing"], + handler: ` + function externalCapturingFunction() { + alert("externalCapturingFunction"); + }`, + }, + ], + }, +]; +/* eslint-enable */ + +add_task(async function () { + info( + "Switch to 2 pane inspector to avoid sidebar width issues with opening events" + ); + await pushPref("devtools.inspector.three-pane-enabled", false); + await pushPref("devtools.toolsidebar-width.inspector", 350); + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_react_development_15.4.1_jsx.js b/devtools/client/inspector/markup/test/browser_markup_events_react_development_15.4.1_jsx.js new file mode 100644 index 0000000000..fee94fea98 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_react_development_15.4.1_jsx.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +requestLongerTimeout(4); + +// Test that markup view event bubbles show the correct event info for React +// events (React development version 15.4.1) using JSX. + +const TEST_LIB = URL_ROOT_SSL + "lib_react_dom_15.4.1.js"; +const TEST_LIB_BABEL = URL_ROOT_SSL + "lib_babel_6.21.0_min.js"; +const TEST_EXTERNAL_LISTENERS = URL_ROOT_SSL + "react_external_listeners.js"; +const TEST_URL = + URL_ROOT_SSL + "doc_markup_events_react_development_15.4.1_jsx.html"; +const TEST_INLINE_BABEL_ORIGINAL = URL_ROOT_SSL + "Inline%20Babel%20script:9"; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "#inlinejsx", + isSourceMapped: true, + expected: [ + { + type: "click", + filename: TEST_LIB + ":17530:42", + attributes: ["Bubbling"], + handler: `function emptyFunction() {}`, + }, + { + type: "onClick", + filename: TEST_INLINE_BABEL_ORIGINAL, + attributes: ["React", "Bubbling"], + handler: ` + function inlineFunction() { + alert("inlineFunction"); + }`, + }, + ], + }, + { + selector: "#externaljsx", + expected: [ + { + type: "click", + filename: TEST_LIB + ":17530:42", + attributes: ["Bubbling"], + handler: `function emptyFunction() {}`, + }, + { + type: "onClick", + filename: TEST_EXTERNAL_LISTENERS + ":4:25", + attributes: ["React", "Bubbling"], + handler: ` + function externalFunction() { + alert("externalFunction"); + }`, + }, + ], + }, + { + selector: "#externalinlinejsx", + expected: [ + { + type: "click", + filename: TEST_LIB + ":17530:42", + attributes: ["Bubbling"], + handler: `function emptyFunction() {}`, + }, + { + type: "onClick", + filename: TEST_EXTERNAL_LISTENERS + ":4:25", + attributes: ["React", "Bubbling"], + handler: ` + function externalFunction() { + alert("externalFunction"); + }`, + }, + { + type: "onMouseUp", + filename: TEST_LIB_BABEL + ":11:41", + attributes: ["React", "Bubbling"], + handler: ` + function inlineFunction() { + alert("inlineFunction"); + }`, + }, + ], + }, + { + selector: "#externalcapturingjsx", + expected: [ + { + type: "onClickCapture", + filename: TEST_EXTERNAL_LISTENERS + ":8:34", + attributes: ["React", "Capturing"], + handler: ` + function externalCapturingFunction() { + alert("externalCapturingFunction"); + }`, + }, + ], + }, +]; +/* eslint-enable */ + +add_task(async function () { + info( + "Switch to 2 pane inspector to avoid sidebar width issues with opening events" + ); + await pushPref("devtools.inspector.three-pane-enabled", false); + await pushPref("devtools.toolsidebar-width.inspector", 350); + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_react_production_15.3.1.js b/devtools/client/inspector/markup/test/browser_markup_events_react_production_15.3.1.js new file mode 100644 index 0000000000..bbd0145bb3 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_react_production_15.3.1.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +requestLongerTimeout(4); + +// Test that markup view event bubbles show the correct event info for React +// events (React production version 15.3.1) without JSX. + +const TEST_LIB = URL_ROOT_SSL + "lib_react_with_addons_15.3.1_min.js"; +const TEST_EXTERNAL_LISTENERS = URL_ROOT_SSL + "react_external_listeners.js"; +const TEST_URL = + URL_ROOT_SSL + "doc_markup_events_react_production_15.3.1.html"; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "#inline", + expected: [ + { + type: "click", + filename: TEST_LIB + ":16:27180", + attributes: ["Bubbling"], + handler: `function() {}`, + }, + { + type: "onClick", + filename: TEST_URL + ":22:33", + attributes: ["React", "Bubbling"], + handler: ` + function() { + alert("inlineFunction"); + }`, + }, + ], + }, + { + selector: "#external", + expected: [ + { + type: "click", + filename: TEST_LIB + ":16:27180", + attributes: ["Bubbling"], + handler: `function() {}`, + }, + { + type: "onClick", + filename: TEST_EXTERNAL_LISTENERS + ":4:25", + attributes: ["React", "Bubbling"], + handler: ` + function externalFunction() { + alert("externalFunction"); + }`, + }, + ], + }, + { + selector: "#externalinline", + expected: [ + { + type: "click", + filename: TEST_LIB + ":16:27180", + attributes: ["Bubbling"], + handler: `function() {}`, + }, + { + type: "onClick", + filename: TEST_EXTERNAL_LISTENERS + ":4:25", + attributes: ["React", "Bubbling"], + handler: ` + function externalFunction() { + alert("externalFunction"); + }`, + }, + { + type: "onMouseUp", + filename: TEST_URL + ":22:33", + attributes: ["React", "Bubbling"], + handler: ` + function() { + alert("inlineFunction"); + }`, + }, + ], + }, + { + selector: "#externalcapturing", + expected: [ + { + type: "onClickCapture", + filename: TEST_EXTERNAL_LISTENERS + ":8:34", + attributes: ["React", "Capturing"], + handler: ` + function externalCapturingFunction() { + alert("externalCapturingFunction"); + }`, + }, + ], + }, +]; +/* eslint-enable */ + +add_task(async function () { + info( + "Switch to 2 pane inspector to avoid sidebar width issues with opening events" + ); + await pushPref("devtools.inspector.three-pane-enabled", false); + await pushPref("devtools.toolsidebar-width.inspector", 350); + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_react_production_15.3.1_jsx.js b/devtools/client/inspector/markup/test/browser_markup_events_react_production_15.3.1_jsx.js new file mode 100644 index 0000000000..f2b405c617 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_react_production_15.3.1_jsx.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +requestLongerTimeout(4); + +// Test that markup view event bubbles show the correct event info for React +// events (React production version 15.3.1) using JSX. + +const TEST_LIB = URL_ROOT_SSL + "lib_react_with_addons_15.3.1_min.js"; +const TEST_LIB_BABEL = URL_ROOT_SSL + "lib_babel_6.21.0_min.js"; +const TEST_EXTERNAL_LISTENERS = URL_ROOT_SSL + "react_external_listeners.js"; +const TEST_URL = + URL_ROOT_SSL + "doc_markup_events_react_production_15.3.1_jsx.html"; +const TEST_INLINE_BABEL_ORIGINAL = URL_ROOT_SSL + "Inline%20Babel%20script:9"; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "#inlinejsx", + isSourceMapped: true, + expected: [ + { + type: "click", + filename: TEST_LIB + ":16:27180", + attributes: ["Bubbling"], + handler: `function() {}`, + }, + { + type: "onClick", + filename: TEST_INLINE_BABEL_ORIGINAL, + attributes: ["React", "Bubbling"], + handler: ` + function() { + alert("inlineFunction"); + }`, + }, + ], + }, + { + selector: "#externaljsx", + expected: [ + { + type: "click", + filename: TEST_LIB + ":16:27180", + attributes: ["Bubbling"], + handler: `function() {}`, + }, + { + type: "onClick", + filename: TEST_EXTERNAL_LISTENERS + ":4:25", + attributes: ["React", "Bubbling"], + handler: ` + function externalFunction() { + alert("externalFunction"); + }`, + }, + ], + }, + { + selector: "#externalinlinejsx", + expected: [ + { + type: "click", + filename: TEST_LIB + ":16:27180", + attributes: ["Bubbling"], + handler: `function() {}`, + }, + { + type: "onClick", + filename: TEST_EXTERNAL_LISTENERS + ":4:25", + attributes: ["React", "Bubbling"], + handler: ` + function externalFunction() { + alert("externalFunction"); + }`, + }, + { + type: "onMouseUp", + filename: TEST_LIB_BABEL + ":11:41", + attributes: ["React", "Bubbling"], + handler: ` + function() { + alert("inlineFunction"); + }`, + }, + ], + }, + { + selector: "#externalcapturingjsx", + expected: [ + { + type: "onClickCapture", + filename: TEST_EXTERNAL_LISTENERS + ":8:34", + attributes: ["React", "Capturing"], + handler: ` + function externalCapturingFunction() { + alert("externalCapturingFunction"); + }`, + }, + ], + }, +]; +/* eslint-enable */ + +add_task(async function () { + info( + "Switch to 2 pane inspector to avoid sidebar width issues with opening events" + ); + await pushPref("devtools.inspector.three-pane-enabled", false); + await pushPref("devtools.toolsidebar-width.inspector", 350); + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_react_production_16.2.0.js b/devtools/client/inspector/markup/test/browser_markup_events_react_production_16.2.0.js new file mode 100644 index 0000000000..3034330f14 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_react_production_16.2.0.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +requestLongerTimeout(4); + +// Test that markup view event bubbles show the correct event info for React +// events (React production version 16.2.0) without JSX. + +const TEST_LIB = URL_ROOT_SSL + "lib_react_dom_16.2.0_min.js"; +const TEST_EXTERNAL_LISTENERS = URL_ROOT_SSL + "react_external_listeners.js"; +const TEST_URL = + URL_ROOT_SSL + "doc_markup_events_react_production_16.2.0.html"; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "#inline", + expected: [ + { + type: "click", + filename: TEST_LIB + ":93:417", + attributes: ["Bubbling"], + handler: `function() {}`, + }, + { + type: "onClick", + filename: TEST_URL + ":21:22", + attributes: ["React", "Bubbling"], + handler: ` + inlineFunction() { + alert("inlineFunction"); + }`, + }, + ], + }, + { + selector: "#external", + expected: [ + { + type: "click", + filename: TEST_LIB + ":93:417", + attributes: ["Bubbling"], + handler: `function() {}`, + }, + { + type: "onClick", + filename: TEST_EXTERNAL_LISTENERS + ":4:25", + attributes: ["React", "Bubbling"], + handler: ` + function externalFunction() { + alert("externalFunction"); + }`, + }, + ], + }, + { + selector: "#externalinline", + expected: [ + { + type: "click", + filename: TEST_LIB + ":93:417", + attributes: ["Bubbling"], + handler: `function() {}`, + }, + { + type: "onClick", + filename: TEST_EXTERNAL_LISTENERS + ":4:25", + attributes: ["React", "Bubbling"], + handler: ` + function externalFunction() { + alert("externalFunction"); + }`, + }, + { + type: "onMouseUp", + filename: TEST_URL + ":21:22", + attributes: ["React", "Bubbling"], + handler: ` + inlineFunction() { + alert("inlineFunction"); + }`, + }, + ], + }, + { + selector: "#externalcapturing", + expected: [ + { + type: "onClickCapture", + filename: TEST_EXTERNAL_LISTENERS + ":8:34", + attributes: ["React", "Capturing"], + handler: ` + function externalCapturingFunction() { + alert("externalCapturingFunction"); + }`, + }, + ], + }, + { + selector: "#doublebind", + expected: [ + { + type: "click", + filename: TEST_LIB + ":93:417", + attributes: ["Bubbling"], + handler: `function() {}`, + }, + { + type: "onClick", + filename: TEST_URL + ":21:22", + attributes: ["React", "Bubbling"], + handler: ` + function() { + alert("inlineFunction"); + }`, + }, + ], + }, +]; +/* eslint-enable */ + +add_task(async function () { + info( + "Switch to 2 pane inspector to avoid sidebar width issues with opening events" + ); + await pushPref("devtools.inspector.three-pane-enabled", false); + await pushPref("devtools.toolsidebar-width.inspector", 350); + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_react_production_16.2.0_jsx.js b/devtools/client/inspector/markup/test/browser_markup_events_react_production_16.2.0_jsx.js new file mode 100644 index 0000000000..4aedb216e2 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_react_production_16.2.0_jsx.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +requestLongerTimeout(4); + +// Test that markup view event bubbles show the correct event info for React +// events (React production version 16.2.0) using JSX. + +const TEST_LIB = URL_ROOT_SSL + "lib_react_dom_16.2.0_min.js"; +const TEST_LIB_BABEL = URL_ROOT_SSL + "lib_babel_6.21.0_min.js"; +const TEST_EXTERNAL_LISTENERS = URL_ROOT_SSL + "react_external_listeners.js"; +const TEST_URL = + URL_ROOT_SSL + "doc_markup_events_react_production_16.2.0_jsx.html"; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "#inlinejsx", + expected: [ + { + type: "click", + filename: TEST_LIB + ":93:417", + attributes: ["Bubbling"], + handler: `function() {}`, + }, + { + type: "onClick", + filename: TEST_LIB_BABEL + ":26:34", + attributes: ["React", "Bubbling"], + handler: ` + function inlineFunction() { + alert("inlineFunction"); + }`, + }, + ], + }, + { + selector: "#externaljsx", + expected: [ + { + type: "click", + filename: TEST_LIB + ":93:417", + attributes: ["Bubbling"], + handler: `function() {}`, + }, + { + type: "onClick", + filename: TEST_EXTERNAL_LISTENERS + ":4:25", + attributes: ["React", "Bubbling"], + handler: ` + function externalFunction() { + alert("externalFunction"); + }`, + }, + ], + }, + { + selector: "#externalinlinejsx", + expected: [ + { + type: "click", + filename: TEST_LIB + ":93:417", + attributes: ["Bubbling"], + handler: `function() {}`, + }, + { + type: "onClick", + filename: TEST_EXTERNAL_LISTENERS + ":4:25", + attributes: ["React", "Bubbling"], + handler: ` + function externalFunction() { + alert("externalFunction"); + }`, + }, + { + type: "onMouseUp", + filename: TEST_LIB_BABEL + ":26:34", + attributes: ["React", "Bubbling"], + handler: ` + function inlineFunction() { + alert("inlineFunction"); + }`, + }, + ], + }, + { + selector: "#externalcapturingjsx", + expected: [ + { + type: "onClickCapture", + filename: TEST_EXTERNAL_LISTENERS + ":8:34", + attributes: ["React", "Capturing"], + handler: ` + function externalCapturingFunction() { + alert("externalCapturingFunction"); + }`, + }, + ], + }, +]; +/* eslint-enable */ + +add_task(async function () { + info( + "Switch to 2 pane inspector to avoid sidebar width issues with opening events" + ); + await pushPref("devtools.inspector.three-pane-enabled", false); + await pushPref("devtools.toolsidebar-width.inspector", 350); + await runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_source_map.js b/devtools/client/inspector/markup/test/browser_markup_events_source_map.js new file mode 100644 index 0000000000..39fb3c0575 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_source_map.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that source maps work in the event popup. + +const INITIAL_URL = URL_ROOT_SSL + "doc_markup_void_elements.html"; +const TEST_URL = URL_ROOT_SSL + "doc_markup_events-source_map.html"; + +/* import-globals-from helper_events_test_runner.js */ +loadHelperScript("helper_events_test_runner.js"); + +const TEST_DATA = [ + { + selector: "#clicky", + isSourceMapped: true, + expected: [ + { + type: "click", + filename: "webpack:///events_original.js:7", + attributes: ["Bubbling"], + handler: `function clickme() { + console.log("clickme"); +}`, + }, + ], + }, +]; + +add_task(async function () { + // Load some other URL before opening the toolbox, then navigate to + // the test URL. This ensures that source map service will see the + // sources as they are loaded, avoiding any races. + const { toolbox, inspector } = await openInspectorForURL(INITIAL_URL); + + // Ensure the source map service is operating. This looks a bit + // funny, but sourceMapURLService is a getter, and we don't need the + // result. + toolbox.sourceMapURLService; + + await navigateTo(TEST_URL); + + await inspector.markup.expandAll(); + + for (const test of TEST_DATA) { + await checkEventsForNode(test, inspector); + } + + // Wait for promises to avoid leaks when running this as a single test. + // We need to do this because we have opened a bunch of popups and don't them + // to affect other test runs when they are GCd. + await promiseNextTick(); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_toggle.js b/devtools/client/inspector/markup/test/browser_markup_events_toggle.js new file mode 100644 index 0000000000..5e1e437298 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_toggle.js @@ -0,0 +1,295 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ + +"use strict"; + +// Test that event listeners can be disabled and re-enabled from the markup view event bubble. + +const TEST_URL = URL_ROOT_SSL + "doc_markup_events_toggle.html"; + +loadHelperScript("helper_events_test_runner.js"); + +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL(TEST_URL); + const { resourceCommand } = toolbox.commands; + await inspector.markup.expandAll(); + await selectNode("#target", inspector); + + info( + "Click on the target element to make sure the event listeners are properly set" + ); + // There's a "mouseup" event listener that is `console.info` (so we can check "native" events). + // In order to know if it was called, we listen for the next console.info resource. + let { onResource: onConsoleInfoMessage } = + await resourceCommand.waitForNextResource( + resourceCommand.TYPES.CONSOLE_MESSAGE, + { + ignoreExistingResources: true, + predicate(resource) { + return resource.message.level == "info"; + }, + } + ); + await safeSynthesizeMouseEventAtCenterInContentPage("#target"); + + let data = await getTargetElementHandledEventData(); + is(data.click, 1, `target handled one "click" event`); + is(data.mousedown, 1, `target handled one "mousedown" event`); + await onConsoleInfoMessage; + ok(true, `the "mouseup" event listener (console.info) was called`); + + info("Check that the event tooltip has the expected content"); + const container = await getContainerForSelector("#target", inspector); + const eventTooltipBadge = container.elt.querySelector( + ".inspector-badge.interactive[data-event]" + ); + ok(eventTooltipBadge, "The event tooltip badge is displayed"); + + const tooltip = inspector.markup.eventDetailsTooltip; + let onTooltipShown = tooltip.once("shown"); + eventTooltipBadge.click(); + await onTooltipShown; + ok(true, "The tooltip is shown"); + + Assert.deepEqual( + getAsciiHeadersViz(tooltip), + ["click [x]", "mousedown [x]", "mouseup [x]"], + "The expected events are displayed, all enabled" + ); + ok( + !eventTooltipBadge.classList.contains("has-disabled-events"), + "The event badge does not have the has-disabled-events class" + ); + + const [clickHeader, mousedownHeader, mouseupHeader] = + getHeadersInEventTooltip(tooltip); + + info("Uncheck the mousedown event checkbox"); + await toggleEventListenerCheckbox(tooltip, mousedownHeader); + Assert.deepEqual( + getAsciiHeadersViz(tooltip), + ["click [x]", "mousedown []", "mouseup [x]"], + "mousedown checkbox was unchecked" + ); + ok( + eventTooltipBadge.classList.contains("has-disabled-events"), + "Unchecking an event applied the has-disabled-events class to the badge" + ); + await safeSynthesizeMouseEventAtCenterInContentPage("#target"); + data = await getTargetElementHandledEventData(); + is(data.click, 2, `target handled another "click" event…`); + is(data.mousedown, 1, `… but not a mousedown one`); + + info( + "Check that the event badge style is reset when re-enabling all disabled events" + ); + await toggleEventListenerCheckbox(tooltip, mousedownHeader); + Assert.deepEqual( + getAsciiHeadersViz(tooltip), + ["click [x]", "mousedown [x]", "mouseup [x]"], + "mousedown checkbox is checked again" + ); + ok( + !eventTooltipBadge.classList.contains("has-disabled-events"), + "The event badge does not have the has-disabled-events class after re-enabling disabled event" + ); + info("Disable mousedown again for the rest of the test"); + await toggleEventListenerCheckbox(tooltip, mousedownHeader); + Assert.deepEqual( + getAsciiHeadersViz(tooltip), + ["click [x]", "mousedown []", "mouseup [x]"], + "mousedown checkbox is unchecked again" + ); + + info("Uncheck the click event checkbox"); + await toggleEventListenerCheckbox(tooltip, clickHeader); + Assert.deepEqual( + getAsciiHeadersViz(tooltip), + ["click []", "mousedown []", "mouseup [x]"], + "click checkbox was unchecked" + ); + ok( + eventTooltipBadge.classList.contains("has-disabled-events"), + "event badge still has the has-disabled-events class" + ); + await safeSynthesizeMouseEventAtCenterInContentPage("#target"); + data = await getTargetElementHandledEventData(); + is(data.click, 2, `click event listener was disabled`); + is(data.mousedown, 1, `and mousedown still is disabled as well`); + + info("Uncheck the mouseup event checkbox"); + await toggleEventListenerCheckbox(tooltip, mouseupHeader); + Assert.deepEqual( + getAsciiHeadersViz(tooltip), + ["click []", "mousedown []", "mouseup []"], + "mouseup checkbox was unchecked" + ); + + ({ onResource: onConsoleInfoMessage } = + await resourceCommand.waitForNextResource( + resourceCommand.TYPES.CONSOLE_MESSAGE, + { + ignoreExistingResources: true, + predicate(resource) { + return resource.message.level == "info"; + }, + } + )); + const onTimeout = wait(500).then(() => "TIMEOUT"); + await safeSynthesizeMouseEventAtCenterInContentPage("#target"); + const raceResult = await Promise.race([onConsoleInfoMessage, onTimeout]); + is( + raceResult, + "TIMEOUT", + "The mouseup event didn't trigger a console.info call, meaning the event listener was disabled" + ); + + info("Re-enable the mousedown event"); + await toggleEventListenerCheckbox(tooltip, mousedownHeader); + Assert.deepEqual( + getAsciiHeadersViz(tooltip), + ["click []", "mousedown [x]", "mouseup []"], + "mousedown checkbox is checked again" + ); + ok( + eventTooltipBadge.classList.contains("has-disabled-events"), + "event badge still has the has-disabled-events class" + ); + await safeSynthesizeMouseEventAtCenterInContentPage("#target"); + data = await getTargetElementHandledEventData(); + is(data.click, 2, `no additional "click" event were handled`); + is( + data.mousedown, + 2, + `but we did get a new "mousedown", the event listener was re-enabled` + ); + + info("Hide the tooltip and show it again"); + const tooltipHidden = tooltip.once("hidden"); + tooltip.hide(); + await tooltipHidden; + + onTooltipShown = tooltip.once("shown"); + eventTooltipBadge.click(); + await onTooltipShown; + ok(true, "The tooltip is shown again"); + + Assert.deepEqual( + getAsciiHeadersViz(tooltip), + ["click []", "mousedown [x]", "mouseup []"], + "Only mousedown checkbox is checked" + ); + + info("Re-enable mouseup events"); + await toggleEventListenerCheckbox( + tooltip, + getHeadersInEventTooltip(tooltip).at(-1) + ); + Assert.deepEqual( + getAsciiHeadersViz(tooltip), + ["click []", "mousedown [x]", "mouseup [x]"], + "mouseup is checked again" + ); + + ({ onResource: onConsoleInfoMessage } = + await resourceCommand.waitForNextResource( + resourceCommand.TYPES.CONSOLE_MESSAGE, + { + ignoreExistingResources: true, + predicate(resource) { + return resource.message.level == "info"; + }, + } + )); + await safeSynthesizeMouseEventAtCenterInContentPage("#target"); + await onConsoleInfoMessage; + ok(true, "The mouseup event was re-enabled"); + data = await getTargetElementHandledEventData(); + is(data.click, 2, `"click" is still disabled`); + is( + data.mousedown, + 3, + `we received a new "mousedown" event as part of the click` + ); + + info("Close DevTools to check that event listeners are re-enabled"); + await closeToolbox(); + await safeSynthesizeMouseEventAtCenterInContentPage("#target"); + data = await getTargetElementHandledEventData(); + is( + data.click, + 3, + `a new "click" event was handled after the devtools was closed` + ); + is( + data.mousedown, + 4, + `a new "mousedown" event was handled after the devtools was closed` + ); +}); + +function getHeadersInEventTooltip(tooltip) { + return Array.from(tooltip.panel.querySelectorAll(".event-header")); +} + +/** + * Get an array of string representing a header in its state, e.g. + * [ + * "click [x]", + * "mousedown []", + * ] + * + * represents an event tooltip with a click and a mousedown event, where the mousedown + * event has been disabled. + * + * @param {EventTooltip} tooltip + * @returns Array + */ +function getAsciiHeadersViz(tooltip) { + return getHeadersInEventTooltip(tooltip).map( + el => + `${el.querySelector(".event-tooltip-event-type").textContent} [${ + getHeaderCheckbox(el).checked ? "x" : "" + }]` + ); +} + +function getHeaderCheckbox(headerEl) { + return headerEl.querySelector("input[type=checkbox]"); +} + +async function toggleEventListenerCheckbox(tooltip, headerEl) { + const onEventToggled = tooltip.eventTooltip.once( + "event-tooltip-listener-toggled" + ); + const checkbox = getHeaderCheckbox(headerEl); + const previousValue = checkbox.checked; + EventUtils.synthesizeMouseAtCenter( + getHeaderCheckbox(headerEl), + {}, + headerEl.ownerGlobal + ); + await onEventToggled; + is(checkbox.checked, !previousValue, "The checkbox was toggled"); + is( + headerEl.classList.contains("content-expanded"), + false, + "Clicking on the checkbox did not expand the header" + ); +} + +/** + * @returns Promise The object keys are event names (e.g. "click", "mousedown"), and + * the values are number representing the number of time the event was handled. + * Note that "mouseup" isn't handled here. + */ +function getTargetElementHandledEventData() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + // In doc_markup_events_toggle.html , we count the events handled by the target in + // a stringified object in dataset.handledEvents. + return JSON.parse( + content.document.getElementById("target").dataset.handledEvents + ); + }); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_flex_display_badge.js b/devtools/client/inspector/markup/test/browser_markup_flex_display_badge.js new file mode 100644 index 0000000000..a40f50dcee --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_flex_display_badge.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the flex display badge toggles on the flexbox highlighter. + +const TEST_URI = ` + +
+`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector } = await openLayoutView(); + const { store } = inspector; + const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX; + const { + getActiveHighlighter, + getNodeForActiveHighlighter, + waitForHighlighterTypeShown, + waitForHighlighterTypeHidden, + } = getHighlighterTestHelpers(inspector); + + info("Check the flex display badge is shown and not active."); + await selectNode("#flex", inspector); + + info("Wait until the flexbox store has been updated"); + await waitUntilState( + store, + state => + state.flexbox.flexContainer.nodeFront === inspector.selection.nodeFront + ); + + const flexContainer = await getContainerForSelector("#flex", inspector); + const flexDisplayBadge = flexContainer.elt.querySelector( + ".inspector-badge.interactive[data-display]" + ); + ok( + !flexDisplayBadge.classList.contains("active"), + "flex display badge is not active." + ); + ok( + flexDisplayBadge.classList.contains("interactive"), + "flex display badge is interactive." + ); + + info("Check the initial state of the flex highlighter."); + ok( + !getActiveHighlighter(HIGHLIGHTER_TYPE), + "No flexbox highlighter exists in the highlighters overlay." + ); + ok( + !getNodeForActiveHighlighter(HIGHLIGHTER_TYPE), + "No flexbox highlighter is shown." + ); + + info("Toggling ON the flexbox highlighter from the flex display badge."); + const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE); + let onCheckboxChange = waitUntilState( + store, + state => state.flexbox.highlighted + ); + flexDisplayBadge.click(); + await onHighlighterShown; + await onCheckboxChange; + + info( + "Check the flexbox highlighter is created and flex display badge state." + ); + ok( + getActiveHighlighter(HIGHLIGHTER_TYPE), + "Flexbox highlighter is created in the highlighters overlay." + ); + ok( + getNodeForActiveHighlighter(HIGHLIGHTER_TYPE), + "Flexbox highlighter is shown." + ); + ok( + flexDisplayBadge.classList.contains("active"), + "flex display badge is active." + ); + ok( + flexDisplayBadge.classList.contains("interactive"), + "flex display badge is interactive." + ); + + info("Toggling OFF the flexbox highlighter from the flex display badge."); + const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE); + onCheckboxChange = waitUntilState(store, state => !state.flexbox.highlighted); + flexDisplayBadge.click(); + await onHighlighterHidden; + await onCheckboxChange; + + ok( + !flexDisplayBadge.classList.contains("active"), + "flex display badge is not active." + ); + ok( + flexDisplayBadge.classList.contains("interactive"), + "flex display badge is interactive." + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_flex_display_badge_telemetry.js b/devtools/client/inspector/markup/test/browser_markup_flex_display_badge_telemetry.js new file mode 100644 index 0000000000..e65173fcde --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_flex_display_badge_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 markup view. + +const TEST_URI = ` + +
+`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + startTelemetry(); + const { inspector } = await openLayoutView(); + const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX; + const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } = + getHighlighterTestHelpers(inspector); + + await selectNode("#flex", inspector); + const flexContainer = await getContainerForSelector("#flex", inspector); + const flexDisplayBadge = flexContainer.elt.querySelector( + ".inspector-badge.interactive[data-display]" + ); + + info("Toggling ON the flexbox highlighter from the flex display badge."); + const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE); + flexDisplayBadge.click(); + await onHighlighterShown; + + info("Toggling OFF the flexbox highlighter from the flex display badge."); + const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE); + flexDisplayBadge.click(); + await onHighlighterHidden; + + checkResults(); +}); + +function checkResults() { + checkTelemetry("devtools.markup.flexboxhighlighter.opened", "", 1, "scalar"); + checkTelemetry( + "DEVTOOLS_FLEXBOX_HIGHLIGHTER_TIME_ACTIVE_SECONDS", + "", + null, + "hasentries" + ); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_grid_display_badge_01.js b/devtools/client/inspector/markup/test/browser_markup_grid_display_badge_01.js new file mode 100644 index 0000000000..a68b972006 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_grid_display_badge_01.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the grid display badge toggles on the grid highlighter. + +const TEST_URI = ` + +
+`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector } = await openLayoutView(); + const { highlighters, store } = inspector; + const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.GRID; + const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } = + getHighlighterTestHelpers(inspector); + + info("Check the grid display badge is shown and not active."); + await selectNode("#grid", inspector); + const gridContainer = await getContainerForSelector("#grid", inspector); + const gridDisplayBadge = gridContainer.elt.querySelector( + ".inspector-badge.interactive[data-display]" + ); + ok( + !gridDisplayBadge.classList.contains("active"), + "grid display badge is not active." + ); + ok( + gridDisplayBadge.classList.contains("interactive"), + "grid display badge is interactive." + ); + + info("Check the initial state of the grid highlighter."); + ok( + !highlighters.gridHighlighters.size, + "No CSS grid highlighter exists in the highlighters overlay." + ); + + info("Toggling ON the CSS grid highlighter from the grid display badge."); + const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE); + let onCheckboxChange = waitUntilState( + store, + state => state.grids.length === 1 && state.grids[0].highlighted + ); + gridDisplayBadge.click(); + await onHighlighterShown; + await onCheckboxChange; + + info( + "Check that the CSS grid highlighter is created and the display badge state." + ); + is( + highlighters.gridHighlighters.size, + 1, + "CSS grid highlighter is created in the highlighters overlay." + ); + ok( + gridDisplayBadge.classList.contains("active"), + "grid display badge is active." + ); + ok( + gridDisplayBadge.classList.contains("interactive"), + "grid display badge is interactive." + ); + + info("Toggling OFF the CSS grid highlighter from the grid display badge."); + const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE); + onCheckboxChange = waitUntilState( + store, + state => state.grids.length == 1 && !state.grids[0].highlighted + ); + gridDisplayBadge.click(); + await onHighlighterHidden; + await onCheckboxChange; + + ok( + !gridDisplayBadge.classList.contains("active"), + "grid display badge is not active." + ); + ok( + gridDisplayBadge.classList.contains("interactive"), + "grid display badge is interactive." + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_grid_display_badge_02.js b/devtools/client/inspector/markup/test/browser_markup_grid_display_badge_02.js new file mode 100644 index 0000000000..7c66688f4e --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_grid_display_badge_02.js @@ -0,0 +1,256 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests toggling multiple grid highlighters in the markup view with the grid display +// badges. + +const TEST_URI = ` + +
+
cell1
+
cell2
+
+
+
cell1
+
cell2
+
+
+
cell1
+
cell2
+
+`; + +add_task(async function () { + await pushPref("devtools.gridinspector.maxHighlighters", 2); + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector } = await openLayoutView(); + const { highlighters } = inspector; + const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.GRID; + const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } = + getHighlighterTestHelpers(inspector); + + const grid1 = await getContainerForSelector("#grid1", inspector); + const grid2 = await getContainerForSelector("#grid2", inspector); + const grid3 = await getContainerForSelector("#grid3", inspector); + const gridDisplayBadge1 = grid1.elt.querySelector( + ".inspector-badge.interactive[data-display]" + ); + const gridDisplayBadge2 = grid2.elt.querySelector( + ".inspector-badge.interactive[data-display]" + ); + const gridDisplayBadge3 = grid3.elt.querySelector( + ".inspector-badge.interactive[data-display]" + ); + + info( + "Check the initial state of the grid display badges and grid highlighters" + ); + ok( + !gridDisplayBadge1.classList.contains("active"), + "#grid1 display badge is not active." + ); + ok( + !gridDisplayBadge2.classList.contains("active"), + "#grid2 display badge is not active." + ); + ok( + !gridDisplayBadge3.classList.contains("active"), + "#grid3 display badge is not active." + ); + ok( + gridDisplayBadge1.classList.contains("interactive"), + "#grid1 display badge is interactive" + ); + ok( + gridDisplayBadge2.classList.contains("interactive"), + "#grid2 display badge is interactive" + ); + ok( + gridDisplayBadge3.classList.contains("interactive"), + "#grid3 display badge is interactive" + ); + ok( + !highlighters.gridHighlighters.size, + "No CSS grid highlighter exists in the highlighters overlay." + ); + + info("Toggling ON the CSS grid highlighter from the #grid1 display badge."); + let onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE); + gridDisplayBadge1.click(); + await onHighlighterShown; + + ok( + gridDisplayBadge1.classList.contains("active"), + "#grid1 display badge is active." + ); + ok( + !gridDisplayBadge2.classList.contains("active"), + "#grid2 display badge is not active." + ); + ok( + !gridDisplayBadge3.classList.contains("active"), + "#grid3 display badge is not active." + ); + ok( + gridDisplayBadge1.classList.contains("interactive"), + "#grid1 display badge is interactive" + ); + ok( + gridDisplayBadge2.classList.contains("interactive"), + "#grid2 display badge is interactive" + ); + ok( + gridDisplayBadge3.classList.contains("interactive"), + "#grid3 display badge is interactive" + ); + is( + highlighters.gridHighlighters.size, + 1, + "Got expected number of grid highlighters shown." + ); + + info("Toggling ON the CSS grid highlighter from the #grid2 display badge."); + onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE); + gridDisplayBadge2.click(); + await onHighlighterShown; + + ok( + gridDisplayBadge1.classList.contains("active"), + "#grid1 display badge is active." + ); + ok( + gridDisplayBadge2.classList.contains("active"), + "#grid2 display badge is active." + ); + ok( + !gridDisplayBadge3.classList.contains("active"), + "#grid3 display badge is not active." + ); + ok( + gridDisplayBadge1.classList.contains("interactive"), + "#grid1 display badge is interactive" + ); + ok( + gridDisplayBadge2.classList.contains("interactive"), + "#grid2 display badge is interactive" + ); + ok( + !gridDisplayBadge3.classList.contains("interactive"), + "#grid3 display badge is not interactive" + ); + is( + highlighters.gridHighlighters.size, + 2, + "Got expected number of grid highlighters shown." + ); + + info( + "Attempt to toggle ON the CSS grid highlighter from the #grid3 display badge." + ); + gridDisplayBadge3.click(); + + ok( + gridDisplayBadge1.classList.contains("active"), + "#grid1 display badge is active." + ); + ok( + gridDisplayBadge2.classList.contains("active"), + "#grid2 display badge is active." + ); + ok( + !gridDisplayBadge3.classList.contains("active"), + "#grid3 display badge is not active." + ); + ok( + gridDisplayBadge1.classList.contains("interactive"), + "#grid1 display badge is interactive" + ); + ok( + gridDisplayBadge2.classList.contains("interactive"), + "#grid2 display badge is interactive" + ); + ok( + !gridDisplayBadge3.classList.contains("interactive"), + "#grid3 display badge is not interactive" + ); + is( + highlighters.gridHighlighters.size, + 2, + "Got expected number of grid highlighters shown." + ); + + info("Toggling OFF the CSS grid highlighter from the #grid2 display badge."); + let onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE); + gridDisplayBadge2.click(); + await onHighlighterHidden; + + ok( + gridDisplayBadge1.classList.contains("active"), + "#grid1 display badge is active." + ); + ok( + !gridDisplayBadge2.classList.contains("active"), + "#grid2 display badge is not active." + ); + ok( + !gridDisplayBadge3.classList.contains("active"), + "#grid3 display badge is not active." + ); + ok( + gridDisplayBadge1.classList.contains("interactive"), + "#grid1 display badge is interactive" + ); + ok( + gridDisplayBadge2.classList.contains("interactive"), + "#grid2 display badge is interactive" + ); + ok( + gridDisplayBadge3.classList.contains("interactive"), + "#grid3 display badge is interactive" + ); + is( + highlighters.gridHighlighters.size, + 1, + "Got expected number of grid highlighters shown." + ); + + info("Toggling OFF the CSS grid highlighter from the #grid1 display badge."); + onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE); + gridDisplayBadge1.click(); + await onHighlighterHidden; + + ok( + !gridDisplayBadge1.classList.contains("active"), + "#grid1 display badge is not active." + ); + ok( + !gridDisplayBadge2.classList.contains("active"), + "#grid2 display badge is not active." + ); + ok( + !gridDisplayBadge3.classList.contains("active"), + "#grid3 display badge is not active." + ); + ok( + gridDisplayBadge1.classList.contains("interactive"), + "#grid1 display badge is interactive" + ); + ok( + gridDisplayBadge2.classList.contains("interactive"), + "#grid2 display badge is interactive" + ); + ok( + gridDisplayBadge3.classList.contains("interactive"), + "#grid3 display badge is interactive" + ); + ok( + !highlighters.gridHighlighters.size, + "No CSS grid highlighter exists in the highlighters overlay." + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_grid_display_badge_03.js b/devtools/client/inspector/markup/test/browser_markup_grid_display_badge_03.js new file mode 100644 index 0000000000..6c4cb06b45 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_grid_display_badge_03.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that toggling a flex highlighter does not change a grid highlighter badge. +// Bug 1592604 + +const TEST_URI = ` + +
+
+
+
+`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector } = await openLayoutView(); + + const gridBadge = await enableHighlighterByBadge("grid", ".grid", inspector); + const flexBadge = await enableHighlighterByBadge("flex", ".flex", inspector); + + info("Check that both display badges are active"); + ok(flexBadge.classList.contains("active"), `flex display badge is active.`); + ok(gridBadge.classList.contains("active"), `grid display badge is active.`); +}); + +/** + * Enable the flex or grid highlighter by clicking on the corresponding badge + * next to a node in the markup view. Returns the badge element. + * + * @param {String} type + * Either "flex" or "grid" + * @param {String} selector + * Selector matching the flex or grid container element. + * @param {Inspector} inspector + * Instance of Inspector panel + * @return {Element} The DOM element of the display badge that shows next to the element + * mathched by the selector in the markup view. + */ +async function enableHighlighterByBadge(type, selector, inspector) { + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + + info(`Check the ${type} display badge is shown and not active.`); + const container = await getContainerForSelector(selector, inspector); + const badge = container.elt.querySelector( + ".inspector-badge.interactive[data-display]" + ); + ok(!badge.classList.contains("active"), `${type} badge is not active.`); + ok(badge.classList.contains("interactive"), `${type} badge is interactive.`); + + info(`Toggling ON the ${type} highlighter from the ${type} display badge.`); + let onHighlighterShown; + switch (type) { + case "grid": + onHighlighterShown = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.GRID + ); + break; + case "flex": + onHighlighterShown = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.FLEXBOX + ); + break; + } + + badge.click(); + await onHighlighterShown; + + ok(badge.classList.contains("active"), `${type} badge is active.`); + ok(badge.classList.contains("interactive"), `${type} badge is interactive.`); + + return badge; +} diff --git a/devtools/client/inspector/markup/test/browser_markup_grid_display_badge_telemetry.js b/devtools/client/inspector/markup/test/browser_markup_grid_display_badge_telemetry.js new file mode 100644 index 0000000000..8bb3cc1441 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_grid_display_badge_telemetry.js @@ -0,0 +1,45 @@ +/* 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 markup view. + +const TEST_URI = ` + +
+`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + startTelemetry(); + const { inspector } = await openLayoutView(); + const { highlighters, store } = inspector; + + await selectNode("#grid", inspector); + const gridContainer = await getContainerForSelector("#grid", inspector); + const gridDisplayBadge = gridContainer.elt.querySelector( + ".inspector-badge.interactive[data-display]" + ); + + info("Toggling ON the CSS grid highlighter from the grid display badge."); + const onHighlighterShown = highlighters.once("grid-highlighter-shown"); + const onCheckboxChange = waitUntilState( + store, + state => state.grids.length === 1 && state.grids[0].highlighted + ); + gridDisplayBadge.click(); + await onHighlighterShown; + await onCheckboxChange; + + checkResults(); +}); + +function checkResults() { + checkTelemetry("devtools.markup.gridinspector.opened", "", 1, "scalar"); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_html_edit_01.js b/devtools/client/inspector/markup/test/browser_markup_html_edit_01.js new file mode 100644 index 0000000000..5179902da6 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_html_edit_01.js @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_outerhtml_test_runner.js */ +"use strict"; + +// Test outerHTML edition via the markup-view + +requestLongerTimeout(2); + +loadHelperScript("helper_outerhtml_test_runner.js"); + +const TEST_DATA = [ + { + selector: "#one", + oldHTML: '
First Div
', + newHTML: '
First Div
', + async validate() { + const text = await getContentPageElementProperty("#one", "textContent"); + is(text, "First Div", "New div has expected text content"); + const num = await getNumberOfMatchingElementsInContentPage("#one em"); + is(num, 0, "No em remaining"); + }, + }, + { + selector: "#removedChildren", + oldHTML: + '
removedChild ' + + "Italic Bold Underline Normal
", + newHTML: '
removedChild
', + }, + { + selector: "#addedChildren", + oldHTML: '
addedChildren
', + newHTML: + '
addedChildren ' + + "Italic Bold Underline Normal
", + }, + { + selector: "#addedAttribute", + oldHTML: '
addedAttribute
', + newHTML: + '
' + + "addedAttribute
", + async validate({ pageNodeFront, selectedNodeFront }) { + is(pageNodeFront, selectedNodeFront, "Original element is selected"); + const html = await getContentPageElementProperty( + "#addedAttribute", + "outerHTML" + ); + is( + html, + '
addedAttribute
', + "Attributes have been added" + ); + }, + }, + { + selector: "#changedTag", + oldHTML: '
changedTag
', + newHTML: '

changedTag

', + }, + { + selector: "#siblings", + oldHTML: '
siblings
', + newHTML: + '
before sibling
' + + '
siblings (updated)
' + + '
after sibling
', + async validate({ selectedNodeFront, inspector }) { + const beforeSiblingFront = await getNodeFront( + "#siblings-before-sibling", + inspector + ); + is(beforeSiblingFront, selectedNodeFront, "Sibling has been selected"); + + const text = await getContentPageElementProperty( + "#siblings", + "textContent" + ); + is(text, "siblings (updated)", "New div has expected text content"); + + const beforeText = await getContentPageElementProperty( + "#siblings-before-sibling", + "textContent" + ); + is(beforeText, "before sibling", "Sibling has been inserted"); + + const afterText = await getContentPageElementProperty( + "#siblings-after-sibling", + "textContent" + ); + is(afterText, "after sibling", "Sibling has been inserted"); + }, + }, +]; + +const TEST_URL = + "data:text/html," + + "" + + "" + + "" + + TEST_DATA.map(outer => outer.oldHTML).join("\n") + + "" + + ""; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + inspector.markup._frame.focus(); + await runEditOuterHTMLTests(TEST_DATA, inspector); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_html_edit_02.js b/devtools/client/inspector/markup/test/browser_markup_html_edit_02.js new file mode 100644 index 0000000000..832ff21921 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_html_edit_02.js @@ -0,0 +1,157 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_outerhtml_test_runner.js */ +"use strict"; + +// Test outerHTML edition via the markup-view + +loadHelperScript("helper_outerhtml_test_runner.js"); +requestLongerTimeout(2); + +const TEST_DATA = [ + { + selector: "#badMarkup1", + oldHTML: '
badMarkup1
', + newHTML: '
badMarkup1
hanging', + async validate({ pageNodeFront, selectedNodeFront }) { + is(pageNodeFront, selectedNodeFront, "Original element is selected"); + + const [textNodeName, textNodeData] = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + const node = + content.document.querySelector("#badMarkup1").nextSibling; + return [node.nodeName, node.data]; + } + ); + is(textNodeName, "#text", "Sibling is a text element"); + is(textNodeData, " hanging", "New text node has expected text content"); + }, + }, + { + selector: "#badMarkup2", + oldHTML: '
badMarkup2
', + newHTML: + '
badMarkup2
hanging
' + + "", + async validate({ pageNodeFront, selectedNodeFront }) { + is(pageNodeFront, selectedNodeFront, "Original element is selected"); + + const [textNodeName, textNodeData] = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + const node = + content.document.querySelector("#badMarkup2").nextSibling; + return [node.nodeName, node.data]; + } + ); + is(textNodeName, "#text", "Sibling is a text element"); + is(textNodeData, " hanging", "New text node has expected text content"); + }, + }, + { + selector: "#badMarkup3", + oldHTML: '
badMarkup3
', + newHTML: + '
badMarkup3 Emphasized ' + + "and strong
", + async validate({ pageNodeFront, selectedNodeFront }) { + is(pageNodeFront, selectedNodeFront, "Original element is selected"); + + const emText = await getContentPageElementProperty( + "#badMarkup3 em", + "textContent" + ); + const strongText = await getContentPageElementProperty( + "#badMarkup3 strong", + "textContent" + ); + is(emText, "Emphasized and strong", " was auto created"); + is(strongText, " and strong", " was auto created"); + }, + }, + { + selector: "#badMarkup4", + oldHTML: '
badMarkup4
', + newHTML: '
badMarkup4

', + async validate({ pageNodeFront, selectedNodeFront }) { + is(pageNodeFront, selectedNodeFront, "Original element is selected"); + + const divText = await getContentPageElementProperty( + "#badMarkup4", + "textContent" + ); + const divTag = await getContentPageElementProperty( + "#badMarkup4", + "tagName" + ); + + const pText = await getContentPageElementProperty( + "#badMarkup4 p", + "textContent" + ); + const pTag = await getContentPageElementProperty( + "#badMarkup4 p", + "tagName" + ); + + is(divText, "badMarkup4", "textContent is correct"); + is(divTag, "DIV", "did not change to

tag"); + is(pText, "", "The

tag has no children"); + is(pTag, "P", "Created an empty

tag"); + }, + }, + { + selector: "#badMarkup5", + oldHTML: '

badMarkup5

', + newHTML: '

badMarkup5

with a nested div

', + async validate({ pageNodeFront, selectedNodeFront }) { + is(pageNodeFront, selectedNodeFront, "Original element is selected"); + + const num = await getNumberOfMatchingElementsInContentPage( + "#badMarkup5 div" + ); + + const pText = await getContentPageElementProperty( + "#badMarkup5", + "textContent" + ); + const pTag = await getContentPageElementProperty( + "#badMarkup5", + "tagName" + ); + + const divText = await getContentPageElementProperty( + "#badMarkup5 ~ div", + "textContent" + ); + const divTag = await getContentPageElementProperty( + "#badMarkup5 ~ div", + "tagName" + ); + + is(num, 0, "The invalid markup got created as a sibling"); + is(pText, "badMarkup5 ", "The p tag does not take in the div content"); + is(pTag, "P", "Did not change to a
tag"); + is(divText, "with a nested div", "textContent is correct"); + is(divTag, "DIV", "Did not change to

tag"); + }, + }, +]; + +const TEST_URL = + "data:text/html," + + "" + + "" + + "" + + TEST_DATA.map(outer => outer.oldHTML).join("\n") + + "" + + ""; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + inspector.markup._frame.focus(); + await runEditOuterHTMLTests(TEST_DATA, inspector); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_html_edit_03.js b/devtools/client/inspector/markup/test/browser_markup_html_edit_03.js new file mode 100644 index 0000000000..af5dc091f3 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_html_edit_03.js @@ -0,0 +1,305 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that outerHTML editing keybindings work as expected and that *special* +// elements like , and can be edited correctly. + +const TEST_URL = + "data:text/html," + + "" + + "" + + "" + + '

' + + "" + + ""; +const SELECTOR = "#keyboard"; +const OLD_HTML = '
'; +const NEW_HTML = '
Edited
'; + +requestLongerTimeout(2); + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + inspector.markup._frame.focus(); + + info("Check that pressing escape cancels edits"); + await testEscapeCancels(inspector); + + info("Check that pressing F2 commits edits"); + await testF2Commits(inspector); + + info("Check that editing the element works like other nodes"); + await testBody(inspector); + + info("Check that editing the element works like other nodes"); + await testHead(inspector); + + info("Check that editing the element works like other nodes"); + await testDocumentElement(inspector); + + info("Check (again) that editing the element works like other nodes"); + await testDocumentElement2(inspector); +}); + +async function testEscapeCancels(inspector) { + await selectNode(SELECTOR, inspector); + + const onHtmlEditorCreated = once(inspector.markup, "begin-editing"); + EventUtils.sendKey("F2", inspector.markup._frame.contentWindow); + await onHtmlEditorCreated; + ok(inspector.markup.htmlEditor._visible, "HTML Editor is visible"); + + is( + await getContentPageElementProperty(SELECTOR, "outerHTML"), + OLD_HTML, + "The node is starting with old HTML." + ); + + inspector.markup.htmlEditor.editor.setText(NEW_HTML); + + const onEditorHiddem = once(inspector.markup.htmlEditor, "popuphidden"); + EventUtils.sendKey("ESCAPE", inspector.markup.htmlEditor.doc.defaultView); + await onEditorHiddem; + ok(!inspector.markup.htmlEditor._visible, "HTML Editor is not visible"); + + is( + await getContentPageElementProperty(SELECTOR, "outerHTML"), + OLD_HTML, + "Escape cancels edits" + ); +} + +async function testF2Commits(inspector) { + const onEditorShown = once(inspector.markup.htmlEditor, "popupshown"); + inspector.markup._frame.contentDocument.documentElement.focus(); + EventUtils.sendKey("F2", inspector.markup._frame.contentWindow); + await onEditorShown; + ok(inspector.markup.htmlEditor._visible, "HTML Editor is visible"); + + is( + await getContentPageElementProperty(SELECTOR, "outerHTML"), + OLD_HTML, + "The node is starting with old HTML." + ); + + const onMutations = inspector.once("markupmutation"); + inspector.markup.htmlEditor.editor.setText(NEW_HTML); + EventUtils.sendKey("F2", inspector.markup._frame.contentWindow); + await onMutations; + + ok(!inspector.markup.htmlEditor._visible, "HTML Editor is not visible"); + + is( + await getContentPageElementProperty(SELECTOR, "outerHTML"), + NEW_HTML, + "F2 commits edits - the node has new HTML." + ); +} + +async function testBody(inspector) { + const currentBodyHTML = await getContentPageElementProperty( + "body", + "outerHTML" + ); + const bodyHTML = '

'; + const bodyFront = await getNodeFront("body", inspector); + + const onUpdated = inspector.once("inspector-updated"); + const onReselected = inspector.markup.once("reselectedonremoved"); + await inspector.markup.updateNodeOuterHTML( + bodyFront, + bodyHTML, + currentBodyHTML + ); + await onReselected; + await onUpdated; + + const newBodyHTML = await getContentPageElementProperty("body", "outerHTML"); + is(newBodyHTML, bodyHTML, " HTML has been updated"); + + const headsNum = await getNumberOfMatchingElementsInContentPage("head"); + is(headsNum, 1, "no extra s have been added"); +} + +async function testHead(inspector) { + await selectNode("head", inspector); + + const currentHeadHTML = await getContentPageElementProperty( + "head", + "outerHTML" + ); + const headHTML = + 'New Title' + + ''; + const headFront = await getNodeFront("head", inspector); + + const onUpdated = inspector.once("inspector-updated"); + const onReselected = inspector.markup.once("reselectedonremoved"); + await inspector.markup.updateNodeOuterHTML( + headFront, + headHTML, + currentHeadHTML + ); + await onReselected; + await onUpdated; + + is(await getDocumentTitle(), "New Title", "New title has been added"); + is(await getWindowFoo(), undefined, "Script has not been executed"); + is( + await getContentPageElementProperty("head", "outerHTML"), + headHTML, + " HTML has been updated" + ); + is( + await getNumberOfMatchingElementsInContentPage("body"), + 1, + "no extra s have been added" + ); +} + +async function testDocumentElement(inspector) { + const currentDocElementOuterHMTL = await getDocumentOuterHTML(); + const docElementHTML = + '' + + "Updated from document element" + + '' + + "

Hello

"; + const docElementFront = await inspector.markup.walker.documentElement(); + + const onReselected = inspector.markup.once("reselectedonremoved"); + await inspector.markup.updateNodeOuterHTML( + docElementFront, + docElementHTML, + currentDocElementOuterHMTL + ); + await onReselected; + + is( + await getDocumentTitle(), + "Updated from document element", + "New title has been added" + ); + is(await getWindowFoo(), undefined, "Script has not been executed"); + is( + await getContentPageElementAttribute("html", "id"), + "updated", + " ID has been updated" + ); + is( + await getContentPageElementAttribute("html", "class"), + null, + " class has been updated" + ); + is( + await getContentPageElementAttribute("html", "foo"), + "bar", + " attribute has been updated" + ); + is( + await getContentPageElementProperty("html", "outerHTML"), + docElementHTML, + " HTML has been updated" + ); + is( + await getNumberOfMatchingElementsInContentPage("head"), + 1, + "no extra s have been added" + ); + is( + await getNumberOfMatchingElementsInContentPage("body"), + 1, + "no extra s have been added" + ); + is( + await getContentPageElementProperty("body", "textContent"), + "Hello", + "document.body.textContent has been updated" + ); +} + +async function testDocumentElement2(inspector) { + const currentDocElementOuterHMTL = await getDocumentOuterHTML(); + const docElementHTML = + '' + + "Updated again from document element" + + '' + + "

Hello again

"; + const docElementFront = await inspector.markup.walker.documentElement(); + + const onReselected = inspector.markup.once("reselectedonremoved"); + inspector.markup.updateNodeOuterHTML( + docElementFront, + docElementHTML, + currentDocElementOuterHMTL + ); + await onReselected; + + is( + await getDocumentTitle(), + "Updated again from document element", + "New title has been added" + ); + is(await getWindowFoo(), undefined, "Script has not been executed"); + is( + await getContentPageElementAttribute("html", "id"), + "somethingelse", + " ID has been updated" + ); + is( + await getContentPageElementAttribute("html", "class"), + "updated", + " class has been updated" + ); + is( + await getContentPageElementAttribute("html", "foo"), + null, + " attribute has been removed" + ); + is( + await getContentPageElementProperty("html", "outerHTML"), + docElementHTML, + " HTML has been updated" + ); + is( + await getNumberOfMatchingElementsInContentPage("head"), + 1, + "no extra s have been added" + ); + is( + await getNumberOfMatchingElementsInContentPage("body"), + 1, + "no extra s have been added" + ); + is( + await getContentPageElementProperty("body", "textContent"), + "Hello again", + "document.body.textContent has been updated" + ); +} + +function getDocumentTitle() { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => content.document.title + ); +} + +function getDocumentOuterHTML() { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => content.document.documentElement.outerHTML + ); +} + +function getWindowFoo() { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => content.wrappedJSObject.foo + ); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_html_edit_04.js b/devtools/client/inspector/markup/test/browser_markup_html_edit_04.js new file mode 100644 index 0000000000..be56b47368 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_html_edit_04.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that outerHTML editing keybindings work as expected and that the +// root element can be edited correctly. + +const TEST_URL = + "data:image/svg+xml," + + '' + + '' + + ""; + +requestLongerTimeout(2); + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + inspector.markup._frame.focus(); + + info("Check that editing the element works like other nodes"); + await testDocumentElement(inspector); + + info("Check (again) that editing the element works like other nodes"); + await testDocumentElement2(inspector); +}); + +async function testDocumentElement(inspector) { + const currentDocElementOuterHTML = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => content.document.documentElement.outerHTML + ); + const docElementSVG = + '' + + '' + + ""; + const docElementFront = await inspector.markup.walker.documentElement(); + + const onReselected = inspector.markup.once("reselectedonremoved"); + await inspector.markup.updateNodeOuterHTML( + docElementFront, + docElementSVG, + currentDocElementOuterHTML + ); + await onReselected; + + is( + await getContentPageElementAttribute("svg", "width"), + "200", + " width has been updated" + ); + is( + await getContentPageElementAttribute("svg", "height"), + "200", + " height has been updated" + ); + is( + await getContentPageElementProperty("svg", "outerHTML"), + docElementSVG, + " markup has been updated" + ); +} + +async function testDocumentElement2(inspector) { + const currentDocElementOuterHTML = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => content.document.documentElement.outerHTML + ); + const docElementSVG = + '' + + '' + + ""; + const docElementFront = await inspector.markup.walker.documentElement(); + + const onReselected = inspector.markup.once("reselectedonremoved"); + inspector.markup.updateNodeOuterHTML( + docElementFront, + docElementSVG, + currentDocElementOuterHTML + ); + await onReselected; + + is( + await getContentPageElementAttribute("svg", "width"), + "300", + " width has been updated" + ); + is( + await getContentPageElementAttribute("svg", "height"), + "300", + " height has been updated" + ); + is( + await getContentPageElementProperty("svg", "outerHTML"), + docElementSVG, + " markup has been updated" + ); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_html_edit_undo-redo.js b/devtools/client/inspector/markup/test/browser_markup_html_edit_undo-redo.js new file mode 100644 index 0000000000..6f76d524d8 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_html_edit_undo-redo.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the undo/redo stack is correctly cleared when opening the HTML editor on a +// new node. Bug 1327674. + +const DIV1_HTML = '
content1
'; +const DIV2_HTML = '
content2
'; +const DIV2_HTML_UPDATED = '
content2_updated
'; + +const TEST_URL = + "data:text/html," + + "" + + "" + + "" + + DIV1_HTML + + DIV2_HTML + + "" + + ""; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + inspector.markup._frame.focus(); + + await selectNode("#d1", inspector); + + info("Open the HTML editor on node #d1"); + let onHtmlEditorCreated = once(inspector.markup, "begin-editing"); + EventUtils.sendKey("F2", inspector.markup._frame.contentWindow); + await onHtmlEditorCreated; + + ok(inspector.markup.htmlEditor._visible, "HTML Editor is visible"); + is( + inspector.markup.htmlEditor.editor.getText(), + DIV1_HTML, + "The editor content for d1 is correct." + ); + + info("Hide the HTML editor for #d1"); + let onEditorHidden = once(inspector.markup.htmlEditor, "popuphidden"); + EventUtils.sendKey("ESCAPE", inspector.markup.htmlEditor.doc.defaultView); + await onEditorHidden; + ok(!inspector.markup.htmlEditor._visible, "HTML Editor is not visible"); + + await selectNode("#d2", inspector); + + info("Open the HTML editor on node #d2"); + onHtmlEditorCreated = once(inspector.markup, "begin-editing"); + EventUtils.sendKey("F2", inspector.markup._frame.contentWindow); + await onHtmlEditorCreated; + + ok(inspector.markup.htmlEditor._visible, "HTML Editor is visible"); + is( + inspector.markup.htmlEditor.editor.getText(), + DIV2_HTML, + "The editor content for d2 is correct." + ); + + inspector.markup.htmlEditor.editor.setText(DIV2_HTML_UPDATED); + is( + inspector.markup.htmlEditor.editor.getText(), + DIV2_HTML_UPDATED, + "The editor content for d2 is updated." + ); + + inspector.markup.htmlEditor.editor.undo(); + is( + inspector.markup.htmlEditor.editor.getText(), + DIV2_HTML, + "The editor content for d2 is reverted." + ); + + inspector.markup.htmlEditor.editor.undo(); + is( + inspector.markup.htmlEditor.editor.getText(), + DIV2_HTML, + "The editor content for d2 has not been set to content1." + ); + + info("Hide the HTML editor for #d2"); + onEditorHidden = once(inspector.markup.htmlEditor, "popuphidden"); + EventUtils.sendKey("ESCAPE", inspector.markup.htmlEditor.doc.defaultView); + await onEditorHidden; + ok(!inspector.markup.htmlEditor._visible, "HTML Editor is not visible"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_iframe_blocked_by_csp.js b/devtools/client/inspector/markup/test/browser_markup_iframe_blocked_by_csp.js new file mode 100644 index 0000000000..559919f2ef --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_iframe_blocked_by_csp.js @@ -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"; + +// Test that iframe blocked because of CSP doesn't cause the browser to freeze. + +const IFRAME_TEST_URI = `https://example.com/document-builder.sjs?html=${encodeURIComponent(` +

Test expanding CSP-blocked iframe

+ +`)}&headers=content-security-policy:default-src 'self'`; +const FRAME_TEST_URI = `https://example.com/document-builder.sjs?html=${encodeURIComponent(` + + + +`)}&headers=content-security-policy:default-src 'self'`; + +const BYPASS_WALKERFRONT_CHILDREN_IFRAME_GUARD_PREF = + "devtools.testing.bypass-walker-children-iframe-guard"; + +add_task(async function () { + await pushPref(BYPASS_WALKERFRONT_CHILDREN_IFRAME_GUARD_PREF, true); + const { inspector } = await openInspectorForURL(IFRAME_TEST_URI); + await testElementBlockedByCSP("iframe", inspector); + + // Don't wait for the load event as it doesn't happen because the frame is blocked. + await navigateTo(FRAME_TEST_URI, { waitForLoad: false }); + await testElementBlockedByCSP("frame", inspector); +}); + +async function testElementBlockedByCSP(selector, inspector) { + await inspector.markup.expandAll(); + info(`Check that markup node for "${selector}" can't be expanded`); + let container = await getContainerForSelector(selector, inspector); + + is( + container.expander.style.visibility, + "hidden", + "Expand icon is hidden, even without the safe guard in WalkerFront#children" + ); + + info("Reload the page and do same assertion with the guard"); + Services.prefs.clearUserPref(BYPASS_WALKERFRONT_CHILDREN_IFRAME_GUARD_PREF); + await reloadBrowser(); + + await inspector.markup.expandAll(); + container = await getContainerForSelector(selector, inspector); + is( + container.expander.style.visibility, + "hidden", + "Expand icon is still hidden" + ); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_image_tooltip.js b/devtools/client/inspector/markup/test/browser_markup_image_tooltip.js new file mode 100644 index 0000000000..f4a45c5079 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_image_tooltip.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that image preview tooltips are shown on img and canvas tags in the +// markup-view and that the tooltip actually contains an image and shows the +// right dimension label + +const TEST_NODES = [ + { selector: "img.local", size: "192" + " \u00D7 " + "192" }, + { selector: "img.data", size: "64" + " \u00D7 " + "64" }, + { selector: "img.remote", size: "22" + " \u00D7 " + "23" }, + { selector: ".canvas", size: "600" + " \u00D7 " + "600" }, +]; + +add_task(async function () { + await addTab(URL_ROOT + "doc_markup_image_and_canvas_2.html"); + const { inspector } = await openInspector(); + + info("Selecting the first tag"); + await selectNode("img", inspector); + + for (const testNode of TEST_NODES) { + const target = await getImageTooltipTarget(testNode, inspector); + await assertTooltipShownOnHover( + inspector.markup.imagePreviewTooltip, + target + ); + checkImageTooltip(testNode, inspector); + await assertTooltipHiddenOnMouseOut( + inspector.markup.imagePreviewTooltip, + target + ); + } +}); + +async function getImageTooltipTarget({ selector }, inspector) { + const nodeFront = await getNodeFront(selector, inspector); + const isImg = nodeFront.tagName.toLowerCase() === "img"; + + const container = getContainerForNodeFront(nodeFront, inspector); + + let target = container.editor.tag; + if (isImg) { + target = container.editor.getAttributeElement("src").querySelector(".link"); + } + return target; +} + +function checkImageTooltip({ selector, size }, { markup }) { + const panel = markup.imagePreviewTooltip.panel; + const images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip for [" + selector + "] contains an image"); + + const label = panel.querySelector(".devtools-tooltip-caption"); + is( + label.textContent, + size, + "Tooltip label for [" + selector + "] displays the right image size" + ); + + markup.imagePreviewTooltip.hide(); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js b/devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js new file mode 100644 index 0000000000..3a2cc358a7 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that image preview tooltip shows updated content when the image src +// changes. + +// prettier-ignore +const INITIAL_SRC = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAADI5JREFUeNrsWwuQFNUVPf1m5z87szv7HWSWj8CigBFMEFZKiQsB1PgJwUAZg1HBpIQsKmokEhNjWUnFVPnDWBT+KolJYbRMoqUVq0yCClpqiX8sCchPWFwVlt2db7+X93pez7zu6Vn2NxsVWh8987p7pu+9555z7+tZjTGGY3kjOMa34w447oBjfKsY7i/UNM3Y8eFSAkD50Plgw03K5P9gvGv7U5ieeR3PszeREiPNX3/0DL4hjslzhm8THh+OITfXk3dhiv4GDtGPVzCaeJmPLYzuu5qJuWfuw2QTlcN1X9pwQU7LhdZ/ZAseD45cOh9hHvDkc/yAF/DNhdb5Mrr3PvBMaAYW8fMSIi2G497IMEK/YutGtAYr6+ej+nxu/NN8Ks3N7AR6HgcLz0Eg1Ljg1UcxZzi5qewIkMYLRweTr2Kzp+nmyXAd5pS3XQDd+N/4h4zgu9FI7brlXf90nMEnuwQxlvv+hosE3TuexmWeysmT4W+WxkMaLzf9Y8ATgjcUn7T9H1gqrpFq8eV1gMn6t16NhngjfoX6q4DUP032Rd4LJgpSLwJ1yzFqBG69eRkah0MVyo0Acfe+yy9AG4nMiYCkeM53KKFXncBLAXqEm+wCqZwaueq7WCmuLTcKSJmj737ol2hurA9eq9VdyiO8yWa3NNyog+SB5CZodSsQq/dfu34tJpYbBaTMzvVddDZu16q5smXf4G8zEvqm4cyaAmJPuTJk3oJWdS4WzcVtfMZbThSQckb/pYfRGgo3zNOqZnEHbJPGK4abaDCQIIsT8V/qTaBqHkLh6LzXH8XZQhbLhYKyyCC/WeHYcNdmvOgfe8skzbWL270/T3wf7tSx/lGCbTu8xlzzmCSWLc5iwmgikcCHi3Mga0Ry913vBFvQwg90l6M4ImWKfsWOp7DSWxmfpPlCFuPFfsNfKrCnPYpQKIRgqBK7D0SxYaNHwkEiJMtl0ReDp3Lc5D3PGoTo/sKngCl7a5chFqvBatKwjBd7WwqIlzB/78NcoUcp5VSgGxm+7b8eqQRGnHMO634epO4S1EZww09/iFg5UmGoESDuznP1xVhTUX1WWHPzjpd25wyH0hRxI3LGM75nxmuNEEUVpAN0XgxmPoKralakbQnWlIMQyVBD/w+3orkq4lvualjKyWwzt4MaxqspQHVhPOWG64bxYuhZXSFGWhipbSDVragOu5Y9eAsmDDUKyBA703vemVhHoueD6e9wAzJK1WfmN0Umk5GGM4kEMZcuIECqgjm0nldAqmbjwtm4VxZH5AvlADP6mx9Eqy9Q0+KqW8Ch+47FaMMYmnNGfY1iPMshoC6qFxme4wQ+0p+ARE6H3+9veWEDWgUhDhUKyFARn4jM5BNxT0XsMg7bfymGK1ov3wtjDfhL4w0HVGUVBEjDaaE+QNdrcNWch1PG4W6xrjBUXECGivg++Cva3JUT4iQUz3V2RsSVaKLwOuDT89A3HdBQoxhNC+fnVm74ual2EG893P6G+PuP4SfiO4cCBWQooL9qCWKNXPbcI37Aa/lnlZxXRt4RFONGwSDCPAHqOuqjWct1QiEMw5mChM5X4K47FyNqcd3aK9AwFH0CGYLoe1ctxk2eWi57rg5JfGp9rzC6ggCdFlAgHBDw5Yxlcg6G8SyHCjMlsgmDD9zhSeHlF+JnAgWDTQUy2NxfdwOao1UVV3pi3+bE97YSbWpLAbn6zefHNQkp1PMpIBwwvslKgIYTKM2nEpNzrGcH3FXTEal0L38kJ4uDQgEZbO4vnI173LXf5NHZaiUxtaCxyZuo/rK6LpUg54yg3zTWRAArvDcRIPZ6BqzrQ1REpmL+DNw32OKIDCb3X1qPVn8wNNMT4w2bvs+q4bAZrqBh2skaL3yyhhIIZ4i6oHkUK0RckcB8GigEyRIH4A6Mgc8fatl0/+BkkQxC9gIT4ljna1rIZW9rEdNbjJcNjsnoYj7LHWCUwpITzEgzRQKZ3XAFHbTzA3hrz8TEUUZxFBhoKpABQt/97p+w0hMZG68I8R6FtlsJT3FELndZntjM+VMnylKYq8GJI3UZaRMpquGSGFVOEfv0YZBMNzz+uvjbfzS6xQERIhlI9FcvQWNdFVb7x1zCb+QNK8vb9NsiifmI5hBgVoOCBC1sb0ab5RomqENxLO3eA1/0NDRU47q2RQNbRCUDIb7lF2CNL3ZGxEV4n08TVvZWYG4pZyV0zUdS45tyCBByOHWiyvZmxFXDCyRo1ge5+Sy0TA+8lWMiP/6O0S32exGV9Jf4fr8azdUR3zL/CZz4MtvzdX5uOYs6NDOmpkuj5Huh+7qUQSYl0ThHzw0YQzcGo6bhzEqoYq5rN3yRiYiG3Vfe2Ybm/qKA9NNZ3nNm4F7/yDkg9AN+U1mHiBcXP8zuDN76jj8hg1QyiWQigalj02BJPhK8I0zxijAjhp5zhlpLUDvS+BCy2HMAvvB4XDgL9/SXC0g/ou/5+6/xLX8w0uJrOIkXfPvyhY0F6gr7M8H0KWFYikcqAXakB+xwD9CdREBLoau7Gz3cAdSIdLFxFtJTCqRChSjnutvhDcREtzjz2Tswtz+yeNRFUeXZXtWux7C1fuoVcbd3J//ipDX3uZZDLGrwweS+UBLL5TDliVBnF8P7H+XI8aRRGsIBJg/Zlslt1+W+D1JWoSyi+kD9jfhs78t7mhZhSl+fLfY1Bdyv3I8V/qpY3B1McgN7ZFT5/vNO0I5DPLLdPBIJA8qc4h2I0QplYfDpJwHT+aj0246r5S8rToG8OjCle8wk4OLvvYGa+Ovr84uo2qBSwJS9G5egoZFLTfiEqWDtbwGfHgKOdPHcS+ai7XDzMPW/FJRLGGcxnBbK4YJC2K+h+T6Bdu5CqHqCWERd3bawb7JI+iJ735+LNaHaprBLLHBm08U3XxShEsdt+f3eTh3v7aC95Dct4RCWL5OZWh/oXBZThxAIxyOXLzBk8aiEWJID8rK3CpPOmeHaGpvCS+7EHv5FujVHUSJPLXvIFeHcNc+9xrB2gws9KZdxuLFax/WLM5gzzSm/lTXF/OdAcapyvjxPqxqHjr2v4ckX2bS2dRBrc5lSdpKjEJ9/9tdwX2WMd53ZQ2IVo3RES+UwVSpCPvYepNx4gmTGDUKIMQ4eduPnD7mx9xOn/KZKOlFbStjONxHTtR+BYAPmnoZ1Zp8wkBRwP/EL3u0F/C2hGl7vpz7vW37T3vP7if8wroKuoh8ribknX9BK5rcF+mo1qKaKyRPJTgTDjbzY8szcuLb3bpH00u35T47j7prRpwDJTxzyG0dHgxPp5bPG8VdkpfPbUg3SgoOo2mwVukb98D5EqpswZTTulCggTk4gpYhv0++wIhCJxr0+Hq1sondis0SE2oxQe3qWXwWyO4DSQg9gJ8Iiw1VFcGqXxet0N9xE4ygIxv/9W6wo9WyROEX/R+eiobYSq2vHTOR631Eiv2lRfh9dvxkumkXh92Qsx8XrAJ+7YGbWuhxOi/U+31NQmzyqNYG8N/3wfo6CRtRHcN01FzkvojohwLu0VVvDa56IS/xcj2b7nN+O+m0jqpE1wMPXZxAN9iCVThtDvH7gmiRGRpU8Lspv1Uhq4wIVdQoyuGSLNYPKUCS8+CzNURbzMmjK3i8u0U793lmuV0ef9nWQ5MGC/DiUqEUSaCtXna9RJEspZS1lrXINK/pcq+SpT50t98QKMq1FRmDfx3vxty102k0PM4ssEnvuz5+G26Ij4yDpz6z9fV8bkyIkqBFkhej0Ib+ZQ34XJK9AfozaiimqIoX3Jp3tiISrcfYpuN2+iFph/02P36PNC9fVcCnp6H9jYouKyfaWufz5Tp9tVxcUniw7IohZv4dZz81/ns67z3AYPrc2n0+Ix2q8k0PWjgBy88XaibnfK9A+5LdDY2Ivhy36fbT8Zv3Lb1U1qLqUxorXEEXIs0mjjrtxoTZWtdvigNs2sgPiujTv6DIZLld6b/V5742JZV3fUsUVFy5gdsNtKWFzUCEVbNepD1MkSMVbsb6SZm7jI3/zODtQKgUMsOw8wDZ63t5xcV1TnaEAxoc6wrqY+Fj+N4DsqOnhOIdicrQSm1MPYCPlIqHn5bbHg8/bj2D3QfZnCX3mpAICDZV8jH5kpbZqTD0W+DxaA74CWzLN2nd14OlL72J38Lf7+TjC7dadZFDoZJQPrtaIKL/G0L6ktptPZVJ8fMqHYPZOKYPMyQGadIJfDvdXwAFiZOTvDBPydf5vk4rWA+RfdhBlaF/yDDBRoMu9pfnSjv/p7DG+HXfAcQcc49v/BBgAcFAO4DmB2GQAAAAASUVORK5CYII="; + +const UPDATED_SRC = URL_ROOT + "doc_markup_tooltip.png"; + +const INITIAL_SRC_SIZE = "64" + " \u00D7 " + "64"; +const UPDATED_SRC_SIZE = "22" + " \u00D7 " + "23"; + +add_task(async function () { + const { inspector } = await openInspectorForURL( + "data:text/html,

markup view tooltip test

" + ); + + info("Retrieving NodeFront for the element."); + const img = await getNodeFront("img", inspector); + + info("Selecting the element"); + await selectNode(img, inspector); + + info("Adding src attribute to the image."); + await updateImageSrc(img, INITIAL_SRC, inspector); + + const container = getContainerForNodeFront(img, inspector); + ok(container, "Found markup container for the image."); + + let target = container.editor + .getAttributeElement("src") + .querySelector(".link"); + ok(target, "Found the src attribute in the markup view."); + + info("Showing tooltip on the src link."); + await assertTooltipShownOnHover(inspector.markup.imagePreviewTooltip, target); + + checkImageTooltip(INITIAL_SRC_SIZE, inspector); + + await assertTooltipHiddenOnMouseOut( + inspector.markup.imagePreviewTooltip, + target + ); + + info("Updating the image src."); + await updateImageSrc(img, UPDATED_SRC, inspector); + + target = container.editor.getAttributeElement("src").querySelector(".link"); + ok(target, "Found the src attribute in the markup view after mutation."); + + info("Showing tooltip on the src link."); + await assertTooltipShownOnHover(inspector.markup.imagePreviewTooltip, target); + + info("Checking that the new image was shown."); + checkImageTooltip(UPDATED_SRC_SIZE, inspector); + + await assertTooltipHiddenOnMouseOut( + inspector.markup.imagePreviewTooltip, + target + ); +}); + +/** + * Updates the src attribute of the image. Return a Promise. + */ +function updateImageSrc(img, newSrc, inspector) { + const onMutated = inspector.once("markupmutation"); + const onModified = img.modifyAttributes([ + { + attributeName: "src", + newValue: newSrc, + }, + ]); + + return Promise.all([onMutated, onModified]); +} + +/** + * Checks that the markup view tooltip contains an image element with the given + * size. + */ +function checkImageTooltip(size, { markup }) { + const panel = markup.imagePreviewTooltip.panel; + const images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + + const label = panel.querySelector(".devtools-tooltip-caption"); + is(label.textContent, size, "Tooltip label displays the right image size"); + + markup.imagePreviewTooltip.hide(); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_01.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_01.js new file mode 100644 index 0000000000..9488670d3c --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_01.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Tests tabbing through attributes on a node + +const TEST_URL = "data:text/html;charset=utf8,
"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Focusing the tag editor of the test element"); + const { editor } = await focusNode("div", inspector); + editor.tag.focus(); + + info("Pressing tab and expecting to focus the ID attribute, always first"); + EventUtils.sendKey("tab", inspector.panelWin); + checkFocusedAttribute("id"); + + info("Hit enter to turn the attribute to edit mode"); + EventUtils.sendKey("return", inspector.panelWin); + checkFocusedAttribute("id", true); + + // Check the order of the other attributes in the DOM to the check they appear + // correctly in the markup-view + const attributes = (await getAttributesFromEditor("div", inspector)).slice(1); + + info("Tabbing forward through attributes in edit mode"); + for (const attribute of attributes) { + collapseSelectionAndTab(inspector); + checkFocusedAttribute(attribute, true); + } + + info("Tabbing backward through attributes in edit mode"); + + // Just reverse the attributes other than id and remove the first one since + // it's already focused now. + const reverseAttributes = attributes.reverse(); + reverseAttributes.shift(); + + for (const attribute of reverseAttributes) { + collapseSelectionAndShiftTab(inspector); + checkFocusedAttribute(attribute, true); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_02.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_02.js new file mode 100644 index 0000000000..8b82a83aee --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_02.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that pressing ESC when a node in the markup-view is focused toggles +// the split-console (see bug 988278) + +const TEST_URL = "data:text/html;charset=utf8,
"; + +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL(TEST_URL); + + info("Focusing the tag editor of the test element"); + const { editor } = await getContainerForSelector("div", inspector); + editor.tag.focus(); + + info("Pressing ESC and wait for the split-console to open"); + let onSplitConsole = toolbox.once("split-console"); + const onConsoleReady = toolbox.once("webconsole-ready"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, inspector.panelWin); + await onSplitConsole; + await onConsoleReady; + ok(toolbox.splitConsole, "The split console is shown."); + + info("Pressing ESC again and wait for the split-console to close"); + onSplitConsole = toolbox.once("split-console"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, inspector.panelWin); + await onSplitConsole; + ok(!toolbox.splitConsole, "The split console is hidden."); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_03.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_03.js new file mode 100644 index 0000000000..db918defd6 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_03.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that selecting a node with the mouse (by clicking on the line) focuses +// the first focusable element in the corresponding MarkupContainer so that the +// keyboard can be used immediately. + +const TEST_URL = `data:text/html;charset=utf8, +
Text node`; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const { walker } = inspector; + + info("Select the test node to have the 2 test containers visible"); + await selectNode("div", inspector); + + const divFront = await walker.querySelector(walker.rootNode, "div"); + const textFront = await walker.nextSibling(divFront); + + info("Click on the MarkupContainer element for the text node"); + await clickContainer(textFront, inspector); + is( + inspector.markup.doc.activeElement, + getContainerForNodeFront(textFront, inspector).editor.textNode.valuePreRef + .current, + "The currently focused element is the node's text content" + ); + + info("Click on the MarkupContainer element for the
node"); + await clickContainer(divFront, inspector); + is( + inspector.markup.doc.activeElement, + getContainerForNodeFront(divFront, inspector).editor.tag, + "The currently focused element is the div's tagname" + ); + + info("Click on the test-class attribute, to make sure it gets focused"); + const editor = getContainerForNodeFront(divFront, inspector).editor; + const attributeEditor = editor.attrElements + .get("class") + .querySelector(".editable"); + + const onFocus = once(attributeEditor, "focus"); + EventUtils.synthesizeMouseAtCenter( + attributeEditor, + { type: "mousedown" }, + inspector.markup.doc.defaultView + ); + EventUtils.synthesizeMouseAtCenter( + attributeEditor, + { type: "mouseup" }, + inspector.markup.doc.defaultView + ); + await onFocus; + + is( + inspector.markup.doc.activeElement, + attributeEditor, + "The currently focused element is the div's class attribute" + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_04.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_04.js new file mode 100644 index 0000000000..078fc571a8 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_04.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Tests that selecting a node using the browser context menu (inspect element) +// or the element picker focuses that node so that the keyboard can be used +// immediately. + +const TEST_URL = "data:text/html;charset=utf8,
test element
"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Select the test node with the browser ctx menu"); + await clickOnInspectMenuItem("div"); + assertNodeSelected(inspector, "div"); + + info( + "Press arrowUp to focus " + + "(which works if the node was focused properly)" + ); + await selectPreviousNodeWithArrowUp(inspector); + assertNodeSelected(inspector, "body"); + + info("Select the test node with the element picker"); + await selectWithElementPicker(inspector); + assertNodeSelected(inspector, "div"); + + info( + "Press arrowUp to focus " + + "(which works if the node was focused properly)" + ); + await selectPreviousNodeWithArrowUp(inspector); + assertNodeSelected(inspector, "body"); +}); + +function assertNodeSelected(inspector, tagName) { + is( + inspector.selection.nodeFront.tagName.toLowerCase(), + tagName, + `The <${tagName}> node is selected` + ); +} + +function selectPreviousNodeWithArrowUp(inspector) { + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + const onNodeHighlighted = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + const onUpdated = inspector.once("inspector-updated"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + return Promise.all([onUpdated, onNodeHighlighted]); +} + +async function selectWithElementPicker(inspector) { + await startPicker(inspector.toolbox); + + await safeSynthesizeMouseEventAtCenterInContentPage( + "div", + { + type: "mousemove", + }, + gBrowser.selectedBrowser + ); + + BrowserTestUtils.synthesizeKey("KEY_Enter", {}, gBrowser.selectedBrowser); + await inspector.once("inspector-updated"); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_delete_attributes.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_delete_attributes.js new file mode 100644 index 0000000000..ffa5e9d86c --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_delete_attributes.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that attributes can be deleted from the markup-view with the delete key +// when they are focused. + +const HTML = '
'; +const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML); + +// List of all the test cases. Each item is an object with the following props: +// - selector: the css selector of the node that should be selected +// - attribute: the name of the attribute that should be focused. Do not +// specify an attribute that would make it impossible to find the node using +// selector. +// Note that after each test case, undo is called. +const TEST_DATA = [ + { + selector: "#id", + attribute: "class", + }, + { + selector: "#id", + attribute: "data-id", + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const { walker } = inspector; + + for (const { selector, attribute } of TEST_DATA) { + info("Get the container for node " + selector); + const { editor } = await getContainerForSelector(selector, inspector); + + info("Focus attribute " + attribute); + const attr = editor.attrElements.get(attribute).querySelector(".editable"); + attr.focus(); + + info("Delete the attribute by pressing delete"); + const mutated = inspector.once("markupmutation"); + EventUtils.sendKey("delete", inspector.panelWin); + await mutated; + + info("Check that the node is still here"); + let node = await walker.querySelector(walker.rootNode, selector); + ok(node, "The node hasn't been deleted"); + + info("Check that the attribute has been deleted"); + node = await walker.querySelector( + walker.rootNode, + selector + "[" + attribute + "]" + ); + ok(!node, "The attribute does not exist anymore in the DOM"); + ok( + !editor.attrElements.get(attribute), + "The attribute has been removed from the container" + ); + + info("Undo the change"); + await undoChange(inspector); + node = await walker.querySelector( + walker.rootNode, + selector + "[" + attribute + "]" + ); + ok(node, "The attribute is back in the DOM"); + ok( + editor.attrElements.get(attribute), + "The attribute is back on the container" + ); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_scrolltonode.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_scrolltonode.js new file mode 100644 index 0000000000..faf8bc575c --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_scrolltonode.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the keyboard shortcut "S" used to scroll to the selected node. + +const HTML = `
+
+ TOP
+
+ BOTTOM
+
`; +const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML); + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Make sure the markup frame has the focus"); + inspector.markup._frame.focus(); + + info("Before test starts, #scroll-top is visible, #scroll-bottom is hidden"); + await checkElementIsInViewport("#scroll-top", true); + await checkElementIsInViewport("#scroll-bottom", false); + + info("Select the #scroll-bottom node"); + await selectNode("#scroll-bottom", inspector); + info("Press S to scroll to the bottom node"); + let waitForScroll = BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "scroll" + ); + await EventUtils.synthesizeKey("S", {}, inspector.panelWin); + await waitForScroll; + ok(true, "Scroll event received"); + + info("#scroll-top should be scrolled out, #scroll-bottom should be visible"); + await checkElementIsInViewport("#scroll-top", false); + await checkElementIsInViewport("#scroll-bottom", true); + + info("Select the #scroll-top node"); + await selectNode("#scroll-top", inspector); + info("Press S to scroll to the top node"); + waitForScroll = BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "scroll" + ); + await EventUtils.synthesizeKey("S", {}, inspector.panelWin); + await waitForScroll; + ok(true, "Scroll event received"); + + info("#scroll-top should be visible, #scroll-bottom should be scrolled out"); + await checkElementIsInViewport("#scroll-top", true); + await checkElementIsInViewport("#scroll-bottom", false); + + info("Select #scroll-bottom node"); + await selectNode("#scroll-bottom", inspector); + info("Press shift + S, nothing should happen due to the modifier"); + await EventUtils.synthesizeKey("S", { shiftKey: true }, inspector.panelWin); + + info("Same state, #scroll-top is visible, #scroll-bottom is scrolled out"); + await checkElementIsInViewport("#scroll-top", true); + await checkElementIsInViewport("#scroll-bottom", false); +}); + +/** + * Verify that the element matching the provided selector is either in or out + * of the viewport, depending on the provided "expected" argument. + * Returns a promise that will resolve when the test has been performed. + * + * @param {String} selector + * css selector for the element to test + * @param {Boolean} expected + * true if the element is expected to be in the viewport, false otherwise + * @return {Promise} promise + */ +async function checkElementIsInViewport(selector, expected) { + const isInViewport = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector], + _selector => { + const node = content.document.querySelector(_selector); + const rect = node.getBoundingClientRect(); + return ( + rect.bottom >= 0 && + rect.right >= 0 && + rect.top <= content.innerHeight && + rect.left <= content.innerWidth + ); + } + ); + + is( + isInViewport, + expected, + selector + " in the viewport: expected to be " + expected + ); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_links_01.js b/devtools/client/inspector/markup/test/browser_markup_links_01.js new file mode 100644 index 0000000000..dbb1402074 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_links_01.js @@ -0,0 +1,178 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that links are shown in attributes when the values (or part of the +// values) are URIs or pointers to IDs. + +const TEST_URL = URL_ROOT + "doc_markup_links.html"; + +const TEST_DATA = [ + { + selector: "link", + attributes: [ + { + attributeName: "href", + links: [{ type: "cssresource", value: "style.css" }], + }, + ], + }, + { + selector: "link[rel=icon]", + attributes: [ + { + attributeName: "href", + links: [ + { + type: "uri", + value: "/media/img/firefox/favicon-196.223e1bcaf067.png", + }, + ], + }, + ], + }, + { + selector: "form", + attributes: [ + { + attributeName: "action", + links: [{ type: "uri", value: "/post_message" }], + }, + ], + }, + { + selector: "label[for=name]", + attributes: [ + { + attributeName: "for", + links: [{ type: "idref", value: "name" }], + }, + ], + }, + { + selector: "label[for=message]", + attributes: [ + { + attributeName: "for", + links: [{ type: "idref", value: "message" }], + }, + ], + }, + { + selector: "output", + attributes: [ + { + attributeName: "form", + links: [{ type: "idref", value: "message-form" }], + }, + { + attributeName: "for", + links: [ + { type: "idref", value: "name" }, + { type: "idref", value: "message" }, + { type: "idref", value: "invalid" }, + ], + }, + ], + }, + { + selector: "a", + attributes: [ + { + attributeName: "href", + links: [{ type: "uri", value: "/go/somewhere/else" }], + }, + { + attributeName: "ping", + links: [ + { type: "uri", value: "/analytics?page=pageA" }, + { type: "uri", value: "/analytics?user=test" }, + ], + }, + ], + }, + { + selector: "li[contextmenu=menu1]", + attributes: [ + { + attributeName: "contextmenu", + links: [{ type: "idref", value: "menu1" }], + }, + ], + }, + { + selector: "li[contextmenu=menu2]", + attributes: [ + { + attributeName: "contextmenu", + links: [{ type: "idref", value: "menu2" }], + }, + ], + }, + { + selector: "li[contextmenu=menu3]", + attributes: [ + { + attributeName: "contextmenu", + links: [{ type: "idref", value: "menu3" }], + }, + ], + }, + { + selector: "video", + attributes: [ + { + attributeName: "poster", + links: [{ type: "uri", value: "doc_markup_tooltip.png" }], + }, + { + attributeName: "src", + links: [{ type: "uri", value: "code-rush.mp4" }], + }, + ], + }, + { + selector: "script", + attributes: [ + { + attributeName: "src", + links: [{ type: "jsresource", value: "lib_jquery_1.0.js" }], + }, + ], + }, +]; + +requestLongerTimeout(2); + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + for (const { selector, attributes } of TEST_DATA) { + info("Testing attributes on node " + selector); + await selectNode(selector, inspector); + const { editor } = await getContainerForSelector(selector, inspector); + + for (const { attributeName, links } of attributes) { + info("Testing attribute " + attributeName); + const linkEls = editor.attrElements + .get(attributeName) + .querySelectorAll(".link"); + + is(linkEls.length, links.length, "The right number of links were found"); + + for (let i = 0; i < links.length; i++) { + is( + linkEls[i].dataset.type, + links[i].type, + `Link ${i} has the right type` + ); + is( + linkEls[i].textContent, + links[i].value, + `Link ${i} has the right value` + ); + } + } + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_links_02.js b/devtools/client/inspector/markup/test/browser_markup_links_02.js new file mode 100644 index 0000000000..88de585c5e --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_links_02.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that attributes are linkified correctly when attributes are updated +// and created. + +const TEST_URL = URL_ROOT + "doc_markup_links.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Adding a contextmenu attribute to the body node"); + await addNewAttributes("body", 'contextmenu="menu1"', inspector); + + info("Checking for links in the new attribute"); + let { editor } = await getContainerForSelector("body", inspector); + let linkEls = editor.attrElements + .get("contextmenu") + .querySelectorAll(".link"); + is(linkEls.length, 1, "There is one link in the contextmenu attribute"); + is(linkEls[0].dataset.type, "idref", "The link has the right type"); + is(linkEls[0].textContent, "menu1", "The link has the right value"); + + info("Editing the contextmenu attribute on the body node"); + const nodeMutated = inspector.once("markupmutation"); + const attr = editor.attrElements + .get("contextmenu") + .querySelector(".editable"); + setEditableFieldValue(attr, 'contextmenu="menu2"', inspector); + await nodeMutated; + + info("Checking for links in the updated attribute"); + ({ editor } = await getContainerForSelector("body", inspector)); + linkEls = editor.attrElements.get("contextmenu").querySelectorAll(".link"); + is(linkEls.length, 1, "There is one link in the contextmenu attribute"); + is(linkEls[0].dataset.type, "idref", "The link has the right type"); + is(linkEls[0].textContent, "menu2", "The link has the right value"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_links_03.js b/devtools/client/inspector/markup/test/browser_markup_links_03.js new file mode 100644 index 0000000000..8817c7c818 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_links_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 links appear correctly in attributes created in content. + +const TEST_URL = URL_ROOT + "doc_markup_links.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Adding a contextmenu attribute to the body node via the content"); + let onMutated = inspector.once("markupmutation"); + await setContentPageElementAttribute("body", "contextmenu", "menu1"); + await onMutated; + + info("Checking for links in the new attribute"); + let { editor } = await getContainerForSelector("body", inspector); + let linkEls = editor.attrElements + .get("contextmenu") + .querySelectorAll(".link"); + is(linkEls.length, 1, "There is one link in the contextmenu attribute"); + is(linkEls[0].dataset.type, "idref", "The link has the right type"); + is(linkEls[0].textContent, "menu1", "The link has the right value"); + + info("Editing the contextmenu attribute on the body node"); + onMutated = inspector.once("markupmutation"); + + await setContentPageElementAttribute("body", "contextmenu", "menu2"); + await onMutated; + + info("Checking for links in the updated attribute"); + ({ editor } = await getContainerForSelector("body", inspector)); + linkEls = editor.attrElements.get("contextmenu").querySelectorAll(".link"); + is(linkEls.length, 1, "There is one link in the contextmenu attribute"); + is(linkEls[0].dataset.type, "idref", "The link has the right type"); + is(linkEls[0].textContent, "menu2", "The link has the right value"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_links_04.js b/devtools/client/inspector/markup/test/browser_markup_links_04.js new file mode 100644 index 0000000000..fe77b4e870 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_links_04.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 contextual menu shows the right items when clicking on a link +// in an attribute. Run the action to copy the link and check the clipboard. + +const TEST_URL = URL_ROOT + "doc_markup_links.html"; + +const TOOLBOX_L10N = new LocalizationHelper( + "devtools/client/locales/toolbox.properties" +); + +// The test case array contains objects with the following properties: +// - selector: css selector for the node to select in the inspector +// - attributeName: name of the attribute to test +// - popupNodeSelector: css selector for the element inside the attribute +// element to use as the contextual menu anchor +// - isLinkFollowItemVisible: is the follow-link item expected to be displayed +// - isLinkCopyItemVisible: is the copy-link item expected to be displayed +// - linkFollowItemLabel: the expected label of the follow-link item +// - linkCopyItemLabel: the expected label of the copy-link item +const TEST_DATA = [ + { + selector: "link", + attributeName: "href", + popupNodeSelector: ".link", + isLinkFollowItemVisible: true, + isLinkCopyItemVisible: true, + linkFollowItemLabel: TOOLBOX_L10N.getStr( + "toolbox.viewCssSourceInStyleEditor.label" + ), + linkCopyItemLabel: INSPECTOR_L10N.getStr( + "inspector.menu.copyUrlToClipboard.label" + ), + }, + { + selector: "link[rel=icon]", + attributeName: "href", + popupNodeSelector: ".link", + isLinkFollowItemVisible: true, + isLinkCopyItemVisible: true, + linkFollowItemLabel: INSPECTOR_L10N.getStr( + "inspector.menu.openUrlInNewTab.label" + ), + linkCopyItemLabel: INSPECTOR_L10N.getStr( + "inspector.menu.copyUrlToClipboard.label" + ), + }, + { + selector: "link", + attributeName: "rel", + popupNodeSelector: ".attr-value", + isLinkFollowItemVisible: false, + isLinkCopyItemVisible: false, + }, + { + selector: "output", + attributeName: "for", + popupNodeSelector: ".link", + isLinkFollowItemVisible: true, + isLinkCopyItemVisible: false, + linkFollowItemLabel: INSPECTOR_L10N.getFormatStr( + "inspector.menu.selectElement.label", + "name" + ), + }, + { + selector: "script", + attributeName: "src", + popupNodeSelector: ".link", + isLinkFollowItemVisible: true, + isLinkCopyItemVisible: true, + linkFollowItemLabel: TOOLBOX_L10N.getStr( + "toolbox.viewJsSourceInDebugger.label" + ), + linkCopyItemLabel: INSPECTOR_L10N.getStr( + "inspector.menu.copyUrlToClipboard.label" + ), + }, + { + selector: "p[for]", + attributeName: "for", + popupNodeSelector: ".attr-value", + isLinkFollowItemVisible: false, + isLinkCopyItemVisible: false, + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + for (const test of TEST_DATA) { + info("Selecting test node " + test.selector); + await selectNode(test.selector, inspector); + const nodeFront = inspector.selection.nodeFront; + + info("Finding the popupNode to anchor the context-menu to"); + const { editor } = await getContainerForSelector(test.selector, inspector); + const popupNode = editor.attrElements + .get(test.attributeName) + .querySelector(test.popupNodeSelector); + ok(popupNode, "Found the popupNode in attribute " + test.attributeName); + + info("Simulating a context click on the popupNode"); + const allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: popupNode, + }); + + const linkFollow = allMenuItems.find(i => i.id === "node-menu-link-follow"); + const linkCopy = allMenuItems.find(i => i.id === "node-menu-link-copy"); + + is( + linkFollow.visible, + test.isLinkFollowItemVisible, + "The follow-link item display is correct" + ); + is( + linkCopy.visible, + test.isLinkCopyItemVisible, + "The copy-link item display is correct" + ); + + if (test.isLinkFollowItemVisible) { + is( + linkFollow.label, + test.linkFollowItemLabel, + "the follow-link label is correct" + ); + } + if (test.isLinkCopyItemVisible) { + is( + linkCopy.label, + test.linkCopyItemLabel, + "the copy-link label is correct" + ); + + info("Get link from node attribute"); + const link = await nodeFront.getAttribute(test.attributeName); + info("Resolve link to absolue URL"); + const expected = await inspector.inspectorFront.resolveRelativeURL( + link, + nodeFront + ); + info("Check the clipboard to see if the correct URL was copied"); + await waitForClipboardPromise(() => linkCopy.click(), expected); + } + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_links_05.js b/devtools/client/inspector/markup/test/browser_markup_links_05.js new file mode 100644 index 0000000000..62d3a7d8f6 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_links_05.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the contextual menu items shown when clicking on links in +// attributes actually do the right things. + +const TEST_URL = URL_ROOT_SSL + "doc_markup_links.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Select a node with a URI attribute"); + await selectNode("video", inspector); + + info("Set the popupNode to the node that contains the uri"); + let { editor } = await getContainerForSelector("video", inspector); + openContextMenuAndGetAllItems(inspector, { + target: editor.attrElements.get("poster").querySelector(".link"), + }); + + info("Follow the link and wait for the new tab to open"); + const onTabOpened = once(gBrowser.tabContainer, "TabOpen"); + inspector.markup.contextMenu._onFollowLink(); + const { target: tab } = await onTabOpened; + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + ok(true, "A new tab opened"); + is( + tab.linkedBrowser.currentURI.spec, + URL_ROOT_SSL + "doc_markup_tooltip.png", + "The URL for the new tab is correct" + ); + gBrowser.removeTab(tab); + + info("Select a node with a IDREF attribute"); + await selectNode("label", inspector); + + info("Set the popupNode to the node that contains the ref"); + ({ editor } = await getContainerForSelector("label", inspector)); + openContextMenuAndGetAllItems(inspector, { + target: editor.attrElements.get("for").querySelector(".link"), + }); + + info("Follow the link and wait for the new node to be selected"); + const onSelection = inspector.selection.once("new-node-front"); + inspector.markup.contextMenu._onFollowLink(); + await onSelection; + + ok(true, "A new node was selected"); + is(inspector.selection.nodeFront.id, "name", "The right node was selected"); + + info("Select a node with an invalid IDREF attribute"); + await selectNode("output", inspector); + + info("Set the popupNode to the node that contains the ref"); + ({ editor } = await getContainerForSelector("output", inspector)); + openContextMenuAndGetAllItems(inspector, { + target: editor.attrElements.get("for").querySelectorAll(".link")[2], + }); + + info("Try to follow the link and check that no new node were selected"); + const onFailed = inspector.markup.once("idref-attribute-link-failed"); + inspector.markup.contextMenu._onFollowLink(); + await onFailed; + + ok(true, "The node selection failed"); + is( + inspector.selection.nodeFront.tagName.toLowerCase(), + "output", + "The node is still selected" + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_links_06.js b/devtools/client/inspector/markup/test/browser_markup_links_06.js new file mode 100644 index 0000000000..9e546760d2 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_links_06.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 contextual menu items shown when clicking on linked attributes +// for `; + +// Test that the "Screenshot Node" feature works with a node inside a shadow root. +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL(encodeURI(TEST_URL)); + + info("Select the green node"); + const greenNode = await getNodeFrontInShadowDom( + "div", + "test-component", + inspector + ); + await selectNode(greenNode, inspector); + + info("Take a screenshot of the green node and verify it looks as expected"); + const greenScreenshot = await takeNodeScreenshot(inspector); + await assertSingleColorScreenshotImage(greenScreenshot, 30, 30, { + r: 0, + g: 128, + b: 0, + }); + + await toolbox.destroy(); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_screenshot_node_warning.js b/devtools/client/inspector/markup/test/browser_markup_screenshot_node_warning.js new file mode 100644 index 0000000000..2ae713455a --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_screenshot_node_warning.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = `data:text/html;charset=utf8, +
`; + +// Test taking a screenshot of a tall node displays a warning message in the notification box. +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL(encodeURI(TEST_URL)); + + info("Select the blue node"); + await selectNode("#blue-node", inspector); + + info("Take a screenshot of the blue node and verify it looks as expected"); + const blueScreenshot = await takeNodeScreenshot(inspector); + await assertSingleColorScreenshotImage(blueScreenshot, 30, 10000, { + r: 0, + g: 0, + b: 255, + }); + + info( + "Check that a warning message was displayed to indicate the screenshot was truncated" + ); + const notificationBox = await waitFor(() => + toolbox.doc.querySelector(".notificationbox") + ); + + const message = notificationBox.querySelector(".notification").textContent; + ok( + message.startsWith("The image was cut off"), + `The warning message is rendered as expected (${message})` + ); + + await toolbox.destroy(); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_scrollable_badge.js b/devtools/client/inspector/markup/test/browser_markup_scrollable_badge.js new file mode 100644 index 0000000000..2f6a414249 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_scrollable_badge.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 scrollable badge is shown next to scrollable elements, and is updated +// dynamically when necessary. + +const TEST_URI = ` + +
+
+
+`; + +add_task(async function () { + const { inspector } = await openInspectorForURL( + "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI) + ); + + let badge = await getBadgeEl(inspector); + ok(badge, "The scrollable badge exists on the test node"); + + info("Make the test node non-scrollable"); + let onStateChanged = inspector.walker.once("scrollable-change"); + await toggleScrollableClass(); + await onStateChanged; + + badge = await getBadgeEl(inspector); + ok(!badge, "The scrollable badge doesn't exist anymore"); + + info("Make the test node scrollable again"); + onStateChanged = inspector.walker.once("scrollable-change"); + await toggleScrollableClass(); + await onStateChanged; + + badge = await getBadgeEl(inspector); + ok(badge, "The scrollable badge exists again"); +}); + +async function getBadgeEl(inspector) { + const wrapperMarkupContainer = await getContainerForSelector( + "#wrapper", + inspector + ); + return wrapperMarkupContainer.elt.querySelector( + ".inspector-badge.scrollable-badge" + ); +} + +async function toggleScrollableClass() { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { + content.document.querySelector("#wrapper").classList.toggle("no-scroll"); + }); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_scrollable_badge_click.js b/devtools/client/inspector/markup/test/browser_markup_scrollable_badge_click.js new file mode 100644 index 0000000000..3699cd97be --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_scrollable_badge_click.js @@ -0,0 +1,155 @@ +/* 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"; + +// Tests that the correct elements show up and get highlighted in the markup view when the +// scrollable badge is clicked. + +const TEST_URI = ` + +
+
+
+
+
+
+
+
+`; + +add_task(async function () { + const { inspector } = await openInspectorForURL( + "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI) + ); + + const container = await getContainerForSelector("#top", inspector); + + info( + "Clicking on the scrollable badge so that the overflow causing elements show up in the markup view." + ); + container.editor._scrollableBadge.click(); + + await waitForContainers(["#child1", "#child3", "#child4"], inspector); + + await checkOverflowHighlight( + ["#child1", "#child4"], + ["#child2", "#child3"], + inspector + ); + + ok( + container.editor._scrollableBadge.classList.contains("active"), + "Scrollable badge is active" + ); + + checkTelemetry("devtools.markup.scrollable.badge.clicked", "", 1, "scalar"); + + info( + "Changing CSS so elements update their overflow highlights accordingly." + ); + await toggleClass(inspector); + + // By default, #child2 will not be visible in the markup view, + // so expand its parent to make it visible. + const child1 = await getContainerForSelector("#child1", inspector); + await expandContainer(inspector, child1); + + await checkOverflowHighlight( + ["#child2", "#child3"], + ["#child1", "#child4"], + inspector + ); + + info( + "Clicking on the scrollable badge again so that all the overflow highlight gets removed." + ); + container.editor._scrollableBadge.click(); + + await checkOverflowHighlight( + [], + ["#child1", "#child2", "#child3", "#child4"], + inspector + ); + + ok( + !container.editor._scrollableBadge.classList.contains("active"), + "Scrollable badge is not active" + ); + + checkTelemetry("devtools.markup.scrollable.badge.clicked", "", 2, "scalar"); + + info("Double-click on the scrollable badge"); + EventUtils.sendMouseEvent( + { type: "dblclick" }, + container.editor._scrollableBadge + ); + ok( + container.expanded, + "Double clicking on the badge did not collapse the container" + ); +}); + +async function getContainerForSelector(selector, inspector) { + const nodeFront = await getNodeFront(selector, inspector); + return getContainerForNodeFront(nodeFront, inspector); +} + +async function waitForContainers(selectors, inspector) { + for (const selector of selectors) { + info(`Wait for markup container of ${selector}`); + await asyncWaitUntil(() => getContainerForSelector(selector, inspector)); + } +} + +async function elementHasHighlight(selector, inspector) { + const container = await getContainerForSelector(selector, inspector); + return container?.tagState.classList.contains("overflow-causing-highlighted"); +} + +async function checkOverflowHighlight( + selectorWithHighlight, + selectorWithNoHighlight, + inspector +) { + for (const selector of selectorWithHighlight) { + ok( + await elementHasHighlight(selector, inspector), + `${selector} contains overflow highlight` + ); + } + for (const selector of selectorWithNoHighlight) { + ok( + !(await elementHasHighlight(selector, inspector)), + `${selector} does not contain overflow highlight` + ); + } +} + +async function toggleClass(inspector) { + const onStateChanged = inspector.walker.once("overflow-change"); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { + content.document.querySelector("#child1").classList.toggle("fixed"); + content.document.querySelector("#child3").classList.toggle("fixed"); + }); + + await onStateChanged; +} diff --git a/devtools/client/inspector/markup/test/browser_markup_search_01.js b/devtools/client/inspector/markup/test/browser_markup_search_01.js new file mode 100644 index 0000000000..6288bcb942 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_search_01.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that searching for nodes using the selector-search input expands and +// selects the right nodes in the markup-view, even when those nodes are deeply +// nested (and therefore not attached yet when the markup-view is initialized). + +const TEST_URL = URL_ROOT + "doc_markup_search.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + let container = await getContainerForSelector("em", inspector, true); + ok(!container, "The tag isn't present yet in the markup-view"); + + // Searching for the innermost element first makes sure that the inspector + // back-end is able to attach the resulting node to the tree it knows at the + // moment. When the inspector is started, the is the default selected + // node, and only the parents up to the ROOT are known, and its direct + // children. + info("searching for the innermost child: "); + await searchFor("em", inspector); + + container = await getContainerForSelector("em", inspector); + ok(container, "The tag is now imported in the markup-view"); + + let nodeFront = await getNodeFront("em", inspector); + is( + inspector.selection.nodeFront, + nodeFront, + "The tag is the currently selected node" + ); + + info("searching for other nodes too"); + for (const node of ["span", "li", "ul"]) { + await searchFor(node, inspector); + + nodeFront = await getNodeFront(node, inspector); + is( + inspector.selection.nodeFront, + nodeFront, + "The <" + node + "> tag is the currently selected node" + ); + } +}); + +async function searchFor(selector, inspector) { + const onNewNodeFront = inspector.selection.once("new-node-front"); + + searchUsingSelectorSearch(selector, inspector); + + await onNewNodeFront; + await inspector.once("inspector-updated"); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom.js new file mode 100644 index 0000000000..1b701f872e --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom.js @@ -0,0 +1,290 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Test a few static pages using webcomponents and check that they are displayed as +// expected in the markup view. + +const TEST_DATA = [ + { + // Test that expanding a shadow host shows a shadow root node and direct children. + // Test that expanding a shadow root shows the shadow dom. + // Test that slotted elements are visible in the shadow dom. + title: "generic shadow dom test", + url: `data:text/html;charset=utf-8, + +
slotted-1
inner
+
slotted-2
inner
+
no-slot-text
inner
+
+ + `, + tree: ` + test-component + #shadow-root + name="slot1" + div!slotted + name="slot2" + div!slotted + slot + div!slotted + slot="slot1" + slotted-1 + inner + slot="slot2" + slotted-2 + inner + class="no-slot-class" + no-slot-text + inner`, + }, + { + // Test that components without any direct children still display a shadow root node, + // if a shadow root is attached to the host. + title: "shadow root without direct children", + url: `data:text/html;charset=utf-8, + + `, + tree: ` + test-component + #shadow-root + slot + fallback-content`, + }, + { + // Test that markup view is correctly displayed for non-trivial shadow DOM nesting. + title: "nested components", + url: `data:text/html;charset=utf-8, + +
slot1-1
+ +
+ + `, + tree: ` + test-component + #shadow-root + test-container + slot + div!slotted + slot + third-component!slotted + other-component + #shadow-root + div + slot + div!slotted + div + div + third-component + #shadow-root + div`, + }, + { + // Test that ::before and ::after pseudo elements are correctly displayed in host + // components and in slot elements. + title: "pseudo elements", + url: `data:text/html;charset=utf-8, + + + +
+
+ + `, + tree: ` + test-component + #shadow-root + style + slot { display: block } + slot + ::before + div!slotted + default content + ::after + ::before + class="light-dom" + ::after`, + }, + { + // Test empty web components are still displayed correctly. + title: "empty components", + url: `data:text/html;charset=utf-8, + + + `, + tree: ` + test-component + #shadow-root`, + }, + { + // Test shadow hosts show their shadow root even if they contain just a short text. + title: "shadow host with inline-text-child", + url: `data:text/html;charset=utf-8, + + short-text-outside + + + `, + tree: ` + test-component + #shadow-root + div + slot + inner-component!slotted + inner-component + #shadow-root + short-text-inside + short-text-outside`, + }, + { + // Test for Bug 1537877, crash with nested custom elements without slot. + title: "nested components without slot", + url: `data:text/html;charset=utf-8, + + + + + `, + tree: ` + test-component + #shadow-root + div + inner-component + #shadow-root + inner-component-content`, + }, +]; + +for (const { url, tree, title } of TEST_DATA) { + // Test each configuration in both open and closed modes + add_task(async function () { + info(`Testing: [${title}] in OPEN mode`); + const { inspector, tab } = await openInspectorForURL( + url.replace(/#MODE#/g, "open") + ); + await assertMarkupViewAsTree(tree, "test-component", inspector); + await removeTab(tab); + }); + add_task(async function () { + info(`Testing: [${title}] in CLOSED mode`); + const { inspector, tab } = await openInspectorForURL( + url.replace(/#MODE#/g, "closed") + ); + await assertMarkupViewAsTree(tree, "test-component", inspector); + await removeTab(tab); + }); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal.js new file mode 100644 index 0000000000..778420dba3 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal.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 corresponding non-slotted node container gets selected when clicking on +// the reveal link for a slotted node. + +const TEST_URL = `data:text/html;charset=utf-8, + +
slot1-1
+
slot1-2
+
+ + `; + +// Test reveal link with mouse navigation +add_task(async function () { + const checkWithMouse = checkRevealLink.bind(null, clickOnRevealLink); + await testRevealLink(checkWithMouse, checkWithMouse); +}); + +// Test reveal link with keyboard navigation (Enter and Spacebar keys) +add_task(async function () { + const checkWithEnter = checkRevealLink.bind( + null, + keydownOnRevealLink.bind(null, "KEY_Enter") + ); + const checkWithSpacebar = checkRevealLink.bind( + null, + keydownOnRevealLink.bind(null, " ") + ); + + await testRevealLink(checkWithEnter, checkWithSpacebar); +}); + +async function testRevealLink(revealFnFirst, revealFnSecond) { + const { inspector } = await openInspectorForURL(TEST_URL); + const { markup } = inspector; + + info("Find and expand the test-component shadow DOM host."); + const hostFront = await getNodeFront("test-component", inspector); + const hostContainer = markup.getContainer(hostFront); + await expandContainer(inspector, hostContainer); + + info("Expand the shadow root"); + const shadowRootContainer = hostContainer.getChildContainers()[0]; + await expandContainer(inspector, shadowRootContainer); + + info("Expand the slot"); + const slotContainer = shadowRootContainer.getChildContainers()[0]; + await expandContainer(inspector, slotContainer); + + const slotChildContainers = slotContainer.getChildContainers(); + is(slotChildContainers.length, 2, "Expecting 2 slotted children"); + + await revealFnFirst(inspector, slotChildContainers[0].node); + is(inspector.selection.nodeFront.id, "el1", "The right node was selected"); + is(hostContainer.getChildContainers()[1].node, inspector.selection.nodeFront); + + await revealFnSecond(inspector, slotChildContainers[1].node); + is(inspector.selection.nodeFront.id, "el2", "The right node was selected"); + is(hostContainer.getChildContainers()[2].node, inspector.selection.nodeFront); +} + +async function checkRevealLink(actionFn, inspector, node) { + const slottedContainer = inspector.markup.getContainer(node, true); + info("Select the slotted container for the element"); + await selectNode(node, inspector, "no-reason", true); + ok(inspector.selection.isSlotted(), "The selection is the slotted version"); + ok( + inspector.markup.getSelectedContainer().isSlotted(), + "The selected container is slotted" + ); + + const link = slottedContainer.elt.querySelector(".reveal-link"); + is( + link.getAttribute("role"), + "link", + "Reveal link has the role=link attribute" + ); + + info("Click on the reveal link and wait for the new node to be selected"); + await actionFn(inspector, slottedContainer); + const selectedFront = inspector.selection.nodeFront; + is(selectedFront, node, "The same node front is still selected"); + ok( + !inspector.selection.isSlotted(), + "The selection is not the slotted version" + ); + // wait until the selected container isn't the one we had before. + await waitFor( + () => inspector.markup.getSelectedContainer() !== slottedContainer + ); + ok( + !inspector.markup.getSelectedContainer().isSlotted(), + "The selected container is not slotted" + ); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal_scroll.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal_scroll.js new file mode 100644 index 0000000000..7e3714420b --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal_scroll.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that clicking on "reveal" always scrolls the view to show the real container, even +// if the node is already selected. + +const TEST_URL = `data:text/html;charset=utf-8, + +
slot1 content
+
+ + `; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const { markup } = inspector; + + info("Find and expand the test-component shadow DOM host."); + const hostFront = await getNodeFront("test-component", inspector); + const hostContainer = markup.getContainer(hostFront); + await expandContainer(inspector, hostContainer); + + info("Expand the shadow root"); + const shadowRootContainer = hostContainer.getChildContainers()[0]; + await expandContainer(inspector, shadowRootContainer); + + info("Expand the slot"); + const slotContainer = shadowRootContainer.getChildContainers()[0]; + await expandContainer(inspector, slotContainer); + + const slotChildContainers = slotContainer.getChildContainers(); + is(slotChildContainers.length, 1, "Expecting 1 slotted child"); + + const slottedContainer = slotChildContainers[0]; + const realContainer = inspector.markup.getContainer(slottedContainer.node); + const slottedElement = slottedContainer.elt; + const realElement = realContainer.elt; + + info("Click on the reveal link"); + await clickOnRevealLink(inspector, slottedContainer); + // "new-node-front" will also trigger the scroll, so make sure we are testing after + // the scroll was performed. + await waitUntil(() => isScrolledOut(slottedElement)); + is(isScrolledOut(slottedElement), true, "slotted element is scrolled out"); + await waitUntil(() => !isScrolledOut(realElement)); + is(isScrolledOut(realElement), false, "real element is not scrolled out"); + + info("Scroll back to see the slotted element"); + slottedElement.scrollIntoView(); + is( + isScrolledOut(slottedElement), + false, + "slotted element is not scrolled out" + ); + is(isScrolledOut(realElement), true, "real element is scrolled out"); + + info("Click on the reveal link again"); + await clickOnRevealLink(inspector, slottedContainer); + await waitUntil(() => isScrolledOut(slottedElement)); + is(isScrolledOut(slottedElement), true, "slotted element is scrolled out"); + await waitUntil(() => !isScrolledOut(realElement)); + is(isScrolledOut(realElement), false, "real element is not scrolled out"); +}); + +function isScrolledOut(element) { + const win = element.ownerGlobal; + const rect = element.getBoundingClientRect(); + return rect.top < 0 || rect.top + rect.height > win.innerHeight; +} diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_copy_paths.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_copy_paths.js new file mode 100644 index 0000000000..81af9ea3f0 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_copy_paths.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that using copyCssPath, copyXPath and copyUniqueSelector with an element under a +// shadow root returns values relevant to the selected element, and relative to the shadow +// root. + +const TEST_URL = `data:text/html;charset=utf-8, + + + `; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const { markup } = inspector; + + info("Find and expand the test-component shadow DOM host."); + const hostFront = await getNodeFront("test-component", inspector); + const hostContainer = markup.getContainer(hostFront); + await expandContainer(inspector, hostContainer); + + info("Expand the shadow root"); + const shadowRootContainer = hostContainer.getChildContainers()[0]; + await expandContainer(inspector, shadowRootContainer); + + info("Select the div under the shadow root"); + const divContainer = shadowRootContainer.getChildContainers()[0]; + await selectNode(divContainer.node, inspector); + + info("Check the copied values for the various copy*Path helpers"); + await waitForClipboardPromise( + () => inspector.markup.contextMenu._copyXPath(), + '//*[@id="el1"]' + ); + await waitForClipboardPromise( + () => inspector.markup.contextMenu._copyCssPath(), + "div#el1" + ); + await waitForClipboardPromise( + () => inspector.markup.contextMenu._copyUniqueSelector(), + "#el1" + ); + + info("Expand the div"); + await expandContainer(inspector, divContainer); + + info("Select the third span"); + const spanContainer = divContainer.getChildContainers()[2]; + await selectNode(spanContainer.node, inspector); + + info("Check the copied values for the various copy*Path helpers"); + await waitForClipboardPromise( + () => inspector.markup.contextMenu._copyXPath(), + "/div/span[3]" + ); + await waitForClipboardPromise( + () => inspector.markup.contextMenu._copyCssPath(), + "div#el1 span" + ); + await waitForClipboardPromise( + () => inspector.markup.contextMenu._copyUniqueSelector(), + "#el1 > span:nth-child(3)" + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_delete.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_delete.js new file mode 100644 index 0000000000..f5b707bb6a --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_delete.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that slot elements are correctly updated when slotted elements are being removed +// from the DOM. + +const TEST_URL = `data:text/html;charset=utf-8, + +
slot1-1
+
slot1-2
+
+ + `; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + // is a shadow host. + info("Find and expand the test-component shadow DOM host."); + const hostFront = await getNodeFront("test-component", inspector); + await inspector.markup.expandNode(hostFront); + await waitForMultipleChildrenUpdates(inspector); + + info( + "Test that expanding a shadow host shows shadow root and direct host children." + ); + const { markup } = inspector; + const hostContainer = markup.getContainer(hostFront); + const childContainers = hostContainer.getChildContainers(); + + is( + childContainers.length, + 3, + "Expecting 3 children: shadowroot, 2 host children" + ); + assertContainerHasText(childContainers[0], "#shadow-root"); + assertContainerHasText(childContainers[1], "div"); + assertContainerHasText(childContainers[2], "div"); + + info("Expand the shadow root"); + const shadowRootContainer = childContainers[0]; + await expandContainer(inspector, shadowRootContainer); + + const shadowChildContainers = shadowRootContainer.getChildContainers(); + is(shadowChildContainers.length, 1, "Expecting 1 child slot"); + assertContainerHasText(shadowChildContainers[0], "slot"); + + info("Expand the slot"); + const slotContainer = shadowChildContainers[0]; + await expandContainer(inspector, slotContainer); + + let slotChildContainers = slotContainer.getChildContainers(); + is( + slotChildContainers.length, + 3, + "Expecting 3 children (2 slotted, fallback)" + ); + assertContainerSlotted(slotChildContainers[0]); + assertContainerSlotted(slotChildContainers[1]); + assertContainerHasText(slotChildContainers[2], "div"); + + await deleteNode(inspector, "#el1"); + slotChildContainers = slotContainer.getChildContainers(); + is( + slotChildContainers.length, + 2, + "Expecting 2 children (1 slotted, fallback)" + ); + assertContainerSlotted(slotChildContainers[0]); + assertContainerHasText(slotChildContainers[1], "div"); + + await deleteNode(inspector, "#el2"); + slotChildContainers = slotContainer.getChildContainers(); + // After deleting the last host direct child we expect the slot to show the default + // content
default
+ is(slotChildContainers.length, 1, "Expecting 1 child"); + ok( + !slotChildContainers[0].isSlotted(), + "Container is a not slotted container" + ); +}); + +async function deleteNode(inspector, selector) { + info("Select node " + selector + " and make sure it is focused"); + await selectNode(selector, inspector); + await clickContainer(selector, inspector); + + info("Delete the node"); + const mutated = inspector.once("markupmutation"); + const updated = inspector.once("inspector-updated"); + EventUtils.sendKey("delete", inspector.panelWin); + await mutated; + await updated; +} diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_dynamic.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_dynamic.js new file mode 100644 index 0000000000..597623ebf4 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_dynamic.js @@ -0,0 +1,155 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the inspector is correctly updated when shadow roots are attached to +// components after displaying them in the markup view. + +const TEST_URL = + `data:text/html;charset=utf-8,` + + encodeURIComponent(` +
+ +
slot1-1
+
slot1-2
+
+ inline text +
+ + `); + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + const tree = ` + div + test-component + slot1-1 + slot1-2 + inline text`; + await assertMarkupViewAsTree(tree, "#root", inspector); + + info("Attach a shadow root to test-component"); + let mutated = waitForMutation(inspector, "shadowRootAttached"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + content.wrappedJSObject.attachTestComponent(); + }); + await mutated; + + const treeAfterTestAttach = ` + div + test-component + #shadow-root + slot1-container + slot + div!slotted + div!slotted + other-component + slot2-1 + slot1-1 + slot1-2 + inline text`; + await assertMarkupViewAsTree(treeAfterTestAttach, "#root", inspector); + + info("Attach a shadow root to other-component, nested in test-component"); + mutated = waitForMutation(inspector, "shadowRootAttached"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + content.wrappedJSObject.attachOtherComponent(); + }); + await mutated; + + const treeAfterOtherAttach = ` + div + test-component + #shadow-root + slot1-container + slot + div!slotted + div!slotted + other-component + #shadow-root + slot2-container + slot + div!slotted + some-other-node + slot2-1 + slot1-1 + slot1-2 + inline text`; + await assertMarkupViewAsTree(treeAfterOtherAttach, "#root", inspector); + + info( + "Attach a shadow root to inline-component, check the inline text child." + ); + mutated = waitForMutation(inspector, "shadowRootAttached"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + content.wrappedJSObject.attachInlineComponent(); + }); + await mutated; + + const treeAfterInlineAttach = ` + div + test-component + #shadow-root + slot1-container + slot + div!slotted + div!slotted + other-component + #shadow-root + slot2-container + slot + div!slotted + some-other-node + slot2-1 + slot1-1 + slot1-2 + inline-component + #shadow-root + inline-component-content + some-inline-content + inline text`; + await assertMarkupViewAsTree(treeAfterInlineAttach, "#root", inspector); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_hover.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_hover.js new file mode 100644 index 0000000000..c386e26d6d --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_hover.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Bug 1465873 +// Tests that hovering nodes in the content page with the element picked and finally +// picking one does not break the markup view. The markup and sequence used here is a bit +// eccentric but the issue from Bug 1465873 is tricky to reproduce. + +const TEST_URL = + `data:text/html;charset=utf-8,` + + encodeURIComponent(` + +
slot1-1
+
+ `); + +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL(TEST_URL); + + info("Waiting for element picker to become active."); + await startPicker(toolbox); + + info("Move mouse over the padding of the test-component"); + await hoverElement(inspector, "test-component", 10, 10); + + info("Move mouse over the pick-target"); + // Note we can't reach pick-target with a selector because this element lives in the + // shadow-dom of test-component. We aim for PADDING + 5 pixels + await hoverElement(inspector, "test-component", 10, 25); + + info("Click and pick the pick-target"); + await pickElement(inspector, "test-component", 10, 25); + + info( + "Check that the markup view has the expected content after using the picker" + ); + const tree = ` + test-component + #shadow-root + wrapper + a + pick-target + slot1-container + slot1 + div!slotted + div`; + await assertMarkupViewAsTree(tree, "test-component", inspector); + + const hostFront = await getNodeFront("test-component", inspector); + const hostContainer = inspector.markup.getContainer(hostFront); + const moreNodesLink = hostContainer.elt.querySelector(".more-nodes"); + ok( + !moreNodesLink, + "There is no 'more nodes' button displayed in the host container" + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_marker_and_before_pseudos.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_marker_and_before_pseudos.js new file mode 100644 index 0000000000..4462671354 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_marker_and_before_pseudos.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(1); + +// Test a few static pages using webcomponents with ::marker and ::before +// pseudos and check that they are displayed as expected in the markup view. + +const TEST_DATA = [ + { + // Test that ::before on an empty shadow host is displayed when the host + // has a ::marker. + title: "::before after ::marker, empty node", + url: `data:text/html;charset=utf-8, + + + + + `, + tree: ` + test-component + #shadow-root + ::marker + ::before`, + }, + { + // Test ::before on a shadow host with content is displayed when the host + // has a ::marker. + title: "::before after ::marker, non-empty node", + url: `data:text/html;charset=utf-8, + + + +
+
+ + `, + tree: ` + test-component + #shadow-root + slot + div!slotted + default content + ::marker + ::before + class="light-dom"`, + }, + { + // Test just ::marker on a shadow host + title: "just ::marker, no ::before", + url: `data:text/html;charset=utf-8, + + + + + `, + tree: ` + test-component + #shadow-root + ::marker`, + }, +]; + +for (const { url, tree, title } of TEST_DATA) { + // Test each configuration in both open and closed modes + add_task(async function () { + info(`Testing: [${title}] in OPEN mode`); + const { inspector, tab } = await openInspectorForURL( + url.replace(/#MODE#/g, "open") + ); + await assertMarkupViewAsTree(tree, "test-component", inspector); + await removeTab(tab); + }); + add_task(async function () { + info(`Testing: [${title}] in CLOSED mode`); + const { inspector, tab } = await openInspectorForURL( + url.replace(/#MODE#/g, "closed") + ); + await assertMarkupViewAsTree(tree, "test-component", inspector); + await removeTab(tab); + }); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_maxchildren.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_maxchildren.js new file mode 100644 index 0000000000..5b0f359cee --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_maxchildren.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the markup view properly displays the "more nodes" button both for host +// elements and for slot elements. + +const TEST_URL = `data:text/html;charset=utf-8, + +
node 1
node 2
node 3
+
node 4
node 5
node 6
+
+ +`; + +const MAX_CHILDREN = 5; + +add_task(async function () { + await pushPref("devtools.markup.pagesize", MAX_CHILDREN); + + const { inspector } = await openInspectorForURL(TEST_URL); + + // is a shadow host. + info("Find and expand the test-component shadow DOM host."); + const hostFront = await getNodeFront("test-component", inspector); + await inspector.markup.expandNode(hostFront); + await waitForMultipleChildrenUpdates(inspector); + + info( + "Test that expanding a shadow host shows shadow root and direct host children." + ); + const { markup } = inspector; + const hostContainer = markup.getContainer(hostFront); + let childContainers = hostContainer.getChildContainers(); + + is( + childContainers.length, + MAX_CHILDREN, + "Expecting 5 children: shadowroot, 4 host children" + ); + assertContainerHasText(childContainers[0], "#shadow-root"); + for (let i = 1; i < 5; i++) { + assertContainerHasText(childContainers[i], "div"); + assertContainerHasText(childContainers[i], "node " + i); + } + + info("Click on the more nodes button under the host element"); + let moreNodesLink = hostContainer.elt.querySelector(".more-nodes"); + ok( + !!moreNodesLink, + "A 'more nodes' button is displayed in the host container" + ); + moreNodesLink.querySelector("button").click(); + await inspector.markup._waitForChildren(); + + childContainers = hostContainer.getChildContainers(); + is(childContainers.length, 7, "Expecting one additional host child"); + assertContainerHasText(childContainers[6], "div"); + assertContainerHasText(childContainers[6], "node 6"); + + info("Expand the shadow root"); + const shadowRootContainer = childContainers[0]; + const shadowRootFront = shadowRootContainer.node; + await inspector.markup.expandNode(shadowRootFront); + await waitForMultipleChildrenUpdates(inspector); + + const shadowChildContainers = shadowRootContainer.getChildContainers(); + is(shadowChildContainers.length, 1, "Expecting 1 slot child"); + assertContainerHasText(shadowChildContainers[0], "slot"); + + info("Expand the slot"); + const slotContainer = shadowChildContainers[0]; + const slotFront = slotContainer.node; + await inspector.markup.expandNode(slotFront); + await waitForMultipleChildrenUpdates(inspector); + + let slotChildContainers = slotContainer.getChildContainers(); + is(slotChildContainers.length, MAX_CHILDREN, "Expecting 5 slotted children"); + for (const slotChildContainer of slotChildContainers) { + assertContainerHasText(slotChildContainer, "div"); + ok( + slotChildContainer.elt.querySelector(".reveal-link"), + "Slotted container has a reveal link element" + ); + } + + info("Click on the more nodes button under the slot element"); + moreNodesLink = slotContainer.elt.querySelector(".more-nodes"); + ok( + !!moreNodesLink, + "A 'more nodes' button is displayed in the host container" + ); + EventUtils.sendMouseEvent( + { type: "click" }, + moreNodesLink.querySelector("button") + ); + await inspector.markup._waitForChildren(); + + slotChildContainers = slotContainer.getChildContainers(); + is( + slotChildContainers.length, + 7, + "Expecting one additional slotted element and fallback" + ); + assertContainerHasText(slotChildContainers[5], "div"); + ok( + slotChildContainers[5].elt.querySelector(".reveal-link"), + "Slotted container has a reveal link element" + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_mutations_shadow.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_mutations_shadow.js new file mode 100644 index 0000000000..5e8a967c27 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_mutations_shadow.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 markup view is correctly updated when elements under a shadow root are +// deleted or updated. + +const TEST_URL = `data:text/html;charset=utf-8, + +
slot1-1
+
slot1-2
+
+ + `; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + const tree = ` + test-component + #shadow-root + slot1-container + slot + div!slotted + div!slotted + another-div + div + div`; + await assertMarkupViewAsTree(tree, "test-component", inspector); + + info("Delete a shadow dom element and check the updated markup view"); + let mutated = waitForMutation(inspector, "childList"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const shadowRoot = + content.document.querySelector("test-component").shadowRoot; + const slotContainer = shadowRoot.getElementById("slot1-container"); + slotContainer.remove(); + }); + await mutated; + + const treeAfterDelete = ` + test-component + #shadow-root + another-div + div + div`; + await assertMarkupViewAsTree(treeAfterDelete, "test-component", inspector); + + mutated = inspector.once("markupmutation"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const shadowRoot = + content.document.querySelector("test-component").shadowRoot; + const shadowDiv = shadowRoot.getElementById("another-div"); + shadowDiv.setAttribute("random-attribute", "1"); + }); + await mutated; + + info( + "Add an attribute on a shadow dom element and check the updated markup view" + ); + const treeAfterAttrChange = ` + test-component + #shadow-root + random-attribute + div + div`; + await assertMarkupViewAsTree( + treeAfterAttrChange, + "test-component", + inspector + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_navigation.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_navigation.js new file mode 100644 index 0000000000..ece16815ad --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_navigation.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the markup-view navigation works correctly with shadow dom slotted nodes. +// Each slotted nodes has two containers representing the same node front in the markup +// view, we need to make sure that navigating to the slotted version selects the slotted +// container, and navigating to the non-slotted element selects the non-slotted container. + +const TEST_URL = `data:text/html;charset=utf-8, + +
slot1-1
+
slot1-2
+
+ + `; + +const TEST_DATA = [ + ["KEY_PageUp", "html"], + ["KEY_ArrowDown", "head"], + ["KEY_ArrowDown", "body"], + ["KEY_ArrowDown", "test-component"], + ["KEY_ArrowRight", "test-component"], + ["KEY_ArrowDown", "shadow-root"], + ["KEY_ArrowRight", "shadow-root"], + ["KEY_ArrowDown", "slot1"], + ["KEY_ArrowRight", "slot1"], + ["KEY_ArrowDown", "div", "slotted1"], + ["KEY_ArrowDown", "div", "slotted2"], + ["KEY_ArrowDown", "slotted1"], + ["KEY_ArrowRight", "slotted1"], + ["KEY_ArrowDown", "slot1-child"], + ["KEY_ArrowDown", "slotted2"], +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Making sure the markup-view frame is focused"); + inspector.markup._frame.focus(); + + info("Starting to iterate through the test data"); + for (const [key, expected, slottedClassName] of TEST_DATA) { + info("Testing step: " + key + " to navigate to " + expected); + EventUtils.synthesizeKey(key); + + info("Making sure markup-view children get updated"); + await waitForChildrenUpdated(inspector); + + info("Checking the right node is selected"); + checkSelectedNode(key, expected, slottedClassName, inspector); + } + + // Same as in browser_markup_navigation.js, use a single catch-call event listener. + await inspector.once("inspector-updated"); +}); + +function checkSelectedNode(key, expected, slottedClassName, inspector) { + const selectedContainer = inspector.markup.getSelectedContainer(); + const slotted = !!slottedClassName; + + is( + selectedContainer.isSlotted(), + slotted, + `Selected container is ${slotted ? "slotted" : "not slotted"} as expected` + ); + is( + inspector.selection.isSlotted(), + slotted, + `Inspector selection is also ${slotted ? "slotted" : "not slotted"}` + ); + ok( + selectedContainer.elt.textContent.includes(expected), + "Found expected content: " + + expected + + " in container after pressing " + + key + ); + + if (slotted) { + is( + selectedContainer.node.className, + slottedClassName, + "Slotted has the expected classname " + slottedClassName + ); + } +} diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_nested_pick_inspect.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_nested_pick_inspect.js new file mode 100644 index 0000000000..ee7fde1584 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_nested_pick_inspect.js @@ -0,0 +1,132 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the markup view is correctly expanded when inspecting an element nested +// in several shadow roots: +// - when using the context-menu "Inspect element" +// - when using the element picker + +const TEST_URL = + `data:text/html;charset=utf-8,` + + encodeURIComponent(` + + `); + +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL(TEST_URL); + + info("Waiting for element picker to become active"); + await startPicker(toolbox); + info("Click and pick the pick-target"); + await pickElement(inspector, "test-outer", 10, 10); + info("Check that the markup view is displayed as expected"); + await assertMarkupView(inspector); + + info("Close DevTools before testing Inspect Element"); + await toolbox.destroy(); + + info("Click on Inspect Element for our test-image
"); + // Note: we click on test-outer, because we can't find the
using a simple + // querySelector. However the click is simulated in the middle of the + // component, and will always hit the test
which takes all the space. + const newInspector = await clickOnInspectMenuItem("test-outer"); + info("Check again that the markup view is displayed as expected"); + await assertMarkupView(newInspector); +}); + +async function assertMarkupView(inspector) { + const outerFront = await getNodeFront("test-outer", inspector); + const outerContainer = inspector.markup.getContainer(outerFront); + assertContainer(outerContainer, { + expanded: true, + text: "test-outer", + children: 1, + }); + + const outerShadowContainer = outerContainer.getChildContainers()[0]; + assertContainer(outerShadowContainer, { + expanded: true, + text: "#shadow-root", + children: 1, + }); + + const innerContainer = outerShadowContainer.getChildContainers()[0]; + assertContainer(innerContainer, { + expanded: true, + text: "test-inner", + children: 2, + }); + + const innerShadowContainer = innerContainer.getChildContainers()[0]; + const imageContainer = innerContainer.getChildContainers()[1]; + assertContainer(innerShadowContainer, { + expanded: false, + text: "#shadow-root", + }); + assertContainer(imageContainer, { + expanded: true, + text: "test-image", + children: 1, + }); + + const imageShadowContainer = imageContainer.getChildContainers()[0]; + assertContainer(imageShadowContainer, { + expanded: true, + text: "#shadow-root", + children: 1, + }); + + const redDivContainer = imageShadowContainer.getChildContainers()[0]; + assertContainer(redDivContainer, { expanded: false, text: "div" }); + is(redDivContainer.selected, true, "Div element is selected as expected"); +} + +/** + * Check if the provided markup container is expanded, has the expected text and the + * expected number of children. + */ +function assertContainer(container, { expanded, text, children }) { + is(container.expanded, expanded, "Container is expanded"); + assertContainerHasText(container, text); + if (expanded) { + const childContainers = container.getChildContainers(); + is( + childContainers.length, + children, + "Container has expected number of children" + ); + } +} diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_noslot.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_noslot.js new file mode 100644 index 0000000000..4c4a9c2e90 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_noslot.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 markup view is correctly displayed when a component has children but no +// slots are available under the shadow root. + +const TEST_URL = `data:text/html;charset=utf-8, + + +
+ +
light
+
+
+
dummy for Bug 1441863
+
+
+ +
light
+
+
+
+
+
+ + `; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + // We expect that host children are correctly displayed when no slots are defined. + const beforeTree = ` + class="root" + no-slot-component + #shadow-root + no-slot-div + class="not-nested" + class="nested" + class="has-before" + dummy for Bug 1441863 + slot-component + #shadow-root + slot + div!slotted + div!slotted + class="not-nested" + class="nested" + class="has-before" + ::before`; + await assertMarkupViewAsTree(beforeTree, ".root", inspector); + + info( + "Move the non-slotted element with class has-before and check the pseudo appears" + ); + const mutated = waitForNMutations(inspector, "childList", 3); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const root = content.document.querySelector(".root"); + const hasBeforeEl = content.document.querySelector( + "no-slot-component .has-before" + ); + root.appendChild(hasBeforeEl); + }); + await mutated; + + // As the non-slotted has-before is moved into the tree, the before pseudo is expected + // to appear. + const afterTree = ` + class="root" + no-slot-component + #shadow-root + no-slot-div + class="not-nested" + class="nested" + dummy for Bug 1441863 + slot-component + #shadow-root + slot + div!slotted + div!slotted + class="not-nested" + class="nested" + class="has-before" + ::before + class="has-before" + ::before`; + await assertMarkupViewAsTree(afterTree, ".root", inspector); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_open_debugger.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_open_debugger.js new file mode 100644 index 0000000000..2b7e670eb0 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_open_debugger.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that markup view displays a "custom" badge for custom elements. +// Test that the context menu also has a menu item to show the custom element definition. +// Test that clicking on any of those opens the debugger. +// Test that the markup view is correctly updated to show those items if the custom +// element definition happens after opening the inspector. + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/shared-head.js", + this +); + +const TEST_URL = + `data:text/html;charset=utf-8,` + + encodeURIComponent(` + +some-content + +`); + +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL(TEST_URL); + + // Test with an element to which we attach a shadow. + await runTest(inspector, toolbox, "test-component", "attachTestComponent"); + + // Test with an element to which we only add a custom element definition. + await runTest(inspector, toolbox, "other-component", "defineOtherComponent"); +}); + +async function runTest(inspector, toolbox, selector, contentMethod) { + // Test element is a regular element (no shadow root or custom element definition). + info(`Select <${selector}>.`); + await selectNode(selector, inspector); + const testFront = await getNodeFront(selector, inspector); + const testContainer = inspector.markup.getContainer(testFront); + let customBadge = testContainer.elt.querySelector( + ".inspector-badge.interactive[data-custom]" + ); + + // Verify that the "custom" badge and menu item are hidden. + ok(!customBadge, "[custom] badge is hidden"); + let menuItem = getMenuItem("node-menu-jumptodefinition", inspector); + ok( + !menuItem, + selector + ": The menu item was not found in the contextual menu" + ); + + info( + "Call the content method that should attach a custom element definition" + ); + const mutated = waitForMutation(inspector, "customElementDefined"); + SpecialPowers.spawn( + gBrowser.selectedBrowser, + [{ contentMethod }], + function (args) { + content.wrappedJSObject[args.contentMethod](); + } + ); + await mutated; + + // Test element should now have a custom element definition. + + // Check that the badge opens the debugger. + customBadge = testContainer.elt.querySelector( + ".inspector-badge.interactive[data-custom]" + ); + ok(customBadge, "[custom] badge is visible"); + + info("Click on the `custom` badge and verify that the debugger opens."); + let onDebuggerReady = toolbox.getPanelWhenReady("jsdebugger"); + customBadge.click(); + await onDebuggerReady; + + const debuggerContext = createDebuggerContext(toolbox); + await waitUntilDebuggerReady(debuggerContext); + + info("Switch to the inspector"); + await toolbox.selectTool("inspector"); + + // Check that the menu item also opens the debugger. + menuItem = getMenuItem("node-menu-jumptodefinition", inspector); + ok(menuItem, selector + ": The menu item was found in the contextual menu"); + ok(!menuItem.disabled, selector + ": The menu item is not disabled"); + + info("Click on `Jump to Definition` and verify that the debugger opens."); + onDebuggerReady = toolbox.getPanelWhenReady("jsdebugger"); + menuItem.click(); + await onDebuggerReady; + + await waitUntilDebuggerReady(debuggerContext); + + info("Switch to the inspector"); + await toolbox.selectTool("inspector"); +} + +function getMenuItem(id, inspector) { + const allMenuItems = openContextMenuAndGetAllItems(inspector); + return allMenuItems.find(i => i.id === "node-menu-jumptodefinition"); +} + +async function waitUntilDebuggerReady(debuggerContext) { + info("Wait until source is loaded in the debugger"); + + // We have to wait until the debugger has fully loaded the source otherwise + // we will get unhandled promise rejections. + await waitForLoadedSource(debuggerContext, TEST_URL); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_open_debugger_pretty_printed.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_open_debugger_pretty_printed.js new file mode 100644 index 0000000000..30d1f0748b --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_open_debugger_pretty_printed.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that clicking on the "custom" badge opens the debugger to the pretty-printed +// custom element definition. + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/shared-head.js", + this +); + +const TEST_URL = + URL_ROOT + "doc_markup_shadowdom_open_debugger_pretty_printed.html"; + +add_task(async function () { + info("Open inspector."); + await clearDebuggerPreferences(); + const { inspector, toolbox } = await openInspectorForURL(TEST_URL); + + await selectNode("test-component", inspector); + const testFront = await getNodeFront("test-component", inspector); + + const testContainer = inspector.markup.getContainer(testFront); + const customBadge = testContainer.elt.querySelector( + ".inspector-badge.interactive[data-custom]" + ); + + info("Click custom badge."); + customBadge.click(); + + await toolbox.getPanelWhenReady("jsdebugger"); + const dbg = createDebuggerContext(toolbox); + + await waitForSelectedSource(dbg, "shadowdom_open_debugger.min.js"); + await waitForSelectedLocation(dbg, 1); + + info("Pretty-print source."); + clickElement(dbg, "prettyPrintButton"); + await waitForSelectedSource(dbg, "shadowdom_open_debugger.min.js:formatted"); + info("Switch back to the original source."); + await selectSource(dbg, "shadowdom_open_debugger.min.js"); + + info("Return to inspector."); + await toolbox.selectTool("inspector"); + + info("Click custom badge again."); + customBadge.click(); + + await waitForSelectedSource(dbg, "shadowdom_open_debugger.min.js:formatted"); + await waitForSelectedLocation(dbg, 5); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_shadowroot_mode.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_shadowroot_mode.js new file mode 100644 index 0000000000..fb98b7cb35 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_shadowroot_mode.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the shadow root mode is displayed properly + +const TEST_URL = `data:text/html;charset=utf-8, + + + + +`; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const { markup } = inspector; + + info("Find and expand the closed-component shadow DOM host."); + const closedHostFront = await getNodeFront("closed-component", inspector); + const closedHostContainer = markup.getContainer(closedHostFront); + await expandContainer(inspector, closedHostContainer); + + info("Check the shadow root mode"); + const closedShadowRootContainer = closedHostContainer.getChildContainers()[0]; + assertContainerHasText(closedShadowRootContainer, "#shadow-root (closed)"); + + info("Find and expand the open-component shadow DOM host."); + const openHostFront = await getNodeFront("open-component", inspector); + const openHostContainer = markup.getContainer(openHostFront); + await expandContainer(inspector, openHostContainer); + + info("Check the shadow root mode"); + const openShadowRootContainer = openHostContainer.getChildContainers()[0]; + assertContainerHasText(openShadowRootContainer, "#shadow-root (open)"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_show_nodes_button.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_show_nodes_button.js new file mode 100644 index 0000000000..54685bc470 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_show_nodes_button.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the "Show all 'N' nodes" button displays the proper value + +const NODE_COUNT = 101; +const TEST_URL = `data:text/html;charset=utf-8, + + + + `; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const { markup } = inspector; + + info("Find and expand the component shadow DOM host."); + const hostFront = await getNodeFront("test-component", inspector); + const hostContainer = markup.getContainer(hostFront); + await expandContainer(inspector, hostContainer); + const shadowRootContainer = hostContainer.getChildContainers()[0]; + await expandContainer(inspector, shadowRootContainer); + + info("Expand the slot"); + const slotContainer = shadowRootContainer.getChildContainers()[0]; + await expandContainer(inspector, slotContainer); + + info("Find the 'Show all nodes' button"); + const button = slotContainer.elt.querySelector("button"); + console.log(button); + ok( + button.innerText.includes(NODE_COUNT), + "'Show all nodes' button contains correct node count" + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_slotted_keyboard_focus.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_slotted_keyboard_focus.js new file mode 100644 index 0000000000..86ee603f56 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_slotted_keyboard_focus.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that cycling focus with keyboard (via TAB key) in slotted nodes works. + +const TEST_URL = `data:text/html;charset=utf-8, + +
slot1-1
+
+ + `; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const { markup } = inspector; + const win = inspector.markup.doc.defaultView; + + info("Find and expand the test-component shadow DOM host."); + const hostFront = await getNodeFront("test-component", inspector); + const hostContainer = markup.getContainer(hostFront); + await expandContainer(inspector, hostContainer); + + info("Expand the shadow root"); + const shadowRootContainer = hostContainer.getChildContainers()[0]; + await expandContainer(inspector, shadowRootContainer); + + info("Expand the slot"); + const slotContainer = shadowRootContainer.getChildContainers()[0]; + await expandContainer(inspector, slotContainer); + + info("Select the slotted container for the element"); + const node = slotContainer.getChildContainers()[0].node; + const container = inspector.markup.getContainer(node, true); + await selectNode(node, inspector, "no-reason", true); + + const root = inspector.markup.getContainer(inspector.markup._rootNode); + root.elt.focus(); + const tagSpan = container.elt.querySelector(".tag"); + const revealLink = container.elt.querySelector(".reveal-link"); + + info("Hit Enter to focus on the first element"); + let tagFocused = once(tagSpan, "focus"); + EventUtils.synthesizeAndWaitKey("KEY_Enter", {}, win); + await tagFocused; + + info("Hit Tab to focus on the next element"); + const linkFocused = once(revealLink, "focus"); + EventUtils.synthesizeKey("KEY_Tab", {}, win); + await linkFocused; + + info("Hit Tab again to cycle focus to the first element"); + tagFocused = once(tagSpan, "focus"); + EventUtils.synthesizeKey("KEY_Tab", {}, win); + await tagFocused; + + ok( + inspector.markup.doc.activeElement === tagSpan, + "Focus has gone back to first element" + ); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_slotupdate.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_slotupdate.js new file mode 100644 index 0000000000..c8f6d029cb --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_slotupdate.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that slotted elements are correctly updated when the slot attribute is modified +// on already slotted elements. + +const TEST_URL = `data:text/html;charset=utf-8, + +
slot1-1
+
slot1-2
+
slot2-1
+
slot2-2
+
+ + `; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + const tree = ` + test-component + #shadow-root + name="slot1" + div!slotted + div!slotted + name="slot2" + div!slotted + div!slotted + slot1-1 + slot1-2 + slot2-1 + slot2-2`; + await assertMarkupViewAsTree(tree, "test-component", inspector); + + info("Listening for the markupmutation event"); + const mutated = inspector.once("markupmutation"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + content.document.getElementById("to-update").setAttribute("slot", "slot1"); + }); + await mutated; + + // After mutation we expect slot1 to have one more slotted node, and slot2 one less. + const mutatedTree = ` + test-component + #shadow-root + name="slot1" + div!slotted + div!slotted + div!slotted + name="slot2" + div!slotted + slot1-1 + slot1-2 + slot2-1 + slot2-2`; + await assertMarkupViewAsTree(mutatedTree, "test-component", inspector); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_shadowdom_ua_widgets.js b/devtools/client/inspector/markup/test/browser_markup_shadowdom_ua_widgets.js new file mode 100644 index 0000000000..d4acaf1380 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_ua_widgets.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = `data:text/html;charset=utf-8, + + `; + +add_task(async function () { + info("Test a