From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../browser_allocations_browser_console.js | 69 + .../browser_allocations_browser_console.toml | 17 + .../browser_allocations_reload_debugger.js | 13 + .../browser_allocations_reload_debugger.toml | 20 + .../browser_allocations_reload_inspector.js | 13 + .../browser_allocations_reload_inspector.toml | 20 + .../browser_allocations_reload_netmonitor.js | 13 + .../browser_allocations_reload_netmonitor.toml | 20 + .../browser_allocations_reload_no_devtools.js | 42 + .../browser_allocations_reload_no_devtools.toml | 19 + .../browser_allocations_reload_webconsole.js | 13 + .../browser_allocations_reload_webconsole.toml | 20 + .../test/allocations/browser_allocations_target.js | 51 + .../allocations/browser_allocations_target.toml | 17 + .../allocations/browser_allocations_toolbox.js | 53 + .../allocations/browser_allocations_toolbox.toml | 17 + .../framework/test/allocations/docs/index.md | 241 ++ devtools/client/framework/test/allocations/head.js | 250 ++ .../client/framework/test/allocations/moz.build | 16 + .../framework/test/allocations/reload-test.js | 84 + .../framework/test/allocations/reloaded-page.html | 11 + .../client/framework/test/allocations/reloaded.png | Bin 0 -> 580 bytes .../framework/test/browser-telemetry-startup.toml | 14 + devtools/client/framework/test/browser.toml | 315 ++ .../test/browser_about-devtools-toolbox_load.js | 31 + .../test/browser_about-devtools-toolbox_reload.js | 73 + .../framework/test/browser_commands_from_url.js | 161 + .../framework/test/browser_devtools_api_destroy.js | 72 + .../test/browser_dynamic_tool_enabling.js | 44 + .../framework/test/browser_front_parentFront.js | 39 + .../browser_ignore_toolbox_network_requests.js | 30 + .../framework/test/browser_keybindings_01.js | 107 + .../framework/test/browser_keybindings_02.js | 66 + .../framework/test/browser_keybindings_03.js | 51 + devtools/client/framework/test/browser_menu_api.js | 239 ++ .../test/browser_new_activation_workflow.js | 79 + .../client/framework/test/browser_source_map-01.js | 71 + .../framework/test/browser_source_map-absolute.js | 36 + .../test/browser_source_map-cross-domain.js | 41 + .../framework/test/browser_source_map-init.js | 51 + .../framework/test/browser_source_map-inline.js | 42 + .../test/browser_source_map-late-script.js | 52 + .../framework/test/browser_source_map-no-race.js | 43 + .../framework/test/browser_source_map-pub-sub.js | 97 + .../framework/test/browser_source_map-reload.js | 56 + .../framework/test/browser_tab_commands_factory.js | 52 + .../test/browser_tab_descriptor_fission.js | 68 + .../framework/test/browser_target_cached-front.js | 23 + .../test/browser_target_cached-resource.js | 50 + .../framework/test/browser_target_get-front.js | 112 + .../framework/test/browser_target_listeners.js | 33 + .../framework/test/browser_target_loading.js | 37 + .../framework/test/browser_target_parents.js | 189 ++ .../client/framework/test/browser_target_remote.js | 15 + .../test/browser_target_server_compartment.js | 126 + .../framework/test/browser_target_support.js | 40 + .../browser_toolbox_backward_forward_navigation.js | 188 ++ .../test/browser_toolbox_browsertoolbox_host.js | 27 + .../browser_toolbox_contentpage_contextmenu.js | 83 + .../framework/test/browser_toolbox_disable_f12.js | 102 + .../test/browser_toolbox_dynamic_registration.js | 87 + .../framework/test/browser_toolbox_error_count.js | 183 ++ ...wser_toolbox_error_count_reset_on_navigation.js | 84 + .../test/browser_toolbox_fission_navigation.js | 56 + .../framework/test/browser_toolbox_frames_list.js | 150 + .../test/browser_toolbox_getpanelwhenready.js | 39 + .../framework/test/browser_toolbox_highlight.js | 122 + .../client/framework/test/browser_toolbox_hosts.js | 226 ++ .../framework/test/browser_toolbox_hosts_size.js | 136 + .../test/browser_toolbox_hosts_telemetry.js | 49 + .../test/browser_toolbox_keyboard_navigation.js | 136 + ...toolbox_keyboard_navigation_notification_box.js | 49 + .../framework/test/browser_toolbox_meatball.js | 135 + .../framework/test/browser_toolbox_options.js | 556 ++++ .../browser_toolbox_options_disable_buttons.js | 270 ++ .../browser_toolbox_options_disable_cache-01.js | 36 + .../browser_toolbox_options_disable_cache-02.js | 52 + .../browser_toolbox_options_disable_cache-03.js | 60 + .../browser_toolbox_options_disable_cache.css.sjs | 10 + .../test/browser_toolbox_options_disable_cache.sjs | 31 + .../test/browser_toolbox_options_disable_js.html | 47 + .../test/browser_toolbox_options_disable_js.js | 141 + .../browser_toolbox_options_disable_js_iframe.html | 34 + ...lbox_options_enable_serviceworkers_testing.html | 81 + ...oolbox_options_enable_serviceworkers_testing.js | 76 + .../test/browser_toolbox_options_frames_button.js | 104 + .../test/browser_toolbox_options_multiple_tabs.js | 130 + .../test/browser_toolbox_options_panel_toggle.js | 82 + .../test/browser_toolbox_popups_debugging.js | 56 + .../client/framework/test/browser_toolbox_races.js | 96 + .../client/framework/test/browser_toolbox_raise.js | 68 + .../client/framework/test/browser_toolbox_ready.js | 22 + .../test/browser_toolbox_remoteness_change.js | 52 + .../test/browser_toolbox_screenshot_tool.js | 126 + .../framework/test/browser_toolbox_select_event.js | 98 + .../browser_toolbox_selected_tool_unavailable.js | 46 + .../test/browser_toolbox_selectionchanged_event.js | 40 + .../browser_toolbox_show_toolbox_tool_ready.js | 69 + .../test/browser_toolbox_split_console.js | 84 + .../test/browser_toolbox_tabsswitch_shortcuts.js | 76 + ...wser_toolbox_telemetry_activate_splitconsole.js | 108 + .../test/browser_toolbox_telemetry_close.js | 63 + .../test/browser_toolbox_telemetry_enter.js | 152 + .../test/browser_toolbox_telemetry_exit.js | 129 + .../test/browser_toolbox_telemetry_open_event.js | 38 + .../test/browser_toolbox_textbox_context_menu.js | 137 + .../client/framework/test/browser_toolbox_theme.js | 33 + .../test/browser_toolbox_theme_registration.js | 166 ++ .../framework/test/browser_toolbox_toggle.js | 111 + .../framework/test/browser_toolbox_tool_ready.js | 33 + .../test/browser_toolbox_tool_remote_reopen.js | 100 + .../test/browser_toolbox_toolbar_minimum_width.js | 43 + .../test/browser_toolbox_toolbar_overflow.js | 83 + ...r_toolbox_toolbar_overflow_button_visibility.js | 72 + .../test/browser_toolbox_toolbar_reorder_by_dnd.js | 188 ++ .../browser_toolbox_toolbar_reorder_by_width.js | 104 + ...owser_toolbox_toolbar_reorder_with_extension.js | 150 + ...oolbox_toolbar_reorder_with_hidden_extension.js | 248 ++ ...owser_toolbox_tools_per_toolbox_registration.js | 138 + .../test/browser_toolbox_view_source_01.js | 31 + .../test/browser_toolbox_view_source_02.js | 38 + .../test/browser_toolbox_view_source_03.js | 51 + ...er_toolbox_view_source_style_editor_fallback.js | 38 + .../test/browser_toolbox_watchedByDevTools.js | 72 + .../test/browser_toolbox_window_reload_target.js | 130 + .../browser_toolbox_window_reload_target_force.js | 55 + .../test/browser_toolbox_window_shortcuts.js | 104 + .../test/browser_toolbox_window_title_changes.js | 98 + .../browser_toolbox_window_title_changes_page.html | 10 + .../browser_toolbox_window_title_frame_select.js | 171 ++ ...ser_toolbox_window_title_frame_select_page.html | 11 + .../client/framework/test/browser_toolbox_zoom.js | 62 + .../framework/test/browser_toolbox_zoom_popup.js | 213 ++ .../test/browser_webextension_descriptor.js | 31 + .../test/browser_webextension_dropdown.js | 135 + .../framework/test/code_binary_search.coffee | 18 + .../client/framework/test/code_binary_search.js | 29 + .../client/framework/test/code_binary_search.map | 10 + .../framework/test/code_binary_search_absolute.js | 29 + .../framework/test/code_binary_search_absolute.map | 10 + .../framework/test/code_bundle_cross_domain.js | 93 + .../framework/test/code_bundle_cross_domain.js.map | 1 + .../framework/test/code_bundle_late_script.js | 116 + .../framework/test/code_bundle_late_script.js.map | 1 + .../client/framework/test/code_bundle_no_race.js | 95 + .../framework/test/code_bundle_no_race.js.map | 1 + .../client/framework/test/code_cross_domain.js | 19 + .../client/framework/test/code_inline_bundle.js | 92 + .../client/framework/test/code_inline_original.js | 14 + devtools/client/framework/test/code_late_script.js | 14 + devtools/client/framework/test/code_math.js | 7 + devtools/client/framework/test/code_no_race.js | 17 + .../test/doc_backward_forward_navigation.html | 40 + .../client/framework/test/doc_cached-resource.html | 15 + .../framework/test/doc_cached-resource_iframe.html | 14 + .../client/framework/test/doc_empty-tab-01.html | 14 + devtools/client/framework/test/doc_lazy_tool.html | 6 + .../client/framework/test/doc_textbox_tool.html | 10 + devtools/client/framework/test/doc_theme.css | 3 + devtools/client/framework/test/doc_viewsource.html | 13 + devtools/client/framework/test/head.js | 490 +++ .../client/framework/test/helper_disable_cache.js | 144 + .../framework/test/metrics/browser_metrics.toml | 19 + .../test/metrics/browser_metrics_debugger.js | 61 + .../test/metrics/browser_metrics_debugger.toml | 18 + .../test/metrics/browser_metrics_inspector.js | 43 + .../test/metrics/browser_metrics_inspector.toml | 18 + .../test/metrics/browser_metrics_netmonitor.js | 89 + .../test/metrics/browser_metrics_netmonitor.toml | 18 + .../framework/test/metrics/browser_metrics_pool.js | 118 + .../test/metrics/browser_metrics_webconsole.js | 56 + .../test/metrics/browser_metrics_webconsole.toml | 18 + devtools/client/framework/test/metrics/head.js | 171 ++ devtools/client/framework/test/node/.eslintrc.js | 22 + devtools/client/framework/test/node/README.md | 22 + .../client/framework/test/node/babel.config.js | 13 + .../__snapshots__/debug-target-info.test.js.snap | 586 ++++ .../test/node/components/debug-target-info.test.js | 319 ++ devtools/client/framework/test/node/jest.config.js | 13 + devtools/client/framework/test/node/package.json | 22 + devtools/client/framework/test/node/setup.js | 10 + .../framework/test/node/store/targets.test.js | 142 + devtools/client/framework/test/node/yarn.lock | 3144 ++++++++++++++++++++ devtools/client/framework/test/reload/.eslintrc.js | 18 + devtools/client/framework/test/reload/README.md | 4 + devtools/client/framework/test/reload/package.json | 12 + .../framework/test/reload/v1/code_bundle_reload.js | 19 + .../test/reload/v1/code_bundle_reload.js.map | 1 + .../framework/test/reload/v1/code_reload_1.js | 10 + .../framework/test/reload/v1/doc_reload.html | 15 + .../framework/test/reload/v2/code_bundle_reload.js | 19 + .../test/reload/v2/code_bundle_reload.js.map | 1 + .../framework/test/reload/v2/code_reload_2.js | 10 + .../framework/test/reload/v2/doc_reload.html | 15 + .../client/framework/test/reload/webpack.config.js | 13 + devtools/client/framework/test/serviceworker.js | 4 + .../framework/test/sjs_cache_controle_header.sjs | 19 + .../client/framework/test/test_chrome_page.html | 9 + .../client/framework/test/xpcshell/.eslintrc.js | 6 + .../test/xpcshell/test_tabs_absolute_order.js | 81 + .../client/framework/test/xpcshell/xpcshell.toml | 6 + 201 files changed, 17814 insertions(+) create mode 100644 devtools/client/framework/test/allocations/browser_allocations_browser_console.js create mode 100644 devtools/client/framework/test/allocations/browser_allocations_browser_console.toml create mode 100644 devtools/client/framework/test/allocations/browser_allocations_reload_debugger.js create mode 100644 devtools/client/framework/test/allocations/browser_allocations_reload_debugger.toml create mode 100644 devtools/client/framework/test/allocations/browser_allocations_reload_inspector.js create mode 100644 devtools/client/framework/test/allocations/browser_allocations_reload_inspector.toml create mode 100644 devtools/client/framework/test/allocations/browser_allocations_reload_netmonitor.js create mode 100644 devtools/client/framework/test/allocations/browser_allocations_reload_netmonitor.toml create mode 100644 devtools/client/framework/test/allocations/browser_allocations_reload_no_devtools.js create mode 100644 devtools/client/framework/test/allocations/browser_allocations_reload_no_devtools.toml create mode 100644 devtools/client/framework/test/allocations/browser_allocations_reload_webconsole.js create mode 100644 devtools/client/framework/test/allocations/browser_allocations_reload_webconsole.toml create mode 100644 devtools/client/framework/test/allocations/browser_allocations_target.js create mode 100644 devtools/client/framework/test/allocations/browser_allocations_target.toml create mode 100644 devtools/client/framework/test/allocations/browser_allocations_toolbox.js create mode 100644 devtools/client/framework/test/allocations/browser_allocations_toolbox.toml create mode 100644 devtools/client/framework/test/allocations/docs/index.md create mode 100644 devtools/client/framework/test/allocations/head.js create mode 100644 devtools/client/framework/test/allocations/moz.build create mode 100644 devtools/client/framework/test/allocations/reload-test.js create mode 100644 devtools/client/framework/test/allocations/reloaded-page.html create mode 100644 devtools/client/framework/test/allocations/reloaded.png create mode 100644 devtools/client/framework/test/browser-telemetry-startup.toml create mode 100644 devtools/client/framework/test/browser.toml create mode 100644 devtools/client/framework/test/browser_about-devtools-toolbox_load.js create mode 100644 devtools/client/framework/test/browser_about-devtools-toolbox_reload.js create mode 100644 devtools/client/framework/test/browser_commands_from_url.js create mode 100644 devtools/client/framework/test/browser_devtools_api_destroy.js create mode 100644 devtools/client/framework/test/browser_dynamic_tool_enabling.js create mode 100644 devtools/client/framework/test/browser_front_parentFront.js create mode 100644 devtools/client/framework/test/browser_ignore_toolbox_network_requests.js create mode 100644 devtools/client/framework/test/browser_keybindings_01.js create mode 100644 devtools/client/framework/test/browser_keybindings_02.js create mode 100644 devtools/client/framework/test/browser_keybindings_03.js create mode 100644 devtools/client/framework/test/browser_menu_api.js create mode 100644 devtools/client/framework/test/browser_new_activation_workflow.js create mode 100644 devtools/client/framework/test/browser_source_map-01.js create mode 100644 devtools/client/framework/test/browser_source_map-absolute.js create mode 100644 devtools/client/framework/test/browser_source_map-cross-domain.js create mode 100644 devtools/client/framework/test/browser_source_map-init.js create mode 100644 devtools/client/framework/test/browser_source_map-inline.js create mode 100644 devtools/client/framework/test/browser_source_map-late-script.js create mode 100644 devtools/client/framework/test/browser_source_map-no-race.js create mode 100644 devtools/client/framework/test/browser_source_map-pub-sub.js create mode 100644 devtools/client/framework/test/browser_source_map-reload.js create mode 100644 devtools/client/framework/test/browser_tab_commands_factory.js create mode 100644 devtools/client/framework/test/browser_tab_descriptor_fission.js create mode 100644 devtools/client/framework/test/browser_target_cached-front.js create mode 100644 devtools/client/framework/test/browser_target_cached-resource.js create mode 100644 devtools/client/framework/test/browser_target_get-front.js create mode 100644 devtools/client/framework/test/browser_target_listeners.js create mode 100644 devtools/client/framework/test/browser_target_loading.js create mode 100644 devtools/client/framework/test/browser_target_parents.js create mode 100644 devtools/client/framework/test/browser_target_remote.js create mode 100644 devtools/client/framework/test/browser_target_server_compartment.js create mode 100644 devtools/client/framework/test/browser_target_support.js create mode 100644 devtools/client/framework/test/browser_toolbox_backward_forward_navigation.js create mode 100644 devtools/client/framework/test/browser_toolbox_browsertoolbox_host.js create mode 100644 devtools/client/framework/test/browser_toolbox_contentpage_contextmenu.js create mode 100644 devtools/client/framework/test/browser_toolbox_disable_f12.js create mode 100644 devtools/client/framework/test/browser_toolbox_dynamic_registration.js create mode 100644 devtools/client/framework/test/browser_toolbox_error_count.js create mode 100644 devtools/client/framework/test/browser_toolbox_error_count_reset_on_navigation.js create mode 100644 devtools/client/framework/test/browser_toolbox_fission_navigation.js create mode 100644 devtools/client/framework/test/browser_toolbox_frames_list.js create mode 100644 devtools/client/framework/test/browser_toolbox_getpanelwhenready.js create mode 100644 devtools/client/framework/test/browser_toolbox_highlight.js create mode 100644 devtools/client/framework/test/browser_toolbox_hosts.js create mode 100644 devtools/client/framework/test/browser_toolbox_hosts_size.js create mode 100644 devtools/client/framework/test/browser_toolbox_hosts_telemetry.js create mode 100644 devtools/client/framework/test/browser_toolbox_keyboard_navigation.js create mode 100644 devtools/client/framework/test/browser_toolbox_keyboard_navigation_notification_box.js create mode 100644 devtools/client/framework/test/browser_toolbox_meatball.js create mode 100644 devtools/client/framework/test/browser_toolbox_options.js create mode 100644 devtools/client/framework/test/browser_toolbox_options_disable_buttons.js create mode 100644 devtools/client/framework/test/browser_toolbox_options_disable_cache-01.js create mode 100644 devtools/client/framework/test/browser_toolbox_options_disable_cache-02.js create mode 100644 devtools/client/framework/test/browser_toolbox_options_disable_cache-03.js create mode 100644 devtools/client/framework/test/browser_toolbox_options_disable_cache.css.sjs create mode 100644 devtools/client/framework/test/browser_toolbox_options_disable_cache.sjs create mode 100644 devtools/client/framework/test/browser_toolbox_options_disable_js.html create mode 100644 devtools/client/framework/test/browser_toolbox_options_disable_js.js create mode 100644 devtools/client/framework/test/browser_toolbox_options_disable_js_iframe.html create mode 100644 devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.html create mode 100644 devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js create mode 100644 devtools/client/framework/test/browser_toolbox_options_frames_button.js create mode 100644 devtools/client/framework/test/browser_toolbox_options_multiple_tabs.js create mode 100644 devtools/client/framework/test/browser_toolbox_options_panel_toggle.js create mode 100644 devtools/client/framework/test/browser_toolbox_popups_debugging.js create mode 100644 devtools/client/framework/test/browser_toolbox_races.js create mode 100644 devtools/client/framework/test/browser_toolbox_raise.js create mode 100644 devtools/client/framework/test/browser_toolbox_ready.js create mode 100644 devtools/client/framework/test/browser_toolbox_remoteness_change.js create mode 100644 devtools/client/framework/test/browser_toolbox_screenshot_tool.js create mode 100644 devtools/client/framework/test/browser_toolbox_select_event.js create mode 100644 devtools/client/framework/test/browser_toolbox_selected_tool_unavailable.js create mode 100644 devtools/client/framework/test/browser_toolbox_selectionchanged_event.js create mode 100644 devtools/client/framework/test/browser_toolbox_show_toolbox_tool_ready.js create mode 100644 devtools/client/framework/test/browser_toolbox_split_console.js create mode 100644 devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js create mode 100644 devtools/client/framework/test/browser_toolbox_telemetry_activate_splitconsole.js create mode 100644 devtools/client/framework/test/browser_toolbox_telemetry_close.js create mode 100644 devtools/client/framework/test/browser_toolbox_telemetry_enter.js create mode 100644 devtools/client/framework/test/browser_toolbox_telemetry_exit.js create mode 100644 devtools/client/framework/test/browser_toolbox_telemetry_open_event.js create mode 100644 devtools/client/framework/test/browser_toolbox_textbox_context_menu.js create mode 100644 devtools/client/framework/test/browser_toolbox_theme.js create mode 100644 devtools/client/framework/test/browser_toolbox_theme_registration.js create mode 100644 devtools/client/framework/test/browser_toolbox_toggle.js create mode 100644 devtools/client/framework/test/browser_toolbox_tool_ready.js create mode 100644 devtools/client/framework/test/browser_toolbox_tool_remote_reopen.js create mode 100644 devtools/client/framework/test/browser_toolbox_toolbar_minimum_width.js create mode 100644 devtools/client/framework/test/browser_toolbox_toolbar_overflow.js create mode 100644 devtools/client/framework/test/browser_toolbox_toolbar_overflow_button_visibility.js create mode 100644 devtools/client/framework/test/browser_toolbox_toolbar_reorder_by_dnd.js create mode 100644 devtools/client/framework/test/browser_toolbox_toolbar_reorder_by_width.js create mode 100644 devtools/client/framework/test/browser_toolbox_toolbar_reorder_with_extension.js create mode 100644 devtools/client/framework/test/browser_toolbox_toolbar_reorder_with_hidden_extension.js create mode 100644 devtools/client/framework/test/browser_toolbox_tools_per_toolbox_registration.js create mode 100644 devtools/client/framework/test/browser_toolbox_view_source_01.js create mode 100644 devtools/client/framework/test/browser_toolbox_view_source_02.js create mode 100644 devtools/client/framework/test/browser_toolbox_view_source_03.js create mode 100644 devtools/client/framework/test/browser_toolbox_view_source_style_editor_fallback.js create mode 100644 devtools/client/framework/test/browser_toolbox_watchedByDevTools.js create mode 100644 devtools/client/framework/test/browser_toolbox_window_reload_target.js create mode 100644 devtools/client/framework/test/browser_toolbox_window_reload_target_force.js create mode 100644 devtools/client/framework/test/browser_toolbox_window_shortcuts.js create mode 100644 devtools/client/framework/test/browser_toolbox_window_title_changes.js create mode 100644 devtools/client/framework/test/browser_toolbox_window_title_changes_page.html create mode 100644 devtools/client/framework/test/browser_toolbox_window_title_frame_select.js create mode 100644 devtools/client/framework/test/browser_toolbox_window_title_frame_select_page.html create mode 100644 devtools/client/framework/test/browser_toolbox_zoom.js create mode 100644 devtools/client/framework/test/browser_toolbox_zoom_popup.js create mode 100644 devtools/client/framework/test/browser_webextension_descriptor.js create mode 100644 devtools/client/framework/test/browser_webextension_dropdown.js create mode 100644 devtools/client/framework/test/code_binary_search.coffee create mode 100644 devtools/client/framework/test/code_binary_search.js create mode 100644 devtools/client/framework/test/code_binary_search.map create mode 100644 devtools/client/framework/test/code_binary_search_absolute.js create mode 100644 devtools/client/framework/test/code_binary_search_absolute.map create mode 100644 devtools/client/framework/test/code_bundle_cross_domain.js create mode 100644 devtools/client/framework/test/code_bundle_cross_domain.js.map create mode 100644 devtools/client/framework/test/code_bundle_late_script.js create mode 100644 devtools/client/framework/test/code_bundle_late_script.js.map create mode 100644 devtools/client/framework/test/code_bundle_no_race.js create mode 100644 devtools/client/framework/test/code_bundle_no_race.js.map create mode 100644 devtools/client/framework/test/code_cross_domain.js create mode 100644 devtools/client/framework/test/code_inline_bundle.js create mode 100644 devtools/client/framework/test/code_inline_original.js create mode 100644 devtools/client/framework/test/code_late_script.js create mode 100644 devtools/client/framework/test/code_math.js create mode 100644 devtools/client/framework/test/code_no_race.js create mode 100644 devtools/client/framework/test/doc_backward_forward_navigation.html create mode 100644 devtools/client/framework/test/doc_cached-resource.html create mode 100644 devtools/client/framework/test/doc_cached-resource_iframe.html create mode 100644 devtools/client/framework/test/doc_empty-tab-01.html create mode 100644 devtools/client/framework/test/doc_lazy_tool.html create mode 100644 devtools/client/framework/test/doc_textbox_tool.html create mode 100644 devtools/client/framework/test/doc_theme.css create mode 100644 devtools/client/framework/test/doc_viewsource.html create mode 100644 devtools/client/framework/test/head.js create mode 100644 devtools/client/framework/test/helper_disable_cache.js create mode 100644 devtools/client/framework/test/metrics/browser_metrics.toml create mode 100644 devtools/client/framework/test/metrics/browser_metrics_debugger.js create mode 100644 devtools/client/framework/test/metrics/browser_metrics_debugger.toml create mode 100644 devtools/client/framework/test/metrics/browser_metrics_inspector.js create mode 100644 devtools/client/framework/test/metrics/browser_metrics_inspector.toml create mode 100644 devtools/client/framework/test/metrics/browser_metrics_netmonitor.js create mode 100644 devtools/client/framework/test/metrics/browser_metrics_netmonitor.toml create mode 100644 devtools/client/framework/test/metrics/browser_metrics_pool.js create mode 100644 devtools/client/framework/test/metrics/browser_metrics_webconsole.js create mode 100644 devtools/client/framework/test/metrics/browser_metrics_webconsole.toml create mode 100644 devtools/client/framework/test/metrics/head.js create mode 100644 devtools/client/framework/test/node/.eslintrc.js create mode 100644 devtools/client/framework/test/node/README.md create mode 100644 devtools/client/framework/test/node/babel.config.js create mode 100644 devtools/client/framework/test/node/components/__snapshots__/debug-target-info.test.js.snap create mode 100644 devtools/client/framework/test/node/components/debug-target-info.test.js create mode 100644 devtools/client/framework/test/node/jest.config.js create mode 100644 devtools/client/framework/test/node/package.json create mode 100644 devtools/client/framework/test/node/setup.js create mode 100644 devtools/client/framework/test/node/store/targets.test.js create mode 100644 devtools/client/framework/test/node/yarn.lock create mode 100644 devtools/client/framework/test/reload/.eslintrc.js create mode 100644 devtools/client/framework/test/reload/README.md create mode 100644 devtools/client/framework/test/reload/package.json create mode 100644 devtools/client/framework/test/reload/v1/code_bundle_reload.js create mode 100644 devtools/client/framework/test/reload/v1/code_bundle_reload.js.map create mode 100644 devtools/client/framework/test/reload/v1/code_reload_1.js create mode 100644 devtools/client/framework/test/reload/v1/doc_reload.html create mode 100644 devtools/client/framework/test/reload/v2/code_bundle_reload.js create mode 100644 devtools/client/framework/test/reload/v2/code_bundle_reload.js.map create mode 100644 devtools/client/framework/test/reload/v2/code_reload_2.js create mode 100644 devtools/client/framework/test/reload/v2/doc_reload.html create mode 100644 devtools/client/framework/test/reload/webpack.config.js create mode 100644 devtools/client/framework/test/serviceworker.js create mode 100644 devtools/client/framework/test/sjs_cache_controle_header.sjs create mode 100644 devtools/client/framework/test/test_chrome_page.html create mode 100644 devtools/client/framework/test/xpcshell/.eslintrc.js create mode 100644 devtools/client/framework/test/xpcshell/test_tabs_absolute_order.js create mode 100644 devtools/client/framework/test/xpcshell/xpcshell.toml (limited to 'devtools/client/framework/test') diff --git a/devtools/client/framework/test/allocations/browser_allocations_browser_console.js b/devtools/client/framework/test/allocations/browser_allocations_browser_console.js new file mode 100644 index 0000000000..13d0171dfa --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_browser_console.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Record allocations while opening and closing the Browser Console + +const TEST_URL = + "http://example.com/browser/devtools/client/framework/test/allocations/reloaded-page.html"; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { + BrowserConsoleManager, +} = require("resource://devtools/client/webconsole/browser-console-manager.js"); + +async function testScript() { + // Open + await BrowserConsoleManager.toggleBrowserConsole(); + + // Reload the tab to make the test slightly more real + const hud = BrowserConsoleManager.getBrowserConsole(); + const onTargetProcessed = hud.commands.targetCommand.once( + "processed-available-target" + ); + + gBrowser.reloadTab(gBrowser.selectedTab); + + info("Wait for target of the new document to be fully processed"); + await onTargetProcessed; + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Close + await BrowserConsoleManager.toggleBrowserConsole(); + + // Browser console still cleanup stuff after the resolution of toggleBrowserConsole. + // So wait for a little while to ensure it completes all cleanups. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 500)); +} + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["devtools.browsertoolbox.scope", "everything"]], + }); + + const tab = await addTab(TEST_URL); + + // Run the test scenario first before recording in order to load all the + // modules. Otherwise they get reported as "still allocated" objects, + // whereas we do expect them to be kept in memory as they are loaded via + // the main DevTools loader, which keeps the module loaded until the + // shutdown of Firefox + await testScript(); + + // Now, run the test script. This time, we record this run. + await startRecordingAllocations(); + + for (let i = 0; i < 3; i++) { + await testScript(); + } + + await stopRecordingAllocations("browser-console"); + + gBrowser.removeTab(tab); +}); diff --git a/devtools/client/framework/test/allocations/browser_allocations_browser_console.toml b/devtools/client/framework/test/allocations/browser_allocations_browser_console.toml new file mode 100644 index 0000000000..785f665b85 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_browser_console.toml @@ -0,0 +1,17 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "!/devtools/shared/test-helpers/allocation-tracker.js", + "head.js", +] + +# Each metrics tests is loaded in a separate .ini file. This way the test is executed +# individually, without any other test being executed before or after. + +["browser_allocations_browser_console.js"] +run-if = ["os == 'linux'"] # Results should be platform agnostic - only run on linux64-opt +skip-if = [ + "debug", + "asan", +] diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_debugger.js b/devtools/client/framework/test/allocations/browser_allocations_reload_debugger.js new file mode 100644 index 0000000000..748a5e906e --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_debugger.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Record allocations while reloading the page with the debugger opened + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/framework/test/allocations/reload-test.js", + this +); + +add_task(createPanelReloadTest("reload-debugger", "jsdebugger")); diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_debugger.toml b/devtools/client/framework/test/allocations/browser_allocations_reload_debugger.toml new file mode 100644 index 0000000000..aabb21dc1a --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_debugger.toml @@ -0,0 +1,20 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "!/devtools/shared/test-helpers/allocation-tracker.js", + "head.js", + "reload-test.js", + "reloaded-page.html", + "reloaded.png", +] + +# Each metrics tests is loaded in a separate .ini file. This way the test is executed +# individually, without any other test being executed before or after. + +["browser_allocations_reload_debugger.js"] +run-if = ["os == 'linux'"] # Results should be platform agnostic - only run on linux64-opt +skip-if = [ + "debug", + "asan", +] diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_inspector.js b/devtools/client/framework/test/allocations/browser_allocations_reload_inspector.js new file mode 100644 index 0000000000..3369c54f24 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_inspector.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Record allocations while reloading the page with the inspector opened + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/framework/test/allocations/reload-test.js", + this +); + +add_task(createPanelReloadTest("reload-inspector", "inspector")); diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_inspector.toml b/devtools/client/framework/test/allocations/browser_allocations_reload_inspector.toml new file mode 100644 index 0000000000..f2046ea621 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_inspector.toml @@ -0,0 +1,20 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "!/devtools/shared/test-helpers/allocation-tracker.js", + "head.js", + "reload-test.js", + "reloaded-page.html", + "reloaded.png", +] + +# Each metrics tests is loaded in a separate .ini file. This way the test is executed +# individually, without any other test being executed before or after. + +["browser_allocations_reload_inspector.js"] +run-if = ["os == 'linux'"] # Results should be platform agnostic - only run on linux64-opt +skip-if = [ + "debug", + "asan", +] diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_netmonitor.js b/devtools/client/framework/test/allocations/browser_allocations_reload_netmonitor.js new file mode 100644 index 0000000000..2a57652ac5 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_netmonitor.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Record allocations while reloading the page with the netmonitor opened + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/framework/test/allocations/reload-test.js", + this +); + +add_task(createPanelReloadTest("reload-netmonitor", "netmonitor")); diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_netmonitor.toml b/devtools/client/framework/test/allocations/browser_allocations_reload_netmonitor.toml new file mode 100644 index 0000000000..3a4a0b8464 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_netmonitor.toml @@ -0,0 +1,20 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "!/devtools/shared/test-helpers/allocation-tracker.js", + "head.js", + "reload-test.js", + "reloaded-page.html", + "reloaded.png", +] + +# Each metrics tests is loaded in a separate .ini file. This way the test is executed +# individually, without any other test being executed before or after. + +["browser_allocations_reload_netmonitor.js"] +run-if = ["os == 'linux'"] # Results should be platform agnostic - only run on linux64-opt +skip-if = [ + "debug", + "asan", +] diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_no_devtools.js b/devtools/client/framework/test/allocations/browser_allocations_reload_no_devtools.js new file mode 100644 index 0000000000..e2f344fcb5 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_no_devtools.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Record allocations while reloading the page without anything related to DevTools running + +const TEST_URL = + "http://example.com/browser/devtools/client/framework/test/allocations/reloaded-page.html"; + +async function testScript() { + await BrowserTestUtils.reloadTab(gBrowser.selectedTab); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); +} + +add_task(async function () { + const tab = await addTab(TEST_URL); + + // Run the test scenario first before recording in order to load all the + // modules. Otherwise they get reported as "still allocated" objects, + // whereas we do expect them to be kept in memory as they are loaded via + // the main DevTools loader, which keeps the module loaded until the + // shutdown of Firefox + await testScript(); + + await startRecordingAllocations({ + alsoRecordContentProcess: true, + }); + + // Now, run the test script. This time, we record this run. + for (let i = 0; i < 10; i++) { + await testScript(); + } + + await stopRecordingAllocations("reload-no-devtools", { + alsoRecordContentProcess: true, + }); + + gBrowser.removeTab(tab); +}); diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_no_devtools.toml b/devtools/client/framework/test/allocations/browser_allocations_reload_no_devtools.toml new file mode 100644 index 0000000000..58dfadc598 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_no_devtools.toml @@ -0,0 +1,19 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "!/devtools/shared/test-helpers/allocation-tracker.js", + "head.js", + "reloaded-page.html", + "reloaded.png", +] + +# Each metrics tests is loaded in a separate .ini file. This way the test is executed +# individually, without any other test being executed before or after. + +["browser_allocations_reload_no_devtools.js"] +run-if = ["os == 'linux'"] # Results should be platform agnostic - only run on linux64-opt +skip-if = [ + "debug", + "asan", +] diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_webconsole.js b/devtools/client/framework/test/allocations/browser_allocations_reload_webconsole.js new file mode 100644 index 0000000000..a60fd03b3c --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_webconsole.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Record allocations while reloading the page with the webconsole opened + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/framework/test/allocations/reload-test.js", + this +); + +add_task(createPanelReloadTest("reload-webconsole", "webconsole")); diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_webconsole.toml b/devtools/client/framework/test/allocations/browser_allocations_reload_webconsole.toml new file mode 100644 index 0000000000..58bb9b733e --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_webconsole.toml @@ -0,0 +1,20 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "!/devtools/shared/test-helpers/allocation-tracker.js", + "head.js", + "reload-test.js", + "reloaded-page.html", + "reloaded.png", +] + +# Each metrics tests is loaded in a separate .ini file. This way the test is executed +# individually, without any other test being executed before or after. + +["browser_allocations_reload_webconsole.js"] +run-if = ["os == 'linux'"] # Results should be platform agnostic - only run on linux64-opt +skip-if = [ + "debug", + "asan", +] diff --git a/devtools/client/framework/test/allocations/browser_allocations_target.js b/devtools/client/framework/test/allocations/browser_allocations_target.js new file mode 100644 index 0000000000..a93d6b51c9 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_target.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Record allocations while spawning Commands and the first top level target + +const TEST_URL = + "data:text/html;charset=UTF-8,
Target allocations test
"; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { + CommandsFactory, +} = require("resource://devtools/shared/commands/commands-factory.js"); + +async function testScript(tab) { + const commands = await CommandsFactory.forTab(tab); + await commands.targetCommand.startListening(); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + // destroy the commands to also destroy the dedicated client. + await commands.destroy(); + + // Spin the event loop to ensure commands destruction is fully completed + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 0)); +} + +add_task(async function () { + const tab = await addTab(TEST_URL); + + // Run the test scenario first before recording in order to load all the + // modules. Otherwise they get reported as "still allocated" objects, + // whereas we do expect them to be kept in memory as they are loaded via + // the main DevTools loader, which keeps the module loaded until the + // shutdown of Firefox + await testScript(tab); + + await startRecordingAllocations(); + + // Now, run the test script. This time, we record this run. + await testScript(tab); + + await stopRecordingAllocations("target"); + + gBrowser.removeTab(tab); +}); diff --git a/devtools/client/framework/test/allocations/browser_allocations_target.toml b/devtools/client/framework/test/allocations/browser_allocations_target.toml new file mode 100644 index 0000000000..d5b4b158d5 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_target.toml @@ -0,0 +1,17 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "!/devtools/shared/test-helpers/allocation-tracker.js", + "head.js", +] + +# Each metrics tests is loaded in a separate .ini file. This way the test is executed +# individually, without any other test being executed before or after. + +["browser_allocations_target.js"] +run-if = ["os == 'linux'"] # Results should be platform agnostic - only run on linux64-opt +skip-if = [ + "debug", + "asan", +] diff --git a/devtools/client/framework/test/allocations/browser_allocations_toolbox.js b/devtools/client/framework/test/allocations/browser_allocations_toolbox.js new file mode 100644 index 0000000000..e0f86511bc --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_toolbox.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Record allocations while opening and closing DevTools + +const TEST_URL = + "data:text/html;charset=UTF-8,
Target allocations test
"; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { + gDevTools, +} = require("resource://devtools/client/framework/devtools.js"); + +async function testScript(tab) { + const toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: "inspector", + }); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + await toolbox.destroy(); + + // Spin the event loop to ensure toolbox destroy is fully completed + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 0)); +} + +add_task(async function () { + const tab = await addTab(TEST_URL); + + // Run the test scenario first before recording in order to load all the + // modules. Otherwise they get reported as "still allocated" objects, + // whereas we do expect them to be kept in memory as they are loaded via + // the main DevTools loader, which keeps the module loaded until the + // shutdown of Firefox + await testScript(tab); + + await startRecordingAllocations(); + + // Now, run the test script. This time, we record this run. + for (let i = 0; i < 3; i++) { + await testScript(tab); + } + + await stopRecordingAllocations("toolbox"); + + gBrowser.removeTab(tab); +}); diff --git a/devtools/client/framework/test/allocations/browser_allocations_toolbox.toml b/devtools/client/framework/test/allocations/browser_allocations_toolbox.toml new file mode 100644 index 0000000000..bbeeb14e45 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_toolbox.toml @@ -0,0 +1,17 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "!/devtools/shared/test-helpers/allocation-tracker.js", + "head.js", +] + +# Each metrics tests is loaded in a separate .ini file. This way the test is executed +# individually, without any other test being executed before or after. + +["browser_allocations_toolbox.js"] +run-if = ["os == 'linux'"] # Results should be platform agnostic - only run on linux64-opt +skip-if = [ + "debug", + "asan", +] diff --git a/devtools/client/framework/test/allocations/docs/index.md b/devtools/client/framework/test/allocations/docs/index.md new file mode 100644 index 0000000000..f0d6921325 --- /dev/null +++ b/devtools/client/framework/test/allocations/docs/index.md @@ -0,0 +1,241 @@ +# Allocation tests + +The [allocations](https://searchfox.org/mozilla-central/source/devtools/client/framework/test/allocations) folder contains special mochitests which are meant to record data about the memory usage of DevTools. +This uses Spidermonkey's Memory API implemented next to the debugger API. +For more info, see the following doc: + + +# Test example + +```javascript +add_task(async function() { + // Execute preliminary setup in order to be able to run your scenario + // You would typicaly load modules, open a tab, a toolbox, ... + ... + + // Run the test scenario first before recording in order to load all the + // modules. Otherwise they get reported as "still allocated" objects, + // whereas we do expect them to be kept in memory as they are loaded via + // the main DevTools loader, which keeps the module loaded until the + // shutdown of Firefox + await testScript(); + + // Pass alsoRecordContentProcess if you want to record the content process + // of the current tab. Otherwise it will only record parent process objects. + await startRecordingAllocations({ alsoRecordContentProcess: true }); + + // Now, run the test script. This time, we record this run. + await testScript(toolbox); + + // This will stop the record and also publish the results to Talos database + // Second argument will be the name of the test displayed in Talos. + // Many tests will be recorded, but all of them will be prefixed with this string. + await stopRecordingAllocations("reload", { alsoRecordContentProcess: true }); + + // Then, here you can execute cleanup. + // You would typically close the tab, toolbox, ... +}); +``` + +# How to run them locally + +```bash +$ ./mach mochitest --headless devtools/client/framework/test/allocations/ +``` + +And to only see the results: +```bash +$ ./mach mochitest --headless devtools/client/framework/test/allocations/ | grep " test leaked " +``` + +# Debug leaks + +If you are seeing a regression or an improvement, only seeing the number of objects being leaked isn't super helpful. +The tests includes some special debug modes which are printing lots of data to figure out what is leaking and why. + +You may run the test with the following env variable to turn debug mode on: +```bash +DEBUG_DEVTOOLS_ALLOCATIONS=leak|allocations $ ./mach mochitest --headless devtools/client/framework/test/allocations/the-fault-test.js +``` + +**DEBUG_DEVTOOLS_ALLOCATIONS** can enable two distinct debug output. (Only one can be enabled at a given time) + +**DEBUG_DEVTOOLS_ALLOCATIONS=allocations** will report all allocation sites that have been made +while running your test. This will include allocations which has been freed. +This view is especially useful if you want to reduce allocations in order to reduce GC overload. + +**DEBUG_DEVTOOLS_ALLOCATIONS=leak** will report only the allocations which are still allocated +at the end of your test. Sometimes it will only report allocations with missing stack trace. +Thus making the preview view helpful. + +## Example + +Let's assume we have the following code: + +```javascript + 1: exports.MyModule = { + 2: globalArray: [], + 3: test() { + 3: // The following object will be allocated but not leaked, + 5: // as we keep no reference to it anywhere + 6: const transientObject = {}; + 7: + 8: // The following object will be allocated on this line, + 9: // but leaked on the following one. By storing a reference +10: // to it in the global array which is never cleared. +11: const leakedObject = {}; +12: this.globalArray.push(leakedObject); +13: }, +14: }; +``` + +And that, we have a memory test doing this: + +```javascript + const { MyModule } = require("devtools/my-module"); + + await startRecordingAllocations(); + + MyModule.test(); + + await stopRecordingAllocations("target"); +``` + +We can first review all the allocations by running: + +```bash +DEBUG_DEVTOOLS_ALLOCATIONS=allocations $ ./mach mochitest --headless devtools/client/framework/test/allocations/browser_allocation_myTest.js + +``` + +which will print at the end: + +```javascript +DEVTOOLS ALLOCATION: all allocations (which may be freed or are still allocated): +[ + { + "src": "UNKNOWN", + "count": 80, + "lines": [ + "?: 80" + ] + }, + { + "src": "resource://devtools/my-module.js", + "count": 2, + "lines": [ + "11: 1" + "6: 1" + ] + } +] +``` + +The first part, with `UNKNOWN` can be ignored. This is about objects with missing allocation sites. +The second part of this logs tells us that 2 objects were allocated from my-module.js when running the test. +One has been allocated at line 6, it is `transcientObject`. +Another one has been allocated at line 11, it is `leakedObject`. + +Now, we can use the second view to focus only on objects that have been kept allocated: + +```bash +DEBUG_DEVTOOLS_ALLOCATIONS=leaks $ ./mach mochitest --headless devtools/client/framework/test/allocations/browser_allocation_myTest.js + +``` + +which will print at the end: + +```javascript +DEVTOOLS ALLOCATION: allocations which leaked: +[ + { + "src": "UNKNOWN", + "count": 80, + "lines": [ + "?: 80" + ] + }, + { + "src": "resource://devtools/shared/commands/commands-factory.js", + "count": 1, + "lines": [ + "11: 1" + ] + } +] +``` + +Similarly, we can focus only on the second part, which tells us that only one object is being leaked +and this object has been originally created from line 11, this is `leakedObject`. +This doesn't tell us why the object is being kept allocated, but at least we know which one is being kept in memory. + + +## Debug leaks via dominators + +This last feature might be the most powerful and isn't bound to DEBUG_DEVTOOLS_ALLOCATIONS. +This is always enabled. +Also, it requires to know which particular object is being leaked and also require to hack +the codebase in order to pass a reference of the suspicious object to the test helper. + +You can instruct the test helper to track a given object by doing this: + +```javascript + 1: // Let's say it is some code running from "my-module.js" + 2: + 3: // From a DevTools CommonJS module: + 4: const { track } = require("devtools/shared/test-helpers/tracked-objects.sys.mjs"); + 5: // From anything else, JSM, XPCOM module,...: + 6: const { track } = ChromeUtils.importESModule("resource://devtools/shared/test-helpers/tracked-objects.sys.mjs"); + 7: + 8: const g = []; + 9: function someFunctionInDevToolsCalledBySomething() { +10: const myLeakedObject = {}; +11: track(myLeakedObject); +12: +13: // Simulate a leak by holding a reference to the object in a global `g` array +14: g.push({ seeMyCustomAttributeHere: myLeakedObject }); +15: } +``` + +Then, when running the test you will get such output: + +```bash + 0:41.26 GECKO(644653) # Tracing: Object@my-module:10 + 0:40.65 GECKO(644653) ### Path(s) from root: + 0:41.26 GECKO(644653) - other@no-stack:undefined.WeakMap entry value + 0:41.26 GECKO(644653) \--> LexicalEnvironment@base-loader.sys.mjs:160.**UNKNOWN SLOT 1** + 0:41.26 GECKO(644653) \--> Object@base-loader.sys.mjs:155.g + 0:41.26 GECKO(644653) \--> Array@my-module.js:8.objectElements[0] + 0:41.26 GECKO(644653) \--> Object@my-module.js:14.seeMyCustomAttributeHere + 0:41.26 GECKO(644653) \--> Object@my-module.js:10 +``` + +This output means that `myLeakedObject` was originally allocated from my-module.js at line 10. +And is being held allocated because it is kept in an Object allocated from my-module.js at line 14. +This is our custom object we stored in `g` global Array. +This custom object it hold by the Array allocated at line 8 of my-module.js. +And this array is held allocated from an Object, itself allocated by base-loader.sys.mjs at line 155. +This is the global of the my-module.js's module, created by DevTools loader. +Then we see some more low level object up to another global object, which misses its allocation site. + +# How to easily get data from try run + +```bash +$ ./mach try fuzzy devtools/client/framework/test/allocations/ --query "'linux 'chrome-e10s 'opt '64-qr/opt" +``` + +You might also pass `--rebuild 3` if the test result is having some noise and you want more test runs. + +# Following trends for these tests + +You may try looking at: + + +Or at: + + +Link that you get from: +by looking at last year data for "DevTools" in the first dropdown, +and double clicking on the relevant line in "Tests" menulist. + +Significant improvements and regressions will be notified through [the following dashboard](https://treeherder.mozilla.org/perfherder/alerts?hideDwnToInv=1&page=1&framework=12). diff --git a/devtools/client/framework/test/allocations/head.js b/devtools/client/framework/test/allocations/head.js new file mode 100644 index 0000000000..a7e7f56a36 --- /dev/null +++ b/devtools/client/framework/test/allocations/head.js @@ -0,0 +1,250 @@ +/* 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"; + +// Load the tracker very first in order to ensure tracking all objects created by DevTools. +// This is especially important for allocation sites. We need to catch the global the +// earliest possible in order to ensure that all allocation objects come with a stack. +// +// If we want to track DevTools module loader we should ensure loading Loader.sys.mjs within +// the `testScript` Function. i.e. after having calling startRecordingAllocations. +let tracker, releaseTrackerLoader; +{ + const { + useDistinctSystemPrincipalLoader, + releaseDistinctSystemPrincipalLoader, + } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs" + ); + + const requester = {}; + const loader = useDistinctSystemPrincipalLoader(requester); + releaseTrackerLoader = () => releaseDistinctSystemPrincipalLoader(requester); + const { allocationTracker } = loader.require( + "chrome://mochitests/content/browser/devtools/shared/test-helpers/allocation-tracker.js" + ); + tracker = allocationTracker({ watchDevToolsGlobals: true }); +} + +// /!\ Be careful about imports/require +// +// Some tests may record the very first time we load a module. +// If we start loading them from here, we might only retrieve the already loaded +// module from the loader's cache. This would no longer highlight the cost +// of loading a new module from scratch. +// +// => Avoid loading devtools module as much as possible +// => If you really have to, lazy load them + +ChromeUtils.defineLazyGetter(this, "TrackedObjects", () => { + return ChromeUtils.importESModule( + "resource://devtools/shared/test-helpers/tracked-objects.sys.mjs" + ); +}); + +// So that PERFHERDER data can be extracted from the logs. +SimpleTest.requestCompleteLog(); + +// We have to disable testing mode, or various debug instructions are enabled. +// We especially want to disable redux store history, which would leak all the actions! +SpecialPowers.pushPrefEnv({ + set: [["devtools.testing", false]], +}); + +// Set DEBUG_DEVTOOLS_ALLOCATIONS=allocations|leaks in order print debug informations. +const DEBUG_ALLOCATIONS = Services.env.get("DEBUG_DEVTOOLS_ALLOCATIONS"); + +async function addTab(url) { + const tab = BrowserTestUtils.addTab(gBrowser, url); + gBrowser.selectedTab = tab; + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + return tab; +} + +/** + * This function will force some garbage collection before recording + * data about allocated objects. + * + * This accept an optional boolean to also record the content process objects + * of the current tab. That, in addition of objects from the parent process, + * which are always recorded. + * + * This return same data object which is meant to be passed to `stopRecordingAllocations` as-is. + * + * See README.md file in this folder. + */ +async function startRecordingAllocations({ + alsoRecordContentProcess = false, +} = {}) { + // Also start recording allocations in the content process, if requested + if (alsoRecordContentProcess) { + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [DEBUG_ALLOCATIONS], + async debug_allocations => { + const { DevToolsLoader } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" + ); + + const { + useDistinctSystemPrincipalLoader, + releaseDistinctSystemPrincipalLoader, + } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs" + ); + + const requester = {}; + const loader = useDistinctSystemPrincipalLoader(requester); + const { allocationTracker } = loader.require( + "chrome://mochitests/content/browser/devtools/shared/test-helpers/allocation-tracker.js" + ); + // We watch all globals in the content process, (instead of only DevTools global in parent process) + // because we may easily leak web page objects, which aren't in DevTools global. + const tracker = allocationTracker({ watchAllGlobals: true }); + + // /!\ HACK: store tracker and releaseTrackerLoader on DevToolsLoader in order + // to be able to reuse them in a following call to SpecialPowers.spawn + DevToolsLoader.tracker = tracker; + DevToolsLoader.releaseTrackerLoader = () => + releaseDistinctSystemPrincipalLoader(requester); + + await tracker.startRecordingAllocations(debug_allocations); + } + ); + // Trigger a GC in the parent process as this additional ContentTask + // seems to make harder to release objects created before we start recording. + await tracker.doGC(); + } + + await tracker.startRecordingAllocations(DEBUG_ALLOCATIONS); +} + +/** + * See doc of startRecordingAllocations + */ +async function stopRecordingAllocations( + recordName, + { alsoRecordContentProcess = false } = {} +) { + // Ensure that Memory API didn't ran out of buffers + ok(!tracker.overflowed, "Allocation were all recorded in the parent process"); + + // And finally, retrieve the record *after* having ran the test + const parentProcessData = await tracker.stopRecordingAllocations( + DEBUG_ALLOCATIONS + ); + + const objectNodeIds = TrackedObjects.getAllNodeIds(); + if (objectNodeIds.length) { + tracker.traceObjects(objectNodeIds); + } + + let contentProcessData = null; + if (alsoRecordContentProcess) { + contentProcessData = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [DEBUG_ALLOCATIONS], + debug_allocations => { + const { DevToolsLoader } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" + ); + const { tracker } = DevToolsLoader; + ok( + !tracker.overflowed, + "Allocation were all recorded in the content process" + ); + return tracker.stopRecordingAllocations(debug_allocations); + } + ); + } + + const trackedObjectsInContent = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + const TrackedObjects = ChromeUtils.importESModule( + "resource://devtools/shared/test-helpers/tracked-objects.sys.mjs" + ); + const objectNodeIds = TrackedObjects.getAllNodeIds(); + if (objectNodeIds.length) { + const { DevToolsLoader } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" + ); + const { tracker } = DevToolsLoader; + // Record the heap snapshot from the content process, + // and pass the record's filepath to the parent process + // As only the parent process can read the file because + // of sandbox restrictions made to content processes regarding file I/O. + const snapshotFile = tracker.getSnapshotFile(); + return { snapshotFile, objectNodeIds }; + } + return null; + } + ); + if (trackedObjectsInContent) { + tracker.traceObjects( + trackedObjectsInContent.objectNodeIds, + trackedObjectsInContent.snapshotFile + ); + } + + // Craft the JSON object required to save data in talos database + info( + `The ${recordName} test leaked ${parentProcessData.objectsWithStack} objects (${parentProcessData.objectsWithoutStack} with missing allocation site) in the parent process` + ); + const PERFHERDER_DATA = { + framework: { + name: "devtools", + }, + suites: [ + { + name: recordName + ":parent-process", + subtests: [ + { + name: "objects-with-stacks", + value: parentProcessData.objectsWithStack, + }, + { + name: "memory", + value: parentProcessData.memory, + }, + ], + }, + ], + }; + if (alsoRecordContentProcess) { + info( + `The ${recordName} test leaked ${contentProcessData.objectsWithStack} objects (${contentProcessData.objectsWithoutStack} with missing allocation site) in the content process` + ); + PERFHERDER_DATA.suites.push({ + name: recordName + ":content-process", + subtests: [ + { + name: "objects-with-stacks", + value: contentProcessData.objectsWithStack, + }, + { + name: "memory", + value: contentProcessData.memory, + }, + ], + }); + + // Finally release the tracker loader in content process. + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const { DevToolsLoader } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" + ); + DevToolsLoader.releaseTrackerLoader(); + }); + } + + // And release the tracker loader in the parent process + releaseTrackerLoader(); + + // Log it to stdout so that perfherder can collect this data. + // This only works if we called `SimpleTest.requestCompleteLog()`! + info("PERFHERDER_DATA: " + JSON.stringify(PERFHERDER_DATA)); +} diff --git a/devtools/client/framework/test/allocations/moz.build b/devtools/client/framework/test/allocations/moz.build new file mode 100644 index 0000000000..5488c8e2fc --- /dev/null +++ b/devtools/client/framework/test/allocations/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +BROWSER_CHROME_MANIFESTS += [ + "browser_allocations_browser_console.toml", + "browser_allocations_reload_debugger.toml", + "browser_allocations_reload_inspector.toml", + "browser_allocations_reload_netmonitor.toml", + "browser_allocations_reload_no_devtools.toml", + "browser_allocations_reload_webconsole.toml", + "browser_allocations_target.toml", + "browser_allocations_toolbox.toml", +] diff --git a/devtools/client/framework/test/allocations/reload-test.js b/devtools/client/framework/test/allocations/reload-test.js new file mode 100644 index 0000000000..3d09d5ef43 --- /dev/null +++ b/devtools/client/framework/test/allocations/reload-test.js @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from head.js */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +/** + * Generate a test Task to record allocation when reloading a test page + * while having one particular DevTools panel opened + * + * @param String recordName + * Name of the test recorded into PerfHerder/Talos database + * @param String toolId + * ID of the panel to open + */ +function createPanelReloadTest(recordName, toolId) { + return async function panelReloadTest() { + const TEST_URL = + "http://example.com/browser/devtools/client/framework/test/allocations/reloaded-page.html"; + + async function testScript(toolbox) { + const onTargetSwitched = + toolbox.commands.targetCommand.once("switched-target"); + const onReloaded = toolbox.getCurrentPanel().once("reloaded"); + + gBrowser.reloadTab(gBrowser.selectedTab); + + if ( + toolbox.commands.targetCommand.targetFront.targetForm + .followWindowGlobalLifeCycle + ) { + info("Wait for target switched"); + await onTargetSwitched; + } + + info("Wait for panel reload"); + await onReloaded; + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + const tab = await addTab(TEST_URL); + + const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" + ); + const { + gDevTools, + } = require("resource://devtools/client/framework/devtools.js"); + const toolbox = await gDevTools.showToolboxForTab(tab, { + toolId, + }); + + // Run the test scenario first before recording in order to load all the + // modules. Otherwise they get reported as "still allocated" objects, + // whereas we do expect them to be kept in memory as they are loaded via + // the main DevTools loader, which keeps the module loaded until the + // shutdown of Firefox + await testScript(toolbox); + // Running it a second time is helpful for the debugger which allocates different objects + // on the second run... which would be taken as leak otherwise. + await testScript(toolbox); + + await startRecordingAllocations({ + alsoRecordContentProcess: true, + }); + + // Now, run the test script. This time, we record this run. + for (let i = 0; i < 10; i++) { + await testScript(toolbox); + } + + await stopRecordingAllocations(recordName, { + alsoRecordContentProcess: true, + }); + + await toolbox.destroy(); + gBrowser.removeTab(tab); + }; +} diff --git a/devtools/client/framework/test/allocations/reloaded-page.html b/devtools/client/framework/test/allocations/reloaded-page.html new file mode 100644 index 0000000000..4f14c8a0c3 --- /dev/null +++ b/devtools/client/framework/test/allocations/reloaded-page.html @@ -0,0 +1,11 @@ + + + + Reloaded page + + + + The reloaded page + + + diff --git a/devtools/client/framework/test/allocations/reloaded.png b/devtools/client/framework/test/allocations/reloaded.png new file mode 100644 index 0000000000..769c636340 Binary files /dev/null and b/devtools/client/framework/test/allocations/reloaded.png differ diff --git a/devtools/client/framework/test/browser-telemetry-startup.toml b/devtools/client/framework/test/browser-telemetry-startup.toml new file mode 100644 index 0000000000..12b04456e7 --- /dev/null +++ b/devtools/client/framework/test/browser-telemetry-startup.toml @@ -0,0 +1,14 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "head.js", + "!/devtools/client/shared/test/shared-head.js", + "!/devtools/client/shared/test/telemetry-test-helpers.js", +] + +["browser_toolbox_telemetry_open_event.js"] +# This test suite is dedicated to run a test for the telemetry event logged when +# opening the toolbox for the first time. This test has to be the first test +# running for a given instance of Firefox. A dedicated manifest will ensure a +# new browser instance is created just for this test. diff --git a/devtools/client/framework/test/browser.toml b/devtools/client/framework/test/browser.toml new file mode 100644 index 0000000000..e7d979ebb3 --- /dev/null +++ b/devtools/client/framework/test/browser.toml @@ -0,0 +1,315 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "reload/*", + "browser_toolbox_options_disable_js.html", + "browser_toolbox_options_disable_js_iframe.html", + "browser_toolbox_options_disable_cache.sjs", + "browser_toolbox_options_disable_cache.css.sjs", + "browser_toolbox_window_title_changes_page.html", + "browser_toolbox_window_title_frame_select_page.html", + "code_bundle_late_script.js", + "code_bundle_late_script.js.map", + "code_binary_search.coffee", + "code_binary_search.js", + "code_binary_search.map", + "code_binary_search_absolute.js", + "code_binary_search_absolute.map", + "code_bundle_cross_domain.js", + "code_bundle_cross_domain.js.map", + "code_bundle_no_race.js", + "code_bundle_no_race.js.map", + "code_cross_domain.js", + "code_inline_bundle.js", + "code_inline_original.js", + "code_math.js", + "code_no_race.js", + "doc_backward_forward_navigation.html", + "doc_cached-resource.html", + "doc_cached-resource_iframe.html", + "doc_empty-tab-01.html", + "doc_lazy_tool.html", + "doc_textbox_tool.html", + "head.js", + "helper_disable_cache.js", + "doc_theme.css", + "doc_viewsource.html", + "browser_toolbox_options_enable_serviceworkers_testing.html", + "serviceworker.js", + "sjs_cache_controle_header.sjs", + "test_chrome_page.html", + "!/devtools/client/debugger/test/mochitest/shared-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/webconsole/test/browser/shared-head.js", +] +prefs = ["security.allow_unsafe_parent_loads=true"] # This is far from ideal. Bug 1565279 covers removing this pref flip. + +["../../../../browser/base/content/test/static/browser_all_files_referenced.js"] +# We want this test to run for mochitest-dt as well, so we include it here +skip-if = [ + "debug", # no point in running on both opt and debug, and will likely intermittently timeout on debug, Bug 1598726 + "asan", + "ccov", +] + +["../../../../browser/base/content/test/static/browser_parsable_css.js"] +# We want this test to run for mochitest-dt as well, so we include it here +skip-if = [ + "debug", # no point in running on both opt and debug, and will likely intermittently timeout on debug + "asan", +] + +["browser_about-devtools-toolbox_load.js"] + +["browser_about-devtools-toolbox_reload.js"] + +["browser_commands_from_url.js"] + +["browser_devtools_api_destroy.js"] + +["browser_dynamic_tool_enabling.js"] + +["browser_front_parentFront.js"] + +["browser_ignore_toolbox_network_requests.js"] + +["browser_keybindings_01.js"] + +["browser_keybindings_02.js"] + +["browser_keybindings_03.js"] + +["browser_menu_api.js"] + +["browser_new_activation_workflow.js"] + +["browser_source_map-01.js"] + +["browser_source_map-absolute.js"] + +["browser_source_map-cross-domain.js"] + +["browser_source_map-init.js"] + +["browser_source_map-inline.js"] + +["browser_source_map-late-script.js"] + +["browser_source_map-no-race.js"] +skip-if = [ + "http3", # Bug 1829298 + "http2", +] + +["browser_source_map-pub-sub.js"] +skip-if = [ + "http3", # Bug 1829298 + "http2", +] + +["browser_source_map-reload.js"] +skip-if = [ + "http3", # Bug 1829298 + "http2", +] + +["browser_tab_commands_factory.js"] + +["browser_tab_descriptor_fission.js"] + +["browser_target_cached-front.js"] + +["browser_target_cached-resource.js"] + +["browser_target_get-front.js"] + +["browser_target_listeners.js"] + +["browser_target_loading.js"] + +["browser_target_parents.js"] +skip-if = ["tsan"] # bug 1807041 + +["browser_target_remote.js"] + +["browser_target_server_compartment.js"] + +["browser_target_support.js"] + +["browser_toolbox_backward_forward_navigation.js"] +skip-if = [ + "os == 'linux' && bits == 64", # Bug 1770314 + "os == 'mac'", # Bug 1770314 +] + +["browser_toolbox_browsertoolbox_host.js"] + +["browser_toolbox_contentpage_contextmenu.js"] + +["browser_toolbox_disable_f12.js"] + +["browser_toolbox_dynamic_registration.js"] + +["browser_toolbox_error_count.js"] + +["browser_toolbox_error_count_reset_on_navigation.js"] + +["browser_toolbox_fission_navigation.js"] +skip-if = [ + "os == 'linux'", # Bug 1742672 +] + +["browser_toolbox_frames_list.js"] + +["browser_toolbox_getpanelwhenready.js"] + +["browser_toolbox_highlight.js"] + +["browser_toolbox_hosts.js"] + +["browser_toolbox_hosts_size.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_toolbox_hosts_telemetry.js"] + +["browser_toolbox_keyboard_navigation.js"] + +["browser_toolbox_keyboard_navigation_notification_box.js"] +skip-if = [ + "http3", # Bug 1829298 + "http2", +] + +["browser_toolbox_meatball.js"] + +["browser_toolbox_options.js"] + +["browser_toolbox_options_disable_buttons.js"] +skip-if = ["a11y_checks"] # Bug 1849028 and 1849179 for causing crashes + +["browser_toolbox_options_disable_cache-01.js"] + +["browser_toolbox_options_disable_cache-02.js"] + +["browser_toolbox_options_disable_cache-03.js"] +skip-if = [ + "http3", # Bug 1829298 + "http2", +] + +["browser_toolbox_options_disable_js.js"] + +["browser_toolbox_options_enable_serviceworkers_testing.js"] +skip-if = [ + "http3", # Bug 1829298 + "http2", +] + +["browser_toolbox_options_frames_button.js"] + +["browser_toolbox_options_multiple_tabs.js"] + +["browser_toolbox_options_panel_toggle.js"] + +["browser_toolbox_popups_debugging.js"] + +["browser_toolbox_races.js"] + +["browser_toolbox_raise.js"] +disabled = "Bug 962258" + +["browser_toolbox_ready.js"] + +["browser_toolbox_remoteness_change.js"] + +["browser_toolbox_screenshot_tool.js"] +skip-if = ["a11y_checks"] # Bugs 1858041 and 1849028 for causing intermittent crashes + +["browser_toolbox_select_event.js"] + +["browser_toolbox_selected_tool_unavailable.js"] + +["browser_toolbox_selectionchanged_event.js"] + +["browser_toolbox_show_toolbox_tool_ready.js"] + +["browser_toolbox_split_console.js"] + +["browser_toolbox_tabsswitch_shortcuts.js"] + +["browser_toolbox_telemetry_activate_splitconsole.js"] + +["browser_toolbox_telemetry_close.js"] + +["browser_toolbox_telemetry_enter.js"] + +["browser_toolbox_telemetry_exit.js"] + +["browser_toolbox_textbox_context_menu.js"] + +["browser_toolbox_theme.js"] + +["browser_toolbox_theme_registration.js"] + +["browser_toolbox_toggle.js"] + +["browser_toolbox_tool_ready.js"] + +["browser_toolbox_tool_remote_reopen.js"] + +["browser_toolbox_toolbar_minimum_width.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled +skip-if = ["os == 'win' && !debug"] # Bug 1709840 + +["browser_toolbox_toolbar_overflow.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_toolbox_toolbar_overflow_button_visibility.js"] + +["browser_toolbox_toolbar_reorder_by_dnd.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_toolbox_toolbar_reorder_by_width.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_toolbox_toolbar_reorder_with_extension.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_toolbox_toolbar_reorder_with_hidden_extension.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_toolbox_tools_per_toolbox_registration.js"] + +["browser_toolbox_view_source_01.js"] + +["browser_toolbox_view_source_02.js"] + +["browser_toolbox_view_source_03.js"] + +["browser_toolbox_view_source_style_editor_fallback.js"] + +["browser_toolbox_watchedByDevTools.js"] + +["browser_toolbox_window_reload_target.js"] + +["browser_toolbox_window_reload_target_force.js"] + +["browser_toolbox_window_shortcuts.js"] + +["browser_toolbox_window_title_changes.js"] + +["browser_toolbox_window_title_frame_select.js"] + +["browser_toolbox_zoom.js"] +skip-if = ["os == 'win' && !debug"] # bug 1683265 + +["browser_toolbox_zoom_popup.js"] + +["browser_webextension_descriptor.js"] + +["browser_webextension_dropdown.js"] +skip-if = ["os == 'linux' && !debug"] # Bug 1714106 +# We want these tests to run for mochitest-dt as well, so we include them here: diff --git a/devtools/client/framework/test/browser_about-devtools-toolbox_load.js b/devtools/client/framework/test/browser_about-devtools-toolbox_load.js new file mode 100644 index 0000000000..abcb59a5d6 --- /dev/null +++ b/devtools/client/framework/test/browser_about-devtools-toolbox_load.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that about:devtools-toolbox shows error an page when opened with invalid + * paramters + */ +add_task(async function () { + // test that error is shown when missing `type` param + let { document, tab } = await openAboutToolbox({ invalid: "invalid" }); + await assertErrorIsShown(document); + await removeTab(tab); + // test that error is shown if `id` is not provided + ({ document, tab } = await openAboutToolbox({ type: "tab" })); + await assertErrorIsShown(document); + await removeTab(tab); + // test that error is shown if `remoteId` refers to an unexisting target + ({ document, tab } = await openAboutToolbox({ + type: "tab", + remoteId: "13371337", + })); + await assertErrorIsShown(document); + await removeTab(tab); + + async function assertErrorIsShown(doc) { + await waitUntil(() => doc.querySelector(".qa-error-page")); + ok(doc.querySelector(".qa-error-page"), "Error page is rendered"); + } +}); diff --git a/devtools/client/framework/test/browser_about-devtools-toolbox_reload.js b/devtools/client/framework/test/browser_about-devtools-toolbox_reload.js new file mode 100644 index 0000000000..f350816b24 --- /dev/null +++ b/devtools/client/framework/test/browser_about-devtools-toolbox_reload.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that about:devtools-toolbox is reloaded correctly when reusing the same debugger + * client instance. + */ +add_task(async function () { + const devToolsClient = await createLocalClient(); + + info( + "Preload a local DevToolsClient as this-firefox in the remoteClientManager" + ); + const { + remoteClientManager, + } = require("resource://devtools/client/shared/remote-debugging/remote-client-manager.js"); + remoteClientManager.setClient( + "this-firefox", + "this-firefox", + devToolsClient, + {} + ); + registerCleanupFunction(() => { + remoteClientManager.removeAllClients(); + }); + + info("Create a dummy target tab"); + const targetTab = await addTab("data:text/html,somehtml"); + + let onToolboxReady = gDevTools.once("toolbox-ready"); + const { tab } = await openAboutToolbox({ + id: targetTab.linkedBrowser.browserId, + remoteId: "this-firefox-this-firefox", + type: "tab", + }); + await onToolboxReady; + + info("Reload about:devtools-toolbox page"); + onToolboxReady = gDevTools.once("toolbox-ready"); + tab.linkedBrowser.reload(); + await onToolboxReady; + + info("Check if about:devtools-toolbox was reloaded correctly"); + const refreshedDoc = tab.linkedBrowser.contentDocument; + ok( + refreshedDoc.querySelector(".debug-target-info"), + "about:devtools-toolbox header is correctly displayed" + ); + + const onToolboxDestroy = gDevTools.once("toolbox-destroyed"); + await removeTab(tab); + await onToolboxDestroy; + await devToolsClient.close(); + await removeTab(targetTab); +}); + +async function createLocalClient() { + const { + DevToolsClient, + } = require("resource://devtools/client/devtools-client.js"); + const { + DevToolsServer, + } = require("resource://devtools/server/devtools-server.js"); + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + DevToolsServer.allowChromeProcess = true; + + const devToolsClient = new DevToolsClient(DevToolsServer.connectPipe()); + await devToolsClient.connect(); + return devToolsClient; +} diff --git a/devtools/client/framework/test/browser_commands_from_url.js b/devtools/client/framework/test/browser_commands_from_url.js new file mode 100644 index 0000000000..6d1412005c --- /dev/null +++ b/devtools/client/framework/test/browser_commands_from_url.js @@ -0,0 +1,161 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_URI = + "data:text/html;charset=utf-8," + "

browser_target-from-url.js

"; + +const { DevToolsLoader } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { + commandsFromURL, +} = require("resource://devtools/client/framework/commands-from-url.js"); + +Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true); +Services.prefs.setBoolPref("devtools.debugger.prompt-connection", false); + +SimpleTest.registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.debugger.remote-enabled"); + Services.prefs.clearUserPref("devtools.debugger.prompt-connection"); +}); + +function assertTarget(target, url) { + is(target.url, url); + is(target.isBrowsingContext, true); +} + +add_task(async function () { + const tab = await addTab(TEST_URI); + const browser = tab.linkedBrowser; + let commands, target; + + info("Test invalid type"); + try { + await commandsFromURL(new URL("https://foo?type=x")); + ok(false, "Shouldn't pass"); + } catch (e) { + is(e.message, "commandsFromURL, unsupported type 'x' parameter"); + } + + info("Test tab"); + commands = await commandsFromURL( + new URL("https://foo?type=tab&id=" + browser.browserId) + ); + // Descriptor's getTarget will only work if the TargetCommand watches for the first top target + await commands.targetCommand.startListening(); + + // For now, we can't spawn a commands flagged as 'local tab' via URL query params + // The only way to has isLocalTab is to create the toolbox via showToolboxForTab + // and spawn the command via CommandsFactory.forTab. + is( + commands.descriptorFront.isLocalTab, + false, + "Even if we refer to a local tab, isLocalTab is false (for now)" + ); + + target = await commands.descriptorFront.getTarget(); + + assertTarget(target, TEST_URI); + await commands.destroy(); + + info("Test invalid tab id"); + try { + await commandsFromURL(new URL("https://foo?type=tab&id=10000")); + ok(false, "Shouldn't pass"); + } catch (e) { + is(e.message, "commandsFromURL, tab with browserId '10000' doesn't exist"); + } + + info("Test parent process"); + commands = await commandsFromURL(new URL("https://foo?type=process")); + target = await commands.descriptorFront.getTarget(); + const topWindow = Services.wm.getMostRecentWindow("navigator:browser"); + assertTarget(target, topWindow.location.href); + await commands.destroy(); + + await testRemoteTCP(); + await testRemoteWebSocket(); + + gBrowser.removeCurrentTab(); +}); + +async function setupDevToolsServer(webSocket) { + info("Create a separate loader instance for the DevToolsServer."); + const loader = new DevToolsLoader(); + const { DevToolsServer } = loader.require( + "resource://devtools/server/devtools-server.js" + ); + const { SocketListener } = loader.require( + "resource://devtools/shared/security/socket.js" + ); + + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + DevToolsServer.allowChromeProcess = true; + const socketOptions = { + // Pass -1 to automatically choose an available port + portOrPath: -1, + webSocket, + }; + + const listener = new SocketListener(DevToolsServer, socketOptions); + ok(listener, "Socket listener created"); + await listener.open(); + is(DevToolsServer.listeningSockets, 1, "1 listening socket"); + + return { DevToolsServer, listener }; +} + +function teardownDevToolsServer({ DevToolsServer, listener }) { + info("Close the listener socket"); + listener.close(); + is(DevToolsServer.listeningSockets, 0, "0 listening sockets"); + + info("Destroy the temporary devtools server"); + DevToolsServer.destroy(); +} + +async function testRemoteTCP() { + info("Test remote process via TCP Connection"); + + const server = await setupDevToolsServer(false); + + const { port } = server.listener; + const commands = await commandsFromURL( + new URL("https://foo?type=process&host=127.0.0.1&port=" + port) + ); + const target = await commands.descriptorFront.getTarget(); + const topWindow = Services.wm.getMostRecentWindow("navigator:browser"); + assertTarget(target, topWindow.location.href); + + const settings = commands.client._transport.connectionSettings; + is(settings.host, "127.0.0.1"); + is(parseInt(settings.port, 10), port); + is(settings.webSocket, false); + + await commands.destroy(); + + teardownDevToolsServer(server); +} + +async function testRemoteWebSocket() { + info("Test remote process via WebSocket Connection"); + + const server = await setupDevToolsServer(true); + + const { port } = server.listener; + const commands = await commandsFromURL( + new URL("https://foo?type=process&host=127.0.0.1&port=" + port + "&ws=true") + ); + const target = await commands.descriptorFront.getTarget(); + const topWindow = Services.wm.getMostRecentWindow("navigator:browser"); + assertTarget(target, topWindow.location.href); + + const settings = commands.client._transport.connectionSettings; + is(settings.host, "127.0.0.1"); + is(parseInt(settings.port, 10), port); + is(settings.webSocket, true); + await commands.destroy(); + + teardownDevToolsServer(server); +} diff --git a/devtools/client/framework/test/browser_devtools_api_destroy.js b/devtools/client/framework/test/browser_devtools_api_destroy.js new file mode 100644 index 0000000000..736455df65 --- /dev/null +++ b/devtools/client/framework/test/browser_devtools_api_destroy.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests devtools API + +function test() { + addTab("about:blank").then(runTests); +} + +async function runTests(aTab) { + const toolDefinition = { + id: "testTool", + visibilityswitch: "devtools.testTool.enabled", + isToolSupported: () => true, + url: "about:blank", + label: "someLabel", + build(iframeWindow, toolbox) { + return new Promise(resolve => { + executeSoon(() => { + resolve({ + target: toolbox.target, + toolbox, + isReady: true, + destroy() {}, + }); + }); + }); + }, + }; + + gDevTools.registerTool(toolDefinition); + + const collectedEvents = []; + + gDevTools + .showToolboxForTab(aTab, { toolId: toolDefinition.id }) + .then(function (toolbox) { + const panel = toolbox.getPanel(toolDefinition.id); + ok(panel, "Tool open"); + + gDevTools.once("toolbox-destroy", (toolbox, iframe) => { + collectedEvents.push("toolbox-destroy"); + }); + + gDevTools.once(toolDefinition.id + "-destroy", (toolbox, iframe) => { + collectedEvents.push("gDevTools-" + toolDefinition.id + "-destroy"); + }); + + toolbox.once("destroy", () => { + collectedEvents.push("destroy"); + }); + + toolbox.once(toolDefinition.id + "-destroy", () => { + collectedEvents.push("toolbox-" + toolDefinition.id + "-destroy"); + }); + + toolbox.destroy().then(function () { + is( + collectedEvents.join(":"), + "toolbox-destroy:destroy:gDevTools-testTool-destroy:toolbox-testTool-destroy", + "Found the right amount of collected events." + ); + + gDevTools.unregisterTool(toolDefinition.id); + gBrowser.removeCurrentTab(); + + executeSoon(function () { + finish(); + }); + }); + }); +} diff --git a/devtools/client/framework/test/browser_dynamic_tool_enabling.js b/devtools/client/framework/test/browser_dynamic_tool_enabling.js new file mode 100644 index 0000000000..56313607cf --- /dev/null +++ b/devtools/client/framework/test/browser_dynamic_tool_enabling.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that toggling prefs immediately (de)activates the relevant menuitem + +var gItemsToTest = { + menu_browserToolbox: [ + "devtools.chrome.enabled", + "devtools.debugger.remote-enabled", + ], +}; + +function expectedAttributeValueFromPrefs(prefs) { + return prefs.every(pref => Services.prefs.getBoolPref(pref)) ? "" : "true"; +} + +function checkItem(el, prefs) { + const expectedValue = expectedAttributeValueFromPrefs(prefs); + is( + el.getAttribute("disabled"), + expectedValue, + "disabled attribute should match current pref state" + ); + is( + el.getAttribute("hidden"), + expectedValue, + "hidden attribute should match current pref state" + ); +} + +function test() { + for (const k in gItemsToTest) { + const el = document.getElementById(k); + const prefs = gItemsToTest[k]; + checkItem(el, prefs); + for (const pref of prefs) { + Services.prefs.setBoolPref(pref, !Services.prefs.getBoolPref(pref)); + checkItem(el, prefs); + Services.prefs.setBoolPref(pref, !Services.prefs.getBoolPref(pref)); + checkItem(el, prefs); + } + } + finish(); +} diff --git a/devtools/client/framework/test/browser_front_parentFront.js b/devtools/client/framework/test/browser_front_parentFront.js new file mode 100644 index 0000000000..106632dd45 --- /dev/null +++ b/devtools/client/framework/test/browser_front_parentFront.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the Front's parentFront attribute returns the correct parent front. + +const TEST_URL = `data:text/html;charset=utf-8,
`; + +add_task(async function () { + const tab = await addTab(TEST_URL); + const target = await createAndAttachTargetForTab(tab); + + const inspectorFront = await target.getFront("inspector"); + const walker = inspectorFront.walker; + const pageStyleFront = await inspectorFront.getPageStyle(); + const nodeFront = await walker.querySelector(walker.rootNode, "#test"); + + is( + inspectorFront.parentFront, + target, + "Got the correct parentFront from the InspectorFront." + ); + is( + walker.parentFront, + inspectorFront, + "Got the correct parentFront from the WalkerFront." + ); + is( + pageStyleFront.parentFront, + inspectorFront, + "Got the correct parentFront from the PageStyleFront." + ); + is( + nodeFront.parentFront, + walker, + "Got the correct parentFront from the NodeFront." + ); +}); diff --git a/devtools/client/framework/test/browser_ignore_toolbox_network_requests.js b/devtools/client/framework/test/browser_ignore_toolbox_network_requests.js new file mode 100644 index 0000000000..65daa4d78d --- /dev/null +++ b/devtools/client/framework/test/browser_ignore_toolbox_network_requests.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that network requests originating from the toolbox don't get recorded in +// the network panel. + +add_task(async function () { + let tab = await addTab(URL_ROOT + "doc_viewsource.html"); + let toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: "styleeditor", + }); + let panel = toolbox.getPanel("styleeditor"); + + is(panel.UI.editors.length, 1, "correct number of editors opened"); + + const monitor = await toolbox.selectTool("netmonitor"); + const { store } = monitor.panelWin; + + is( + store.getState().requests.requests.length, + 0, + "No network requests appear in the network panel" + ); + + await toolbox.destroy(); + tab = toolbox = panel = null; + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/client/framework/test/browser_keybindings_01.js b/devtools/client/framework/test/browser_keybindings_01.js new file mode 100644 index 0000000000..968c3a3d3d --- /dev/null +++ b/devtools/client/framework/test/browser_keybindings_01.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(3); + +// Tests that the keybindings for opening and closing the inspector work as expected +// Can probably make this a shared test that tests all of the tools global keybindings +const TEST_URL = + "data:text/html,Test for the " + + "highlighter keybindings" + + "

Keybindings!

"; + +const { + gDevToolsBrowser, +} = require("resource://devtools/client/framework/devtools-browser.js"); + +const isMac = AppConstants.platform == "macosx"; + +const allKeys = []; +function buildDevtoolsKeysetMap(keyset) { + // Fetches all the keyboard shortcuts which were defined by lazyGetter 'KeyShortcuts' in + // devtools-startup.js and added to the DOM by 'hookKeyShortcuts' + [...keyset.querySelectorAll("key")].forEach(key => { + if (!key.getAttribute("key")) { + return; + } + + const modifiers = key.getAttribute("modifiers"); + allKeys.push({ + toolId: key.id.split("_")[1], + key: key.getAttribute("key"), + modifiers, + modifierOpt: { + shiftKey: modifiers.match("shift"), + ctrlKey: modifiers.match("ctrl"), + altKey: modifiers.match("alt"), + metaKey: modifiers.match("meta"), + accelKey: modifiers.match("accel"), + }, + synthesizeKey() { + EventUtils.synthesizeKey(this.key, this.modifierOpt); + }, + }); + }); +} + +function setupKeyBindingsTest() { + for (const win of gDevToolsBrowser._trackedBrowserWindows) { + buildDevtoolsKeysetMap(win.document.getElementById("devtoolsKeyset")); + } +} + +add_task(async function () { + await addTab(TEST_URL); + await new Promise(done => waitForFocus(done)); + + setupKeyBindingsTest(); + + const tests = [ + { id: "inspector", toolId: "inspector" }, + { id: "webconsole", toolId: "webconsole" }, + { id: "netmonitor", toolId: "netmonitor" }, + { id: "jsdebugger", toolId: "jsdebugger" }, + ]; + + // There are two possible keyboard shortcuts to open the inspector on macOS + if (isMac) { + tests.push({ id: "inspectorMac", toolId: "inspector" }); + } + + // Toolbox reference will be set by first tool to open. + let toolbox; + + for (const test of tests) { + const onToolboxReady = gDevTools.once("toolbox-ready"); + const onSelectTool = gDevTools.once("select-tool-command"); + + info(`Run the keyboard shortcut for ${test.id}`); + const key = allKeys.filter(({ toolId }) => toolId === test.id)[0]; + key.synthesizeKey(); + + if (!toolbox) { + toolbox = await onToolboxReady; + } + + if (test.toolId === "inspector") { + const onPickerStart = toolbox.nodePicker.once("picker-started"); + await onPickerStart; + ok(true, "picker-started event received, highlighter started"); + + info( + `Run the keyboard shortcut for ${test.id} again to stop the node picker` + ); + const onPickerStop = toolbox.nodePicker.once("picker-stopped"); + key.synthesizeKey(); + await onPickerStop; + ok(true, "picker-stopped event received, highlighter stopped"); + } + + await onSelectTool; + is(toolbox.currentToolId, test.toolId, `${test.toolId} should be selected`); + } + + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/client/framework/test/browser_keybindings_02.js b/devtools/client/framework/test/browser_keybindings_02.js new file mode 100644 index 0000000000..3be8309edf --- /dev/null +++ b/devtools/client/framework/test/browser_keybindings_02.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the toolbox keybindings still work after the host is changed. + +const URL = "data:text/html;charset=utf8,test page"; + +var { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +const L10N = new LocalizationHelper( + "devtools/client/locales/toolbox.properties" +); + +function getZoomValue() { + return parseFloat(Services.prefs.getCharPref("devtools.toolbox.zoomValue")); +} + +add_task(async function () { + info("Create a test tab and open the toolbox"); + const tab = await addTab(URL); + const toolbox = await gDevTools.showToolboxForTab(tab, "webconsole"); + + const { RIGHT, BOTTOM } = Toolbox.HostType; + for (const type of [RIGHT, BOTTOM, RIGHT]) { + info("Switch to host type " + type); + await toolbox.switchHost(type); + + info("Try to use the toolbox shortcuts"); + await checkKeyBindings(toolbox); + } + + Services.prefs.clearUserPref("devtools.toolbox.zoomValue"); + Services.prefs.setCharPref("devtools.toolbox.host", BOTTOM); + await toolbox.destroy(); + gBrowser.removeCurrentTab(); +}); + +function zoomWithKey(toolbox, key) { + const shortcut = L10N.getStr(key); + if (!shortcut) { + info("Key was empty, skipping zoomWithKey"); + return; + } + info("Zooming with key: " + key); + const currentZoom = getZoomValue(); + synthesizeKeyShortcut(shortcut, toolbox.win); + isnot( + getZoomValue(), + currentZoom, + "The zoom level was changed in the toolbox" + ); +} + +function checkKeyBindings(toolbox) { + zoomWithKey(toolbox, "toolbox.zoomIn.key"); + zoomWithKey(toolbox, "toolbox.zoomIn2.key"); + + zoomWithKey(toolbox, "toolbox.zoomReset.key"); + + zoomWithKey(toolbox, "toolbox.zoomOut.key"); + zoomWithKey(toolbox, "toolbox.zoomOut2.key"); + + zoomWithKey(toolbox, "toolbox.zoomReset2.key"); +} diff --git a/devtools/client/framework/test/browser_keybindings_03.js b/devtools/client/framework/test/browser_keybindings_03.js new file mode 100644 index 0000000000..71ce51c1e0 --- /dev/null +++ b/devtools/client/framework/test/browser_keybindings_03.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 toolbox 'switch to previous host' feature works. +// Pressing ctrl/cmd+shift+d should switch to the last used host. + +const URL = "data:text/html;charset=utf8,test page for toolbox switching"; + +var { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +const L10N = new LocalizationHelper( + "devtools/client/locales/toolbox.properties" +); + +add_task(async function () { + info("Create a test tab and open the toolbox"); + const tab = await addTab(URL); + const toolbox = await gDevTools.showToolboxForTab(tab, "webconsole"); + + const shortcut = L10N.getStr("toolbox.toggleHost.key"); + + const { RIGHT, BOTTOM, WINDOW } = Toolbox.HostType; + checkHostType(toolbox, BOTTOM, RIGHT); + + info("Switching from bottom to right"); + let onHostChanged = toolbox.once("host-changed"); + synthesizeKeyShortcut(shortcut, toolbox.win); + await onHostChanged; + checkHostType(toolbox, RIGHT, BOTTOM); + + info("Switching from right to bottom"); + onHostChanged = toolbox.once("host-changed"); + synthesizeKeyShortcut(shortcut, toolbox.win); + await onHostChanged; + checkHostType(toolbox, BOTTOM, RIGHT); + + info("Switching to window"); + await toolbox.switchHost(WINDOW); + checkHostType(toolbox, WINDOW, BOTTOM); + + info("Switching from window to bottom"); + onHostChanged = toolbox.once("host-changed"); + synthesizeKeyShortcut(shortcut, toolbox.win); + await onHostChanged; + checkHostType(toolbox, BOTTOM, WINDOW); + + await toolbox.destroy(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/client/framework/test/browser_menu_api.js b/devtools/client/framework/test/browser_menu_api.js new file mode 100644 index 0000000000..daa69cf8dd --- /dev/null +++ b/devtools/client/framework/test/browser_menu_api.js @@ -0,0 +1,239 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the Menu API works + +const URL = "data:text/html;charset=utf8,test page for menu api"; +const Menu = require("resource://devtools/client/framework/menu.js"); +const MenuItem = require("resource://devtools/client/framework/menu-item.js"); + +add_task(async function () { + info("Create a test tab and open the toolbox"); + const tab = await addTab(URL); + const toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: "webconsole", + }); + + // This test will involve localized strings, make sure the necessary FTL file is + // available in the toolbox top window. + toolbox.topWindow.MozXULElement.insertFTLIfNeeded( + "toolkit/global/textActions.ftl" + ); + + loadFTL(toolbox, "toolkit/global/textActions.ftl"); + + await testMenuItems(); + await testMenuPopup(toolbox); + await testSubmenu(toolbox); +}); + +function testMenuItems() { + const menu = new Menu(); + const menuItem1 = new MenuItem(); + const menuItem2 = new MenuItem(); + + menu.append(menuItem1); + menu.append(menuItem2); + + is(menu.items.length, 2, "Correct number of 'items'"); + is(menu.items[0], menuItem1, "Correct reference to MenuItem"); + is(menu.items[1], menuItem2, "Correct reference to MenuItem"); +} + +async function testMenuPopup(toolbox) { + let clickFired = false; + + const menu = new Menu({ + id: "menu-popup", + }); + menu.append(new MenuItem({ type: "separator" })); + + const MENU_ITEMS = [ + new MenuItem({ + id: "menu-item-1", + label: "Normal Item", + click: () => { + info("Click callback has fired for menu item"); + clickFired = true; + }, + }), + new MenuItem({ + label: "Checked Item", + type: "checkbox", + checked: true, + }), + new MenuItem({ + label: "Radio Item", + type: "radio", + }), + new MenuItem({ + label: "Disabled Item", + disabled: true, + }), + new MenuItem({ + l10nID: "text-action-undo", + }), + ]; + + for (const item of MENU_ITEMS) { + menu.append(item); + } + + // Append an invisible MenuItem, which shouldn't show up in the DOM + menu.append( + new MenuItem({ + label: "Invisible", + visible: false, + }) + ); + + menu.popup(0, 0, toolbox.doc); + const popup = toolbox.topDoc.querySelector("#menu-popup"); + ok(popup, "A popup is in the DOM"); + + const menuSeparators = toolbox.topDoc.querySelectorAll( + "#menu-popup > menuseparator" + ); + is(menuSeparators.length, 1, "A separator is in the menu"); + + const menuItems = toolbox.topDoc.querySelectorAll("#menu-popup > menuitem"); + is(menuItems.length, MENU_ITEMS.length, "Correct number of menuitems"); + + is(menuItems[0].id, MENU_ITEMS[0].id, "Correct id for menuitem"); + is(menuItems[0].getAttribute("label"), MENU_ITEMS[0].label, "Correct label"); + + is(menuItems[1].getAttribute("label"), MENU_ITEMS[1].label, "Correct label"); + is(menuItems[1].getAttribute("type"), "checkbox", "Correct type attr"); + is(menuItems[1].getAttribute("checked"), "true", "Has checked attr"); + + is(menuItems[2].getAttribute("label"), MENU_ITEMS[2].label, "Correct label"); + is(menuItems[2].getAttribute("type"), "radio", "Correct type attr"); + ok(!menuItems[2].hasAttribute("checked"), "Doesn't have checked attr"); + + is(menuItems[3].getAttribute("label"), MENU_ITEMS[3].label, "Correct label"); + is(menuItems[3].getAttribute("disabled"), "true", "disabled attr menuitem"); + + is( + menuItems[4].getAttribute("data-l10n-id"), + MENU_ITEMS[4].l10nID, + "Correct localization attribute" + ); + + await once(menu, "open"); + const closed = once(menu, "close"); + popup.activateItem(menuItems[0]); + await closed; + ok(clickFired, "Click has fired"); + + ok( + !toolbox.topDoc.querySelector("#menu-popup"), + "Popup removed from the DOM" + ); +} + +async function testSubmenu(toolbox) { + let clickFired = false; + const menu = new Menu({ + id: "menu-popup", + }); + const submenu = new Menu({ + id: "submenu-popup", + }); + submenu.append( + new MenuItem({ + label: "Submenu item", + click: () => { + info("Click callback has fired for submenu item"); + clickFired = true; + }, + }) + ); + menu.append( + new MenuItem({ + l10nID: "text-action-copy", + submenu, + }) + ); + menu.append( + new MenuItem({ + label: "Submenu parent with attributes", + id: "submenu-parent-with-attrs", + submenu, + accesskey: "A", + disabled: true, + }) + ); + + menu.popup(0, 0, toolbox.doc); + const popup = toolbox.topDoc.querySelector("#menu-popup"); + ok(popup, "A popup is in the DOM"); + is( + toolbox.topDoc.querySelectorAll("#menu-popup > menuitem").length, + 0, + "No menuitem children" + ); + + const menus = toolbox.topDoc.querySelectorAll("#menu-popup > menu"); + is(menus.length, 2, "Correct number of menus"); + ok( + !menus[0].hasAttribute("label"), + "No label: should be set by localization" + ); + ok(!menus[0].hasAttribute("disabled"), "Correct disabled state"); + is( + menus[0].getAttribute("data-l10n-id"), + "text-action-copy", + "Correct localization attribute" + ); + + is(menus[1].getAttribute("accesskey"), "A", "Correct accesskey"); + ok(menus[1].hasAttribute("disabled"), "Correct disabled state"); + is(menus[1].id, "submenu-parent-with-attrs", "Correct id"); + + const subMenuItems = menus[0].querySelectorAll("menupopup > menuitem"); + is(subMenuItems.length, 1, "Correct number of submenu items"); + is(subMenuItems[0].getAttribute("label"), "Submenu item", "Correct label"); + + await once(menu, "open"); + const closed = once(menu, "close"); + + // The following section tests keyboard navigation of the context menus. + // This doesn't work on macOS when native context menus are enabled. + if (Services.prefs.getBoolPref("widget.macos.native-context-menus", false)) { + info("Using openMenu semantics because of macOS native context menus."); + let shown = once(menus[0], "popupshown"); + menus[0].openMenu(true); + await shown; + + const hidden = once(menus[0], "popuphidden"); + menus[0].openMenu(false); + await hidden; + + shown = once(menus[0], "popupshown"); + menus[0].openMenu(true); + await shown; + } else { + info("Using keyboard navigation to open, close, and reopen the submenu"); + let shown = once(menus[0], "popupshown"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowRight"); + await shown; + + const hidden = once(menus[0], "popuphidden"); + EventUtils.synthesizeKey("KEY_ArrowLeft"); + await hidden; + + shown = once(menus[0], "popupshown"); + EventUtils.synthesizeKey("KEY_ArrowRight"); + await shown; + } + + info("Clicking the submenu item"); + const subMenu = subMenuItems[0].closest("menupopup"); + subMenu.activateItem(subMenuItems[0]); + + await closed; + ok(clickFired, "Click has fired"); +} diff --git a/devtools/client/framework/test/browser_new_activation_workflow.js b/devtools/client/framework/test/browser_new_activation_workflow.js new file mode 100644 index 0000000000..583e6d7ca8 --- /dev/null +++ b/devtools/client/framework/test/browser_new_activation_workflow.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests devtools API + +var toolbox; + +function test() { + addTab("about:blank").then(async function () { + loadWebConsole().then(function () { + console.log("loaded"); + }); + }); +} + +function loadWebConsole() { + ok(gDevTools, "gDevTools exists"); + const tab = gBrowser.selectedTab; + return gDevTools + .showToolboxForTab(tab, { toolId: "webconsole" }) + .then(function (aToolbox) { + toolbox = aToolbox; + checkToolLoading(); + }); +} + +function checkToolLoading() { + is(toolbox.currentToolId, "webconsole", "The web console is selected"); + ok(toolbox.isReady, "toolbox is ready"); + + selectAndCheckById("jsdebugger").then(function () { + selectAndCheckById("styleeditor").then(function () { + testToggle(); + }); + }); +} + +function selectAndCheckById(id) { + return toolbox.selectTool(id).then(function () { + const tab = toolbox.doc.getElementById("toolbox-tab-" + id); + is( + tab.classList.contains("selected"), + true, + "The " + id + " tab is selected" + ); + is( + tab.getAttribute("aria-pressed"), + "true", + "The " + id + " tab is pressed" + ); + }); +} + +function testToggle() { + toolbox.once("destroyed", async () => { + // Cannot reuse a target after it's destroyed. + gDevTools + .showToolboxForTab(gBrowser.selectedTab, { toolId: "styleeditor" }) + .then(function (aToolbox) { + toolbox = aToolbox; + is( + toolbox.currentToolId, + "styleeditor", + "The style editor is selected" + ); + finishUp(); + }); + }); + + toolbox.destroy(); +} + +function finishUp() { + toolbox.destroy().then(function () { + toolbox = null; + gBrowser.removeCurrentTab(); + finish(); + }); +} diff --git a/devtools/client/framework/test/browser_source_map-01.js b/devtools/client/framework/test/browser_source_map-01.js new file mode 100644 index 0000000000..373eaebf77 --- /dev/null +++ b/devtools/client/framework/test/browser_source_map-01.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests the SourceMapService updates generated sources when source maps + * are subsequently found. Also checks when no column is provided, and + * when tagging an already source mapped location initially. + */ + +// There are shutdown issues for which multiple rejections are left uncaught. +// See bug 1018184 for resolving these issues. +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally(/this\.worker is null/); +PromiseTestUtils.allowMatchingRejectionsGlobally(/Component not initialized/); + +// Empty page +const PAGE_URL = `${URL_ROOT_SSL}doc_empty-tab-01.html`; +const JS_URL = `${URL_ROOT_SSL}code_binary_search.js`; +const COFFEE_URL = `${URL_ROOT_SSL}code_binary_search.coffee`; + +add_task(async function () { + const toolbox = await openNewTabAndToolbox(PAGE_URL, "jsdebugger"); + const service = toolbox.sourceMapURLService; + + // Inject JS script + const sourceSeen = waitForSourceLoad(toolbox, JS_URL); + await createScript(JS_URL); + await sourceSeen; + + const loc1 = { url: JS_URL, line: 6 }; + const newLoc1 = await new Promise(r => + service.subscribeByURL(loc1.url, loc1.line, 4, r) + ); + checkLoc1(loc1, newLoc1); + + const loc2 = { url: JS_URL, line: 8, column: 3 }; + const newLoc2 = await new Promise(r => + service.subscribeByURL(loc2.url, loc2.line, loc2.column, r) + ); + checkLoc2(loc2, newLoc2); + + await toolbox.destroy(); + gBrowser.removeCurrentTab(); + finish(); +}); + +function checkLoc1(oldLoc, newLoc) { + is(oldLoc.line, 6, "Correct line for JS:6"); + is(oldLoc.column, undefined, "Correct column for JS:6"); + is(oldLoc.url, JS_URL, "Correct url for JS:6"); + is(newLoc.line, 4, "Correct line for JS:6 -> COFFEE"); + is( + newLoc.column, + 2, + "Correct column for JS:6 -> COFFEE -- handles falsy column entries" + ); + is(newLoc.url, COFFEE_URL, "Correct url for JS:6 -> COFFEE"); +} + +function checkLoc2(oldLoc, newLoc) { + is(oldLoc.line, 8, "Correct line for JS:8:3"); + is(oldLoc.column, 3, "Correct column for JS:8:3"); + is(oldLoc.url, JS_URL, "Correct url for JS:8:3"); + is(newLoc.line, 6, "Correct line for JS:8:3 -> COFFEE"); + is(newLoc.column, 10, "Correct column for JS:8:3 -> COFFEE"); + is(newLoc.url, COFFEE_URL, "Correct url for JS:8:3 -> COFFEE"); +} diff --git a/devtools/client/framework/test/browser_source_map-absolute.js b/devtools/client/framework/test/browser_source_map-absolute.js new file mode 100644 index 0000000000..206cbde944 --- /dev/null +++ b/devtools/client/framework/test/browser_source_map-absolute.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that an absolute sourceRoot works. + +"use strict"; + +// There are shutdown issues for which multiple rejections are left uncaught. +// See bug 1018184 for resolving these issues. +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally(/this\.worker is null/); + +// Empty page +const PAGE_URL = `${URL_ROOT_SSL}doc_empty-tab-01.html`; +const JS_URL = `${URL_ROOT_SSL}code_binary_search_absolute.js`; +const ORIGINAL_URL = `${URL_ROOT_SSL}code_binary_search.coffee`; + +add_task(async function () { + const toolbox = await openNewTabAndToolbox(PAGE_URL, "jsdebugger"); + const service = toolbox.sourceMapURLService; + + // Inject JS script + const sourceSeen = waitForSourceLoad(toolbox, JS_URL); + await createScript(JS_URL); + await sourceSeen; + + info(`checking original location for ${JS_URL}:6`); + const newLoc = await new Promise(r => + service.subscribeByURL(JS_URL, 6, 4, r) + ); + + is(newLoc.url, ORIGINAL_URL, "check mapped URL"); + is(newLoc.line, 4, "check mapped line number"); +}); diff --git a/devtools/client/framework/test/browser_source_map-cross-domain.js b/devtools/client/framework/test/browser_source_map-cross-domain.js new file mode 100644 index 0000000000..77fb381260 --- /dev/null +++ b/devtools/client/framework/test/browser_source_map-cross-domain.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the source map service can fetch a source map from a +// different domain. + +"use strict"; + +const JS_URL = URL_ROOT + "code_bundle_cross_domain.js"; + +const PAGE_URL = `data:text/html, + + + + + + Empty test page to test cross domain source map + + + + + + +`; + +const ORIGINAL_URL = "webpack:///code_cross_domain.js"; + +const GENERATED_LINE = 82; +const ORIGINAL_LINE = 12; + +add_task(async function () { + const toolbox = await openNewTabAndToolbox(PAGE_URL, "webconsole"); + const service = toolbox.sourceMapURLService; + + info(`checking original location for ${JS_URL}:${GENERATED_LINE}`); + const newLoc = await new Promise(r => + service.subscribeByURL(JS_URL, GENERATED_LINE, undefined, r) + ); + is(newLoc.url, ORIGINAL_URL, "check mapped URL"); + is(newLoc.line, ORIGINAL_LINE, "check mapped line number"); +}); diff --git a/devtools/client/framework/test/browser_source_map-init.js b/devtools/client/framework/test/browser_source_map-init.js new file mode 100644 index 0000000000..60a3e4672a --- /dev/null +++ b/devtools/client/framework/test/browser_source_map-init.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the source map service initializes properly when source +// actors have already been created. Regression test for bug 1391768. + +"use strict"; + +const JS_URL = URL_ROOT_SSL + "code_bundle_no_race.js"; + +const PAGE_URL = `data:text/html, + + + + + + Empty test page to test race case + + + + + + +`; + +const ORIGINAL_URL = "webpack:///code_no_race.js"; + +const GENERATED_LINE = 84; +const ORIGINAL_LINE = 11; + +add_task(async function () { + // Opening the debugger causes the source actors to be created. + const toolbox = await openNewTabAndToolbox(PAGE_URL, "jsdebugger"); + // In bug 1391768, when the sourceMapURLService was created, it was + // ignoring any source actors that already existed, leading to + // source-mapping failures for those. + const service = toolbox.sourceMapURLService; + + info(`checking original location for ${JS_URL}:${GENERATED_LINE}`); + const newLoc = await new Promise(r => + service.subscribeByURL(JS_URL, GENERATED_LINE, undefined, r) + ); + is(newLoc.url, ORIGINAL_URL, "check mapped URL"); + is(newLoc.line, ORIGINAL_LINE, "check mapped line number"); + + // See Bug 1637793 and Bug 1621337. + // Ideally the debugger should only resolve when the worker targets have been + // retrieved, which should be fixed by Bug 1621337 or a followup. + info("Wait for all pending requests to settle on the DevToolsClient"); + await toolbox.commands.client.waitForRequestsToSettle(); +}); diff --git a/devtools/client/framework/test/browser_source_map-inline.js b/devtools/client/framework/test/browser_source_map-inline.js new file mode 100644 index 0000000000..4e5f8c7fff --- /dev/null +++ b/devtools/client/framework/test/browser_source_map-inline.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that inline source maps work. + +"use strict"; + +// There are shutdown issues for which multiple rejections are left uncaught. +// See bug 1018184 for resolving these issues. +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally(/this\.worker is null/); +PromiseTestUtils.allowMatchingRejectionsGlobally(/Component not initialized/); + +const TEST_ROOT = "https://example.com/browser/devtools/client/framework/test/"; +// Empty page +const PAGE_URL = `${TEST_ROOT}doc_empty-tab-01.html`; +const JS_URL = `${TEST_ROOT}code_inline_bundle.js`; +const ORIGINAL_URL = "webpack:///code_inline_original.js"; + +add_task(async function () { + const toolbox = await openNewTabAndToolbox(PAGE_URL, "jsdebugger"); + const service = toolbox.sourceMapURLService; + + // Inject JS script + const sourceSeen = waitForSourceLoad(toolbox, JS_URL); + await createScript(JS_URL); + await sourceSeen; + + info(`checking original location for ${JS_URL}:84`); + const newLoc = await new Promise(r => + service.subscribeByURL(JS_URL, 84, undefined, r) + ); + + is(newLoc.url, ORIGINAL_URL, "check mapped URL"); + is(newLoc.line, 11, "check mapped line number"); + + await toolbox.destroy(); + gBrowser.removeCurrentTab(); + finish(); +}); diff --git a/devtools/client/framework/test/browser_source_map-late-script.js b/devtools/client/framework/test/browser_source_map-late-script.js new file mode 100644 index 0000000000..f11d530db1 --- /dev/null +++ b/devtools/client/framework/test/browser_source_map-late-script.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that you can subscribe to notifications on a source before it has loaded. + +"use strict"; + +const PAGE_URL = `${URL_ROOT_SSL}doc_empty-tab-01.html`; +const JS_URL = URL_ROOT_SSL + "code_bundle_late_script.js"; + +const ORIGINAL_URL = "webpack:///code_late_script.js"; + +const GENERATED_LINE = 107; +const ORIGINAL_LINE = 11; + +add_task(async function () { + // Start with the empty page, then navigate, so that we can properly + // listen for new sources arriving. + const toolbox = await openNewTabAndToolbox(PAGE_URL, "webconsole"); + const service = toolbox.sourceMapURLService; + + const scriptMapped = new Promise(resolve => { + let count = 0; + service.subscribeByURL( + JS_URL, + GENERATED_LINE, + undefined, + originalLocation => { + if (count === 0) { + resolve(originalLocation); + } + count += 1; + + return () => {}; + } + ); + }); + + // Inject JS script + const sourceSeen = waitForSourceLoad(toolbox, JS_URL); + await createScript(JS_URL); + await sourceSeen; + + // Ensure that the URL service fired an event about the location loading. + const { url, line } = await scriptMapped; + is(url, ORIGINAL_URL, "check mapped URL"); + is(line, ORIGINAL_LINE, "check mapped line number"); + + await toolbox.destroy(); + gBrowser.removeCurrentTab(); + finish(); +}); diff --git a/devtools/client/framework/test/browser_source_map-no-race.js b/devtools/client/framework/test/browser_source_map-no-race.js new file mode 100644 index 0000000000..23751f7bc8 --- /dev/null +++ b/devtools/client/framework/test/browser_source_map-no-race.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the source map service doesn't race against source +// reporting. + +"use strict"; + +const JS_URL = URL_ROOT + "code_bundle_no_race.js"; + +const PAGE_URL = `data:text/html, + + + + + + Empty test page to test race case + + + + + + +`; + +const ORIGINAL_URL = "webpack:///code_no_race.js"; + +const GENERATED_LINE = 84; +const ORIGINAL_LINE = 11; + +add_task(async function () { + // Start with the empty page, then navigate, so that we can properly + // listen for new sources arriving. + const toolbox = await openNewTabAndToolbox(PAGE_URL, "webconsole"); + const service = toolbox.sourceMapURLService; + + info(`checking original location for ${JS_URL}:${GENERATED_LINE}`); + const newLoc = await new Promise(r => + service.subscribeByURL(JS_URL, GENERATED_LINE, undefined, r) + ); + is(newLoc.url, ORIGINAL_URL, "check mapped URL"); + is(newLoc.line, ORIGINAL_LINE, "check mapped line number"); +}); diff --git a/devtools/client/framework/test/browser_source_map-pub-sub.js b/devtools/client/framework/test/browser_source_map-pub-sub.js new file mode 100644 index 0000000000..c7f69c91c7 --- /dev/null +++ b/devtools/client/framework/test/browser_source_map-pub-sub.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the source map service subscribe mechanism work as expected. + +"use strict"; + +const JS_URL = URL_ROOT + "code_bundle_no_race.js"; + +const PAGE_URL = `data:text/html, + + + + + + + + +`; + +const ORIGINAL_URL = "webpack:///code_no_race.js"; + +const SOURCE_MAP_PREF = "devtools.source-map.client-service.enabled"; + +const GENERATED_LINE = 84; +const ORIGINAL_LINE = 11; + +add_task(async function () { + // Push a pref env so any changes will be reset at the end of the test. + await SpecialPowers.pushPrefEnv({}); + + // Opening the debugger causes the source actors to be created. + const toolbox = await openNewTabAndToolbox(PAGE_URL, "jsdebugger"); + const service = toolbox.sourceMapURLService; + + const cbCalls = []; + const cb = originalLocation => cbCalls.push(originalLocation); + const expectedArg = { url: ORIGINAL_URL, line: ORIGINAL_LINE, column: 0 }; + + // Wait for the sources to fully populate so that waitForSubscriptionsToSettle + // can be guaranteed that all actions have been queued. + await service._ensureAllSourcesPopulated(); + + const unsubscribe1 = service.subscribeByURL(JS_URL, GENERATED_LINE, 1, cb); + + // Wait for the query to finish and populate so that all of the later + // logic with this position will run synchronously, and the subscribe has run. + for (const map of service._mapsById.values()) { + for (const query of map.queries.values()) { + await query.action; + } + } + + is( + cbCalls.length, + 1, + "The callback function is called directly when subscribing" + ); + Assert.deepEqual( + cbCalls[0], + expectedArg, + "callback called with expected arguments" + ); + + const unsubscribe2 = service.subscribeByURL(JS_URL, GENERATED_LINE, 1, cb); + is(cbCalls.length, 2, "Subscribing to the same location twice works"); + Assert.deepEqual( + cbCalls[1], + expectedArg, + "callback called with expected arguments" + ); + + info("Manually call the dispatcher to ensure subscribers are called"); + Services.prefs.setBoolPref(SOURCE_MAP_PREF, false); + is(cbCalls.length, 4, "both subscribers were called"); + Assert.deepEqual(cbCalls[2], null, "callback called with expected arguments"); + Assert.deepEqual( + cbCalls[2], + cbCalls[3], + "callbacks were passed the same arguments" + ); + + info("Check unsubscribe functions"); + unsubscribe1(); + Services.prefs.setBoolPref(SOURCE_MAP_PREF, true); + is(cbCalls.length, 5, "Only remainer subscriber callback was called"); + Assert.deepEqual( + cbCalls[4], + expectedArg, + "callback called with expected arguments" + ); + + unsubscribe2(); + Services.prefs.setBoolPref(SOURCE_MAP_PREF, false); + Services.prefs.setBoolPref(SOURCE_MAP_PREF, true); + is(cbCalls.length, 5, "No callbacks were called"); +}); diff --git a/devtools/client/framework/test/browser_source_map-reload.js b/devtools/client/framework/test/browser_source_map-reload.js new file mode 100644 index 0000000000..13902062a7 --- /dev/null +++ b/devtools/client/framework/test/browser_source_map-reload.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that reloading re-reads the source maps. + +"use strict"; +const INITIAL_URL = + "data:text/html,html>Empty test page 1"; +const ORIGINAL_URL_1 = "webpack://code-reload/v1/code_reload_1.js"; +const ORIGINAL_URL_2 = "webpack://code-reload/v2/code_reload_2.js"; + +const GENERATED_LINE = 13; +const ORIGINAL_LINE = 7; + +const testServer = createVersionizedHttpTestServer("reload"); + +const PAGE_URL = testServer.urlFor("doc_reload.html"); +const JS_URL = testServer.urlFor("code_bundle_reload.js"); + +add_task(async function () { + // Start with the empty page, then navigate, so that we can properly + // listen for new sources arriving. + const toolbox = await openNewTabAndToolbox(INITIAL_URL, "webconsole"); + const service = toolbox.sourceMapURLService; + + let sourceSeen = waitForSourceLoad(toolbox, JS_URL); + await navigateTo(PAGE_URL); + await sourceSeen; + + info(`checking original location for ${JS_URL}:${GENERATED_LINE}`); + let newLoc = await new Promise(r => + service.subscribeByURL(JS_URL, GENERATED_LINE, undefined, r) + ); + + is(newLoc.url, ORIGINAL_URL_1, "check mapped URL"); + is(newLoc.line, ORIGINAL_LINE, "check mapped line number"); + + testServer.switchToNextVersion(); + + // Reload the page. A different source file will be loaded. + sourceSeen = waitForSourceLoad(toolbox, JS_URL); + await reloadBrowser(); + await sourceSeen; + + info( + `checking post-reload original location for ${JS_URL}:${GENERATED_LINE}` + ); + newLoc = await new Promise(r => + service.subscribeByURL(JS_URL, GENERATED_LINE, undefined, r) + ); + is(newLoc.url, ORIGINAL_URL_2, "check post-reload mapped URL"); + is(newLoc.line, ORIGINAL_LINE, "check post-reload mapped line number"); + + await toolbox.destroy(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/client/framework/test/browser_tab_commands_factory.js b/devtools/client/framework/test/browser_tab_commands_factory.js new file mode 100644 index 0000000000..d8f2f44ca8 --- /dev/null +++ b/devtools/client/framework/test/browser_tab_commands_factory.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test LocalTabCommandsFactory + +const { + LocalTabCommandsFactory, +} = require("resource://devtools/client/framework/local-tab-commands-factory.js"); + +add_task(async function () { + await testTabDescriptorWithURL("data:text/html;charset=utf-8,foo"); + + // Bug 1699497: Also test against a page in the parent process + // which can hit some race with frame-connector's frame scripts. + await testTabDescriptorWithURL("about:robots"); +}); + +async function testTabDescriptorWithURL(url) { + info(`Test TabDescriptor against url ${url}\n`); + const tab = await addTab(url); + + const commands = await LocalTabCommandsFactory.createCommandsForTab(tab); + is( + commands.descriptorFront.localTab, + tab, + "TabDescriptor's localTab is set correctly" + ); + + info( + "Calling a second time createCommandsForTab with the same tab, will return the same commands" + ); + const secondCommands = await LocalTabCommandsFactory.createCommandsForTab( + tab + ); + is(commands, secondCommands, "second commands is the same"); + + // We have to involve TargetCommand in order to have a function TabDescriptor.getTarget. + await commands.targetCommand.startListening(); + + info("Wait for descriptor's target"); + const target = await commands.descriptorFront.getTarget(); + + info("Call any method to ensure that each target works"); + await target.logInPage("foo"); + + info("Destroy the command"); + await commands.destroy(); + + gBrowser.removeCurrentTab(); +} diff --git a/devtools/client/framework/test/browser_tab_descriptor_fission.js b/devtools/client/framework/test/browser_tab_descriptor_fission.js new file mode 100644 index 0000000000..bf15f01293 --- /dev/null +++ b/devtools/client/framework/test/browser_tab_descriptor_fission.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that tab descriptor survives after the page navigates and changes + * process. + */ + +const EXAMPLE_COM_URI = + "https://example.com/document-builder.sjs?html=
com"; +const EXAMPLE_ORG_URI = + "https://example.org/document-builder.sjs?html=
org"; + +add_task(async function () { + const tab = await addTab(EXAMPLE_COM_URI); + const toolbox = await gDevTools.showToolboxForTab(tab); + const target = toolbox.target; + const client = toolbox.commands.client; + + info("Retrieve the initial list of tab descriptors"); + const tabDescriptors = await client.mainRoot.listTabs(); + const tabDescriptor = tabDescriptors.find( + d => decodeURIComponent(d.url) === EXAMPLE_COM_URI + ); + ok(tabDescriptor, "Should have a descriptor actor for the tab"); + + info("Retrieve the target corresponding to the TabDescriptor"); + const comTabTarget = await tabDescriptor.getTarget(); + is( + target, + comTabTarget, + "The toolbox target is also the target associated with the tab descriptor" + ); + + await navigateTo(EXAMPLE_ORG_URI); + + info("Call list tabs again to update the tab descriptor forms"); + await client.mainRoot.listTabs(); + + is( + decodeURIComponent(tabDescriptor.url), + EXAMPLE_ORG_URI, + "The existing descriptor now points to the new URI" + ); + + const newTarget = toolbox.target; + + is( + comTabTarget.actorID, + null, + "With Fission or server side target switching, example.com target front is destroyed" + ); + Assert.notEqual( + comTabTarget, + newTarget, + "With Fission or server side target switching, a new target was created for example.org" + ); + + const onDescriptorDestroyed = tabDescriptor.once("descriptor-destroyed"); + + await removeTab(tab); + + info("Wait for descriptor destroyed event"); + await onDescriptorDestroyed; + ok(tabDescriptor.isDestroyed(), "the descriptor front is really destroyed"); +}); diff --git a/devtools/client/framework/test/browser_target_cached-front.js b/devtools/client/framework/test/browser_target_cached-front.js new file mode 100644 index 0000000000..43156cbded --- /dev/null +++ b/devtools/client/framework/test/browser_target_cached-front.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + const target = await createAndAttachTargetForTab(gBrowser.selectedTab); + + info("Cached front when getFront has not been called"); + let getCachedFront = target.getCachedFront("accessibility"); + ok(!getCachedFront, "no front exists"); + + info("Cached front when getFront has been called but has not finished"); + const asyncFront = target.getFront("accessibility"); + getCachedFront = target.getCachedFront("accessibility"); + ok(!getCachedFront, "no front exists"); + + info("Cached front when getFront has been called and has finished"); + const front = await asyncFront; + getCachedFront = target.getCachedFront("accessibility"); + is(getCachedFront, front, "front is the same as async front"); +}); diff --git a/devtools/client/framework/test/browser_target_cached-resource.js b/devtools/client/framework/test/browser_target_cached-resource.js new file mode 100644 index 0000000000..b6f53fdee0 --- /dev/null +++ b/devtools/client/framework/test/browser_target_cached-resource.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// The target front holds resources that happend before ResourceCommand addeed listeners. +// Test whether that feature works correctly or not. +const TEST_URI = + "https://example.com/browser/devtools/client/framework/test/doc_cached-resource.html"; +const PARENT_MESSAGE = "Hello from parent"; +const CHILD_MESSAGE = "Hello from child"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/shared-head.js", + this +); + +add_task(async function () { + info("Open console"); + const tab = await addTab(TEST_URI); + const toolbox = await openToolboxForTab(tab, "webconsole"); + const hud = toolbox.getCurrentPanel().hud; + + info("Check the initial messages"); + ok( + findMessageByType(hud, PARENT_MESSAGE, ".console-api"), + "Message from parent document is in console" + ); + ok( + findMessageByType(hud, CHILD_MESSAGE, ".console-api"), + "Message from child document is in console" + ); + + info("Clear the messages"); + hud.ui.window.document.querySelector(".devtools-clear-icon").click(); + await waitUntil( + () => !findMessageByType(hud, PARENT_MESSAGE, ".console-api") + ); + + info("Reload the browsing page"); + await navigateTo(TEST_URI); + + info("Check the messages after reloading"); + await waitUntil( + () => + findMessageByType(hud, PARENT_MESSAGE, ".console-api") && + findMessageByType(hud, CHILD_MESSAGE, ".console-api") + ); + ok(true, "All messages are shown correctly"); +}); diff --git a/devtools/client/framework/test/browser_target_get-front.js b/devtools/client/framework/test/browser_target_get-front.js new file mode 100644 index 0000000000..9dac79d196 --- /dev/null +++ b/devtools/client/framework/test/browser_target_get-front.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + const tab = await addTab("about:blank"); + const target = await createAndAttachTargetForTab(tab); + + const tab2 = await addTab("about:blank"); + const target2 = await createAndAttachTargetForTab(tab2); + + info("Test the targetFront attribute for the root"); + const { client } = target; + is( + client.mainRoot.targetFront, + null, + "got null from the targetFront attribute for the root" + ); + is( + client.mainRoot.parentFront, + null, + "got null from the parentFront attribute for the root" + ); + + info("Test getting a front twice"); + const getAccessibilityFront = await target.getFront("accessibility"); + const getAccessibilityFront2 = await target.getFront("accessibility"); + is( + getAccessibilityFront, + getAccessibilityFront2, + "got the same front when calling getFront twice" + ); + is( + getAccessibilityFront.targetFront, + target, + "got the correct targetFront attribute from the front" + ); + is( + getAccessibilityFront2.targetFront, + target, + "got the correct targetFront attribute from the front" + ); + is( + getAccessibilityFront.parentFront, + target, + "got the correct parentFront attribute from the front" + ); + is( + getAccessibilityFront2.parentFront, + target, + "got the correct parentFront attribute from the front" + ); + + info("Test getting a front on different targets"); + const target1Front = await target.getFront("accessibility"); + const target2Front = await target2.getFront("accessibility"); + is( + target1Front !== target2Front, + true, + "got different fronts when calling getFront on different targets" + ); + is( + target1Front.targetFront !== target2Front.targetFront, + true, + "got different targetFront from different fronts from different targets" + ); + is( + target2Front.targetFront, + target2, + "got the correct targetFront attribute from the front" + ); + + info("Test async front retrieval"); + // use two fronts that are initialized one after the other. + const asyncFront1 = target.getFront("accessibility"); + const asyncFront2 = target.getFront("accessibility"); + + info("waiting on async fronts returns a real front"); + const awaitedAsyncFront1 = await asyncFront1; + const awaitedAsyncFront2 = await asyncFront2; + is( + awaitedAsyncFront1, + awaitedAsyncFront2, + "got the same front when requesting the front first async then sync" + ); + await target.destroy(); + await target2.destroy(); + + info("destroying a front immediately is possible"); + await testDestroy(); +}); + +async function testDestroy() { + // initialize a clean target + const tab = await addTab("about:blank"); + const target = await createAndAttachTargetForTab(tab); + + // do not wait for the front to finish loading + target.getFront("accessibility"); + + try { + await target.destroy(); + ok( + true, + "calling destroy on an async front instantiated with getFront does not throw" + ); + } catch (e) { + ok( + false, + "calling destroy on an async front instantiated with getFront does not throw" + ); + } +} diff --git a/devtools/client/framework/test/browser_target_listeners.js b/devtools/client/framework/test/browser_target_listeners.js new file mode 100644 index 0000000000..942ac3bed1 --- /dev/null +++ b/devtools/client/framework/test/browser_target_listeners.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + const target = await createAndAttachTargetForTab(gBrowser.selectedTab); + + info("Test applying watchFronts to a front that will be created"); + const promise = new Promise(resolve => { + target.watchFronts("accessibility", resolve); + }); + const getFrontFront = await target.getFront("accessibility"); + const watchFrontsFront = await promise; + is( + getFrontFront, + watchFrontsFront, + "got the front instantiated in the future and it's the same" + ); + + info("Test applying watchFronts to an existing front"); + await new Promise(resolve => { + target.watchFronts("accessibility", front => { + is( + front, + getFrontFront, + "got the already instantiated front and it's the same" + ); + resolve(); + }); + }); +}); diff --git a/devtools/client/framework/test/browser_target_loading.js b/devtools/client/framework/test/browser_target_loading.js new file mode 100644 index 0000000000..fe579bdaa9 --- /dev/null +++ b/devtools/client/framework/test/browser_target_loading.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that toolbox can be opened right after a tab is added, while the document +// is still loading. + +add_task(async function testOpenToolboxOnLoadingDocument() { + const TEST_URI = + `https://example.com/document-builder.sjs?` + + `html=Test`; + + // ⚠️ Note that we don't await for `addTab` here, as we want to open the toolbox just + // after the tab is addded, with the document still loading. + info("Add tab…"); + const onTabAdded = addTab(TEST_URI); + const tab = gBrowser.selectedTab; + info("…and open the toolbox right away"); + const onToolboxShown = gDevTools.showToolboxForTab(tab, { + toolId: "webconsole", + }); + + await onTabAdded; + ok(true, "The tab as done loading"); + + const toolbox = await onToolboxShown; + ok(true, "The toolbox is shown"); + + info("Check that the console opened and has the message from the page"); + const { hud } = toolbox.getPanel("webconsole"); + await waitFor(() => + Array.from(hud.ui.window.document.querySelectorAll(".message-body")).some( + el => el.innerText.includes("page loaded") + ) + ); + ok(true, "The console opened with the expected content"); +}); diff --git a/devtools/client/framework/test/browser_target_parents.js b/devtools/client/framework/test/browser_target_parents.js new file mode 100644 index 0000000000..795219abef --- /dev/null +++ b/devtools/client/framework/test/browser_target_parents.js @@ -0,0 +1,189 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test a given Target's parentFront attribute returns the correct parent front. + +const { + DevToolsClient, +} = require("resource://devtools/client/devtools-client.js"); +const { + DevToolsServer, +} = require("resource://devtools/server/devtools-server.js"); +const { + createCommandsDictionary, +} = require("resource://devtools/shared/commands/index.js"); + +const TEST_URL = `data:text/html;charset=utf-8,
`; + +// Test against Tab targets +add_task(async function () { + const tab = await addTab(TEST_URL); + + const client = await setupDebuggerClient(); + const mainRoot = client.mainRoot; + + const tabDescriptors = await mainRoot.listTabs(); + + const concurrentCommands = []; + for (const descriptor of tabDescriptors) { + concurrentCommands.push( + (async () => { + const commands = await createCommandsDictionary(descriptor); + // Descriptor's getTarget will only work if the TargetCommand watches for the first top target + await commands.targetCommand.startListening(); + })() + ); + } + info("Instantiate all tab's commands and initialize their TargetCommand"); + await Promise.all(concurrentCommands); + + await testGetTargetWithConcurrentCalls(tabDescriptors, tabTarget => { + // We only call BrowsingContextTargetFront.attach and not TargetMixin.attachAndInitThread. + // So very few things are done. + return !!tabTarget.targetForm?.traits; + }); + + await client.close(); + await removeTab(tab); +}); + +// Test against Process targets +add_task(async function () { + const client = await setupDebuggerClient(); + const mainRoot = client.mainRoot; + + const processes = await mainRoot.listProcesses(); + + // Assert that concurrent calls to getTarget resolves the same target and that it is already attached + // With that, we were chasing a precise race, where a second call to ProcessDescriptor.getTarget() + // happens between the instantiation of ContentProcessTarget and its call to attach() from getTarget + // function. + await testGetTargetWithConcurrentCalls(processes, processTarget => { + // We only call ContentProcessTargetFront.attach and not TargetMixin.attachAndInitThread. + // So nothing is done for content process targets. + return true; + }); + + await client.close(); +}); + +// Test against Webextension targets +add_task(async function () { + const client = await setupDebuggerClient(); + + const mainRoot = client.mainRoot; + + const addons = await mainRoot.listAddons(); + await Promise.all( + // some extensions, such as themes, are not debuggable. Filter those out + // before trying to connect. + addons + .filter(a => a.debuggable) + .map(async addonDescriptorFront => { + const addonFront = await addonDescriptorFront.getTarget(); + ok(addonFront, "Got the addon target"); + }) + ); + + await client.close(); +}); + +// Test against worker targets on parent process +add_task(async function () { + const client = await setupDebuggerClient(); + + const mainRoot = client.mainRoot; + + const { workers } = await mainRoot.listWorkers(); + + ok(!!workers.length, "list workers returned a non-empty list of workers"); + + for (const workerDescriptorFront of workers) { + let targetFront; + try { + targetFront = await workerDescriptorFront.getTarget(); + } catch (e) { + // Ignore race condition where we are trying to connect to a worker + // related to a previous test which is being destroyed. + if ( + e.message.includes("nsIWorkerDebugger.initialize") || + workerDescriptorFront.isDestroyed() || + !workerDescriptorFront.name + ) { + info("Failed to connect to " + workerDescriptorFront.url); + continue; + } + throw e; + } + // Bug 1767760: name might be null on some worker which are probably initializing or destroying. + if (!workerDescriptorFront.name) { + info("Failed to connect to " + workerDescriptorFront.url); + continue; + } + + is( + workerDescriptorFront, + targetFront, + "For now, worker descriptors and targets are the same object (see bug 1667404)" + ); + // Check that accessing descriptor#name getter doesn't throw (See Bug 1714974). + ok( + workerDescriptorFront.name.includes(".js") || + workerDescriptorFront.name.includes(".mjs"), + `worker descriptor front holds the worker file name (${workerDescriptorFront.name})` + ); + is( + workerDescriptorFront.isWorkerDescriptor, + true, + "isWorkerDescriptor is true" + ); + } + + await client.close(); +}); + +async function setupDebuggerClient() { + // Instantiate a minimal server + DevToolsServer.init(); + DevToolsServer.allowChromeProcess = true; + if (!DevToolsServer.createRootActor) { + DevToolsServer.registerAllActors(); + } + const transport = DevToolsServer.connectPipe(); + const client = new DevToolsClient(transport); + await client.connect(); + return client; +} + +async function testGetTargetWithConcurrentCalls(descriptors, isTargetAttached) { + // Assert that concurrent calls to getTarget resolves the same target and that it is already attached + await Promise.all( + descriptors.map(async descriptor => { + const promises = []; + const concurrentCalls = 10; + for (let i = 0; i < concurrentCalls; i++) { + const targetPromise = descriptor.getTarget(); + // Every odd runs, wait for a tick to introduce some more randomness + if (i % 2 == 0) { + await wait(0); + } + promises.push( + targetPromise.then(target => { + ok(isTargetAttached(target), "The target is attached"); + return target; + }) + ); + } + const targets = await Promise.all(promises); + for (let i = 1; i < concurrentCalls; i++) { + is( + targets[0], + targets[i], + "All the targets returned by concurrent calls to getTarget are the same" + ); + } + }) + ); +} diff --git a/devtools/client/framework/test/browser_target_remote.js b/devtools/client/framework/test/browser_target_remote.js new file mode 100644 index 0000000000..272797d626 --- /dev/null +++ b/devtools/client/framework/test/browser_target_remote.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Ensure target is closed if client is closed directly +function test() { + waitForExplicitFinish(); + + getParentProcessActors((client, target) => { + target.on("target-destroyed", () => { + ok(true, "Target was destroyed"); + finish(); + }); + client.close(); + }); +} diff --git a/devtools/client/framework/test/browser_target_server_compartment.js b/devtools/client/framework/test/browser_target_server_compartment.js new file mode 100644 index 0000000000..c0bd8e56f0 --- /dev/null +++ b/devtools/client/framework/test/browser_target_server_compartment.js @@ -0,0 +1,126 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Bug 1515290 - Ensure that DevToolsServer runs in its own compartment when debugging +// chrome context. If not, Debugger API's addGlobal will throw when trying to attach +// to chrome scripts as debugger actor's module and the chrome script will be in the same +// compartment. Debugger and debuggee can't be running in the same compartment. + +const CHROME_PAGE = + "chrome://mochitests/content/browser/devtools/client/framework/" + + "test/test_chrome_page.html"; + +add_task(async function () { + await testChromeTab(); + await testMainProcess(); +}); + +// Test that Tab Target can debug chrome pages +async function testChromeTab() { + const tab = await addTab(CHROME_PAGE); + const browser = tab.linkedBrowser; + ok(!browser.isRemoteBrowser, "chrome page is not remote"); + ok( + browser.contentWindow.document.nodePrincipal.isSystemPrincipal, + "chrome page is a privileged document" + ); + + const onThreadActorInstantiated = new Promise(resolve => { + const observe = function (subject, topic, data) { + if (topic === "devtools-thread-ready") { + Services.obs.removeObserver(observe, "devtools-thread-ready"); + const threadActor = subject.wrappedJSObject; + resolve(threadActor); + } + }; + Services.obs.addObserver(observe, "devtools-thread-ready"); + }); + + const commands = await CommandsFactory.forTab(tab); + await commands.targetCommand.startListening(); + + const sources = []; + await commands.resourceCommand.watchResources( + [commands.resourceCommand.TYPES.SOURCE], + { + onAvailable(resources) { + sources.push(...resources); + }, + } + ); + ok( + sources.find(s => s.url == CHROME_PAGE), + "The thread actor is able to attach to the chrome page and its sources" + ); + + const threadActor = await onThreadActorInstantiated; + const serverGlobal = Cu.getGlobalForObject(threadActor); + isnot( + loader.id, + serverGlobal.loader.id, + "The actors are loaded in a distinct loader in order for the actors to use its very own compartment" + ); + + const onDedicatedLoaderDestroy = new Promise(resolve => { + const observe = function (subject, topic, data) { + if (topic === "devtools:loader:destroy") { + Services.obs.removeObserver(observe, "devtools:loader:destroy"); + resolve(); + } + }; + Services.obs.addObserver(observe, "devtools:loader:destroy"); + }); + + await commands.destroy(); + + // Wait for the dedicated loader used for DevToolsServer to be destroyed + // in order to prevent leak reports on try + await onDedicatedLoaderDestroy; +} + +// Test that Main process Target can debug chrome scripts +async function testMainProcess() { + const onThreadActorInstantiated = new Promise(resolve => { + const observe = function (subject, topic, data) { + if (topic === "devtools-thread-ready") { + Services.obs.removeObserver(observe, "devtools-thread-ready"); + const threadActor = subject.wrappedJSObject; + resolve(threadActor); + } + }; + Services.obs.addObserver(observe, "devtools-thread-ready"); + }); + + const client = await CommandsFactory.spawnClientToDebugSystemPrincipal(); + const commands = await CommandsFactory.forMainProcess({ client }); + await commands.targetCommand.startListening(); + + const sources = []; + await commands.resourceCommand.watchResources( + [commands.resourceCommand.TYPES.SOURCE], + { + onAvailable(resources) { + sources.push(...resources); + }, + } + ); + ok( + sources.find( + s => s.url == "resource://devtools/client/framework/devtools.js" + ), + "The thread actor is able to attach to the chrome script, like client modules" + ); + + const threadActor = await onThreadActorInstantiated; + const serverGlobal = Cu.getGlobalForObject(threadActor); + isnot( + loader.id, + serverGlobal.loader.id, + "The actors are loaded in a distinct loader in order for the actors to use its very own compartment" + ); + + // As this target is remote (i.e. isn't a local tab) calling Target.destroy won't close + // the client. So do it manually here in order to ensure cleaning up the DevToolsServer + // spawn for this main process actor. + await commands.destroy(); +} diff --git a/devtools/client/framework/test/browser_target_support.js b/devtools/client/framework/test/browser_target_support.js new file mode 100644 index 0000000000..ff87b9fad4 --- /dev/null +++ b/devtools/client/framework/test/browser_target_support.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test support methods on Target, such as `hasActor` and `getTrait`. + +async function testTarget(client, target) { + is( + target.hasActor("inspector"), + true, + "target.hasActor() true when actor exists." + ); + is( + target.hasActor("notreal"), + false, + "target.hasActor() false when actor does not exist." + ); + + is( + target.getTrait("giddyup"), + undefined, + "target.getTrait() returns undefined when trait does not exist" + ); + + close(target, client); +} + +// Ensure target is closed if client is closed directly +function test() { + waitForExplicitFinish(); + + getParentProcessActors(testTarget); +} + +function close(target, client) { + target.on("target-destroyed", () => { + ok(true, "Target was destroyed"); + finish(); + }); + client.close(); +} diff --git a/devtools/client/framework/test/browser_toolbox_backward_forward_navigation.js b/devtools/client/framework/test/browser_toolbox_backward_forward_navigation.js new file mode 100644 index 0000000000..7350c2601c --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_backward_forward_navigation.js @@ -0,0 +1,188 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// The test can take a while to run +requestLongerTimeout(3); + +const FILENAME = "doc_backward_forward_navigation.html"; +const TEST_URI_ORG = `${URL_ROOT_ORG_SSL}${FILENAME}`; +const TEST_URI_COM = `${URL_ROOT_COM_SSL}${FILENAME}`; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/shared-head.js", + this +); + +add_task(async function testMultipleNavigations() { + // Disable bfcache for Fission for now. + // If Fission is disabled, the pref is no-op. + await SpecialPowers.pushPrefEnv({ + set: [["fission.bfcacheInParent", false]], + }); + + info( + "Test that DevTools works fine after multiple backward/forward navigations" + ); + // Don't show the third panel to limit the logs and activity. + await pushPref("devtools.inspector.three-pane-enabled", false); + await pushPref("devtools.inspector.activeSidebar", "ruleview"); + const DATA_URL = `data:text/html,`; + const tab = await addTab(DATA_URL); + + // Select the debugger so there will be more activity + const toolbox = await openToolboxForTab(tab, "jsdebugger"); + const inspector = await toolbox.selectTool("inspector"); + + info("Navigate to the ORG test page"); + // We don't use `navigateTo` as the page is adding stylesheets and js files which might + // delay the load event indefinitely (and we don't need for anything to be loaded, or + // ready, just to register the initial navigation so we can go back and forth between urls) + let onLocationChange = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_URI_ORG + ); + BrowserTestUtils.startLoadingURIString(gBrowser, TEST_URI_ORG); + await onLocationChange; + + info("And then navigate to a different origin"); + onLocationChange = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_URI_COM + ); + BrowserTestUtils.startLoadingURIString(gBrowser, TEST_URI_COM); + await onLocationChange; + + info( + "Navigate backward and forward multiple times between the two origins, with different delays" + ); + await navigateBackAndForth(TEST_URI_ORG, TEST_URI_COM); + + // Navigate one last time to a document with less activity so we don't have to deal + // with pending promises when we destroy the toolbox + const onInspectorReloaded = inspector.once("reloaded"); + info("Navigate to final document"); + await navigateTo(`${TEST_URI_ORG}?no-mutation`); + info("Waiting for inspector to reload…"); + await onInspectorReloaded; + info("-> inspector reloaded"); + await checkToolboxState(toolbox); +}); + +add_task(async function testSingleBackAndForthInstantNavigation() { + // Disable bfcache for Fission for now. + // If Fission is disabled, the pref is no-op. + await SpecialPowers.pushPrefEnv({ + set: [["fission.bfcacheInParent", false]], + }); + + info( + "Test that DevTools works fine after navigating backward and forward right after" + ); + + // Don't show the third panel to limit the logs and activity. + await pushPref("devtools.inspector.three-pane-enabled", false); + await pushPref("devtools.inspector.activeSidebar", "ruleview"); + const DATA_URL = `data:text/html,`; + const tab = await addTab(DATA_URL); + + // Select the debugger so there will be more activity + const toolbox = await openToolboxForTab(tab, "jsdebugger"); + const inspector = await toolbox.selectTool("inspector"); + + info("Navigate to a different origin"); + await navigateTo(TEST_URI_COM); + + info("Then navigate back, and forth immediatly"); + // We can't call goBack and right away goForward as goForward and even the call to navigateTo + // a bit later might be ignored. So we wait at least for the location to change. + await safelyGoBack(DATA_URL); + await safelyGoForward(TEST_URI_COM); + + // Navigate one last time to a document with less activity so we don't have to deal + // with pending promises when we destroy the toolbox + const onInspectorReloaded = inspector.once("reloaded"); + info("Navigate to final document"); + await navigateTo(`${TEST_URI_ORG}?no-mutation`); + info("Waiting for inspector to reload…"); + await onInspectorReloaded; + info("-> inspector reloaded"); + await checkToolboxState(toolbox); +}); + +async function checkToolboxState(toolbox) { + info("Check that the toolbox toolbar is still visible"); + const toolboxTabsEl = toolbox.doc.querySelector(".toolbox-tabs"); + ok(toolboxTabsEl, "Toolbar is still visible"); + + info( + "Check that the markup view is rendered correctly and elements can be selected" + ); + const inspector = await toolbox.selectTool("inspector"); + await waitFor( + () => + inspector.markup && + inspector.markup.win.document.body.innerText.includes( + `` + ), + `wait for to be displayed in the markup view, got: ${inspector.markup?.win.document.body.innerText}`, + 100, + 100 + ); + ok(true, "the markup view is still rendered fine"); + await selectNode("ul.logs", inspector); + ok(true, "Nodes can be selected"); + + info("Check that the debugger has some sources"); + const dbgPanel = await toolbox.selectTool("jsdebugger"); + const dbg = createDebuggerContext(toolbox); + + info(`Wait for ${FILENAME} to be displayed in the debugger source panel`); + const rootNode = await waitFor(() => + dbgPanel.panelWin.document.querySelector(selectors.sourceTreeRootNode) + ); + await expandAllSourceNodes(dbg, rootNode); + const sourcesTreeScriptNode = await waitFor(() => + findSourceNodeWithText(dbg, FILENAME) + ); + + ok( + sourcesTreeScriptNode.innerText.includes(FILENAME), + "The debugger has the expected source" + ); +} + +async function navigateBackAndForth( + expectedUrlAfterBackwardNavigation, + expectedUrlAfterForwardNavigation +) { + const delays = [100, 0, 500]; + for (const delay of delays) { + // For each delays, do 3 back/forth navigations + for (let i = 0; i < 3; i++) { + await safelyGoBack(expectedUrlAfterBackwardNavigation); + await wait(delay); + await safelyGoForward(expectedUrlAfterForwardNavigation); + await wait(delay); + } + } +} + +async function safelyGoBack(expectedUrl) { + const onLocationChange = BrowserTestUtils.waitForLocationChange( + gBrowser, + expectedUrl + ); + gBrowser.goBack(); + await onLocationChange; +} + +async function safelyGoForward(expectedUrl) { + const onLocationChange = BrowserTestUtils.waitForLocationChange( + gBrowser, + expectedUrl + ); + gBrowser.goForward(); + await onLocationChange; +} diff --git a/devtools/client/framework/test/browser_toolbox_browsertoolbox_host.js b/devtools/client/framework/test/browser_toolbox_browsertoolbox_host.js new file mode 100644 index 0000000000..8efb7959ce --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_browsertoolbox_host.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_URL = "data:text/html,test browsertoolbox host"; + +add_task(async function () { + const { + Toolbox, + } = require("resource://devtools/client/framework/toolbox.js"); + + const tab = await addTab(TEST_URL); + const options = { doc: document }; + const toolbox = await gDevTools.showToolboxForTab(tab, { + hostType: Toolbox.HostType.BROWSERTOOLBOX, + hostOptions: options, + }); + + is(toolbox.topWindow, window, "Toolbox is included in browser.xhtml"); + const iframe = document.querySelector( + ".devtools-toolbox-browsertoolbox-iframe" + ); + ok(iframe, "A toolbox iframe was created in the provided document"); + is(toolbox.doc, iframe.contentDocument, "Toolbox is in the custom iframe"); + + await toolbox.destroy(); + iframe.remove(); +}); diff --git a/devtools/client/framework/test/browser_toolbox_contentpage_contextmenu.js b/devtools/client/framework/test/browser_toolbox_contentpage_contextmenu.js new file mode 100644 index 0000000000..63363e4cf3 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_contentpage_contextmenu.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL = "data:text/html;charset=utf8,
test content context menu
"; + +/** + * Check that the DevTools context menu opens without triggering the content + * context menu. See Bug 1591140. + */ +add_task(async function () { + const tab = await addTab(URL); + + info("Test context menu conflict with dom.event.contextmenu.enabled=true"); + await pushPref("dom.event.contextmenu.enabled", true); + await checkConflictWithContentPageMenu(tab); + + info("Test context menu conflict with dom.event.contextmenu.enabled=false"); + await pushPref("dom.event.contextmenu.enabled", false); + await checkConflictWithContentPageMenu(tab); +}); + +async function checkConflictWithContentPageMenu(tab) { + const toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: "inspector", + }); + + info("Check that the content page context menu works as expected"); + const contextMenu = document.getElementById("contentAreaContextMenu"); + is(contextMenu.state, "closed", "Content contextmenu is closed"); + + info("Show the content context menu"); + const awaitPopupShown = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "div", + { + type: "contextmenu", + button: 2, + centered: true, + }, + gBrowser.selectedBrowser + ); + await awaitPopupShown; + is(contextMenu.state, "open", "Content contextmenu is open"); + + info("Hide the content context menu"); + const awaitPopupHidden = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.hidePopup(); + await awaitPopupHidden; + is(contextMenu.state, "closed", "Content contextmenu is closed again"); + + info("Check the DevTools menu opens without opening the content menu"); + const onContextMenuPopup = toolbox.once("menu-open"); + // Use inspector search box for the test, any other element should be ok as + // well. + const inspector = toolbox.getPanel("inspector"); + synthesizeContextMenuEvent(inspector.searchBox); + await onContextMenuPopup; + + const textboxContextMenu = toolbox.getTextBoxContextMenu(); + is(contextMenu.state, "closed", "Content contextmenu is still closed"); + is(textboxContextMenu.state, "open", "Toolbox contextmenu is open"); + + info("Check that the toolbox context menu is closed when pressing ESCAPE"); + const onContextMenuHidden = toolbox.once("menu-close"); + if (Services.prefs.getBoolPref("widget.macos.native-context-menus", false)) { + info("Using hidePopup semantics because of macOS native context menus."); + textboxContextMenu.hidePopup(); + } else { + EventUtils.sendKey("ESCAPE", toolbox.win); + } + await onContextMenuHidden; + is(textboxContextMenu.state, "closed", "Toolbox contextmenu is closed."); + + await toolbox.destroy(); +} diff --git a/devtools/client/framework/test/browser_toolbox_disable_f12.js b/devtools/client/framework/test/browser_toolbox_disable_f12.js new file mode 100644 index 0000000000..df0a755714 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_disable_f12.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + const tab = await addTab( + "https://example.com/document-builder.sjs?html=test" + ); + + info("Enable F12 and check that devtools open"); + await pushPref("devtools.f12_enabled", true); + await assertToolboxOpens(tab, { shouldOpen: true }); + await assertToolboxCloses(tab, { shouldClose: true }); + + info("Disable F12 and check that devtools will not open"); + await pushPref("devtools.f12_enabled", false); + await assertToolboxOpens(tab, { shouldOpen: false }); + + info("Enable F12 again and open devtools"); + await pushPref("devtools.f12_enabled", true); + await assertToolboxOpens(tab, { shouldOpen: true }); + + info("Disable F12 and check F12 no longer closes devtools"); + await pushPref("devtools.f12_enabled", false); + await assertToolboxCloses(tab, { shouldClose: false }); + + info("Enable F12 and close devtools"); + await pushPref("devtools.f12_enabled", true); + await assertToolboxCloses(tab, { shouldClose: true }); + + info("Disable F12 and check other shortcuts still work"); + await pushPref("devtools.f12_enabled", false); + const isMac = Services.appinfo.OS == "Darwin"; + const shortcut = { + key: "i", + options: { accelKey: true, altKey: isMac, shiftKey: !isMac }, + }; + await assertToolboxOpens(tab, { shouldOpen: true, shortcut }); + // Check F12 still doesn't close the toolbox + await assertToolboxCloses(tab, { shouldClose: false }); + await assertToolboxCloses(tab, { shouldClose: true, shortcut }); + + gBrowser.removeTab(tab); +}); + +const assertToolboxCloses = async function (tab, { shortcut, shouldClose }) { + info( + `Use ${ + shortcut ? "shortcut" : "F12" + } to close the toolbox (close expected: ${shouldClose})` + ); + const onToolboxDestroy = gDevTools.once("toolbox-destroyed"); + + if (shortcut) { + EventUtils.synthesizeKey(shortcut.key, shortcut.options); + } else { + EventUtils.synthesizeKey("VK_F12", {}); + } + + if (shouldClose) { + await onToolboxDestroy; + } else { + const onTimeout = wait(1000).then(() => "TIMEOUT"); + const res = await Promise.race([onTimeout, onToolboxDestroy]); + is(res, "TIMEOUT", "No toolbox-destroyed event received"); + } + is( + !gDevTools.getToolboxForTab(tab), + shouldClose, + `Toolbox was ${shouldClose ? "" : "not "}closed for the test tab` + ); +}; + +const assertToolboxOpens = async function (tab, { shortcut, shouldOpen }) { + info( + `Use ${ + shortcut ? "shortcut" : "F12" + } to open the toolbox (open expected: ${shouldOpen})` + ); + const onToolboxReady = gDevTools.once("toolbox-ready"); + + if (shortcut) { + EventUtils.synthesizeKey(shortcut.key, shortcut.options); + } else { + EventUtils.synthesizeKey("VK_F12", {}); + } + + if (shouldOpen) { + await onToolboxReady; + info(`Received toolbox-ready`); + } else { + const onTimeout = wait(1000).then(() => "TIMEOUT"); + const res = await Promise.race([onTimeout, onToolboxReady]); + is(res, "TIMEOUT", "No toolbox-ready event received"); + } + is( + !!gDevTools.getToolboxForTab(tab), + shouldOpen, + `Toolbox was ${shouldOpen ? "" : "not "}opened for the test tab` + ); +}; diff --git a/devtools/client/framework/test/browser_toolbox_dynamic_registration.js b/devtools/client/framework/test/browser_toolbox_dynamic_registration.js new file mode 100644 index 0000000000..0ea7388eec --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_dynamic_registration.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_URL = + "data:text/html,test for dynamically registering and unregistering tools"; + +var toolbox; + +function test() { + addTab(TEST_URL).then(async tab => { + gDevTools.showToolboxForTab(tab).then(testRegister); + }); +} + +function testRegister(aToolbox) { + toolbox = aToolbox; + gDevTools.once("tool-registered", toolRegistered); + + gDevTools.registerTool({ + id: "testTool", + label: "Test Tool", + inMenu: true, + isToolSupported: () => true, + build() {}, + }); +} + +function toolRegistered(toolId) { + is(toolId, "testTool", "tool-registered event handler sent tool id"); + + ok(gDevTools.getToolDefinitionMap().has(toolId), "tool added to map"); + + // test that it appeared in the UI + const doc = toolbox.doc; + const tab = getToolboxTab(doc, toolId); + ok(tab, "new tool's tab exists in toolbox UI"); + + const panel = doc.getElementById("toolbox-panel-" + toolId); + ok(panel, "new tool's panel exists in toolbox UI"); + + for (const win of getAllBrowserWindows()) { + const menuitem = win.document.getElementById("menuitem_" + toolId); + ok(menuitem, "menu item of new tool added to every browser window"); + } + + // then unregister it + testUnregister(); +} + +function getAllBrowserWindows() { + return Array.from(Services.wm.getEnumerator("navigator:browser")); +} + +function testUnregister() { + gDevTools.once("tool-unregistered", toolUnregistered); + + gDevTools.unregisterTool("testTool"); +} + +function toolUnregistered(toolId) { + is(toolId, "testTool", "tool-unregistered event handler sent tool id"); + + ok(!gDevTools.getToolDefinitionMap().has(toolId), "tool removed from map"); + + // test that it disappeared from the UI + const doc = toolbox.doc; + const tab = getToolboxTab(doc, toolId); + ok(!tab, "tool's tab was removed from the toolbox UI"); + + const panel = doc.getElementById("toolbox-panel-" + toolId); + ok(!panel, "tool's panel was removed from toolbox UI"); + + for (const win of getAllBrowserWindows()) { + const menuitem = win.document.getElementById("menuitem_" + toolId); + ok(!menuitem, "menu item removed from every browser window"); + } + + cleanup(); +} + +function cleanup() { + toolbox.destroy().then(() => { + toolbox = null; + gBrowser.removeCurrentTab(); + finish(); + }); +} diff --git a/devtools/client/framework/test/browser_toolbox_error_count.js b/devtools/client/framework/test/browser_toolbox_error_count.js new file mode 100644 index 0000000000..e4dcf0214f --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_error_count.js @@ -0,0 +1,183 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +// Test for error icon and the error count displayed at right of the +// toolbox toolbar + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/shared-head.js", + this +); + +const TEST_URI = `https://example.com/document-builder.sjs?html= +`; + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +add_task(async function () { + // Make sure we start the test with the split console disabled. + await pushPref("devtools.toolbox.splitconsoleEnabled", false); + const tab = await addTab(TEST_URI); + + const toolbox = await openToolboxForTab( + tab, + "inspector", + Toolbox.HostType.BOTTOM + ); + + info("Check for cached errors"); + // (console.error + console.exception + console.assert + error) + let expectedErrorCount = 4; + + await waitFor(() => getErrorIcon(toolbox)); + is( + getErrorIcon(toolbox).getAttribute("title"), + "Show Split Console", + "Icon has expected title" + ); + is( + getErrorIconCount(toolbox), + expectedErrorCount, + "Correct count is displayed" + ); + + info("Check that calling console.clear clears the error count"); + ContentTask.spawn(tab.linkedBrowser, null, function () { + content.console.clear(); + }); + await waitFor( + () => !getErrorIcon(toolbox), + "Wait until the error button hides" + ); + ok(true, "The button was hidden after calling console.clear()"); + + info("Check that realtime errors increase the counter"); + ContentTask.spawn(tab.linkedBrowser, null, function () { + content.console.error("Live Error1"); + content.console.error("Live Error2"); + content.console.exception("Live Exception"); + content.console.warn("Live warning"); + content.console.assert(false, "Live assert"); + content.fetch("unknown-url-that-will-404"); + const script = content.document.createElement("script"); + script.textContent = `a.b.c.d`; + content.document.body.append(script); + }); + + expectedErrorCount = 6; + await waitFor(() => getErrorIconCount(toolbox) === expectedErrorCount); + + info("Check if split console opens on clicking the error icon"); + const onSplitConsoleOpen = toolbox.once("split-console"); + getErrorIcon(toolbox).click(); + await onSplitConsoleOpen; + ok( + toolbox.splitConsole, + "The split console was opened after clicking on the icon." + ); + + // Select the console and check that the icon title is updated + await toolbox.selectTool("webconsole"); + is( + getErrorIcon(toolbox).getAttribute("title"), + null, + "When the console is selected, the icon does not have a title" + ); + + const hud = toolbox.getCurrentPanel().hud; + const webconsoleDoc = hud.ui.window.document; + // wait until all error messages are displayed in the console + await waitFor( + async () => (await findAllErrors(hud)).length === expectedErrorCount + ); + + info("Clear the console output and check that the error icon is hidden"); + webconsoleDoc.querySelector(".devtools-clear-icon").click(); + await waitFor(() => !getErrorIcon(toolbox)); + ok(true, "Clearing the console does hide the icon"); + await waitFor(async () => (await findAllErrors(hud)).length === 0); + + info("Check that the error count is capped at 99"); + expectedErrorCount = 100; + ContentTask.spawn(tab.linkedBrowser, expectedErrorCount, function (count) { + for (let i = 0; i < count; i++) { + content.console.error(i); + } + }); + + // Wait until all the messages are displayed in the console + await waitFor( + async () => (await findAllErrors(hud)).length === expectedErrorCount + ); + + await waitFor(() => getErrorIconCount(toolbox) === "99+"); + ok(true, "The message count doesn't go higher than 99"); + + info( + "Reload the page and check that the error icon has the expected content" + ); + await reloadBrowser(); + + // (console.error, console.exception, console.assert and exception) + expectedErrorCount = 4; + await waitFor(() => getErrorIconCount(toolbox) === expectedErrorCount); + ok(true, "Correct count is displayed"); + + // wait until all error messages are displayed in the console + await waitFor( + async () => (await findAllErrors(hud)).length === expectedErrorCount + ); + + info("Disable the error icon from the options panel"); + const onOptionsSelected = toolbox.once("options-selected"); + toolbox.selectTool("options"); + const optionsPanel = await onOptionsSelected; + const errorCountButtonToggleEl = optionsPanel.panelWin.document.querySelector( + "input#command-button-errorcount" + ); + errorCountButtonToggleEl.click(); + + await waitFor(() => !getErrorIcon(toolbox)); + ok(true, "The error icon hides when disabling it from the settings panel"); + + info("Check that emitting new errors don't show the icon"); + ContentTask.spawn(tab.linkedBrowser, null, function () { + content.console.error("Live Error1 while disabled"); + content.console.error("Live Error2 while disabled"); + }); + + expectedErrorCount = expectedErrorCount + 2; + // Wait until messages are displayed in the console, so the toolbar would have the time + // to render the error icon again. + await toolbox.selectTool("webconsole"); + await waitFor( + async () => (await findAllErrors(hud)).length === expectedErrorCount + ); + is( + getErrorIcon(toolbox), + null, + "The icon is still hidden even after generating new errors" + ); + + info("Re-enable the error icon"); + await toolbox.selectTool("options"); + errorCountButtonToggleEl.click(); + await waitFor(() => getErrorIconCount(toolbox) === expectedErrorCount); + ok( + true, + "The error is displayed again, with the correct error count, after enabling it from the settings panel" + ); + + toolbox.destroy(); +}); + +function findAllErrors(hud) { + return findMessagesVirtualizedByType({ hud, typeSelector: ".error" }); +} diff --git a/devtools/client/framework/test/browser_toolbox_error_count_reset_on_navigation.js b/devtools/client/framework/test/browser_toolbox_error_count_reset_on_navigation.js new file mode 100644 index 0000000000..53f5068655 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_error_count_reset_on_navigation.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +// Test for error count in toolbar when navigating and webconsole isn't enabled +const TEST_URI = `http://example.org/document-builder.sjs?html= +`; + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +add_task(async function () { + // Disable bfcache for Fission for now. + // If Fission is disabled, the pref is no-op. + await SpecialPowers.pushPrefEnv({ + set: [["fission.bfcacheInParent", false]], + }); + + // Make sure we start the test with the split console disabled. + // ⚠️ In this test it's important to _not_ enable the console. + await pushPref("devtools.toolbox.splitconsoleEnabled", false); + const tab = await addTab(TEST_URI); + + const toolbox = await openToolboxForTab( + tab, + "inspector", + Toolbox.HostType.BOTTOM + ); + + info("Check for cached errors"); + // (console.error + console.exception + console.assert + error) + const expectedErrorCount = 4; + + await waitFor(() => getErrorIcon(toolbox)); + is( + getErrorIcon(toolbox).getAttribute("title"), + "Show Split Console", + "Icon has expected title" + ); + is( + getErrorIconCount(toolbox), + expectedErrorCount, + "Correct count is displayed" + ); + + info("Add another error so we have a different count"); + ContentTask.spawn(tab.linkedBrowser, null, function () { + content.console.error("Live Error1"); + }); + + const newExpectedErrorCount = expectedErrorCount + 1; + await waitFor(() => getErrorIconCount(toolbox) === newExpectedErrorCount); + + info( + "Reload the page and check that the error icon has the expected content" + ); + await reloadBrowser(); + + await waitFor( + () => getErrorIconCount(toolbox) === expectedErrorCount, + "Error count is cleared on navigation and then populated with the expected number of errors" + ); + ok(true, "Correct count is displayed"); + + info( + "Navigate to an error-less page and check that the error icon is hidden" + ); + await navigateTo(`data:text/html;charset=utf8,No errors`); + await waitFor( + () => !getErrorIcon(toolbox), + "Error count is cleared on navigation" + ); + ok( + true, + "The error icon was hidden when navigating to a new page without errors" + ); + + toolbox.destroy(); +}); diff --git a/devtools/client/framework/test/browser_toolbox_fission_navigation.js b/devtools/client/framework/test/browser_toolbox_fission_navigation.js new file mode 100644 index 0000000000..123a06cce2 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_fission_navigation.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const EXAMPLE_COM_URI = + "https://example.com/document-builder.sjs?html=
com"; +const EXAMPLE_ORG_URI = + "https://example.org/document-builder.sjs?html=
org"; + +add_task(async function () { + const tab = await addTab(EXAMPLE_COM_URI); + + const toolbox = await openToolboxForTab(tab, "inspector"); + const comNode = await getNodeBySelector(toolbox, "#com"); + ok(comNode, "Found node for the COM page"); + + info("Navigate to the ORG page"); + await navigateTo(EXAMPLE_ORG_URI); + const orgNode = await getNodeBySelector(toolbox, "#org"); + ok(orgNode, "Found node for the ORG page"); + + info("Reload the ORG page"); + await navigateTo(EXAMPLE_ORG_URI); + const orgNodeAfterReload = await getNodeBySelector(toolbox, "#org"); + ok(orgNodeAfterReload, "Found node for the ORG page after reload"); + isnot(orgNode, orgNodeAfterReload, "The new node is different"); + + info("Navigate back to the COM page"); + await navigateTo(EXAMPLE_COM_URI); + const comNodeAfterNavigation = await getNodeBySelector(toolbox, "#com"); + ok(comNodeAfterNavigation, "Found node for the COM page after navigation"); + + info("Navigate to about:blank"); + await navigateTo("about:blank"); + const blankBodyAfterNavigation = await getNodeBySelector(toolbox, "body"); + ok( + blankBodyAfterNavigation, + "Found node for the about:blank page after navigation" + ); + + info("Navigate to about:robots"); + await navigateTo("about:robots"); + const aboutRobotsAfterNavigation = await getNodeBySelector( + toolbox, + "div.container" + ); + ok( + aboutRobotsAfterNavigation, + "Found node for the about:robots page after navigation" + ); +}); + +async function getNodeBySelector(toolbox, selector) { + const inspector = await toolbox.selectTool("inspector"); + return inspector.walker.querySelector(inspector.walker.rootNode, selector); +} diff --git a/devtools/client/framework/test/browser_toolbox_frames_list.js b/devtools/client/framework/test/browser_toolbox_frames_list.js new file mode 100644 index 0000000000..f1d7ff0510 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_frames_list.js @@ -0,0 +1,150 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the frames list gets updated as iframes are added/removed from the document, +// and during navigation. + +const TEST_COM_URL = + "https://example.com/document-builder.sjs?html=
com"; +const TEST_ORG_URL = + `https://example.org/document-builder.sjs?html=
org
` + + `` + + ``; + +add_task(async function () { + // Enable the frames button. + await pushPref("devtools.command-button-frames.enabled", true); + + const tab = await addTab(TEST_COM_URL); + + const toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: "webconsole", + }); + + ok( + !getFramesButton(toolbox), + "Frames button is not rendered when there's no iframes in the page" + ); + await checkFramesList(toolbox, []); + + info("Create a same origin (example.com) iframe"); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const comIframe = content.document.createElement("iframe"); + comIframe.src = + "https://example.com/document-builder.sjs?html=example.com iframe"; + content.document.body.appendChild(comIframe); + }); + + await waitFor(() => getFramesButton(toolbox)); + ok(true, "Button is displayed when adding an iframe"); + + info("Check the content of the frames list"); + await checkFramesList(toolbox, [ + TEST_COM_URL, + "https://example.com/document-builder.sjs?html=example.com iframe", + ]); + + info("Create a cross-process origin (example.org) iframe"); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const orgIframe = content.document.createElement("iframe"); + orgIframe.src = + "https://example.org/document-builder.sjs?html=example.org iframe"; + content.document.body.appendChild(orgIframe); + }); + + info("Check that the content of the frames list was updated"); + try { + await checkFramesList(toolbox, [ + TEST_COM_URL, + "https://example.com/document-builder.sjs?html=example.com iframe", + "https://example.org/document-builder.sjs?html=example.org iframe", + ]); + + // If Fission is enabled and EFT is not, we shouldn't hit this line as `checkFramesList` + // should throw (as remote frames are only displayed when EFT is enabled). + ok( + !isFissionEnabled() || isEveryFrameTargetEnabled(), + "iframe picker should only display remote frames when EFT is enabled" + ); + } catch (e) { + ok( + isFissionEnabled() && !isEveryFrameTargetEnabled(), + "iframe picker displays remote frames only when EFT is enabled" + ); + return; + } + + info("Reload and check that the frames list is cleared"); + await reloadBrowser(); + await waitFor(() => !getFramesButton(toolbox)); + ok( + true, + "The button was hidden when reloading as the page does not have iframes" + ); + await checkFramesList(toolbox, []); + + info("Navigate to a different origin, on a page with iframes"); + await navigateTo(TEST_ORG_URL); + await checkFramesList(toolbox, [ + TEST_ORG_URL, + "https://example.org/document-builder.sjs?html=example.org iframe", + "https://example.com/document-builder.sjs?html=example.com iframe", + ]); + + info("Check that frames list is updated when removing same-origin iframe"); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + content.document.querySelector("iframe").remove(); + }); + await checkFramesList(toolbox, [ + TEST_ORG_URL, + "https://example.com/document-builder.sjs?html=example.com iframe", + ]); + + info("Check that frames list is updated when removing cross-origin iframe"); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + content.document.querySelector("iframe").remove(); + }); + await waitFor(() => !getFramesButton(toolbox)); + ok(true, "The button was hidden when removing the last iframe on the page"); + await checkFramesList(toolbox, []); + + info("Check that the list does have expected items after reloading"); + await reloadBrowser(); + await waitFor(() => getFramesButton(toolbox)); + ok(true, "button is displayed after reloading"); + await checkFramesList(toolbox, [ + TEST_ORG_URL, + "https://example.org/document-builder.sjs?html=example.org iframe", + "https://example.com/document-builder.sjs?html=example.com iframe", + ]); +}); + +function getFramesButton(toolbox) { + return toolbox.doc.getElementById("command-button-frames"); +} + +async function checkFramesList(toolbox, expectedFrames) { + const frames = await waitFor(() => { + // items might be added in the list before their url is known, so exclude empty items. + const f = getFramesLabels(toolbox).filter(t => t !== ""); + if (f.length !== expectedFrames.length) { + return false; + } + + return f; + }); + + is( + JSON.stringify(frames.sort()), + JSON.stringify(expectedFrames.sort()), + "The expected frames are displayed" + ); +} + +function getFramesLabels(toolbox) { + return Array.from( + toolbox.doc.querySelectorAll("#toolbox-frame-menu .command .label") + ).map(el => el.textContent); +} diff --git a/devtools/client/framework/test/browser_toolbox_getpanelwhenready.js b/devtools/client/framework/test/browser_toolbox_getpanelwhenready.js new file mode 100644 index 0000000000..85436c2925 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_getpanelwhenready.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that getPanelWhenReady returns the correct panel in promise +// resolutions regardless of whether it has opened first. + +var toolbox = null; + +const URL = "data:text/html;charset=utf8,test for getPanelWhenReady"; + +add_task(async function () { + const tab = await addTab(URL); + toolbox = await gDevTools.showToolboxForTab(tab); + + const debuggerPanelPromise = toolbox.getPanelWhenReady("jsdebugger"); + await toolbox.selectTool("jsdebugger"); + const debuggerPanel = await debuggerPanelPromise; + + is( + debuggerPanel, + toolbox.getPanel("jsdebugger"), + "The debugger panel from getPanelWhenReady before loading is the actual panel" + ); + + const debuggerPanel2 = await toolbox.getPanelWhenReady("jsdebugger"); + is( + debuggerPanel2, + toolbox.getPanel("jsdebugger"), + "The debugger panel from getPanelWhenReady after loading is the actual panel" + ); + + await cleanup(); +}); + +async function cleanup() { + await toolbox.destroy(); + gBrowser.removeCurrentTab(); + toolbox = null; +} diff --git a/devtools/client/framework/test/browser_toolbox_highlight.js b/devtools/client/framework/test/browser_toolbox_highlight.js new file mode 100644 index 0000000000..d0712aeed5 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_highlight.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +var toolbox = null; + +function test() { + (async function () { + const URL = "data:text/plain;charset=UTF-8,Nothing to see here, move along"; + + const TOOL_ID_1 = "jsdebugger"; + const TOOL_ID_2 = "webconsole"; + await addTab(URL); + + toolbox = await gDevTools.showToolboxForTab(gBrowser.selectedTab, { + toolId: TOOL_ID_1, + hostType: Toolbox.HostType.BOTTOM, + }); + + // select tool 2 + await toolbox.selectTool(TOOL_ID_2); + // and highlight the first one + await highlightTab(TOOL_ID_1); + // to see if it has the proper class. + await checkHighlighted(TOOL_ID_1); + // Now switch back to first tool + await toolbox.selectTool(TOOL_ID_1); + // to check again. But there is no easy way to test if + // it is showing orange or not. + await checkNoHighlightWhenSelected(TOOL_ID_1); + // Switch to tool 2 again + await toolbox.selectTool(TOOL_ID_2); + // and check again. + await checkHighlighted(TOOL_ID_1); + // Highlight another tool + await highlightTab(TOOL_ID_2); + // Check that both tools are highlighted. + await checkHighlighted(TOOL_ID_1); + // Check second tool being both highlighted and selected. + await checkNoHighlightWhenSelected(TOOL_ID_2); + // Select tool 1 + await toolbox.selectTool(TOOL_ID_1); + // Check second tool is still highlighted + await checkHighlighted(TOOL_ID_2); + // Unhighlight the second tool + await unhighlightTab(TOOL_ID_2); + // to see the classes gone. + await checkNoHighlight(TOOL_ID_2); + // Now unhighlight the tool + await unhighlightTab(TOOL_ID_1); + // to see the classes gone. + await checkNoHighlight(TOOL_ID_1); + + // Now close the toolbox and exit. + executeSoon(() => { + toolbox.destroy().then(() => { + toolbox = null; + gBrowser.removeCurrentTab(); + finish(); + }); + }); + })().catch(error => { + ok(false, "There was an error running the test."); + }); +} + +function highlightTab(toolId) { + info(`Highlighting tool ${toolId}'s tab.`); + return toolbox.highlightTool(toolId); +} + +function unhighlightTab(toolId) { + info(`Unhighlighting tool ${toolId}'s tab.`); + return toolbox.unhighlightTool(toolId); +} + +function checkHighlighted(toolId) { + const tab = toolbox.doc.getElementById("toolbox-tab-" + toolId); + ok( + toolbox.isHighlighted(toolId), + `Toolbox.isHighlighted reports ${toolId} as highlighted` + ); + ok( + tab.classList.contains("highlighted"), + `The highlighted class is present in ${toolId}.` + ); + ok( + !tab.classList.contains("selected"), + `The tab is not selected in ${toolId}` + ); +} + +function checkNoHighlightWhenSelected(toolId) { + const tab = toolbox.doc.getElementById("toolbox-tab-" + toolId); + ok( + toolbox.isHighlighted(toolId), + `Toolbox.isHighlighted reports ${toolId} as highlighted` + ); + ok( + tab.classList.contains("highlighted"), + `The highlighted class is present in ${toolId}` + ); + ok( + tab.classList.contains("selected"), + `And the tab is selected, so the orange glow will not be present. in ${toolId}` + ); +} + +function checkNoHighlight(toolId) { + const tab = toolbox.doc.getElementById("toolbox-tab-" + toolId); + ok( + !toolbox.isHighlighted(toolId), + `Toolbox.isHighlighted reports ${toolId} as not highlighted` + ); + ok( + !tab.classList.contains("highlighted"), + `The highlighted class is not present in ${toolId}` + ); +} diff --git a/devtools/client/framework/test/browser_toolbox_hosts.js b/devtools/client/framework/test/browser_toolbox_hosts.js new file mode 100644 index 0000000000..37738865a9 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_hosts.js @@ -0,0 +1,226 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + gDevToolsBrowser, +} = require("resource://devtools/client/framework/devtools-browser.js"); + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); +const { LEFT, RIGHT, BOTTOM, WINDOW } = Toolbox.HostType; +let toolbox; + +// We are opening/close toolboxes many times, +// which introduces long GC pauses between each sub task +// and requires some more time to run in DEBUG builds. +requestLongerTimeout(2); + +const URL = + "data:text/html;charset=utf8,test for opening toolbox in different hosts"; + +add_task(async function () { + const win = await BrowserTestUtils.openNewBrowserWindow(); + win.gBrowser.selectedTab = BrowserTestUtils.addTab(win.gBrowser, URL); + + const tab = win.gBrowser.selectedTab; + toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: "webconsole", + hostType: Toolbox.HostType.WINDOW, + }); + const onToolboxClosed = toolbox.once("destroyed"); + ok( + gDevToolsBrowser.hasToolboxOpened(win), + "hasToolboxOpened is true before closing the toolbox" + ); + await BrowserTestUtils.closeWindow(win); + ok( + !gDevToolsBrowser.hasToolboxOpened(win), + "hasToolboxOpened is false after closing the window" + ); + + info("Wait for toolbox to be destroyed after browser window is closed"); + await onToolboxClosed; + toolbox = null; +}); + +add_task(async function runTest() { + info("Create a test tab and open the toolbox"); + const tab = await addTab(URL); + toolbox = await gDevTools.showToolboxForTab(tab, { toolId: "webconsole" }); + + await runHostTests(gBrowser); + await toolbox.destroy(); + + toolbox = null; + gBrowser.removeCurrentTab(); +}); + +// We run the same host switching tests in a private window. +// See Bug 1581093 for an example of issue specific to private windows. +add_task(async function runPrivateWindowTest() { + info("Create a private window + tab and open the toolbox"); + await runHostTestsFromSeparateWindow({ + private: true, + }); +}); + +// We run the same host switching tests in a non-fission window. +// See Bug 1650963 for an example of issue specific to private windows. +add_task(async function runNonFissionWindowTest() { + info("Create a non-fission window + tab and open the toolbox"); + await runHostTestsFromSeparateWindow({ + fission: false, + }); +}); + +async function runHostTestsFromSeparateWindow(options) { + const win = await BrowserTestUtils.openNewBrowserWindow(options); + const browser = win.gBrowser; + browser.selectedTab = BrowserTestUtils.addTab(browser, URL); + + const tab = browser.selectedTab; + toolbox = await gDevTools.showToolboxForTab(tab, { toolId: "webconsole" }); + + await runHostTests(browser); + await toolbox.destroy(); + + toolbox = null; + await BrowserTestUtils.closeWindow(win); +} + +async function runHostTests(browser) { + await testBottomHost(browser); + await testLeftHost(browser); + await testRightHost(browser); + await testWindowHost(browser); + await testToolSelect(); + await testDestroy(browser); + await testRememberHost(); + await testPreviousHost(); +} + +function testBottomHost(browser) { + checkHostType(toolbox, BOTTOM); + + // test UI presence + const panel = browser.getPanel(); + const iframe = panel.querySelector(".devtools-toolbox-bottom-iframe"); + ok(iframe, "toolbox bottom iframe exists"); + + checkToolboxLoaded(iframe); +} + +async function testLeftHost(browser) { + await toolbox.switchHost(LEFT); + checkHostType(toolbox, LEFT); + + // test UI presence + const panel = browser.getPanel(); + const bottom = panel.querySelector(".devtools-toolbox-bottom-iframe"); + ok(!bottom, "toolbox bottom iframe doesn't exist"); + + const iframe = panel.querySelector(".devtools-toolbox-side-iframe"); + ok(iframe, "toolbox side iframe exists"); + + checkToolboxLoaded(iframe); +} + +async function testRightHost(browser) { + await toolbox.switchHost(RIGHT); + checkHostType(toolbox, RIGHT); + + // test UI presence + const panel = browser.getPanel(); + const bottom = panel.querySelector(".devtools-toolbox-bottom-iframe"); + ok(!bottom, "toolbox bottom iframe doesn't exist"); + + const iframe = panel.querySelector(".devtools-toolbox-side-iframe"); + ok(iframe, "toolbox side iframe exists"); + + checkToolboxLoaded(iframe); +} + +async function testWindowHost(browser) { + await toolbox.switchHost(WINDOW); + checkHostType(toolbox, WINDOW); + + const panel = browser.getPanel(); + const sidebar = panel.querySelector(".devtools-toolbox-side-iframe"); + ok(!sidebar, "toolbox sidebar iframe doesn't exist"); + + const win = Services.wm.getMostRecentWindow("devtools:toolbox"); + ok(win, "toolbox separate window exists"); + + const iframe = win.document.querySelector(".devtools-toolbox-window-iframe"); + checkToolboxLoaded(iframe); +} + +async function testToolSelect() { + // make sure we can load a tool after switching hosts + await toolbox.selectTool("inspector"); +} + +async function testDestroy(browser) { + await toolbox.destroy(); + toolbox = await gDevTools.showToolboxForTab(browser.selectedTab); +} + +function testRememberHost() { + // last host was the window - make sure it's the same when re-opening + is(toolbox.hostType, WINDOW, "host remembered"); + + const win = Services.wm.getMostRecentWindow("devtools:toolbox"); + ok(win, "toolbox separate window exists"); +} + +async function testPreviousHost() { + // last host was the window - make sure it's the same when re-opening + is(toolbox.hostType, WINDOW, "host remembered"); + + info("Switching to left"); + await toolbox.switchHost(LEFT); + checkHostType(toolbox, LEFT, WINDOW); + + info("Switching to right"); + await toolbox.switchHost(RIGHT); + checkHostType(toolbox, RIGHT, LEFT); + + info("Switching to bottom"); + await toolbox.switchHost(BOTTOM); + checkHostType(toolbox, BOTTOM, RIGHT); + + info("Switching from bottom to right"); + await toolbox.switchToPreviousHost(); + checkHostType(toolbox, RIGHT, BOTTOM); + + info("Switching from right to bottom"); + await toolbox.switchToPreviousHost(); + checkHostType(toolbox, BOTTOM, RIGHT); + + info("Switching to window"); + await toolbox.switchHost(WINDOW); + checkHostType(toolbox, WINDOW, BOTTOM); + + info("Switching from window to bottom"); + await toolbox.switchToPreviousHost(); + checkHostType(toolbox, BOTTOM, WINDOW); + + info("Forcing the previous host to match the current (bottom)"); + Services.prefs.setCharPref("devtools.toolbox.previousHost", BOTTOM); + + info("Switching from bottom to right (since previous=current=bottom"); + await toolbox.switchToPreviousHost(); + checkHostType(toolbox, RIGHT, BOTTOM); + + info("Forcing the previous host to match the current (right)"); + Services.prefs.setCharPref("devtools.toolbox.previousHost", RIGHT); + info("Switching from right to bottom (since previous=current=side"); + await toolbox.switchToPreviousHost(); + checkHostType(toolbox, BOTTOM, RIGHT); +} + +function checkToolboxLoaded(iframe) { + const tabs = iframe.contentDocument.querySelector(".toolbox-tabs"); + ok(tabs, "toolbox UI has been loaded into iframe"); +} diff --git a/devtools/client/framework/test/browser_toolbox_hosts_size.js b/devtools/client/framework/test/browser_toolbox_hosts_size.js new file mode 100644 index 0000000000..81cce09a67 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_hosts_size.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that getPanelWhenReady returns the correct panel in promise +// resolutions regardless of whether it has opened first. + +const URL = "data:text/html;charset=utf8,test for host sizes"; + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +add_task(async function () { + // Set size prefs to make the hosts way too big, so that the size has + // to be clamped to fit into the browser window. + Services.prefs.setIntPref("devtools.toolbox.footer.height", 10000); + Services.prefs.setIntPref("devtools.toolbox.sidebar.width", 10000); + + const tab = await addTab(URL); + const panel = gBrowser.getPanel(); + const { clientHeight: panelHeight, clientWidth: panelWidth } = panel; + const toolbox = await gDevTools.showToolboxForTab(tab); + + is( + panel.clientHeight, + panelHeight, + "Opening the toolbox hasn't changed the height of the panel" + ); + is( + panel.clientWidth, + panelWidth, + "Opening the toolbox hasn't changed the width of the panel" + ); + + let iframe = panel.querySelector(".devtools-toolbox-bottom-iframe"); + is( + iframe.clientHeight, + panelHeight - 25, + "The iframe fits within the available space" + ); + + iframe.style.height = "10000px"; // Set height to something unreasonably large. + Assert.less( + iframe.clientHeight, + panelHeight, + `The iframe fits within the available space (${iframe.clientHeight} < ${panelHeight})` + ); + + await toolbox.switchHost(Toolbox.HostType.RIGHT); + iframe = panel.querySelector(".devtools-toolbox-side-iframe"); + iframe.style.minWidth = "1px"; // Disable the min width set in css + is( + iframe.clientWidth, + panelWidth - 25, + "The iframe fits within the available space" + ); + + const oldWidth = iframe.style.width; + iframe.style.width = "10000px"; // Set width to something unreasonably large. + Assert.less( + iframe.clientWidth, + panelWidth, + `The iframe fits within the available space (${iframe.clientWidth} < ${panelWidth})` + ); + iframe.style.width = oldWidth; + + // on shutdown, the sidebar width will be set to the clientWidth of the iframe + const expectedWidth = iframe.clientWidth; + + info("waiting for cleanup"); + await cleanup(toolbox); + // Wait until the toolbox-host-manager was destroyed and updated the preferences + // to avoid side effects in the next test. + await waitUntil(() => { + const savedWidth = Services.prefs.getIntPref( + "devtools.toolbox.sidebar.width" + ); + info(`waiting for saved pref: ${savedWidth}, ${expectedWidth}`); + return savedWidth === expectedWidth; + }); +}); + +add_task(async function () { + // Set size prefs to something reasonable, so we can check to make sure + // they are being set properly. + Services.prefs.setIntPref("devtools.toolbox.footer.height", 100); + Services.prefs.setIntPref("devtools.toolbox.sidebar.width", 100); + + const tab = await addTab(URL); + const panel = gBrowser.getPanel(); + const { clientHeight: panelHeight, clientWidth: panelWidth } = panel; + const toolbox = await gDevTools.showToolboxForTab(tab); + + is( + panel.clientHeight, + panelHeight, + "Opening the toolbox hasn't changed the height of the panel" + ); + is( + panel.clientWidth, + panelWidth, + "Opening the toolbox hasn't changed the width of the panel" + ); + + let iframe = panel.querySelector(".devtools-toolbox-bottom-iframe"); + is(iframe.clientHeight, 100, "The iframe is resized properly"); + const horzSplitter = panel.querySelector(".devtools-horizontal-splitter"); + dragElement(horzSplitter, { startX: 1, startY: 1, deltaX: 0, deltaY: -50 }); + is(iframe.clientHeight, 150, "The iframe was resized by the splitter"); + + await toolbox.switchHost(Toolbox.HostType.RIGHT); + iframe = panel.querySelector(".devtools-toolbox-side-iframe"); + iframe.style.minWidth = "1px"; // Disable the min width set in css + is(iframe.clientWidth, 100, "The iframe is resized properly"); + + info("Resize the toolbox manually by 50 pixels"); + const sideSplitter = panel.querySelector(".devtools-side-splitter"); + dragElement(sideSplitter, { startX: 1, startY: 1, deltaX: -50, deltaY: 0 }); + is(iframe.clientWidth, 150, "The iframe was resized by the splitter"); + + await cleanup(toolbox); +}); + +function dragElement(el, { startX, startY, deltaX, deltaY }) { + const endX = startX + deltaX; + const endY = startY + deltaY; + EventUtils.synthesizeMouse(el, startX, startY, { type: "mousedown" }, window); + EventUtils.synthesizeMouse(el, endX, endY, { type: "mousemove" }, window); + EventUtils.synthesizeMouse(el, endX, endY, { type: "mouseup" }, window); +} + +async function cleanup(toolbox) { + Services.prefs.clearUserPref("devtools.toolbox.host"); + Services.prefs.clearUserPref("devtools.toolbox.footer.height"); + Services.prefs.clearUserPref("devtools.toolbox.sidebar.width"); + await toolbox.destroy(); + gBrowser.removeCurrentTab(); +} diff --git a/devtools/client/framework/test/browser_toolbox_hosts_telemetry.js b/devtools/client/framework/test/browser_toolbox_hosts_telemetry.js new file mode 100644 index 0000000000..92992048dd --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_hosts_telemetry.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); +const { LEFT, RIGHT, BOTTOM, WINDOW } = Toolbox.HostType; + +const URL = "data:text/html;charset=utf8,browser_toolbox_hosts_telemetry.js"; + +add_task(async function () { + startTelemetry(); + + info("Create a test tab and open the toolbox"); + const tab = await addTab(URL); + const toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: "webconsole", + }); + + await changeToolboxHost(toolbox); + await checkResults(); +}); + +async function changeToolboxHost(toolbox) { + info("Switch toolbox host"); + await toolbox.switchHost(RIGHT); + await toolbox.switchHost(WINDOW); + await toolbox.switchHost(BOTTOM); + await toolbox.switchHost(LEFT); + await toolbox.switchHost(RIGHT); + await toolbox.switchHost(WINDOW); + await toolbox.switchHost(BOTTOM); + await toolbox.switchHost(LEFT); + await toolbox.switchHost(RIGHT); +} + +function checkResults() { + // Check for: + // - 3 "bottom" entries. + // - 2 "left" entries. + // - 3 "right" entries. + // - 2 "window" entries. + checkTelemetry( + "DEVTOOLS_TOOLBOX_HOST", + "", + { 0: 3, 1: 3, 2: 2, 4: 2, 5: 0 }, + "array" + ); +} diff --git a/devtools/client/framework/test/browser_toolbox_keyboard_navigation.js b/devtools/client/framework/test/browser_toolbox_keyboard_navigation.js new file mode 100644 index 0000000000..17ba9efcf9 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_keyboard_navigation.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests keyboard navigation of devtools tabbar. + +const TEST_URL = + "data:text/html;charset=utf8,test page for toolbar keyboard navigation"; + +function containsFocus(aDoc, aElm) { + let elm = aDoc.activeElement; + while (elm) { + if (elm === aElm) { + return true; + } + elm = elm.parentNode; + } + return false; +} + +add_task(async function () { + info("Create a test tab and open the toolbox"); + const toolbox = await openNewTabAndToolbox(TEST_URL, "webconsole"); + const doc = toolbox.doc; + + const toolbar = doc.querySelector(".devtools-tabbar"); + const toolbarControls = [ + ...toolbar.querySelectorAll(".devtools-tab, button"), + ].filter( + elm => + !elm.hidden && + doc.defaultView.getComputedStyle(elm).getPropertyValue("display") !== + "none" + ); + + // Put the keyboard focus onto the first toolbar control. + toolbarControls[0].focus(); + ok(containsFocus(doc, toolbar), "Focus is within the toolbar"); + + // Move the focus away from toolbar to a next focusable element. + EventUtils.synthesizeKey("KEY_Tab"); + ok(!containsFocus(doc, toolbar), "Focus is outside of the toolbar"); + + // Move the focus back to the toolbar. + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + ok(containsFocus(doc, toolbar), "Focus is within the toolbar again"); + + // Move through the toolbar forward using the right arrow key. + for (let i = 0; i < toolbarControls.length; ++i) { + is(doc.activeElement.id, toolbarControls[i].id, "New control is focused"); + if (i < toolbarControls.length - 1) { + EventUtils.synthesizeKey("KEY_ArrowRight"); + } + } + + // Move the focus away from toolbar to a next focusable element. + EventUtils.synthesizeKey("KEY_Tab"); + ok(!containsFocus(doc, toolbar), "Focus is outside of the toolbar"); + + // Move the focus back to the toolbar. + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + ok(containsFocus(doc, toolbar), "Focus is within the toolbar again"); + + // Move through the toolbar backward using the left arrow key. + for (let i = toolbarControls.length - 1; i >= 0; --i) { + is(doc.activeElement.id, toolbarControls[i].id, "New control is focused"); + if (i > 0) { + EventUtils.synthesizeKey("KEY_ArrowLeft"); + } + } + + // Move focus to the 3rd (non-first) toolbar control. + const expectedFocusedControl = toolbarControls[2]; + EventUtils.synthesizeKey("KEY_ArrowRight"); + EventUtils.synthesizeKey("KEY_ArrowRight"); + is(doc.activeElement.id, expectedFocusedControl.id, "New control is focused"); + + // Move the focus away from toolbar to a next focusable element. + EventUtils.synthesizeKey("KEY_Tab"); + ok(!containsFocus(doc, toolbar), "Focus is outside of the toolbar"); + + // Move the focus back to the toolbar, ensure we land on the last active + // descendant control. + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + is(doc.activeElement.id, expectedFocusedControl.id, "New control is focused"); +}); + +// Test that moving the focus of tab button and selecting it. +add_task(async function () { + info("Create a test tab and open the toolbox"); + const toolbox = await openNewTabAndToolbox(TEST_URL, "inspector"); + const doc = toolbox.doc; + + const toolbar = doc.querySelector(".toolbox-tabs"); + const tabButtons = toolbar.querySelectorAll(".devtools-tab, button"); + const win = tabButtons[0].ownerDocument.defaultView; + + // Put the keyboard focus onto the first tab button. + tabButtons[0].focus(); + ok(containsFocus(doc, toolbar), "Focus is within the toolbox"); + is(doc.activeElement.id, tabButtons[0].id, "First tab button is focused."); + + // Move the focused tab and select it by using enter key. + let onKeyEvent = once(win, "keydown"); + EventUtils.synthesizeKey("KEY_ArrowRight"); + await onKeyEvent; + + let onceSelected = toolbox.once("webconsole-selected"); + EventUtils.synthesizeKey("Enter"); + await onceSelected; + is( + doc.activeElement.id, + "toolbox-panel-iframe-" + toolbox.currentToolId, + "Selected tool frame is now focused." + ); + + // Webconsole steal the focus from button after sending "webconsole-selected" + // event. + tabButtons[1].focus(); + + // Return the focused tab with space key. + onKeyEvent = once(win, "keydown"); + EventUtils.synthesizeKey("KEY_ArrowLeft"); + await onKeyEvent; + + onceSelected = toolbox.once("inspector-selected"); + EventUtils.synthesizeKey(" "); + await onceSelected; + + is( + doc.activeElement.id, + "toolbox-panel-iframe-" + toolbox.currentToolId, + "Selected tool frame is now focused." + ); +}); diff --git a/devtools/client/framework/test/browser_toolbox_keyboard_navigation_notification_box.js b/devtools/client/framework/test/browser_toolbox_keyboard_navigation_notification_box.js new file mode 100644 index 0000000000..135559cb2f --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_keyboard_navigation_notification_box.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests keyboard navigation of the DevTools notification box. + +// The test page attempts to load a stylesheet at an invalid URL which will +// trigger a devtools notification to show up on top of the window. +const TEST_PAGE = ``; +const TEST_URL = `data:text/html;charset=utf8,${TEST_PAGE}`; + +add_task(async function () { + info("Create a test tab and open the toolbox"); + const toolbox = await openNewTabAndToolbox(TEST_URL, "styleeditor"); + const doc = toolbox.doc; + + info("Wait until the notification box displays the stylesheet warning"); + const notificationBox = await waitFor(() => + doc.querySelector(".notificationbox") + ); + + ok( + notificationBox.querySelector(".notification"), + "A notification is rendered" + ); + + const toolbar = doc.querySelector(".devtools-tabbar"); + const tabButtons = toolbar.querySelectorAll(".devtools-tab, button"); + + // Put the keyboard focus onto the first tab button. + tabButtons[0].focus(); + is(doc.activeElement.id, tabButtons[0].id, "First tab button is focused."); + + // Move the focus to the notification box. + info("Send a shift+tab key event to focus the previous focusable element"); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + is( + doc.activeElement, + notificationBox.querySelector(".messageCloseButton"), + "The focus is on the close button of the notification" + ); + + info("Send a vk_space key event to click on the close button"); + EventUtils.synthesizeKey("VK_SPACE"); + + info("Wait until the notification is removed"); + await waitUntil(() => !notificationBox.querySelector(".notificationbox")); +}); diff --git a/devtools/client/framework/test/browser_toolbox_meatball.js b/devtools/client/framework/test/browser_toolbox_meatball.js new file mode 100644 index 0000000000..04d9a3a0cd --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_meatball.js @@ -0,0 +1,135 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Sanity test for meatball menu. +// +// We also use this to test the common Menu* components since we don't currently +// have a means of testing React components in isolation. + +const { + focusableSelector, +} = require("resource://devtools/client/shared/focus.js"); +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +add_task(async function () { + const tab = await addTab("about:blank"); + const toolbox = await openToolboxForTab( + tab, + "inspector", + Toolbox.HostType.BOTTOM + ); + + info("Check opening meatball menu by clicking the menu button"); + await openMeatballMenuWithClick(toolbox); + const menuDockToBottom = toolbox.doc.getElementById( + "toolbox-meatball-menu-dock-bottom" + ); + Assert.strictEqual( + menuDockToBottom.getAttribute("aria-checked"), + "true", + "menuDockToBottom has checked" + ); + + info("Check closing meatball menu by clicking outside the popup area"); + await closeMeatballMenuWithClick(toolbox); + + info("Check moving the focus element with key event"); + await openMeatballMenuWithClick(toolbox); + checkKeyHandling(toolbox); + + info("Check closing meatball menu with escape key"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, toolbox.win); + await waitForMeatballMenuToClose(toolbox); + + // F1 should trigger the settings panel and close the menu at the same time. + info("Check closing meatball menu with F1 key"); + await openMeatballMenuWithClick(toolbox); + EventUtils.synthesizeKey("VK_F1", {}, toolbox.win); + await waitForMeatballMenuToClose(toolbox); + + await toolbox.destroy(); +}); + +async function openMeatballMenuWithClick(toolbox) { + const meatballButton = toolbox.doc.getElementById( + "toolbox-meatball-menu-button" + ); + await waitUntil(() => meatballButton.style.pointerEvents !== "none"); + EventUtils.synthesizeMouseAtCenter(meatballButton, {}, toolbox.win); + + const panel = toolbox.doc.querySelectorAll(".tooltip-xul-wrapper"); + const shownListener = new Promise(res => { + panel[0].addEventListener("popupshown", res, { once: true }); + }); + + const menuPanel = toolbox.doc.getElementById( + "toolbox-meatball-menu-button-panel" + ); + ok(menuPanel, "meatball panel is available"); + + info("Waiting for the menu panel to be displayed"); + + await shownListener; + await waitUntil(() => menuPanel.classList.contains("tooltip-visible")); +} + +async function closeMeatballMenuWithClick(toolbox) { + const meatballButton = toolbox.doc.getElementById( + "toolbox-meatball-menu-button" + ); + await waitUntil( + () => toolbox.win.getComputedStyle(meatballButton).pointerEvents === "none" + ); + meatballButton.click(); + + const menuPanel = toolbox.doc.getElementById( + "toolbox-meatball-menu-button-panel" + ); + ok(menuPanel, "meatball panel is available"); + + info("Waiting for the menu panel to be hidden"); + await waitUntil(() => !menuPanel.classList.contains("tooltip-visible")); +} + +async function waitForMeatballMenuToClose(toolbox) { + const menuPanel = toolbox.doc.getElementById( + "toolbox-meatball-menu-button-panel" + ); + ok(menuPanel, "meatball panel is available"); + + info("Waiting for the menu panel to be hidden"); + await waitUntil(() => !menuPanel.classList.contains("tooltip-visible")); +} + +function checkKeyHandling(toolbox) { + const selectable = toolbox.doc + .getElementById("toolbox-meatball-menu") + .querySelectorAll(focusableSelector); + + EventUtils.synthesizeKey("VK_DOWN", {}, toolbox.win); + is( + toolbox.doc.activeElement, + selectable[0], + "First item selected with down key." + ); + EventUtils.synthesizeKey("VK_UP", {}, toolbox.win); + is( + toolbox.doc.activeElement, + selectable[selectable.length - 1], + "End item selected with up key." + ); + EventUtils.synthesizeKey("VK_HOME", {}, toolbox.win); + is( + toolbox.doc.activeElement, + selectable[0], + "First item selected with home key." + ); + EventUtils.synthesizeKey("VK_END", {}, toolbox.win); + is( + toolbox.doc.activeElement, + selectable[selectable.length - 1], + "End item selected with down key." + ); +} diff --git a/devtools/client/framework/test/browser_toolbox_options.js b/devtools/client/framework/test/browser_toolbox_options.js new file mode 100644 index 0000000000..3b5d7f2661 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_options.js @@ -0,0 +1,556 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that changing preferences in the options panel updates the prefs +// and toggles appropriate things in the toolbox. + +var doc = null, + toolbox = null, + panelWin = null, + modifiedPrefs = []; +const L10N = new LocalizationHelper( + "devtools/client/locales/toolbox.properties" +); +const { PrefObserver } = require("resource://devtools/client/shared/prefs.js"); + +add_task(async function () { + const URL = + "data:text/html;charset=utf8,test for dynamically registering " + + "and unregistering tools"; + registerNewTool(); + const tab = await addTab(URL); + toolbox = await gDevTools.showToolboxForTab(tab); + + doc = toolbox.doc; + await registerNewPerToolboxTool(); + await testSelectTool(); + await testOptionsShortcut(); + await testOptions(); + await testToggleTools(); + + // Test that registered WebExtensions becomes entries in the + // options panel and toggling their checkbox toggle the related + // preference. + await registerNewWebExtensions(); + await testToggleWebExtensions(); + + await cleanup(); +}); + +function registerNewTool() { + const toolDefinition = { + id: "testTool", + isToolSupported: () => true, + visibilityswitch: "devtools.test-tool.enabled", + url: "about:blank", + label: "someLabel", + }; + + ok(gDevTools, "gDevTools exists"); + ok( + !gDevTools.getToolDefinitionMap().has("testTool"), + "The tool is not registered" + ); + + gDevTools.registerTool(toolDefinition); + ok( + gDevTools.getToolDefinitionMap().has("testTool"), + "The tool is registered" + ); +} + +// Register a fake WebExtension to check that it is +// listed in the toolbox options. +function registerNewWebExtensions() { + // Register some fake extensions and init the related preferences + // (similarly to ext-devtools.js). + for (let i = 0; i < 2; i++) { + const extPref = `devtools.webextensions.fakeExtId${i}.enabled`; + Services.prefs.setBoolPref(extPref, true); + + toolbox.registerWebExtension(`fakeUUID${i}`, { + name: `Fake WebExtension ${i}`, + pref: extPref, + }); + } +} + +function registerNewPerToolboxTool() { + const toolDefinition = { + id: "test-pertoolbox-tool", + isToolSupported: () => true, + visibilityswitch: "devtools.test-pertoolbox-tool.enabled", + url: "about:blank", + label: "perToolboxSomeLabel", + }; + + ok(gDevTools, "gDevTools exists"); + ok( + !gDevTools.getToolDefinitionMap().has("test-pertoolbox-tool"), + "The per-toolbox tool is not registered globally" + ); + + ok(toolbox, "toolbox exists"); + ok( + !toolbox.hasAdditionalTool("test-pertoolbox-tool"), + "The per-toolbox tool is not yet registered to the toolbox" + ); + + toolbox.addAdditionalTool(toolDefinition); + + ok( + !gDevTools.getToolDefinitionMap().has("test-pertoolbox-tool"), + "The per-toolbox tool is not registered globally" + ); + ok( + toolbox.hasAdditionalTool("test-pertoolbox-tool"), + "The per-toolbox tool has been registered to the toolbox" + ); +} + +async function testSelectTool() { + info("Checking to make sure that the options panel can be selected."); + + const onceSelected = toolbox.once("options-selected"); + toolbox.selectTool("options"); + await onceSelected; + ok(true, "Toolbox selected via selectTool method"); +} + +async function testOptionsShortcut() { + info("Selecting another tool, then reselecting options panel with keyboard."); + + await toolbox.selectTool("webconsole"); + is(toolbox.currentToolId, "webconsole", "webconsole is selected"); + synthesizeKeyShortcut(L10N.getStr("toolbox.help.key")); + is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key"); + synthesizeKeyShortcut(L10N.getStr("toolbox.help.key")); + is(toolbox.currentToolId, "webconsole", "webconsole is reselected"); + synthesizeKeyShortcut(L10N.getStr("toolbox.help.key")); + is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key"); +} + +async function testOptions() { + const tool = toolbox.getPanel("options"); + panelWin = tool.panelWin; + const prefNodes = tool.panelDoc.querySelectorAll( + "input[type=checkbox][data-pref]" + ); + + // Store modified pref names so that they can be cleared on error. + for (const node of tool.panelDoc.querySelectorAll("[data-pref]")) { + const pref = node.getAttribute("data-pref"); + modifiedPrefs.push(pref); + } + + for (const node of prefNodes) { + const prefValue = GetPref(node.getAttribute("data-pref")); + + // Test clicking the checkbox for each options pref + await testMouseClick(node, prefValue); + + // Do again with opposite values to reset prefs + await testMouseClick(node, !prefValue); + } + + const prefSelects = tool.panelDoc.querySelectorAll("select[data-pref]"); + for (const node of prefSelects) { + await testSelect(node); + } +} + +async function testSelect(select) { + const pref = select.getAttribute("data-pref"); + const options = Array.from(select.options); + info("Checking select for: " + pref); + + is( + `${select.options[select.selectedIndex].value}`, + `${GetPref(pref)}`, + "select starts out selected" + ); + + for (const option of options) { + if (options.indexOf(option) === select.selectedIndex) { + continue; + } + + const observer = new PrefObserver("devtools."); + + let changeSeen = false; + const changeSeenPromise = new Promise(resolve => { + observer.once(pref, () => { + changeSeen = true; + is( + `${GetPref(pref)}`, + `${option.value}`, + "Preference been switched for " + pref + ); + resolve(); + }); + }); + + select.selectedIndex = options.indexOf(option); + const changeEvent = new Event("change"); + select.dispatchEvent(changeEvent); + + await changeSeenPromise; + + ok(changeSeen, "Correct pref was changed"); + observer.destroy(); + } +} + +async function testMouseClick(node, prefValue) { + const observer = new PrefObserver("devtools."); + + const pref = node.getAttribute("data-pref"); + let changeSeen = false; + const changeSeenPromise = new Promise(resolve => { + observer.once(pref, () => { + changeSeen = true; + is(GetPref(pref), !prefValue, "New value is correct for " + pref); + resolve(); + }); + }); + + node.scrollIntoView(); + + // We use executeSoon here to ensure that the element is in view and + // clickable. + executeSoon(function () { + info("Click event synthesized for pref " + pref); + EventUtils.synthesizeMouseAtCenter(node, {}, panelWin); + }); + + await changeSeenPromise; + + ok(changeSeen, "Correct pref was changed"); + observer.destroy(); +} + +async function testToggleWebExtensions() { + const disabledExtensions = new Set(); + const toggleableWebExtensions = toolbox.listWebExtensions(); + + function toggleWebExtension(node) { + node.scrollIntoView(); + EventUtils.synthesizeMouseAtCenter(node, {}, panelWin); + } + + function assertExpectedDisabledExtensions() { + for (const ext of toggleableWebExtensions) { + if (disabledExtensions.has(ext)) { + ok( + !toolbox.isWebExtensionEnabled(ext.uuid), + `The WebExtension "${ext.name}" should be disabled` + ); + } else { + ok( + toolbox.isWebExtensionEnabled(ext.uuid), + `The WebExtension "${ext.name}" should be enabled` + ); + } + } + } + + function assertAllExtensionsDisabled() { + const enabledUUIDs = toggleableWebExtensions + .filter(ext => toolbox.isWebExtensionEnabled(ext.uuid)) + .map(ext => ext.uuid); + + Assert.deepEqual( + enabledUUIDs, + [], + "All the registered WebExtensions should be disabled" + ); + } + + function assertAllExtensionsEnabled() { + const disabledUUIDs = toolbox + .listWebExtensions() + .filter(ext => !toolbox.isWebExtensionEnabled(ext.uuid)) + .map(ext => ext.uuid); + + Assert.deepEqual( + disabledUUIDs, + [], + "All the registered WebExtensions should be enabled" + ); + } + + function getWebExtensionNodes() { + const toolNodes = panelWin.document.querySelectorAll( + "#default-tools-box input[type=checkbox]:not([data-unsupported])," + + "#additional-tools-box input[type=checkbox]:not([data-unsupported])" + ); + + return [...toolNodes].filter(node => { + return toggleableWebExtensions.some( + ({ uuid }) => node.getAttribute("id") === `webext-${uuid}` + ); + }); + } + + let webExtensionNodes = getWebExtensionNodes(); + + is( + webExtensionNodes.length, + toggleableWebExtensions.length, + "There should be a toggle checkbox for every WebExtension registered" + ); + + for (const ext of toggleableWebExtensions) { + ok( + toolbox.isWebExtensionEnabled(ext.uuid), + `The WebExtension "${ext.name}" is initially enabled` + ); + } + + // Store modified pref names so that they can be cleared on error. + for (const ext of toggleableWebExtensions) { + modifiedPrefs.push(ext.pref); + } + + // Turn each registered WebExtension to disabled. + for (const node of webExtensionNodes) { + toggleWebExtension(node); + + const toggledExt = toggleableWebExtensions.find(ext => { + return node.id == `webext-${ext.uuid}`; + }); + ok(toggledExt, "Found a WebExtension for the checkbox element"); + disabledExtensions.add(toggledExt); + + assertExpectedDisabledExtensions(); + } + + assertAllExtensionsDisabled(); + + // Turn each registered WebExtension to enabled. + for (const node of webExtensionNodes) { + toggleWebExtension(node); + + const toggledExt = toggleableWebExtensions.find(ext => { + return node.id == `webext-${ext.uuid}`; + }); + ok(toggledExt, "Found a WebExtension for the checkbox element"); + disabledExtensions.delete(toggledExt); + + assertExpectedDisabledExtensions(); + } + + assertAllExtensionsEnabled(); + + // Unregister the WebExtensions one by one, and check that only the expected + // ones have been unregistered, and the remaining onea are still listed. + for (const ext of toggleableWebExtensions) { + ok( + !!toolbox.listWebExtensions().length, + "There should still be extensions registered" + ); + toolbox.unregisterWebExtension(ext.uuid); + + const registeredUUIDs = toolbox.listWebExtensions().map(item => item.uuid); + ok( + !registeredUUIDs.includes(ext.uuid), + `the WebExtension "${ext.name}" should have been unregistered` + ); + + webExtensionNodes = getWebExtensionNodes(); + + const checkboxEl = webExtensionNodes.find( + el => el.id === `webext-${ext.uuid}` + ); + is( + checkboxEl, + undefined, + "The unregistered WebExtension checkbox should have been removed" + ); + + is( + registeredUUIDs.length, + webExtensionNodes.length, + "There should be the expected number of WebExtensions checkboxes" + ); + } + + is( + toolbox.listWebExtensions().length, + 0, + "All WebExtensions have been unregistered" + ); + + webExtensionNodes = getWebExtensionNodes(); + + is( + webExtensionNodes.length, + 0, + "There should not be any checkbox for the unregistered WebExtensions" + ); +} + +function getToolNode(id) { + return panelWin.document.getElementById(id); +} + +async function testToggleTools() { + const toolNodes = panelWin.document.querySelectorAll( + "#default-tools-box input[type=checkbox]:not([data-unsupported])," + + "#additional-tools-box input[type=checkbox]:not([data-unsupported])" + ); + const toolNodeIds = [...toolNodes].map(node => node.id); + const enabledToolIds = [...toolNodes] + .filter(node => node.checked) + .map(node => node.id); + + const toggleableTools = gDevTools + .getDefaultTools() + .filter(tool => { + return tool.visibilityswitch; + }) + .concat(gDevTools.getAdditionalTools()) + .concat(toolbox.getAdditionalTools()); + + for (const node of toolNodes) { + const id = node.getAttribute("id"); + ok( + toggleableTools.some(tool => tool.id === id), + "There should be a toggle checkbox for: " + id + ); + } + + // Store modified pref names so that they can be cleared on error. + for (const tool of toggleableTools) { + const pref = tool.visibilityswitch; + modifiedPrefs.push(pref); + } + + // Toggle each tool + for (const id of toolNodeIds) { + await toggleTool(getToolNode(id)); + } + + // Toggle again to reset tool enablement state + for (const id of toolNodeIds) { + await toggleTool(getToolNode(id)); + } + + // Test that a tool can still be added when no tabs are present: + // Disable all tools + for (const id of enabledToolIds) { + await toggleTool(getToolNode(id)); + } + // Re-enable the tools which are enabled by default + for (const id of enabledToolIds) { + await toggleTool(getToolNode(id)); + } + + // Toggle first, middle, and last tools to ensure that toolbox tabs are + // inserted in order + const firstToolId = toolNodeIds[0]; + const middleToolId = toolNodeIds[(toolNodeIds.length / 2) | 0]; + const lastToolId = toolNodeIds[toolNodeIds.length - 1]; + + await toggleTool(getToolNode(firstToolId)); + await toggleTool(getToolNode(firstToolId)); + await toggleTool(getToolNode(middleToolId)); + await toggleTool(getToolNode(middleToolId)); + await toggleTool(getToolNode(lastToolId)); + await toggleTool(getToolNode(lastToolId)); +} + +/** + * Toggle tool node checkbox. Note: because toggling the checkbox will result in + * re-rendering of the tool list, we must re-query the checkboxes every time. + */ +async function toggleTool(node) { + const toolId = node.getAttribute("id"); + + const registeredPromise = new Promise(resolve => { + if (node.checked) { + gDevTools.once( + "tool-unregistered", + checkUnregistered.bind(null, toolId, resolve) + ); + } else { + gDevTools.once( + "tool-registered", + checkRegistered.bind(null, toolId, resolve) + ); + } + }); + node.scrollIntoView(); + EventUtils.synthesizeMouseAtCenter(node, {}, panelWin); + + await registeredPromise; +} + +function checkUnregistered(toolId, resolve, data) { + if (data == toolId) { + ok(true, "Correct tool removed"); + // checking tab on the toolbox + ok( + !doc.getElementById("toolbox-tab-" + toolId), + "Tab removed for " + toolId + ); + } else { + ok(false, "Something went wrong, " + toolId + " was not unregistered"); + } + resolve(); +} + +async function checkRegistered(toolId, resolve, data) { + if (data == toolId) { + ok(true, "Correct tool added back"); + // checking tab on the toolbox + const button = await lookupButtonForToolId(toolId); + ok(button, "Tab added back for " + toolId); + } else { + ok(false, "Something went wrong, " + toolId + " was not registered"); + } + resolve(); +} + +function GetPref(name) { + const type = Services.prefs.getPrefType(name); + switch (type) { + case Services.prefs.PREF_STRING: + return Services.prefs.getCharPref(name); + case Services.prefs.PREF_INT: + return Services.prefs.getIntPref(name); + case Services.prefs.PREF_BOOL: + return Services.prefs.getBoolPref(name); + default: + throw new Error("Unknown type"); + } +} + +/** + * Find the button from specified toolId. + * Generally, button which access to the tool panel is in toolbox or + * tools menu(in the Chevron menu). + */ +async function lookupButtonForToolId(toolId) { + let button = doc.getElementById("toolbox-tab-" + toolId); + if (!button) { + // search from the tools menu. + await openChevronMenu(toolbox); + button = doc.querySelector("#tools-chevron-menupopup-" + toolId); + + await closeChevronMenu(toolbox); + } + return button; +} + +async function cleanup() { + gDevTools.unregisterTool("testTool"); + await toolbox.destroy(); + gBrowser.removeCurrentTab(); + for (const pref of modifiedPrefs) { + Services.prefs.clearUserPref(pref); + } + toolbox = doc = panelWin = modifiedPrefs = null; +} diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js b/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js new file mode 100644 index 0000000000..417970bc07 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js @@ -0,0 +1,270 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let TEST_URL = + "data:text/html;charset=utf8,test for dynamically " + + "registering and unregistering tools"; + +// The frames button is only shown if the page has at least one iframe so we +// need to add one to the test page. +TEST_URL += ''; +// The error count button is only shown if there are errors on the page +TEST_URL += ''; + +var modifiedPrefs = []; +registerCleanupFunction(() => { + for (const pref of modifiedPrefs) { + Services.prefs.clearUserPref(pref); + } +}); + +const TOGGLE_BUTTONS = [ + "command-button-measure", + "command-button-rulers", + "command-button-responsive", + "command-button-pick", +]; + +add_task(async function test() { + const tab = await addTab(TEST_URL); + let toolbox = await gDevTools.showToolboxForTab(tab); + const optionsPanelWin = await selectOptionsPanel(toolbox); + await testToggleToolboxButtons(toolbox, optionsPanelWin); + toolbox = await testPrefsAreRespectedWhenReopeningToolbox(); + await testButtonStateOnClick(toolbox); + + await toolbox.destroy(); +}); + +async function selectOptionsPanel(toolbox) { + info("Selecting the options panel"); + + const onOptionsSelected = toolbox.once("options-selected"); + toolbox.selectTool("options"); + const optionsPanel = await onOptionsSelected; + ok(true, "Options panel selected via selectTool method"); + return optionsPanel.panelWin; +} + +async function testToggleToolboxButtons(toolbox, optionsPanelWin) { + const checkNodes = [ + ...optionsPanelWin.document.querySelectorAll( + "#enabled-toolbox-buttons-box input[type=checkbox]" + ), + ]; + + // Filter out all the buttons which are not supported on the current target. + // (DevTools Experimental Preferences etc...) + const toolbarButtons = toolbox.toolbarButtons.filter(tool => + tool.isToolSupported(toolbox) + ); + + const visibleToolbarButtons = toolbarButtons.filter(tool => tool.isVisible); + + const toolbarButtonNodes = [ + ...toolbox.doc.querySelectorAll(".command-button"), + ]; + + is( + checkNodes.length, + toolbarButtons.length, + "All of the buttons are toggleable." + ); + is( + visibleToolbarButtons.length, + toolbarButtonNodes.length, + "All of the DOM buttons are toggleable." + ); + + for (const tool of toolbarButtons) { + const id = tool.id; + const matchedCheckboxes = checkNodes.filter(node => node.id === id); + const matchedButtons = toolbarButtonNodes.filter( + button => button.id === id + ); + if (tool.isVisible) { + is( + matchedCheckboxes.length, + 1, + "There should be a single toggle checkbox for: " + id + ); + is( + matchedCheckboxes[0].nextSibling.textContent, + tool.description, + "The label for checkbox matches the tool definition." + ); + is( + matchedButtons.length, + 1, + "There should be a DOM button for the visible: " + id + ); + + // The error count button title isn't its description + if (id !== "command-button-errorcount") { + is( + matchedButtons[0].getAttribute("title"), + tool.description, + "The tooltip for button matches the tool definition." + ); + } + + if (TOGGLE_BUTTONS.includes(id)) { + is( + matchedButtons[0].getAttribute("aria-pressed"), + "false", + `The aria-pressed attribute is set to false for ${id} button` + ); + } else { + is( + matchedButtons[0].getAttribute("aria-pressed"), + null, + `The ${id} button does not have the aria-pressed attribute` + ); + } + } else { + is( + matchedButtons.length, + 0, + "There should not be a DOM button for the invisible: " + id + ); + } + } + + // Store modified pref names so that they can be cleared on error. + for (const tool of toolbarButtons) { + const pref = tool.visibilityswitch; + modifiedPrefs.push(pref); + } + + // Try checking each checkbox, making sure that it changes the preference + for (const node of checkNodes) { + const tool = toolbarButtons.filter( + commandButton => commandButton.id === node.id + )[0]; + const isVisible = getBoolPref(tool.visibilityswitch); + + testPreferenceAndUIStateIsConsistent(toolbox, optionsPanelWin); + node.click(); + testPreferenceAndUIStateIsConsistent(toolbox, optionsPanelWin); + + const isVisibleAfterClick = getBoolPref(tool.visibilityswitch); + + is( + isVisible, + !isVisibleAfterClick, + "Clicking on the node should have toggled visibility preference for " + + tool.visibilityswitch + ); + + if (isVisibleAfterClick) { + const matchedButton = toolbox.doc.getElementById(tool.id); + if (TOGGLE_BUTTONS.includes(tool.id)) { + is( + matchedButton.getAttribute("aria-pressed"), + "false", + `The aria-pressed attribute is set to false for ${tool.id} button` + ); + } else { + is( + matchedButton.getAttribute("aria-pressed"), + null, + `The ${tool.id} button does not have the aria-pressed attribute` + ); + } + } + } +} + +async function testPrefsAreRespectedWhenReopeningToolbox() { + info("Closing toolbox to test after reopening"); + await gDevTools.closeToolboxForTab(gBrowser.selectedTab); + + const toolbox = await gDevTools.showToolboxForTab(gBrowser.selectedTab); + const optionsPanelWin = await selectOptionsPanel(toolbox); + + info("Toolbox has been reopened. Checking UI state."); + await testPreferenceAndUIStateIsConsistent(toolbox, optionsPanelWin); + return toolbox; +} + +function testPreferenceAndUIStateIsConsistent(toolbox, optionsPanelWin) { + const checkNodes = [ + ...optionsPanelWin.document.querySelectorAll( + "#enabled-toolbox-buttons-box input[type=checkbox]" + ), + ]; + const toolboxButtonNodes = [ + ...toolbox.doc.querySelectorAll(".command-button"), + ]; + + for (const tool of toolbox.toolbarButtons) { + const isVisible = getBoolPref(tool.visibilityswitch); + + const button = toolboxButtonNodes.find( + toolboxButton => toolboxButton.id === tool.id + ); + is(!!button, isVisible, "Button visibility matches pref for " + tool.id); + + const check = checkNodes.filter(node => node.id === tool.id)[0]; + if (check) { + is( + check.checked, + isVisible, + "Checkbox should be selected based on current pref for " + tool.id + ); + } + } +} + +async function testButtonStateOnClick(toolbox) { + const toolboxButtons = ["#command-button-rulers", "#command-button-measure"]; + for (const toolboxButton of toolboxButtons) { + const button = toolbox.doc.querySelector(toolboxButton); + if (!button) { + ok(false, `Couldn't find ${toolboxButton}`); + continue; + } + + const isChecked = waitUntil(() => button.classList.contains("checked")); + is( + button.getAttribute("aria-pressed"), + "false", + `${toolboxButton} has aria-pressed set to false when it's off` + ); + + button.click(); + await isChecked; + ok( + button.classList.contains("checked"), + `Button for ${toolboxButton} can be toggled on` + ); + is( + button.getAttribute("aria-pressed"), + "true", + `${toolboxButton} has aria-pressed set to true when it's on` + ); + + const isUnchecked = waitUntil(() => !button.classList.contains("checked")); + button.click(); + await isUnchecked; + ok( + !button.classList.contains("checked"), + `Button for ${toolboxButton} can be toggled off` + ); + is( + button.getAttribute("aria-pressed"), + "false", + `aria-pressed is set back to false on ${toolboxButton} after it has been toggled off` + ); + } +} + +function getBoolPref(key) { + try { + return Services.prefs.getBoolPref(key); + } catch (e) { + return false; + } +} diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_cache-01.js b/devtools/client/framework/test/browser_toolbox_options_disable_cache-01.js new file mode 100644 index 0000000000..77c2b1bccb --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_options_disable_cache-01.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Tests that disabling the cache for a tab works as it should when toolboxes +// are not toggled. +/* import-globals-from helper_disable_cache.js */ +loadHelperScript("helper_disable_cache.js"); + +add_task(async function () { + // Disable rcwn to make cache behavior deterministic. + await pushPref("network.http.rcwn.enabled", false); + + // Ensure that the setting is cleared after the test. + registerCleanupFunction(() => { + info("Resetting devtools.cache.disabled to false."); + Services.prefs.setBoolPref("devtools.cache.disabled", false); + }); + + // Initialise tabs: 1 and 2 with a toolbox, 3 and 4 without. + for (const tab of tabs) { + await initTab(tab, tab.startToolbox); + } + + // Ensure cache is enabled for all tabs. + await checkCacheStateForAllTabs([true, true, true, true]); + + // Check the checkbox in tab 0 and ensure cache is disabled for tabs 0 and 1. + await setDisableCacheCheckboxChecked(tabs[0], true); + await checkCacheStateForAllTabs([false, false, true, true]); + + await finishUp(); +}); diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_cache-02.js b/devtools/client/framework/test/browser_toolbox_options_disable_cache-02.js new file mode 100644 index 0000000000..235893ba60 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_options_disable_cache-02.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Tests that disabling the cache for a tab works as it should when toolboxes +// are toggled. +/* import-globals-from helper_disable_cache.js */ +loadHelperScript("helper_disable_cache.js"); + +add_task(async function () { + // Disable rcwn to make cache behavior deterministic. + await pushPref("network.http.rcwn.enabled", false); + + // Ensure that the setting is cleared after the test. + registerCleanupFunction(() => { + info("Resetting devtools.cache.disabled to false."); + Services.prefs.setBoolPref("devtools.cache.disabled", false); + }); + + // Initialise tabs: 1 and 2 with a toolbox, 3 and 4 without. + for (const tab of tabs) { + await initTab(tab, tab.startToolbox); + } + + // Disable cache in tab 0 + await setDisableCacheCheckboxChecked(tabs[0], true); + + // Open toolbox in tab 2 and ensure the cache is then disabled. + tabs[2].toolbox = await gDevTools.showToolboxForTab(tabs[2].tab, { + toolId: "options", + }); + await checkCacheEnabled(tabs[2], false); + + // Close toolbox in tab 2 and ensure the cache is enabled again + await tabs[2].toolbox.destroy(); + await checkCacheEnabled(tabs[2], true); + + // Open toolbox in tab 2 and ensure the cache is then disabled. + tabs[2].toolbox = await gDevTools.showToolboxForTab(tabs[2].tab, { + toolId: "options", + }); + await checkCacheEnabled(tabs[2], false); + + // Check the checkbox in tab 2 and ensure cache is enabled for all tabs. + await setDisableCacheCheckboxChecked(tabs[2], false); + await checkCacheStateForAllTabs([true, true, true, true]); + + await finishUp(); +}); diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_cache-03.js b/devtools/client/framework/test/browser_toolbox_options_disable_cache-03.js new file mode 100644 index 0000000000..f3a53f6422 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_options_disable_cache-03.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that even when the cache is disabled, the inspector/styleeditor don't fetch again +// stylesheets from the server to display them in devtools, but use the cached version. + +const TEST_CSS = URL_ROOT + "browser_toolbox_options_disable_cache.css.sjs"; +const TEST_PAGE = ` + + + + + +`; + +add_task(async function () { + info("Setup preferences for testing"); + // Disable rcwn to make cache behavior deterministic. + await pushPref("network.http.rcwn.enabled", false); + // Disable the cache. + await pushPref("devtools.cache.disabled", true); + + info("Open inspector"); + const toolbox = await openNewTabAndToolbox( + `data:text/html;charset=UTF-8,${encodeURIComponent(TEST_PAGE)}`, + "inspector" + ); + const inspector = toolbox.getPanel("inspector"); + + info( + "Check that the CSS content loaded in the page " + + "and the one shown in the inspector are the same" + ); + const webContent = await getWebContent(); + const inspectorContent = await getInspectorContent(inspector); + is( + webContent, + inspectorContent, + "The contents of both web and DevTools are same" + ); + + await closeTabAndToolbox(); +}); + +async function getInspectorContent(inspector) { + const ruleView = inspector.getPanel("ruleview").view; + const valueEl = await waitFor(() => + ruleView.styleDocument.querySelector(".ruleview-propertyvalue") + ); + return valueEl.textContent; +} + +async function getWebContent() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const doc = content.document; + return doc.ownerGlobal.getComputedStyle(doc.body, "::before").content; + }); +} diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_cache.css.sjs b/devtools/client/framework/test/browser_toolbox_options_disable_cache.css.sjs new file mode 100644 index 0000000000..0b5932ca02 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_options_disable_cache.css.sjs @@ -0,0 +1,10 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) { + // This returns always new and different CSS content. + const page = `body::before { content: "${Date.now()}"; }`; + response.setHeader("Content-Type", "text/css; charset=utf-8", false); + response.setHeader("Content-Length", page.length + "", false); + response.write(page); +} diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_cache.sjs b/devtools/client/framework/test/browser_toolbox_options_disable_cache.sjs new file mode 100644 index 0000000000..dc67043be1 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_options_disable_cache.sjs @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) { + const Etag = '"4d881ab-b03-435f0a0f9ef00"'; + const IfNoneMatch = request.hasHeader("If-None-Match") + ? request.getHeader("If-None-Match") + : ""; + + const guid = "xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx".replace( + /[xy]/g, + function (c) { + const r = (Math.random() * 16) | 0; + const v = c === "x" ? r : (r & 0x3) | 0x8; + + return v.toString(16); + } + ); + + const page = "

" + guid + "

"; + + response.setHeader("Etag", Etag, false); + + if (IfNoneMatch === Etag) { + response.setStatusLine(request.httpVersion, "304", "Not Modified"); + } else { + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.setHeader("Content-Length", page.length + "", false); + response.write(page); + } +} diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_js.html b/devtools/client/framework/test/browser_toolbox_options_disable_js.html new file mode 100644 index 0000000000..a8c70c2e4d --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_options_disable_js.html @@ -0,0 +1,47 @@ + + + + browser_toolbox_options_disablejs.html + + + + + +

Test in page

+ + +
+
No output
+

Test in iframe

+ + + diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_js.js b/devtools/client/framework/test/browser_toolbox_options_disable_js.js new file mode 100644 index 0000000000..4f08838c9f --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_options_disable_js.js @@ -0,0 +1,141 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that disabling JavaScript for a tab works as it should. + +const TEST_URI = URL_ROOT_SSL + "browser_toolbox_options_disable_js.html"; + +add_task(async function () { + const tab = await addTab(TEST_URI); + + // Start on the options panel from where we will toggle the disabling javascript + // option. + const toolbox = await gDevTools.showToolboxForTab(tab, { toolId: "options" }); + + await testJSEnabled(); + await testJSEnabledIframe(); + + // Disable JS. + await toggleJS(toolbox); + + await testJSDisabled(); + await testJSDisabledIframe(); + + // Navigate and check JS is still disabled + for (let i = 0; i < 10; i++) { + await navigateTo(`${TEST_URI}?nocache=${i}`); + await testJSDisabled(); + await testJSDisabledIframe(); + } + + // Re-enable JS. + await toggleJS(toolbox); + + await testJSEnabled(); + await testJSEnabledIframe(); + + await toolbox.destroy(); + gBrowser.removeCurrentTab(); +}); + +async function testJSEnabled() { + info("Testing that JS is enabled"); + + // We use waitForTick here because switching browsingContext.allowJavascript + // to true takes a while to become live. + await waitForTick(); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const doc = content.document; + const output = doc.getElementById("output"); + doc.querySelector("#logJSEnabled").click(); + is( + output.textContent, + "JavaScript Enabled", + 'Output is "JavaScript Enabled"' + ); + }); +} + +async function testJSEnabledIframe() { + info("Testing that JS is enabled in the iframe"); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const doc = content.document; + const iframe = doc.querySelector("iframe"); + const iframeDoc = iframe.contentDocument; + const output = iframeDoc.getElementById("output"); + iframeDoc.querySelector("#logJSEnabled").click(); + is( + output.textContent, + "JavaScript Enabled", + 'Output is "JavaScript Enabled" in iframe' + ); + }); +} + +async function toggleJS(toolbox) { + const panel = toolbox.getCurrentPanel(); + const cbx = panel.panelDoc.getElementById("devtools-disable-javascript"); + + if (cbx.checked) { + info("Clearing checkbox to re-enable JS"); + } else { + info("Checking checkbox to disable JS"); + } + + let javascriptEnabled = + await toolbox.commands.targetConfigurationCommand.isJavascriptEnabled(); + is( + javascriptEnabled, + !cbx.checked, + "targetConfigurationCommand.isJavascriptEnabled is correct before the toggle" + ); + + const browserLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser + ); + cbx.click(); + await browserLoaded; + + javascriptEnabled = + await toolbox.commands.targetConfigurationCommand.isJavascriptEnabled(); + is( + javascriptEnabled, + !cbx.checked, + "targetConfigurationCommand.isJavascriptEnabled is correctly updated" + ); +} + +async function testJSDisabled() { + info("Testing that JS is disabled"); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const doc = content.document; + const output = doc.getElementById("output"); + doc.querySelector("#logJSDisabled").click(); + + Assert.notStrictEqual( + output.textContent, + "JavaScript Disabled", + 'output is not "JavaScript Disabled"' + ); + }); +} + +async function testJSDisabledIframe() { + info("Testing that JS is disabled in the iframe"); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const doc = content.document; + const iframe = doc.querySelector("iframe"); + const iframeDoc = iframe.contentDocument; + const output = iframeDoc.getElementById("output"); + iframeDoc.querySelector("#logJSDisabled").click(); + Assert.notStrictEqual( + output.textContent, + "JavaScript Disabled", + 'output is not "JavaScript Disabled" in iframe' + ); + }); +} diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_js_iframe.html b/devtools/client/framework/test/browser_toolbox_options_disable_js_iframe.html new file mode 100644 index 0000000000..5006d24c9a --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_options_disable_js_iframe.html @@ -0,0 +1,34 @@ + + + browser_toolbox_options_disablejs.html + + + + + + + +
+
No output
+ + diff --git a/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.html b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.html new file mode 100644 index 0000000000..4065aabc2b --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.html @@ -0,0 +1,81 @@ + + + + browser_toolbox_options_enable_serviceworkers_testing.html + + + +

SW-test

+ + + diff --git a/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js new file mode 100644 index 0000000000..152f64f835 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that enabling Service Workers testing option enables the +// mServiceWorkersTestingEnabled attribute added to nsPIDOMWindow. + +// We explicitly want to test that service worker testing allows to use service +// workers on non-https, so we use mochi.test:8888 to avoid the automatic upgrade +// to https when dom.security.https_first is true. +const TEST_URI = + URL_ROOT_MOCHI_8888 + + "browser_toolbox_options_enable_serviceworkers_testing.html"; +const ELEMENT_ID = "devtools-enable-serviceWorkersTesting"; + +add_task(async function () { + await pushPref("dom.serviceWorkers.exemptFromPerDomainMax", true); + await pushPref("dom.serviceWorkers.enabled", true); + await pushPref("dom.serviceWorkers.testing.enabled", false); + // Force the test to start without service worker testing enabled + await pushPref("devtools.serviceWorkers.testing.enabled", false); + + const tab = await addTab(TEST_URI); + const toolbox = await openToolboxForTab(tab, "options"); + + let data = await register(); + is(data.success, false, "Register should fail with security error"); + + const panel = toolbox.getCurrentPanel(); + const cbx = panel.panelDoc.getElementById(ELEMENT_ID); + is(cbx.checked, false, "The checkbox shouldn't be checked"); + + info(`Checking checkbox to enable service workers testing`); + cbx.scrollIntoView(); + cbx.click(); + + await reloadBrowser(); + + data = await register(); + is(data.success, true, "Register should success"); + + await unregister(); + data = await registerAndUnregisterInFrame(); + is(data.success, true, "Register should success"); + + info("Workers should be turned back off when we closes the toolbox"); + await toolbox.destroy(); + + await reloadBrowser(); + data = await register(); + is(data.success, false, "Register should fail with security error"); +}); + +function sendMessage(name) { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [name], nameChild => { + return new Promise(resolve => { + const channel = new content.MessageChannel(); + content.postMessage(nameChild, "*", [channel.port2]); + channel.port1.onmessage = function (msg) { + resolve(msg.data); + channel.port1.close(); + }; + }); + }); +} + +function register() { + return sendMessage("devtools:sw-test:register"); +} + +function unregister(swr) { + return sendMessage("devtools:sw-test:unregister"); +} + +function registerAndUnregisterInFrame() { + return sendMessage("devtools:sw-test:iframe:register-and-unregister"); +} diff --git a/devtools/client/framework/test/browser_toolbox_options_frames_button.js b/devtools/client/framework/test/browser_toolbox_options_frames_button.js new file mode 100644 index 0000000000..50adeda39b --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_options_frames_button.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the frames button is always visible when the user is on the options panel. +// Test that the button is disabled if the current target has no frames. +// Test that the button is enabled otherwise. + +const TEST_URL = "data:text/html;charset=utf8,test frames button visibility"; +const TEST_IFRAME_URL = "data:text/plain,iframe"; +const TEST_IFRAME_URL2 = "data:text/plain,iframe2"; +const TEST_URL_FRAMES = + TEST_URL + + `` + + ``; +const FRAME_BUTTON_PREF = "devtools.command-button-frames.enabled"; + +add_task(async function () { + // Hide the button by default. + await pushPref(FRAME_BUTTON_PREF, false); + + const tab = await addTab(TEST_URL); + + info("Open the toolbox on the Options panel"); + const toolbox = await gDevTools.showToolboxForTab(tab, { toolId: "options" }); + const doc = toolbox.doc; + + const optionsPanel = toolbox.getCurrentPanel(); + + let framesButton = doc.getElementById("command-button-frames"); + ok(!framesButton, "Frames button is not rendered."); + + const optionsDoc = optionsPanel.panelWin.document; + const framesButtonCheckbox = optionsDoc.getElementById( + "command-button-frames" + ); + framesButtonCheckbox.click(); + + info("Wait for the frame button to be rendered"); + framesButton = await waitFor(() => + doc.getElementById("command-button-frames") + ); + ok(framesButton.disabled, "Frames button is disabled."); + + info("Leave the options panel, the frames button should not be rendered."); + await toolbox.selectTool("webconsole"); + framesButton = doc.getElementById("command-button-frames"); + ok(!framesButton, "Frames button is no longer rendered."); + + info("Go back to the options panel, the frames button should rendered."); + await toolbox.selectTool("options"); + framesButton = doc.getElementById("command-button-frames"); + ok(framesButton, "Frames button is rendered again."); + + // Do not run the rest of this test when both fission and EFT is disabled as + // it prevents creating a target for the iframe + if (!isFissionEnabled() || !isEveryFrameTargetEnabled()) { + return; + } + + info("Navigate to a page with frames, the frames button should be enabled."); + await navigateTo(TEST_URL_FRAMES); + + framesButton = doc.getElementById("command-button-frames"); + ok(framesButton, "Frames button is still rendered."); + + await waitFor(() => { + framesButton = doc.getElementById("command-button-frames"); + return framesButton && !framesButton.disabled; + }); + + const { targetCommand } = toolbox.commands; + const iframeTarget = targetCommand + .getAllTargets([targetCommand.TYPES.FRAME]) + .find(target => target.url == TEST_IFRAME_URL); + ok(iframeTarget, "Found the target for the iframe"); + + ok( + !framesButton.classList.contains("checked"), + "Before selecting an iframe, the button is not checked" + ); + await toolbox.commands.targetCommand.selectTarget(iframeTarget); + ok( + framesButton.classList.contains("checked"), + "After selecting an iframe, the button is checked" + ); + + info("Remove this first iframe, which is currently selected"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + content.document.querySelector("iframe").remove(); + }); + + await waitFor(() => { + return targetCommand.selectedTargetFront == targetCommand.targetFront; + }, "Wait for the selected target to be back on the top target"); + + ok( + !framesButton.classList.contains("checked"), + "The button is back unchecked after having removed the selected iframe" + ); + + Services.prefs.clearUserPref(FRAME_BUTTON_PREF); +}); diff --git a/devtools/client/framework/test/browser_toolbox_options_multiple_tabs.js b/devtools/client/framework/test/browser_toolbox_options_multiple_tabs.js new file mode 100644 index 0000000000..74c0983d4e --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_options_multiple_tabs.js @@ -0,0 +1,130 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL = + "data:text/html;charset=utf8,test for dynamically registering " + + "and unregistering tools across multiple tabs"; + +let tab1, tab2, modifiedPref; + +add_task(async function () { + tab1 = await openToolboxOptionsInNewTab(); + tab2 = await openToolboxOptionsInNewTab(); + + await testToggleTools(); + await cleanup(); +}); + +async function openToolboxOptionsInNewTab() { + const tab = await addTab(URL); + const toolbox = await gDevTools.showToolboxForTab(tab); + const doc = toolbox.doc; + const panel = await toolbox.selectTool("options"); + const { id } = panel.panelDoc.querySelector( + "#default-tools-box input[type=checkbox]:not([data-unsupported], [checked])" + ); + + return { + tab, + toolbox, + doc, + panelWin: panel.panelWin, + // This is a getter becuse toolbox tools list gets re-setup every time there + // is a tool-registered or tool-undregistered event. + get checkbox() { + return panel.panelDoc.getElementById(id); + }, + }; +} + +async function testToggleTools() { + is(tab1.checkbox.id, tab2.checkbox.id, "Default tool box should be in sync."); + + const toolId = tab1.checkbox.id; + const testTool = gDevTools.getDefaultTools().find(tool => tool.id === toolId); + // Store modified pref names so that they can be cleared on error. + modifiedPref = testTool.visibilityswitch; + + info(`Registering tool ${toolId} in the first tab.`); + await toggleTool(tab1, toolId); + + info(`Unregistering tool ${toolId} in the first tab.`); + await toggleTool(tab1, toolId); + + info(`Registering tool ${toolId} in the second tab.`); + await toggleTool(tab2, toolId); + + info(`Unregistering tool ${toolId} in the second tab.`); + await toggleTool(tab2, toolId); + + info(`Registering tool ${toolId} in the first tab.`); + await toggleTool(tab1, toolId); + + info(`Unregistering tool ${toolId} in the second tab.`); + await toggleTool(tab2, toolId); +} + +async function toggleTool({ doc, panelWin, checkbox, tab }, toolId) { + const prevChecked = checkbox.checked; + + (prevChecked ? checkRegistered : checkUnregistered)(toolId); + + const onToggleTool = gDevTools.once( + `tool-${prevChecked ? "unregistered" : "registered"}` + ); + EventUtils.sendMouseEvent({ type: "click" }, checkbox, panelWin); + const id = await onToggleTool; + + is(id, toolId, `Correct event for ${toolId} was fired`); + // await new Promise(resolve => setTimeout(resolve, 60000)); + (prevChecked ? checkUnregistered : checkRegistered)(toolId); +} + +async function checkUnregistered(toolId) { + ok( + !getToolboxTab(tab1.doc, toolId), + `Tab for unregistered tool ${toolId} is not present in first toolbox` + ); + ok( + !tab1.checkbox.checked, + `Checkbox for unregistered tool ${toolId} is not checked in first toolbox` + ); + ok( + !getToolboxTab(tab2.doc, toolId), + `Tab for unregistered tool ${toolId} is not present in second toolbox` + ); + ok( + !tab2.checkbox.checked, + `Checkbox for unregistered tool ${toolId} is not checked in second toolbox` + ); +} + +function checkRegistered(toolId) { + ok( + getToolboxTab(tab1.doc, toolId), + `Tab for registered tool ${toolId} is present in first toolbox` + ); + ok( + tab1.checkbox.checked, + `Checkbox for registered tool ${toolId} is checked in first toolbox` + ); + ok( + getToolboxTab(tab2.doc, toolId), + `Tab for registered tool ${toolId} is present in second toolbox` + ); + ok( + tab2.checkbox.checked, + `Checkbox for registered tool ${toolId} is checked in second toolbox` + ); +} + +async function cleanup() { + await tab1.toolbox.destroy(); + await tab2.toolbox.destroy(); + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); + Services.prefs.clearUserPref(modifiedPref); + tab1 = tab2 = modifiedPref = null; +} diff --git a/devtools/client/framework/test/browser_toolbox_options_panel_toggle.js b/devtools/client/framework/test/browser_toolbox_options_panel_toggle.js new file mode 100644 index 0000000000..d94f7c14fb --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_options_panel_toggle.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether options panel toggled by key event and "Settings" on the meatball menu. + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +add_task(async function () { + const tab = await addTab("about:blank"); + const toolbox = await openToolboxForTab( + tab, + "webconsole", + Toolbox.HostType.BOTTOM + ); + + info("Check the option panel was selected after sending F1 key event"); + await sendOptionsKeyEvent(toolbox); + is(toolbox.currentToolId, "options", "The options panel should be selected"); + + info("Check the last selected panel was selected after sending F1 key event"); + await sendOptionsKeyEvent(toolbox); + is( + toolbox.currentToolId, + "webconsole", + "The webconsole panel should be selected" + ); + + info("Check the option panel was selected after clicking 'Settings' menu"); + await clickSettingsMenu(toolbox); + is(toolbox.currentToolId, "options", "The options panel should be selected"); + + info( + "Check the last selected panel was selected after clicking 'Settings' menu" + ); + await sendOptionsKeyEvent(toolbox); + is( + toolbox.currentToolId, + "webconsole", + "The webconsole panel should be selected" + ); + + info("Check the combination of key event and 'Settings' menu"); + await sendOptionsKeyEvent(toolbox); + await clickSettingsMenu(toolbox); + is( + toolbox.currentToolId, + "webconsole", + "The webconsole panel should be selected" + ); + await clickSettingsMenu(toolbox); + await sendOptionsKeyEvent(toolbox); + is( + toolbox.currentToolId, + "webconsole", + "The webconsole panel should be selected" + ); +}); + +async function sendOptionsKeyEvent(toolbox) { + const onReady = toolbox.once("select"); + EventUtils.synthesizeKey("VK_F1", {}, toolbox.win); + await onReady; +} + +async function clickSettingsMenu(toolbox) { + const onPopupShown = () => { + toolbox.doc.removeEventListener("popupshown", onPopupShown); + const menuItem = toolbox.doc.getElementById( + "toolbox-meatball-menu-settings" + ); + EventUtils.synthesizeMouseAtCenter(menuItem, {}, menuItem.ownerGlobal); + }; + toolbox.doc.addEventListener("popupshown", onPopupShown); + + const button = toolbox.doc.getElementById("toolbox-meatball-menu-button"); + await waitUntil(() => button.style.pointerEvents !== "none"); + EventUtils.synthesizeMouseAtCenter(button, {}, button.ownerGlobal); + + await toolbox.once("select"); +} diff --git a/devtools/client/framework/test/browser_toolbox_popups_debugging.js b/devtools/client/framework/test/browser_toolbox_popups_debugging.js new file mode 100644 index 0000000000..28b2603e80 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_popups_debugging.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test opening toolboxes against a tab and its popup + +const TEST_URL = "data:text/html,test for debugging popups"; +const POPUP_URL = "data:text/html,popup"; + +const POPUP_DEBUG_PREF = "devtools.popups.debug"; + +add_task(async function () { + const isPopupDebuggingEnabled = Services.prefs.getBoolPref(POPUP_DEBUG_PREF); + + info("Open a tab and debug it"); + const tab = await addTab(TEST_URL); + const toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: "webconsole", + }); + + info("Open a popup"); + const onTabOpened = once(gBrowser.tabContainer, "TabOpen"); + const onToolboxSwitchedToTab = toolbox.once("switched-host-to-tab"); + await SpecialPowers.spawn(tab.linkedBrowser, [POPUP_URL], url => { + content.open(url); + }); + const tabOpenEvent = await onTabOpened; + const popupTab = tabOpenEvent.target; + + const popupToolbox = await gDevTools.showToolboxForTab(popupTab); + if (isPopupDebuggingEnabled) { + ok( + !popupToolbox, + "When popup debugging is enabled, the popup should be debugged via the same toolbox as the original tab" + ); + info("Wait for internal event notifying about the toolbox being moved"); + await onToolboxSwitchedToTab; + const browserContainer = gBrowser.getBrowserContainer( + popupTab.linkedBrowser + ); + const iframe = browserContainer.querySelector( + ".devtools-toolbox-bottom-iframe" + ); + ok(iframe, "The original tab's toolbox moved to the popup tab"); + } else { + ok(popupToolbox, "We were able to spawn a toolbox for the popup"); + info("Close the popup toolbox and its tab"); + await popupToolbox.destroy(); + } + + info("Close the popup tab"); + gBrowser.removeCurrentTab(); + + info("Close the original tab toolbox and itself"); + await toolbox.destroy(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/client/framework/test/browser_toolbox_races.js b/devtools/client/framework/test/browser_toolbox_races.js new file mode 100644 index 0000000000..ede038e716 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_races.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Toggling the toolbox three time can take more than 45s on slow test machine +requestLongerTimeout(2); + +// Test toggling the toolbox quickly and see if there is any race breaking it. + +const URL = "data:text/html;charset=utf-8,Toggling devtools quickly"; +const { + gDevToolsBrowser, +} = require("resource://devtools/client/framework/devtools-browser.js"); + +add_task(async function () { + // Make sure this test starts with the selectedTool pref cleared. Previous + // tests select various tools, and that sets this pref. + Services.prefs.clearUserPref("devtools.toolbox.selectedTool"); + + await addTab(URL); + + let ready = 0, + destroy = 0, + destroyed = 0; + const onReady = () => { + ready++; + }; + const onDestroy = () => { + destroy++; + }; + const onDestroyed = () => { + destroyed++; + }; + gDevTools.on("toolbox-ready", onReady); + gDevTools.on("toolbox-destroy", onDestroy); + gDevTools.on("toolbox-destroyed", onDestroyed); + + // The current implementation won't toggle the toolbox many times, + // instead it will ignore toggles that happens while the toolbox is still + // creating or still destroying. + + info("Toggle the toolbox many times in a row"); + toggle(); + toggle(); + toggle(); + toggle(); + toggle(); + await wait(500); + + await waitFor(() => ready == 1); + is( + ready, + 1, + "No matter how many times we called toggle, it will only open the toolbox once" + ); + is( + destroy, + 0, + "All subsequent, synchronous call to toggle will be ignored and the toolbox won't be destroyed" + ); + is(destroyed, 0); + + info("Retoggle the toolbox many times in a row"); + toggle(); + toggle(); + toggle(); + toggle(); + toggle(); + await wait(500); + + await waitFor(() => destroyed == 1); + is(destroyed, 1, "Similarly, the toolbox will be closed"); + is(destroy, 1); + is( + ready, + 1, + "and no other toolbox will be opened. The subsequent toggle will be ignored." + ); + + gDevTools.off("toolbox-ready", onReady); + gDevTools.off("toolbox-destroy", onDestroy); + gDevTools.off("toolbox-destroyed", onDestroyed); + await wait(1000); + + gBrowser.removeCurrentTab(); +}); + +function toggle() { + // When enabling the input event prioritization, we'll reserve some time to + // process input events in each frame. In that case, the synthesized input + // events may delay the normal events. Replace synthesized key events by + // toggleToolboxCommand to prevent the synthesized input events jam the + // content process and cause the test timeout. + gDevToolsBrowser.toggleToolboxCommand(window.gBrowser); +} diff --git a/devtools/client/framework/test/browser_toolbox_raise.js b/devtools/client/framework/test/browser_toolbox_raise.js new file mode 100644 index 0000000000..1912d349d4 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_raise.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_URL = "data:text/html,test for opening toolbox in different hosts"; + +var { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +add_task(async function () { + const tab1 = await addTab(TEST_URL); + const tab2 = BrowserTestUtils.addTab(gBrowser); + + const toolbox = await gDevTools.showToolboxForTab(tab1); + await testBottomHost(toolbox, tab1, tab2); + + await testWindowHost(toolbox); + + Services.prefs.setCharPref("devtools.toolbox.host", Toolbox.HostType.BOTTOM); + + await toolbox.destroy(); + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); +}); + +async function testBottomHost(toolbox, tab1, tab2) { + // switch to another tab and test toolbox.raise() + gBrowser.selectedTab = tab2; + await new Promise(executeSoon); + is( + gBrowser.selectedTab, + tab2, + "Correct tab is selected before calling raise" + ); + + await toolbox.raise(); + is( + gBrowser.selectedTab, + tab1, + "Correct tab was selected after calling raise" + ); +} + +async function testWindowHost(toolbox) { + await toolbox.switchHost(Toolbox.HostType.WINDOW); + + info("Wait for the toolbox to be focused when switching to window host"); + // We can't wait for the "focus" event on toolbox.win.parent as this document is created while calling switchHost. + await waitFor(() => { + return Services.focus.activeWindow == toolbox.topWindow; + }); + + const onBrowserWindowFocused = new Promise(resolve => + window.addEventListener("focus", resolve, { once: true, capture: true }) + ); + + info("Focusing the browser window"); + window.focus(); + + info("Wait for the browser window to be focused"); + await onBrowserWindowFocused; + + // Now raise toolbox. + await toolbox.raise(); + is( + Services.focus.activeWindow, + toolbox.topWindow, + "the toolbox window is immediately focused after raise resolution" + ); +} diff --git a/devtools/client/framework/test/browser_toolbox_ready.js b/devtools/client/framework/test/browser_toolbox_ready.js new file mode 100644 index 0000000000..5d7d6be258 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_ready.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_URL = "data:text/html,test for toolbox being ready"; + +add_task(async function () { + const tab = await addTab(TEST_URL); + + const toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: "webconsole", + }); + ok(toolbox.isReady, "toolbox isReady is set"); + ok(toolbox.threadFront, "toolbox has a thread front"); + + const toolbox2 = await gDevTools.showToolboxForTab(tab, { + toolId: toolbox.toolId, + }); + is(toolbox2, toolbox, "same toolbox"); + + await toolbox.destroy(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/client/framework/test/browser_toolbox_remoteness_change.js b/devtools/client/framework/test/browser_toolbox_remoteness_change.js new file mode 100644 index 0000000000..af5f105214 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_remoteness_change.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const URL_1 = "about:robots"; +const URL_2 = + "data:text/html;charset=UTF-8," + + encodeURIComponent('
foo
'); + +// Testing navigation between processes +add_task(async function () { + info(`Testing navigation between processes`); + + info("Open a tab on a URL supporting only running in parent process"); + const tab = await addTab(URL_1); + is( + tab.linkedBrowser.currentURI.spec, + URL_1, + "We really are on the expected document" + ); + is( + tab.linkedBrowser.getAttribute("remote"), + "", + "And running in parent process" + ); + + const toolbox = await openToolboxForTab(tab); + + info("Navigate to a URL supporting remote process"); + await navigateTo(URL_2); + + is( + tab.linkedBrowser.getAttribute("remote"), + "true", + "Navigated to a data: URI and switching to remote" + ); + + info("Veryify we are inspecting the new document"); + const console = await toolbox.selectTool("webconsole"); + const { ui } = console.hud; + ui.wrapper.dispatchEvaluateExpression("document.location.href"); + await waitUntil(() => ui.outputNode.querySelector(".result")); + const url = ui.outputNode.querySelector(".result"); + + ok( + url.textContent.includes(URL_2), + "The console inspects the second document" + ); + + const { client } = toolbox.target; + await toolbox.destroy(); + ok(client._transportClosed, "The client is closed after closing the toolbox"); +}); diff --git a/devtools/client/framework/test/browser_toolbox_screenshot_tool.js b/devtools/client/framework/test/browser_toolbox_screenshot_tool.js new file mode 100644 index 0000000000..63c8b9fd58 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_screenshot_tool.js @@ -0,0 +1,126 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const exampleOrgDocument = `https://example.org/document-builder.sjs`; +const exampleComDocument = `https://example.com/document-builder.sjs`; + +const TEST_URL = `${exampleOrgDocument}?html= + + + `; + +add_task(async function () { + await pushPref("devtools.command-button-screenshot.enabled", true); + + await addTab(TEST_URL); + + info("Open the toolbox"); + const toolbox = await gDevTools.showToolboxForTab(gBrowser.selectedTab); + + const onScreenshotDownloaded = waitUntilScreenshot(); + toolbox.doc.querySelector("#command-button-screenshot").click(); + const filePath = await onScreenshotDownloaded; + + ok(!!filePath, "The screenshot was taken"); + + info("Create an image using the downloaded file as source"); + const image = new Image(); + const onImageLoad = once(image, "load"); + image.src = PathUtils.toFileURI(filePath); + await onImageLoad; + + const dpr = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => content.wrappedJSObject.devicePixelRatio + ); + + info("Check that the same-origin iframe is rendered in the screenshot"); + await checkImageColorAt({ + image, + y: 10 * dpr, + expectedColor: `rgb(255, 0, 0)`, + label: "The same-origin iframe is rendered properly in the screenshot", + }); + + info("Check that the remote iframe is rendered in the screenshot"); + await checkImageColorAt({ + image, + y: 60 * dpr, + expectedColor: `rgb(0, 255, 0)`, + label: "The remote iframe is rendered properly in the screenshot", + }); + + 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})` + ); + + // Remove the downloaded screenshot file + await IOUtils.remove(filePath); + + info( + "Check that taking a screenshot in a private window doesn't appear in the non-private window" + ); + const privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + ok(PrivateBrowsingUtils.isWindowPrivate(privateWindow), "window is private"); + const privateBrowser = privateWindow.gBrowser; + privateBrowser.selectedTab = BrowserTestUtils.addTab( + privateBrowser, + TEST_URL + ); + + info("private tab opened"); + ok( + PrivateBrowsingUtils.isBrowserPrivate(privateBrowser.selectedBrowser), + "tab window is private" + ); + + const privateToolbox = await gDevTools.showToolboxForTab( + privateBrowser.selectedTab + ); + + const onPrivateScreenshotDownloaded = waitUntilScreenshot({ + isWindowPrivate: true, + }); + privateToolbox.doc.querySelector("#command-button-screenshot").click(); + const privateScreenshotFilePath = await onPrivateScreenshotDownloaded; + ok( + !!privateScreenshotFilePath, + "The screenshot was taken in the private window" + ); + + // Remove the downloaded screenshot file + await IOUtils.remove(privateScreenshotFilePath); + + // cleanup the downloads + await resetDownloads(); + + const closePromise = BrowserTestUtils.windowClosed(privateWindow); + privateWindow.BrowserTryToCloseWindow(); + await closePromise; +}); diff --git a/devtools/client/framework/test/browser_toolbox_select_event.js b/devtools/client/framework/test/browser_toolbox_select_event.js new file mode 100644 index 0000000000..ebdae9af13 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_select_event.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PAGE_URL = "data:text/html;charset=utf-8,test select events"; + +requestLongerTimeout(2); + +add_task(async function () { + const tab = await addTab(PAGE_URL); + + let toolbox = await openToolboxForTab(tab, "webconsole", "bottom"); + await testSelectEvent("inspector"); + await testSelectEvent("webconsole"); + await testSelectEvent("styleeditor"); + await testSelectEvent("inspector"); + await testSelectEvent("webconsole"); + await testSelectEvent("styleeditor"); + + await testToolSelectEvent("inspector"); + await testToolSelectEvent("webconsole"); + await testToolSelectEvent("styleeditor"); + await toolbox.destroy(); + + toolbox = await openToolboxForTab(tab, "webconsole", "right"); + await testSelectEvent("inspector"); + await testSelectEvent("webconsole"); + await testSelectEvent("styleeditor"); + await testSelectEvent("inspector"); + await testSelectEvent("webconsole"); + await testSelectEvent("styleeditor"); + await toolbox.destroy(); + + toolbox = await openToolboxForTab(tab, "webconsole", "window"); + await testSelectEvent("inspector"); + await testSelectEvent("webconsole"); + await testSelectEvent("styleeditor"); + await testSelectEvent("inspector"); + await testSelectEvent("webconsole"); + await testSelectEvent("styleeditor"); + await toolbox.destroy(); + + await testSelectToolRace(); + + /** + * Assert that selecting the given toolId raises a select event + * @param {toolId} Id of the tool to test + */ + async function testSelectEvent(toolId) { + const onSelect = toolbox.once("select"); + toolbox.selectTool(toolId); + const id = await onSelect; + is(id, toolId, toolId + " selected"); + } + + /** + * Assert that selecting the given toolId raises its corresponding + * selected event + * @param {toolId} Id of the tool to test + */ + async function testToolSelectEvent(toolId) { + const onSelected = toolbox.once(toolId + "-selected"); + toolbox.selectTool(toolId); + await onSelected; + is(toolbox.currentToolId, toolId, toolId + " tool selected"); + } + + /** + * Assert that two calls to selectTool won't race + */ + async function testSelectToolRace() { + const toolbox = await openToolboxForTab(tab, "webconsole"); + let selected = false; + const onSelect = (event, id) => { + if (selected) { + ok(false, "Got more than one 'select' event"); + } else { + selected = true; + } + }; + toolbox.once("select", onSelect); + const p1 = toolbox.selectTool("inspector"); + const p2 = toolbox.selectTool("inspector"); + // Check that both promises don't resolve too early + const checkSelectToolResolution = panel => { + ok(selected, "selectTool resolves only after 'select' event is fired"); + const inspector = toolbox.getPanel("inspector"); + is(panel, inspector, "selecTool resolves to the panel instance"); + }; + p1.then(checkSelectToolResolution); + p2.then(checkSelectToolResolution); + await p1; + await p2; + + await toolbox.destroy(); + } +}); diff --git a/devtools/client/framework/test/browser_toolbox_selected_tool_unavailable.js b/devtools/client/framework/test/browser_toolbox_selected_tool_unavailable.js new file mode 100644 index 0000000000..c55ad5867c --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_selected_tool_unavailable.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that opening the toolbox doesn't throw when the previously selected +// tool is not supported. + +const testToolDefinition = { + id: "testTool", + isToolSupported: () => true, + visibilityswitch: "devtools.test-tool.enabled", + url: "about:blank", + label: "someLabel", + build: (iframeWindow, toolbox) => { + return { + target: toolbox.target, + toolbox, + isReady: true, + destroy: () => {}, + panelDoc: iframeWindow.document, + }; + }, +}; + +add_task(async function () { + gDevTools.registerTool(testToolDefinition); + let tab = await addTab("about:blank"); + + let toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: testToolDefinition.id, + }); + is(toolbox.currentToolId, "testTool", "test-tool was selected"); + await toolbox.destroy(); + + // Make the previously selected tool unavailable. + testToolDefinition.isToolSupported = () => false; + + toolbox = await gDevTools.showToolboxForTab(tab); + is(toolbox.currentToolId, "webconsole", "web console was selected"); + + await toolbox.destroy(); + gDevTools.unregisterTool(testToolDefinition.id); + tab = toolbox = null; + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/client/framework/test/browser_toolbox_selectionchanged_event.js b/devtools/client/framework/test/browser_toolbox_selectionchanged_event.js new file mode 100644 index 0000000000..e4e0dcc446 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_selectionchanged_event.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PAGE_URL = "data:text/html;charset=utf-8,
"; + +add_task(async function () { + const tab = await addTab(PAGE_URL); + const toolbox = await openToolboxForTab(tab, "inspector", "bottom"); + const inspector = toolbox.getCurrentPanel(); + + const root = await inspector.walker.getRootNode(); + const body = await inspector.walker.querySelector(root, "body"); + const node = await inspector.walker.querySelector(root, "div"); + + is(inspector.selection.nodeFront, body, "Body is selected by default"); + + // Listen to selection changed + const onSelectionChanged = toolbox.once("selection-changed"); + + info("Select the div and wait for the selection-changed event to be fired."); + inspector.selection.setNodeFront(node, { reason: "browser-context-menu" }); + + await onSelectionChanged; + + is(inspector.selection.nodeFront, node, "Div is now selected"); + + // Listen to cleared selection changed + const onClearSelectionChanged = toolbox.once("selection-changed"); + + info( + "Clear the selection and wait for the selection-changed event to be fired." + ); + inspector.selection.setNodeFront(null); + + await onClearSelectionChanged; + + is(inspector.selection.nodeFront, null, "The selection is null as expected"); +}); diff --git a/devtools/client/framework/test/browser_toolbox_show_toolbox_tool_ready.js b/devtools/client/framework/test/browser_toolbox_show_toolbox_tool_ready.js new file mode 100644 index 0000000000..d24f8cfedf --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_show_toolbox_tool_ready.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL = + "data:text/html;charset=utf8,test for showToolbox called while tool is opened"; +const lazyToolId = "testtool1"; + +registerCleanupFunction(() => { + gDevTools.unregisterTool(lazyToolId); +}); + +// Delay to wait before the lazy tool should finish +const TOOL_OPEN_DELAY = 3000; + +class LazyDevToolsPanel extends DevToolPanel { + constructor(iframeWindow, toolbox) { + super(iframeWindow, toolbox); + } + + async open() { + await wait(TOOL_OPEN_DELAY); + return this; + } +} + +function isPanelReady(toolbox, toolId) { + return !!toolbox.getPanel(toolId); +} + +/** + * Test that showToolbox will wait until the specified tool is completely read before + * returning. See Bug 1543907. + */ +add_task(async function automaticallyBindTexbox() { + info( + "Registering a tool with an input field and making sure the context menu works" + ); + + gDevTools.registerTool({ + id: lazyToolId, + isToolSupported: () => true, + url: CHROME_URL_ROOT + "doc_lazy_tool.html", + label: "Lazy", + build(iframeWindow, toolbox) { + this.panel = new LazyDevToolsPanel(iframeWindow, toolbox); + return this.panel.open(); + }, + }); + + const tab = await addTab(URL); + const toolbox = await openToolboxForTab(tab, "inspector"); + const onLazyToolReady = toolbox.once(lazyToolId + "-ready"); + toolbox.selectTool(lazyToolId); + + info("Wait until toolbox considers the current tool is the lazy tool"); + await waitUntil(() => toolbox.currentToolId == lazyToolId); + + ok(!isPanelReady(toolbox, lazyToolId), "lazyTool should not be ready yet"); + await gDevTools.showToolboxForTab(tab, { toolId: lazyToolId }); + ok( + isPanelReady(toolbox, lazyToolId), + "lazyTool should not ready after showToolbox" + ); + + // Make sure lazyTool is ready before leaving the test. + await onLazyToolReady; +}); diff --git a/devtools/client/framework/test/browser_toolbox_split_console.js b/devtools/client/framework/test/browser_toolbox_split_console.js new file mode 100644 index 0000000000..e69a493df9 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_split_console.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that these toolbox split console APIs work: +// * toolbox.useKeyWithSplitConsole() +// * toolbox.isSplitConsoleFocused + +let gToolbox = null; +let panelWin = null; + +const URL = "data:text/html;charset=utf8,test split console key delegation"; + +add_task(async function () { + const tab = await addTab(URL); + gToolbox = await gDevTools.showToolboxForTab(tab, { toolId: "jsdebugger" }); + panelWin = gToolbox.getPanel("jsdebugger").panelWin; + + await gToolbox.openSplitConsole(); + await testIsSplitConsoleFocused(); + await testUseKeyWithSplitConsole(); + await testUseKeyWithSplitConsoleWrongTool(); + + await cleanup(); +}); + +async function testIsSplitConsoleFocused() { + await gToolbox.openSplitConsole(); + // The newly opened split console should have focus + ok(gToolbox.isSplitConsoleFocused(), "Split console is focused"); + panelWin.focus(); + ok(!gToolbox.isSplitConsoleFocused(), "Split console is no longer focused"); +} + +// A key bound to the selected tool should trigger it's command +function testUseKeyWithSplitConsole() { + let commandCalled = false; + + info("useKeyWithSplitConsole on debugger while debugger is focused"); + gToolbox.useKeyWithSplitConsole( + "F3", + () => { + commandCalled = true; + }, + "jsdebugger" + ); + + info("synthesizeKey with the console focused"); + focusConsoleInput(); + synthesizeKeyShortcut("F3", panelWin); + + ok(commandCalled, "Shortcut key should trigger the command"); +} + +// A key bound to a *different* tool should not trigger it's command +function testUseKeyWithSplitConsoleWrongTool() { + let commandCalled = false; + + info("useKeyWithSplitConsole on inspector while debugger is focused"); + gToolbox.useKeyWithSplitConsole( + "F4", + () => { + commandCalled = true; + }, + "inspector" + ); + + info("synthesizeKey with the console focused"); + focusConsoleInput(); + synthesizeKeyShortcut("F4", panelWin); + + ok(!commandCalled, "Shortcut key shouldn't trigger the command"); +} + +async function cleanup() { + await gToolbox.destroy(); + gBrowser.removeCurrentTab(); + gToolbox = panelWin = null; +} + +function focusConsoleInput() { + gToolbox.getPanel("webconsole").hud.jsterm.focus(); +} diff --git a/devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js b/devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js new file mode 100644 index 0000000000..06123e8d68 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +var { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +const L10N = new LocalizationHelper( + "devtools/client/locales/toolbox.properties" +); + +add_task(async function () { + const tab = await addTab("about:blank"); + + const toolIDs = (await getSupportedToolIds(tab)).filter( + id => id != "options" + ); + const toolbox = await gDevTools.showToolboxForTab(tab, { + hostType: Toolbox.HostType.BOTTOM, + toolId: toolIDs[0], + }); + const nextShortcut = L10N.getStr("toolbox.nextTool.key"); + const prevShortcut = L10N.getStr("toolbox.previousTool.key"); + + // Iterate over all tools, starting from options to netmonitor, in normal + // order. + for (let i = 1; i < toolIDs.length; i++) { + await testShortcuts(toolbox, i, nextShortcut, toolIDs); + } + + // Iterate again, in the same order, starting from netmonitor (so next one is + // 0: options). + for (let i = 0; i < toolIDs.length; i++) { + await testShortcuts(toolbox, i, nextShortcut, toolIDs); + } + + // Iterate over all tools in reverse order, starting from netmonitor to + // options. + for (let i = toolIDs.length - 2; i >= 0; i--) { + await testShortcuts(toolbox, i, prevShortcut, toolIDs); + } + + // Iterate again, in reverse order again, starting from options (so next one + // is length-1: netmonitor). + for (let i = toolIDs.length - 1; i >= 0; i--) { + await testShortcuts(toolbox, i, prevShortcut, toolIDs); + } + + await toolbox.destroy(); + gBrowser.removeCurrentTab(); +}); + +async function testShortcuts(toolbox, index, shortcut, toolIDs) { + info( + "Testing shortcut to switch to tool " + + index + + ":" + + toolIDs[index] + + " using shortcut " + + shortcut + ); + + const onToolSelected = toolbox.once("select"); + synthesizeKeyShortcut(shortcut); + const id = await onToolSelected; + + info("toolbox-select event from " + id); + + is( + toolIDs.indexOf(id), + index, + "Correct tool is selected on pressing the shortcut for " + id + ); +} diff --git a/devtools/client/framework/test/browser_toolbox_telemetry_activate_splitconsole.js b/devtools/client/framework/test/browser_toolbox_telemetry_activate_splitconsole.js new file mode 100644 index 0000000000..cd1a478f5c --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_telemetry_activate_splitconsole.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL = + "data:text/html;charset=utf8,browser_toolbox_telemetry_activate_splitconsole.js"; +const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; +const DATA = [ + { + timestamp: null, + category: "devtools.main", + method: "activate", + object: "split_console", + value: null, + extra: { + host: "bottom", + width: "1300", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "deactivate", + object: "split_console", + value: null, + extra: { + host: "bottom", + width: "1300", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "activate", + object: "split_console", + value: null, + extra: { + host: "bottom", + width: "1300", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "deactivate", + object: "split_console", + value: null, + extra: { + host: "bottom", + width: "1300", + }, + }, +]; + +add_task(async function () { + // See Bug 1500141: this test frequently fails on beta because some highlighter + // requests made by the BoxModel component in the layout view come back when the + // connection between the client and the server has been destroyed. We are forcing + // the computed view here to avoid the failures but ideally we should have an event + // or a promise on the inspector we can wait for to be sure the initialization is over. + // Logged Bug 1500918 to investigate this. + await pushPref("devtools.inspector.activeSidebar", "computedview"); + + // Let's reset the counts. + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + ok(!snapshot.parent, "No events have been logged for the main process"); + + const tab = await addTab(URL); + const toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: "inspector", + }); + + await toolbox.openSplitConsole(); + await toolbox.closeSplitConsole(); + await toolbox.openSplitConsole(); + await toolbox.closeSplitConsole(); + + await checkResults(); +}); + +async function checkResults() { + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + const events = snapshot.parent.filter( + event => + event[1] === "devtools.main" && + (event[2] === "activate" || event[2] === "deactivate") + ); + + for (const i in DATA) { + const [timestamp, category, method, object, value, extra] = events[i]; + const expected = DATA[i]; + + // ignore timestamp + Assert.greater(timestamp, 0, "timestamp is greater than 0"); + is(category, expected.category, "category is correct"); + is(method, expected.method, "method is correct"); + is(object, expected.object, "object is correct"); + is(value, expected.value, "value is correct"); + + // extras + is(extra.host, expected.extra.host, "host is correct"); + ok(extra.width > 0, "width is greater than 0"); + } +} diff --git a/devtools/client/framework/test/browser_toolbox_telemetry_close.js b/devtools/client/framework/test/browser_toolbox_telemetry_close.js new file mode 100644 index 0000000000..47aa1c056b --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_telemetry_close.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const URL = "data:text/html;charset=utf8,browser_toolbox_telemetry_close.js"; +const { RIGHT, BOTTOM } = Toolbox.HostType; +const DATA = [ + { + category: "devtools.main", + method: "close", + object: "tools", + value: null, + extra: { + host: "right", + width: w => w > 0, + }, + }, + { + category: "devtools.main", + method: "close", + object: "tools", + value: null, + extra: { + host: "bottom", + width: w => w > 0, + }, + }, +]; + +add_task(async function () { + // Let's reset the counts. + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + TelemetryTestUtils.assertNumberOfEvents(0); + + await openAndCloseToolbox("webconsole", RIGHT); + await openAndCloseToolbox("webconsole", BOTTOM); + + checkResults(); +}); + +async function openAndCloseToolbox(toolId, host) { + const tab = await addTab(URL); + const toolbox = await gDevTools.showToolboxForTab(tab, { toolId }); + + await toolbox.switchHost(host); + await toolbox.destroy(); +} + +function checkResults() { + TelemetryTestUtils.assertEvents(DATA, { + category: "devtools.main", + method: "close", + object: "tools", + }); +} diff --git a/devtools/client/framework/test/browser_toolbox_telemetry_enter.js b/devtools/client/framework/test/browser_toolbox_telemetry_enter.js new file mode 100644 index 0000000000..4cb4611a97 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_telemetry_enter.js @@ -0,0 +1,152 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL = "data:text/html;charset=utf8,browser_toolbox_telemetry_enter.js"; +const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; +const DATA = [ + { + timestamp: null, + category: "devtools.main", + method: "enter", + object: "inspector", + value: null, + extra: { + host: "bottom", + width: "1300", + start_state: "initial_panel", + panel_name: "inspector", + cold: "true", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "enter", + object: "jsdebugger", + value: null, + extra: { + host: "bottom", + width: "1300", + start_state: "toolbox_show", + panel_name: "jsdebugger", + cold: "true", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "enter", + object: "styleeditor", + value: null, + extra: { + host: "bottom", + width: "1300", + start_state: "toolbox_show", + panel_name: "styleeditor", + cold: "true", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "enter", + object: "netmonitor", + value: null, + extra: { + host: "bottom", + width: "1300", + start_state: "toolbox_show", + panel_name: "netmonitor", + cold: "true", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "enter", + object: "storage", + value: null, + extra: { + host: "bottom", + width: "1300", + start_state: "toolbox_show", + panel_name: "storage", + cold: "true", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "enter", + object: "netmonitor", + value: null, + extra: { + host: "bottom", + width: "1300", + start_state: "toolbox_show", + panel_name: "netmonitor", + cold: "false", + }, + }, +]; + +add_task(async function () { + // Let's reset the counts. + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + ok(!snapshot.parent, "No events have been logged for the main process"); + + const tab = await addTab(URL); + + // Set up some cached messages for the web console. + await SpecialPowers.spawn(tab.linkedBrowser, [], () => { + content.console.log("test 1"); + content.console.log("test 2"); + content.console.log("test 3"); + content.console.log("test 4"); + content.console.log("test 5"); + }); + + // Open the toolbox + await gDevTools.showToolboxForTab(tab, { toolId: "inspector" }); + + // Switch between a few tools + await gDevTools.showToolboxForTab(tab, { toolId: "jsdebugger" }); + await gDevTools.showToolboxForTab(tab, { toolId: "styleeditor" }); + await gDevTools.showToolboxForTab(tab, { toolId: "netmonitor" }); + await gDevTools.showToolboxForTab(tab, { toolId: "storage" }); + await gDevTools.showToolboxForTab(tab, { toolId: "netmonitor" }); + + await checkResults(); +}); + +async function checkResults() { + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + const events = snapshot.parent.filter( + event => + event[1] === "devtools.main" && event[2] === "enter" && event[4] === null + ); + + for (const i in DATA) { + const [timestamp, category, method, object, value, extra] = events[i]; + const expected = DATA[i]; + + // ignore timestamp + Assert.greater(timestamp, 0, "timestamp is greater than 0"); + is(category, expected.category, "category is correct"); + is(method, expected.method, "method is correct"); + is(object, expected.object, "object is correct"); + is(value, expected.value, "value is correct"); + + // extras + is(extra.host, expected.extra.host, "host is correct"); + ok(extra.width > 0, "width is greater than 0"); + is(extra.start_state, expected.extra.start_state, "start_state is correct"); + is(extra.panel_name, expected.extra.panel_name, "panel_name is correct"); + is(extra.cold, expected.extra.cold, "cold is correct"); + } +} diff --git a/devtools/client/framework/test/browser_toolbox_telemetry_exit.js b/devtools/client/framework/test/browser_toolbox_telemetry_exit.js new file mode 100644 index 0000000000..3056b9af8c --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_telemetry_exit.js @@ -0,0 +1,129 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL = "data:text/html;charset=utf8,browser_toolbox_telemetry_enter.js"; +const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; +const DATA = [ + { + timestamp: null, + category: "devtools.main", + method: "exit", + object: "inspector", + value: null, + extra: { + host: "bottom", + width: 1300, + panel_name: "inspector", + next_panel: "jsdebugger", + reason: "toolbox_show", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "exit", + object: "jsdebugger", + value: null, + extra: { + host: "bottom", + width: 1300, + panel_name: "jsdebugger", + next_panel: "styleeditor", + reason: "toolbox_show", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "exit", + object: "styleeditor", + value: null, + extra: { + host: "bottom", + width: 1300, + panel_name: "styleeditor", + next_panel: "netmonitor", + reason: "toolbox_show", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "exit", + object: "netmonitor", + value: null, + extra: { + host: "bottom", + width: 1300, + panel_name: "netmonitor", + next_panel: "storage", + reason: "toolbox_show", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "exit", + object: "storage", + value: null, + extra: { + host: "bottom", + width: 1300, + panel_name: "storage", + next_panel: "netmonitor", + reason: "toolbox_show", + }, + }, +]; + +add_task(async function () { + // Let's reset the counts. + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + ok(!snapshot.parent, "No events have been logged for the main process"); + + const tab = await addTab(URL); + + // Open the toolbox + await gDevTools.showToolboxForTab(tab, { toolId: "inspector" }); + + // Switch between a few tools + await gDevTools.showToolboxForTab(tab, { toolId: "jsdebugger" }); + await gDevTools.showToolboxForTab(tab, { toolId: "styleeditor" }); + await gDevTools.showToolboxForTab(tab, { toolId: "netmonitor" }); + await gDevTools.showToolboxForTab(tab, { toolId: "storage" }); + await gDevTools.showToolboxForTab(tab, { toolId: "netmonitor" }); + + await checkResults(); +}); + +async function checkResults() { + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + const events = snapshot.parent.filter( + event => + event[1] === "devtools.main" && event[2] === "exit" && event[4] === null + ); + + for (const i in DATA) { + const [timestamp, category, method, object, value, extra] = events[i]; + const expected = DATA[i]; + + // ignore timestamp + Assert.greater(timestamp, 0, "timestamp is greater than 0"); + is(category, expected.category, "category is correct"); + is(method, expected.method, "method is correct"); + is(object, expected.object, "object is correct"); + is(value, expected.value, "value is correct"); + + // extras + is(extra.host, expected.extra.host, "host is correct"); + ok(extra.width > 0, "width is greater than 0"); + is(extra.panel_name, expected.extra.panel_name, "panel_name is correct"); + is(extra.next_panel, expected.extra.next_panel, "next_panel is correct"); + is(extra.reason, expected.extra.reason, "reason is correct"); + } +} diff --git a/devtools/client/framework/test/browser_toolbox_telemetry_open_event.js b/devtools/client/framework/test/browser_toolbox_telemetry_open_event.js new file mode 100644 index 0000000000..aa2f7ea2ed --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_telemetry_open_event.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the "open" telemetry event is correctly logged when opening the +// toolbox. +const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; + +add_task(async function () { + Services.prefs.clearUserPref("devtools.toolbox.selectedTool"); + const tab = await addTab("data:text/html;charset=utf-8,Test open event"); + + info("Open the toolbox with a shortcut to trigger the open event"); + const onToolboxReady = gDevTools.once("toolbox-ready"); + EventUtils.synthesizeKey("VK_F12", {}); + await onToolboxReady; + + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + // The telemetry is sent by DevToolsStartup and so isn't flaged against any session id + const events = snapshot.parent.filter( + event => + event[1] === "devtools.main" && + event[2] === "open" && + event[5].session_id == -1 + ); + + is(events.length, 1, "Telemetry open event was logged"); + + const extras = events[0][5]; + is(extras.entrypoint, "KeyShortcut", "entrypoint extra is correct"); + // The logged shortcut is `${modifiers}+${shortcut}`, which adds an + // extra `+` before F12 here. + // See https://searchfox.org/mozilla-central/rev/c7e8bc4996f979e5876b33afae3de3b1ab4f3ae1/devtools/startup/DevToolsStartup.jsm#1070 + is(extras.shortcut, "+F12", "entrypoint shortcut is correct"); + + gBrowser.removeTab(tab); +}); diff --git a/devtools/client/framework/test/browser_toolbox_textbox_context_menu.js b/devtools/client/framework/test/browser_toolbox_textbox_context_menu.js new file mode 100644 index 0000000000..903d0c9912 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_textbox_context_menu.js @@ -0,0 +1,137 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// HTML inputs don't automatically get the 'edit' context menu, so we have +// a helper on the toolbox to do so. Make sure that shows menu items in the +// right state, and that it works for an input inside of a panel. + +const URL = "data:text/html;charset=utf8,test for textbox context menu"; +const textboxToolId = "testtool1"; + +registerCleanupFunction(() => { + gDevTools.unregisterTool(textboxToolId); +}); + +add_task(async function checkMenuEntryStates() { + info("Checking the state of edit menuitems with an empty clipboard"); + const toolbox = await openNewTabAndToolbox(URL, "inspector"); + + emptyClipboard(); + + // Make sure the focus is predictable. + const inspector = toolbox.getPanel("inspector"); + const onFocus = once(inspector.searchBox, "focus"); + inspector.searchBox.focus(); + await onFocus; + + info("Opening context menu"); + const onContextMenuPopup = toolbox.once("menu-open"); + synthesizeContextMenuEvent(inspector.searchBox); + await onContextMenuPopup; + + const textboxContextMenu = toolbox.getTextBoxContextMenu(); + ok(textboxContextMenu, "The textbox context menu is loaded in the toolbox"); + + const cmdUndo = textboxContextMenu.querySelector("#editmenu-undo"); + const cmdDelete = textboxContextMenu.querySelector("#editmenu-delete"); + const cmdSelectAll = textboxContextMenu.querySelector("#editmenu-selectAll"); + const cmdCut = textboxContextMenu.querySelector("#editmenu-cut"); + const cmdCopy = textboxContextMenu.querySelector("#editmenu-copy"); + const cmdPaste = textboxContextMenu.querySelector("#editmenu-paste"); + + is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled"); + is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled"); + is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled"); + is(cmdCut.getAttribute("disabled"), "true", "cmdCut is disabled"); + is(cmdCopy.getAttribute("disabled"), "true", "cmdCopy is disabled"); + + if (isWindows()) { + // emptyClipboard only works on Windows (666254), assert paste only for this OS. + is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled"); + } + + const onContextMenuHidden = toolbox.once("menu-close"); + if (Services.prefs.getBoolPref("widget.macos.native-context-menus", false)) { + info("Using hidePopup semantics because of macOS native context menus."); + textboxContextMenu.hidePopup(); + } else { + EventUtils.sendKey("ESCAPE", toolbox.win); + } + await onContextMenuHidden; +}); + +add_task(async function automaticallyBindTexbox() { + info( + "Registering a tool with an input field and making sure the context menu works" + ); + gDevTools.registerTool({ + id: textboxToolId, + isToolSupported: () => true, + url: CHROME_URL_ROOT + "doc_textbox_tool.html", + label: "Context menu works without tool intervention", + build(iframeWindow, toolbox) { + this.panel = createTestPanel(iframeWindow, toolbox); + return this.panel.open(); + }, + }); + + const toolbox = await openNewTabAndToolbox(URL, textboxToolId); + is(toolbox.currentToolId, textboxToolId, "The custom tool has been opened"); + + const doc = toolbox.getCurrentPanel().document; + await checkTextBox(doc.querySelector("input[type=text]"), toolbox); + await checkTextBox(doc.querySelector("textarea"), toolbox); + await checkTextBox(doc.querySelector("input[type=search]"), toolbox); + await checkTextBox(doc.querySelector("input:not([type])"), toolbox); + await checkNonTextInput(doc.querySelector("input[type=radio]"), toolbox); +}); + +async function checkNonTextInput(input, toolbox) { + let textboxContextMenu = toolbox.getTextBoxContextMenu(); + ok(!textboxContextMenu, "The menu is closed"); + + info( + "Simulating context click on the non text input and expecting no menu to open" + ); + const eventBubbledUp = new Promise(resolve => { + input.ownerDocument.addEventListener("contextmenu", resolve, { + once: true, + }); + }); + synthesizeContextMenuEvent(input); + info("Waiting for event"); + await eventBubbledUp; + + textboxContextMenu = toolbox.getTextBoxContextMenu(); + ok(!textboxContextMenu, "The menu is still closed"); +} + +async function checkTextBox(textBox, toolbox) { + let textboxContextMenu = toolbox.getTextBoxContextMenu(); + ok(!textboxContextMenu, "The menu is closed"); + + info( + "Simulating context click on the textbox and expecting the menu to open" + ); + const onContextMenu = toolbox.once("menu-open"); + synthesizeContextMenuEvent(textBox); + await onContextMenu; + + textboxContextMenu = toolbox.getTextBoxContextMenu(); + ok(textboxContextMenu, "The menu is now visible"); + + info("Closing the menu"); + const onContextMenuHidden = toolbox.once("menu-close"); + if (Services.prefs.getBoolPref("widget.macos.native-context-menus", false)) { + info("Using hidePopup semantics because of macOS native context menus."); + textboxContextMenu.hidePopup(); + } else { + EventUtils.sendKey("ESCAPE", toolbox.win); + } + await onContextMenuHidden; + + textboxContextMenu = toolbox.getTextBoxContextMenu(); + ok(!textboxContextMenu, "The menu is closed again"); +} diff --git a/devtools/client/framework/test/browser_toolbox_theme.js b/devtools/client/framework/test/browser_toolbox_theme.js new file mode 100644 index 0000000000..63d83e8312 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_theme.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const PREF_DEVTOOLS_THEME = "devtools.theme"; + +registerCleanupFunction(() => { + // Set preferences back to their original values + Services.prefs.clearUserPref(PREF_DEVTOOLS_THEME); +}); + +add_task(async function testDevtoolsTheme() { + info("Checking stylesheet and :root attributes based on devtools theme."); + Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "light"); + is( + document.getElementById("appcontent").getAttribute("devtoolstheme"), + "light", + "The element has an attribute based on devtools theme." + ); + + Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark"); + is( + document.getElementById("appcontent").getAttribute("devtoolstheme"), + "dark", + "The element has an attribute based on devtools theme." + ); + + Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "unknown"); + is( + document.getElementById("appcontent").getAttribute("devtoolstheme"), + "light", + "The element has 'light' as a default for the devtoolstheme attribute." + ); +}); diff --git a/devtools/client/framework/test/browser_toolbox_theme_registration.js b/devtools/client/framework/test/browser_toolbox_theme_registration.js new file mode 100644 index 0000000000..6f5d2bc679 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_theme_registration.js @@ -0,0 +1,166 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for dynamically registering and unregistering themes +const CHROME_URL = + "chrome://mochitests/content/browser/devtools/client/framework/test/"; +const TEST_THEME_NAME = "test-theme"; +const LIGHT_THEME_NAME = "light"; + +var toolbox; + +add_task(async function themeRegistration() { + const tab = await addTab("data:text/html,test"); + toolbox = await gDevTools.showToolboxForTab(tab, { toolId: "options" }); + + const themeId = await new Promise(resolve => { + gDevTools.once("theme-registered", registeredThemeId => { + resolve(registeredThemeId); + }); + + gDevTools.registerTheme({ + id: TEST_THEME_NAME, + label: "Test theme", + stylesheets: [CHROME_URL + "doc_theme.css"], + classList: ["theme-test"], + }); + }); + + is(themeId, TEST_THEME_NAME, "theme-registered event handler sent theme id"); + + ok(gDevTools.getThemeDefinitionMap().has(themeId), "theme added to map"); +}); + +add_task(async function themeInOptionsPanel() { + const panelWin = toolbox.getCurrentPanel().panelWin; + const doc = panelWin.frameElement.contentDocument; + const themeBox = doc.getElementById("devtools-theme-box"); + const testThemeOption = themeBox.querySelector( + `input[type=radio][value=${TEST_THEME_NAME}]` + ); + const eventsRecorded = []; + + function onThemeChanged(theme) { + eventsRecorded.push(theme); + } + gDevTools.on("theme-changed", onThemeChanged); + + ok(testThemeOption, "new theme exists in the Options panel"); + + const lightThemeOption = themeBox.querySelector( + `input[type=radio][value=${LIGHT_THEME_NAME}]` + ); + + let color = panelWin.getComputedStyle(themeBox).color; + isnot(color, "rgb(255, 0, 0)", "style unapplied"); + + let onThemeSwithComplete = once(panelWin, "theme-switch-complete"); + + // Select test theme. + testThemeOption.click(); + + info("Waiting for theme to finish loading"); + await onThemeSwithComplete; + + is( + gDevTools.getTheme(), + TEST_THEME_NAME, + "getTheme returns the expected theme" + ); + is( + eventsRecorded.pop(), + TEST_THEME_NAME, + "theme-changed fired with the expected theme" + ); + + color = panelWin.getComputedStyle(themeBox).color; + is(color, "rgb(255, 0, 0)", "style applied"); + + onThemeSwithComplete = once(panelWin, "theme-switch-complete"); + + // Select light theme + lightThemeOption.click(); + + info("Waiting for theme to finish loading"); + await onThemeSwithComplete; + + is( + gDevTools.getTheme(), + LIGHT_THEME_NAME, + "getTheme returns the expected theme" + ); + is( + eventsRecorded.pop(), + LIGHT_THEME_NAME, + "theme-changed fired with the expected theme" + ); + + color = panelWin.getComputedStyle(themeBox).color; + isnot(color, "rgb(255, 0, 0)", "style unapplied"); + + onThemeSwithComplete = once(panelWin, "theme-switch-complete"); + // Select test theme again. + testThemeOption.click(); + await onThemeSwithComplete; + is( + gDevTools.getTheme(), + TEST_THEME_NAME, + "getTheme returns the expected theme" + ); + is( + eventsRecorded.pop(), + TEST_THEME_NAME, + "theme-changed fired with the expected theme" + ); + + gDevTools.off("theme-changed", onThemeChanged); +}); + +add_task(async function themeUnregistration() { + const panelWin = toolbox.getCurrentPanel().panelWin; + const onUnRegisteredTheme = once(gDevTools, "theme-unregistered"); + const onThemeSwitchComplete = once(panelWin, "theme-switch-complete"); + const eventsRecorded = []; + + function onThemeChanged(theme) { + eventsRecorded.push(theme); + } + gDevTools.on("theme-changed", onThemeChanged); + + gDevTools.unregisterTheme(TEST_THEME_NAME); + await onUnRegisteredTheme; + await onThemeSwitchComplete; + + is( + gDevTools.getTheme(), + gDevTools.getAutoTheme(), + "getTheme returns the expected theme" + ); + is( + eventsRecorded.pop(), + gDevTools.getAutoTheme(), + "theme-changed fired with the expected theme" + ); + ok( + !gDevTools.getThemeDefinitionMap().has(TEST_THEME_NAME), + "theme removed from map" + ); + + const doc = panelWin.frameElement.contentDocument; + const themeBox = doc.getElementById("devtools-theme-box"); + + // The default theme must be selected now. + ok( + themeBox.querySelector(`#devtools-theme-box [value=auto]`).checked, + `auto theme must be selected` + ); + + gDevTools.off("theme-changed", onThemeChanged); +}); + +add_task(async function cleanup() { + await toolbox.destroy(); + toolbox = null; +}); diff --git a/devtools/client/framework/test/browser_toolbox_toggle.js b/devtools/client/framework/test/browser_toolbox_toggle.js new file mode 100644 index 0000000000..c3848ae5ad --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_toggle.js @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test toggling the toolbox with ACCEL+SHIFT+I / ACCEL+ALT+I and F12 in docked +// and detached (window) modes. + +const URL = "data:text/html;charset=utf-8,Toggling devtools using shortcuts"; + +var { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +add_task(async function () { + // Make sure this test starts with the selectedTool pref cleared. Previous + // tests select various tools, and that sets this pref. + Services.prefs.clearUserPref("devtools.toolbox.selectedTool"); + + // Test with ACCEL+SHIFT+I / ACCEL+ALT+I (MacOSX) ; modifiers should match : + // - toolbox-key-toggle in devtools/client/framework/toolbox-window.xhtml + // - key_devToolboxMenuItem in browser/base/content/browser.xhtml + info("Test toggle using CTRL+SHIFT+I/CMD+ALT+I"); + await testToggle("I", { + accelKey: true, + shiftKey: !navigator.userAgent.match(/Mac/), + altKey: navigator.userAgent.match(/Mac/), + }); + + // Test with F12 ; no modifiers + info("Test toggle using F12"); + await testToggle("VK_F12", {}); +}); + +async function testToggle(key, modifiers) { + const tab = await addTab(URL + " ; key : '" + key + "'"); + await gDevTools.showToolboxForTab(tab); + + await testToggleDockedToolbox(tab, key, modifiers); + await testToggleDetachedToolbox(tab, key, modifiers); + + await cleanup(); +} + +async function testToggleDockedToolbox(tab, key, modifiers) { + const toolbox = gDevTools.getToolboxForTab(tab); + + isnot( + toolbox.hostType, + Toolbox.HostType.WINDOW, + "Toolbox is docked in the main window" + ); + + info("verify docked toolbox is destroyed when using toggle key"); + const onToolboxDestroyed = gDevTools.once("toolbox-destroyed"); + EventUtils.synthesizeKey(key, modifiers); + await onToolboxDestroyed; + ok(true, "Docked toolbox is destroyed when using a toggle key"); + + info("verify new toolbox is created when using toggle key"); + const onToolboxReady = gDevTools.once("toolbox-ready"); + EventUtils.synthesizeKey(key, modifiers); + await onToolboxReady; + ok(true, "Toolbox is created by using when toggle key"); +} + +async function testToggleDetachedToolbox(tab, key, modifiers) { + const toolbox = gDevTools.getToolboxForTab(tab); + + info("change the toolbox hostType to WINDOW"); + + await toolbox.switchHost(Toolbox.HostType.WINDOW); + is( + toolbox.hostType, + Toolbox.HostType.WINDOW, + "Toolbox opened on separate window" + ); + + info("Wait for focus on the toolbox window"); + await new Promise(res => waitForFocus(res, toolbox.win)); + + info("Focus main window to put the toolbox window in the background"); + + const onMainWindowFocus = once(window, "focus"); + window.focus(); + await onMainWindowFocus; + ok(true, "Main window focused"); + + info( + "Verify windowed toolbox is focused instead of closed when using " + + "toggle key from the main window" + ); + const toolboxWindow = toolbox.topWindow; + const onToolboxWindowFocus = once(toolboxWindow, "focus", true); + EventUtils.synthesizeKey(key, modifiers); + await onToolboxWindowFocus; + ok(true, "Toolbox focused and not destroyed"); + + info( + "Verify windowed toolbox is destroyed when using toggle key from its " + + "own window" + ); + + const onToolboxDestroyed = gDevTools.once("toolbox-destroyed"); + EventUtils.synthesizeKey(key, modifiers, toolboxWindow); + await onToolboxDestroyed; + ok(true, "Toolbox destroyed"); +} + +function cleanup() { + Services.prefs.setCharPref("devtools.toolbox.host", Toolbox.HostType.BOTTOM); + gBrowser.removeCurrentTab(); +} diff --git a/devtools/client/framework/test/browser_toolbox_tool_ready.js b/devtools/client/framework/test/browser_toolbox_tool_ready.js new file mode 100644 index 0000000000..306e4598af --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_tool_ready.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(5); + +async function performChecks(tab) { + let toolbox; + const toolIds = await getSupportedToolIds(tab); + for (const toolId of toolIds) { + info("About to open " + toolId); + toolbox = await gDevTools.showToolboxForTab(tab, { toolId }); + ok(toolbox, "toolbox exists for " + toolId); + is(toolbox.currentToolId, toolId, "currentToolId should be " + toolId); + + const panel = toolbox.getCurrentPanel(); + ok(panel, toolId + " panel has been registered in the toolbox"); + } + + await toolbox.destroy(); +} + +function test() { + (async function () { + toggleAllTools(true); + const tab = await addTab("about:blank"); + await performChecks(tab); + gBrowser.removeCurrentTab(); + toggleAllTools(false); + finish(); + })(); +} diff --git a/devtools/client/framework/test/browser_toolbox_tool_remote_reopen.js b/devtools/client/framework/test/browser_toolbox_tool_remote_reopen.js new file mode 100644 index 0000000000..52a6ea0655 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_tool_remote_reopen.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + DevToolsServer, +} = require("resource://devtools/server/devtools-server.js"); + +// Bug 1277805: Too slow for debug runs +requestLongerTimeout(2); + +/** + * Bug 979536: Ensure fronts are destroyed after toolbox close. + * + * The fronts need to be destroyed manually to unbind their onPacket handlers. + * + * When you initialize a front and call |this.manage|, it adds a client actor + * pool that the DevToolsClient uses to route packet replies to that actor. + * + * Most (all?) tools create a new front when they are opened. When the destroy + * step is skipped and the tool is reopened, a second front is created and also + * added to the client actor pool. When a packet reply is received, is ends up + * being routed to the first (now unwanted) front that is still in the client + * actor pool. Since this is not the same front that was used to make the + * request, an error occurs. + * + * This problem does not occur with the toolbox for a local tab because the + * toolbox target creates its own DevToolsClient for the local tab, and the + * client is destroyed when the toolbox is closed, which removes the client + * actor pools, and avoids this issue. + * + * In remote debugging, we do not destroy the DevToolsClient on toolbox close + * because it can still used for other targets. + * Thus, the same client gets reused across multiple toolboxes, + * which leads to the tools failing if they don't destroy their fronts. + */ + +function runTools(tab) { + return (async function () { + let toolbox; + const toolIds = await getSupportedToolIds(tab); + for (const toolId of toolIds) { + info("About to open " + toolId); + toolbox = await gDevTools.showToolboxForTab(tab, { + toolId, + hostType: "window", + }); + ok(toolbox, "toolbox exists for " + toolId); + is(toolbox.currentToolId, toolId, "currentToolId should be " + toolId); + + const panel = toolbox.getCurrentPanel(); + ok(panel, toolId + " panel has been registered in the toolbox"); + } + + const client = toolbox.commands.client; + await toolbox.destroy(); + + // We need to check the client after the toolbox destruction. + return client; + })(); +} + +function test() { + (async function () { + toggleAllTools(true); + const tab = await addTab("about:blank"); + + const client = await runTools(tab); + + const rootFronts = [...client.mainRoot.fronts.values()]; + + // Actor fronts should be destroyed now that the toolbox has closed, but + // look for any that remain. + for (const pool of client.__pools) { + if (!pool.__poolMap) { + continue; + } + + // Ignore the root fronts, which are top-level pools and aren't released + // on toolbox destroy, but on client close. + if (rootFronts.includes(pool)) { + continue; + } + + for (const actor of pool.__poolMap.keys()) { + // Ignore the root front as it is only release on client close + if (actor == "root") { + continue; + } + ok(false, "Front for " + actor + " still held in pool!"); + } + } + + gBrowser.removeCurrentTab(); + DevToolsServer.destroy(); + toggleAllTools(false); + finish(); + })(); +} diff --git a/devtools/client/framework/test/browser_toolbox_toolbar_minimum_width.js b/devtools/client/framework/test/browser_toolbox_toolbar_minimum_width.js new file mode 100644 index 0000000000..cdd6678e6f --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_toolbar_minimum_width.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that all of buttons of tool tab go to the overflowed menu when the devtool's +// width is narrow. + +const SIDEBAR_WIDTH_PREF = "devtools.toolbox.sidebar.width"; + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +add_task(async function (pickerEnable, commandsEnable) { + // 74px is Chevron(26px) + Meatball(24px) + Close(24px) + // devtools-browser.css defined this minimum width by using min-width. + Services.prefs.setIntPref(SIDEBAR_WIDTH_PREF, 74); + registerCleanupFunction(function () { + Services.prefs.clearUserPref(SIDEBAR_WIDTH_PREF); + }); + const tab = await addTab("about:blank"); + + info("Open devtools on the Inspector in a side dock"); + const toolbox = await openToolboxForTab( + tab, + "inspector", + Toolbox.HostType.RIGHT + ); + await waitUntil(() => toolbox.doc.querySelector(".tools-chevron-menu")); + + await openChevronMenu(toolbox); + + // Check that all of tools is overflowed. + toolbox.panelDefinitions.forEach(({ id }) => { + const menuItem = toolbox.doc.getElementById( + "tools-chevron-menupopup-" + id + ); + const tab = toolbox.doc.getElementById("toolbox-tab-" + id); + ok(menuItem, id + " is in the overflowed menu"); + ok(!tab, id + " tab does not exist"); + }); + + await closeChevronMenu(toolbox); +}); diff --git a/devtools/client/framework/test/browser_toolbox_toolbar_overflow.js b/devtools/client/framework/test/browser_toolbox_toolbar_overflow.js new file mode 100644 index 0000000000..9f964af18e --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_toolbar_overflow.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that a button to access tools hidden by toolbar overflow is displayed when the +// toolbar starts to present an overflow. +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +add_task(async function () { + const tab = await addTab("about:blank"); + + info("Open devtools on the Inspector in a bottom dock"); + const toolbox = await openToolboxForTab( + tab, + "inspector", + Toolbox.HostType.BOTTOM + ); + + const hostWindow = toolbox.topWindow; + const originalWidth = hostWindow.outerWidth; + const originalHeight = hostWindow.outerHeight; + + info( + "Resize devtools window to a width that should not trigger any overflow" + ); + let onResize = once(hostWindow, "resize"); + hostWindow.resizeTo(1350, 300); + await onResize; + + info("Wait for all buttons to be displayed"); + await waitUntil(() => { + return ( + toolbox.panelDefinitions.length === + toolbox.doc.querySelectorAll(".devtools-tab").length + ); + }); + + let chevronMenuButton = toolbox.doc.querySelector(".tools-chevron-menu"); + ok(!chevronMenuButton, "The chevron menu button is not displayed"); + + info("Resize devtools window to a width that should trigger an overflow"); + onResize = once(hostWindow, "resize"); + hostWindow.resizeTo(800, 300); + await onResize; + await waitUntil(() => !toolbox.doc.querySelector(".tools-chevron-menu")); + + info("Wait until the chevron menu button is available"); + await waitUntil(() => toolbox.doc.querySelector(".tools-chevron-menu")); + + chevronMenuButton = toolbox.doc.querySelector(".tools-chevron-menu"); + ok(chevronMenuButton, "The chevron menu button is displayed"); + + info( + "Open the tools-chevron-menupopup and verify that the inspector button is checked" + ); + await openChevronMenu(toolbox); + + const inspectorButton = toolbox.doc.querySelector( + "#tools-chevron-menupopup-inspector" + ); + ok(!inspectorButton, "The chevron menu doesn't have the inspector button."); + + const consoleButton = toolbox.doc.querySelector( + "#tools-chevron-menupopup-webconsole" + ); + ok(!consoleButton, "The chevron menu doesn't have the console button."); + + const storageButton = toolbox.doc.querySelector( + "#tools-chevron-menupopup-storage" + ); + ok(storageButton, "The chevron menu has the storage button."); + + info("Switch to the performance using the tools-chevron-menupopup popup"); + const onSelected = toolbox.once("storage-selected"); + storageButton.click(); + await onSelected; + + info("Restore the original window size"); + onResize = once(hostWindow, "resize"); + hostWindow.resizeTo(originalWidth, originalHeight); + await onResize; +}); diff --git a/devtools/client/framework/test/browser_toolbox_toolbar_overflow_button_visibility.js b/devtools/client/framework/test/browser_toolbox_toolbar_overflow_button_visibility.js new file mode 100644 index 0000000000..f266991109 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_toolbar_overflow_button_visibility.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the toolbox tabs rearrangement when the visibility of toolbox buttons were changed. + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +add_task(async function () { + const tab = await addTab("about:blank"); + const toolbox = await openToolboxForTab( + tab, + "options", + Toolbox.HostType.BOTTOM + ); + const toolboxButtonPreferences = toolbox.toolbarButtons.map( + button => button.visibilityswitch + ); + + const win = getWindow(toolbox); + const { outerWidth: originalWindowWidth, outerHeight: originalWindowHeight } = + win; + registerCleanupFunction(() => { + for (const preference of toolboxButtonPreferences) { + Services.prefs.clearUserPref(preference); + } + + win.resizeTo(originalWindowWidth, originalWindowHeight); + }); + + const optionsTool = toolbox.getCurrentPanel(); + const checkButtons = optionsTool.panelWin.document.querySelectorAll( + "#enabled-toolbox-buttons-box input[type=checkbox]" + ); + + info( + "Test the count of shown devtools tab after making all buttons to be visible" + ); + await resizeWindow(toolbox, 800); + // Once, make all toolbox button to be invisible. + setToolboxButtonsVisibility(checkButtons, false); + // Get count of shown devtools tab elements. + const initialTabCount = toolbox.doc.querySelectorAll(".devtools-tab").length; + // Make all toolbox button to be visible. + setToolboxButtonsVisibility(checkButtons, true); + Assert.less( + toolbox.doc.querySelectorAll(".devtools-tab").length, + initialTabCount, + "Count of shown devtools tab should decreased" + ); + + info( + "Test the count of shown devtools tab after making all buttons to be invisible" + ); + setToolboxButtonsVisibility(checkButtons, false); + is( + toolbox.doc.querySelectorAll(".devtools-tab").length, + initialTabCount, + "Count of shown devtools tab should be same to 1st count" + ); +}); + +function setToolboxButtonsVisibility(checkButtons, doVisible) { + for (const checkButton of checkButtons) { + if (checkButton.checked === doVisible) { + continue; + } + + checkButton.click(); + } +} diff --git a/devtools/client/framework/test/browser_toolbox_toolbar_reorder_by_dnd.js b/devtools/client/framework/test/browser_toolbox_toolbar_reorder_by_dnd.js new file mode 100644 index 0000000000..9ef82ca6e9 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_toolbar_reorder_by_dnd.js @@ -0,0 +1,188 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following reordering operation: +// * DragAndDrop the target component to back +// * DragAndDrop the target component to front +// * DragAndDrop the target component over the starting of the tab +// * DragAndDrop the target component over the ending of the tab +// * Mouse was out from the document while dragging +// * Select overflowed item, then DnD that +// +// This test is on the assumption which default toolbar has following tools. +// * inspector +// * webconsole +// * jsdebugger +// * styleeditor +// * performance +// * memory +// * netmonitor +// * storage +// * accessibility +// * application + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +const TEST_STARTING_ORDER = [ + "inspector", + "webconsole", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + "accessibility", + "application", +]; +const TEST_DATA = [ + { + description: "DragAndDrop the target component to back", + dragTarget: "webconsole", + dropTarget: "jsdebugger", + expectedOrder: [ + "inspector", + "jsdebugger", + "webconsole", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + "accessibility", + "application", + ], + }, + { + description: "DragAndDrop the target component to front", + dragTarget: "webconsole", + dropTarget: "inspector", + expectedOrder: [ + "webconsole", + "inspector", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + "accessibility", + "application", + ], + }, + { + description: + "DragAndDrop the target component over the starting of the tab", + dragTarget: "netmonitor", + passedTargets: [ + "memory", + "performance", + "styleeditor", + "jsdebugger", + "webconsole", + "inspector", + ], + dropTarget: "#toolbox-buttons-start", + expectedOrder: [ + "netmonitor", + "inspector", + "webconsole", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "storage", + "accessibility", + "application", + ], + }, + { + description: "DragAndDrop the target component over the ending of the tab", + dragTarget: "webconsole", + passedTargets: [ + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + ], + dropTarget: "#toolbox-buttons-end", + expectedOrder: [ + "inspector", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + "accessibility", + "application", + "webconsole", + ], + }, +]; + +add_task(async function () { + // Enable the Application panel (atm it's only available on Nightly) + await pushPref("devtools.application.enabled", true); + + const tab = await addTab("about:blank"); + const toolbox = await openToolboxForTab( + tab, + "inspector", + Toolbox.HostType.BOTTOM + ); + + const originalPreference = Services.prefs.getCharPref( + "devtools.toolbox.tabsOrder" + ); + const win = getWindow(toolbox); + const { outerWidth: originalWindowWidth, outerHeight: originalWindowHeight } = + win; + registerCleanupFunction(() => { + Services.prefs.setCharPref( + "devtools.toolbox.tabsOrder", + originalPreference + ); + win.resizeTo(originalWindowWidth, originalWindowHeight); + }); + + for (const testData of TEST_DATA) { + info(`Test for '${testData.description}'`); + prepareToolTabReorderTest(toolbox, TEST_STARTING_ORDER); + await dndToolTab( + toolbox, + testData.dragTarget, + testData.dropTarget, + testData.passedTargets + ); + assertToolTabOrder(toolbox, testData.expectedOrder); + assertToolTabSelected(toolbox, testData.dragTarget); + assertToolTabPreferenceOrder(testData.expectedOrder); + } + + info("Test with overflowing tabs"); + prepareToolTabReorderTest(toolbox, TEST_STARTING_ORDER); + await resizeWindow(toolbox, 800); + await toolbox.selectTool("storage"); + const dragTarget = "storage"; + const dropTarget = "inspector"; + const expectedOrder = [ + "storage", + "inspector", + "webconsole", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "accessibility", + "application", + ]; + await dndToolTab(toolbox, dragTarget, dropTarget); + assertToolTabSelected(toolbox, dragTarget); + assertToolTabPreferenceOrder(expectedOrder); +}); diff --git a/devtools/client/framework/test/browser_toolbox_toolbar_reorder_by_width.js b/devtools/client/framework/test/browser_toolbox_toolbar_reorder_by_width.js new file mode 100644 index 0000000000..3a8cd61d12 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_toolbar_reorder_by_width.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This test will: +// +// * Confirm that currently selected button to access tools will not hide due to overflow. +// In this case, a button which is located on the left of a currently selected will hide. +// * Confirm that a button to access tool will hide when registering a new panel. +// +// Note that this test is based on the tab ordinal is fixed. +// i.e. After changed by Bug 1226272, this test might fail. + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +add_task(async function () { + const tab = await addTab("about:blank"); + + info("Open devtools on the Storage in a sidebar."); + const toolbox = await openToolboxForTab( + tab, + "storage", + Toolbox.HostType.BOTTOM + ); + + const win = getWindow(toolbox); + const { outerWidth: originalWindowWidth, outerHeight: originalWindowHeight } = + win; + registerCleanupFunction(() => { + win.resizeTo(originalWindowWidth, originalWindowHeight); + }); + + info("Waiting for the window to be resized"); + await resizeWindow(toolbox, 800); + + info("Wait until the tools menu button is available"); + await waitUntil(() => toolbox.doc.querySelector(".tools-chevron-menu")); + + const toolsMenuButton = toolbox.doc.querySelector(".tools-chevron-menu"); + ok(toolsMenuButton, "The tools menu button is displayed"); + + info("Confirm that selected tab is not hidden."); + const storageButton = toolbox.doc.querySelector("#toolbox-tab-storage"); + ok(storageButton, "The storage tab is on toolbox."); + + // Reset window size for 2nd test. + await resizeWindow(toolbox, originalWindowWidth); +}); + +add_task(async function () { + const tab = await addTab("about:blank"); + + info("Open devtools on the Storage in a sidebar."); + const toolbox = await openToolboxForTab( + tab, + "storage", + Toolbox.HostType.BOTTOM + ); + + info("Resize devtools window to a width that should trigger an overflow"); + await resizeWindow(toolbox, 800); + + info("Regist a new tab"); + const onRegistered = toolbox.once("tool-registered"); + gDevTools.registerTool({ + id: "test-tools", + label: "Test Tools", + isMenu: true, + isToolSupported: () => true, + build() {}, + }); + await onRegistered; + + info("Open the tools menu button."); + await openChevronMenu(toolbox); + + info("The registered new tool tab should be in the tools menu."); + let testToolsButton = toolbox.doc.querySelector( + "#tools-chevron-menupopup-test-tools" + ); + ok(testToolsButton, "The tools menu has a registered new tool button."); + + await closeChevronMenu(toolbox); + + info("Unregistering test-tools"); + const onUnregistered = toolbox.once("tool-unregistered"); + gDevTools.unregisterTool("test-tools"); + await onUnregistered; + + info("Open the tools menu button."); + await openChevronMenu(toolbox); + + info("An unregistered new tool tab should not be in the tools menu."); + testToolsButton = toolbox.doc.querySelector( + "#tools-chevron-menupopup-test-tools" + ); + ok( + !testToolsButton, + "The tools menu doesn't have a unregistered new tool button." + ); + + await closeChevronMenu(toolbox); +}); diff --git a/devtools/client/framework/test/browser_toolbox_toolbar_reorder_with_extension.js b/devtools/client/framework/test/browser_toolbox_toolbar_reorder_with_extension.js new file mode 100644 index 0000000000..d00aca4b0f --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_toolbar_reorder_with_extension.js @@ -0,0 +1,150 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for reordering with an extension installed. + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +const EXTENSION = "@reorder.test"; + +const TEST_STARTING_ORDER = [ + "inspector", + "webconsole", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + "accessibility", + "application", + EXTENSION, +]; + +add_task(async function () { + // Enable the Application panel (atm it's only available on Nightly) + await pushPref("devtools.application.enabled", true); + + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + devtools_page: "extension.html", + browser_specific_settings: { + gecko: { id: EXTENSION }, + }, + }, + files: { + "extension.html": ` + + + + + + + + `, + "extension.js": async () => { + // eslint-disable-next-line no-undef + await browser.devtools.panels.create( + "extension", + "fake-icon.png", + "empty.html" + ); + // eslint-disable-next-line no-undef + browser.test.sendMessage("devtools-page-ready"); + }, + "empty.html": "", + }, + }); + + await extension.startup(); + + const tab = await addTab("about:blank"); + const toolbox = await openToolboxForTab( + tab, + "webconsole", + Toolbox.HostType.BOTTOM + ); + await extension.awaitMessage("devtools-page-ready"); + + const originalPreference = Services.prefs.getCharPref( + "devtools.toolbox.tabsOrder" + ); + const win = getWindow(toolbox); + const { outerWidth: originalWindowWidth, outerHeight: originalWindowHeight } = + win; + registerCleanupFunction(() => { + Services.prefs.setCharPref( + "devtools.toolbox.tabsOrder", + originalPreference + ); + win.resizeTo(originalWindowWidth, originalWindowHeight); + }); + + info("Test for DragAndDrop the extension tab"); + let dragTarget = EXTENSION; + let dropTarget = "webconsole"; + let expectedOrder = [ + "inspector", + EXTENSION, + "webconsole", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + "accessibility", + "application", + ]; + prepareToolTabReorderTest(toolbox, TEST_STARTING_ORDER); + await dndToolTab(toolbox, dragTarget, dropTarget); + assertToolTabOrder(toolbox, expectedOrder); + assertToolTabSelected(toolbox, dragTarget); + assertToolTabPreferenceOrder(expectedOrder); + + info("Test the case of that the extension tab is overflowed"); + prepareToolTabReorderTest(toolbox, TEST_STARTING_ORDER); + await resizeWindow(toolbox, 800); + await toolbox.selectTool("storage"); + dragTarget = "storage"; + dropTarget = "inspector"; + expectedOrder = [ + "storage", + "inspector", + "webconsole", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "accessibility", + "application", + EXTENSION, + ]; + await dndToolTab(toolbox, dragTarget, dropTarget); + assertToolTabPreferenceOrder(expectedOrder); + await resizeWindow(toolbox, originalWindowWidth, originalWindowHeight); + + info("Test the preference after uninstalling extension"); + prepareToolTabReorderTest(toolbox, TEST_STARTING_ORDER); + await extension.unload(); + dragTarget = "webconsole"; + dropTarget = "inspector"; + expectedOrder = [ + "webconsole", + "inspector", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + "accessibility", + "application", + ]; + await dndToolTab(toolbox, dragTarget, dropTarget); + assertToolTabPreferenceOrder(expectedOrder); +}); diff --git a/devtools/client/framework/test/browser_toolbox_toolbar_reorder_with_hidden_extension.js b/devtools/client/framework/test/browser_toolbox_toolbar_reorder_with_hidden_extension.js new file mode 100644 index 0000000000..6e7b44d5d3 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_toolbar_reorder_with_hidden_extension.js @@ -0,0 +1,248 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for reordering with an hidden extension installed. + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +const EXTENSION = "@reorder.test"; + +const TEST_DATA = [ + { + description: "Test that drags a tab to left beyond the extension's tab", + startingOrder: [ + "inspector", + EXTENSION, + "webconsole", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + "accessibility", + "application", + ], + dragTarget: "webconsole", + dropTarget: "inspector", + expectedOrder: [ + "webconsole", + "inspector", + EXTENSION, + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + "accessibility", + "application", + ], + }, + { + description: "Test that drags a tab to right beyond the extension's tab", + startingOrder: [ + "inspector", + EXTENSION, + "webconsole", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + "accessibility", + "application", + ], + dragTarget: "inspector", + dropTarget: "webconsole", + expectedOrder: [ + EXTENSION, + "webconsole", + "inspector", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + "accessibility", + "application", + ], + }, + { + description: + "Test that drags a tab to left end, but hidden tab is left end", + startingOrder: [ + EXTENSION, + "inspector", + "webconsole", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + "accessibility", + "application", + ], + dragTarget: "webconsole", + dropTarget: "inspector", + expectedOrder: [ + EXTENSION, + "webconsole", + "inspector", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + "accessibility", + "application", + ], + }, + { + description: + "Test that drags a tab to right end, but hidden tab is right end", + startingOrder: [ + "inspector", + "webconsole", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + "accessibility", + "application", + EXTENSION, + ], + dragTarget: "webconsole", + dropTarget: "application", + expectedOrder: [ + "inspector", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + "accessibility", + "application", + EXTENSION, + "webconsole", + ], + }, +]; + +add_task(async function () { + // Enable the Application panel (atm it's only available on Nightly) + await pushPref("devtools.application.enabled", true); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.toolbox.tabsOrder"); + }); + + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + devtools_page: "extension.html", + browser_specific_settings: { + gecko: { id: EXTENSION }, + }, + }, + files: { + "extension.html": ` + + + + + + + + `, + "extension.js": async () => { + // Don't call browser.devtools.panels.create since this need to be as hidden. + // eslint-disable-next-line + browser.test.sendMessage("devtools-page-ready"); + }, + }, + }); + + await extension.startup(); + + const tab = await addTab("about:blank"); + const toolbox = await openToolboxForTab( + tab, + "webconsole", + Toolbox.HostType.BOTTOM + ); + await extension.awaitMessage("devtools-page-ready"); + + for (const { + description, + startingOrder, + dragTarget, + dropTarget, + expectedOrder, + } of TEST_DATA) { + info(description); + prepareTestWithHiddenExtension(toolbox, startingOrder); + await dndToolTab(toolbox, dragTarget, dropTarget); + assertToolTabPreferenceOrder(expectedOrder); + } + + info("Test ordering preference after uninstalling hidden addon"); + const startingOrder = [ + "inspector", + EXTENSION, + "webconsole", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + "accessibility", + "application", + ]; + const dragTarget = "webconsole"; + const dropTarget = "inspector"; + const expectedOrder = [ + "webconsole", + "inspector", + "jsdebugger", + "styleeditor", + "performance", + "memory", + "netmonitor", + "storage", + "accessibility", + "application", + ]; + prepareTestWithHiddenExtension(toolbox, startingOrder); + await extension.unload(); + await dndToolTab(toolbox, dragTarget, dropTarget); + assertToolTabPreferenceOrder(expectedOrder); +}); + +function prepareTestWithHiddenExtension(toolbox, startingOrder) { + Services.prefs.setCharPref( + "devtools.toolbox.tabsOrder", + startingOrder.join(",") + ); + + for (const id of startingOrder) { + if (id === EXTENSION) { + ok( + !getElementByToolId(toolbox, id), + "Hidden extension tab should not exist" + ); + } else { + ok(getElementByToolId(toolbox, id), `Tab element should exist for ${id}`); + } + } +} diff --git a/devtools/client/framework/test/browser_toolbox_tools_per_toolbox_registration.js b/devtools/client/framework/test/browser_toolbox_tools_per_toolbox_registration.js new file mode 100644 index 0000000000..0e9009497f --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_tools_per_toolbox_registration.js @@ -0,0 +1,138 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_URL = `data:text/html, + + + + + + test for registering and unregistering tools to a specific toolbox + + `; + +const TOOL_ID = "test-toolbox-tool"; +var toolbox; + +function test() { + addTab(TEST_URL).then(async tab => { + gDevTools + .showToolboxForTab(tab) + .then(toolboxRegister) + .then(testToolRegistered); + }); +} + +var resolveToolInstanceBuild; +var waitForToolInstanceBuild = new Promise(resolve => { + resolveToolInstanceBuild = resolve; +}); + +var resolveToolInstanceDestroyed; +var waitForToolInstanceDestroyed = new Promise(resolve => { + resolveToolInstanceDestroyed = resolve; +}); + +function toolboxRegister(aToolbox) { + toolbox = aToolbox; + + waitForToolInstanceBuild = new Promise(resolve => { + resolveToolInstanceBuild = resolve; + }); + + info("add per-toolbox tool in the opened toolbox."); + + toolbox.addAdditionalTool({ + id: TOOL_ID, + // The size of the label can make the test fail if it's too long. + // See ok(tab, ...) assert below and Bug 1596345. + label: "Test Tool", + inMenu: true, + isToolSupported: () => true, + build() { + info("per-toolbox tool has been built."); + resolveToolInstanceBuild(); + + return { + destroy: () => { + info("per-toolbox tool has been destroyed."); + resolveToolInstanceDestroyed(); + }, + }; + }, + key: "t", + }); +} + +function testToolRegistered() { + ok( + !gDevTools.getToolDefinitionMap().has(TOOL_ID), + "per-toolbox tool is not registered globally" + ); + ok( + toolbox.hasAdditionalTool(TOOL_ID), + "per-toolbox tool registered to the specific toolbox" + ); + + // Test that the tool appeared in the UI. + const doc = toolbox.doc; + const tab = getToolboxTab(doc, TOOL_ID); + + ok(tab, "new tool's tab exists in toolbox UI"); + + const panel = doc.getElementById("toolbox-panel-" + TOOL_ID); + ok(panel, "new tool's panel exists in toolbox UI"); + + for (const win of getAllBrowserWindows()) { + const key = win.document.getElementById("key_" + TOOL_ID); + if (win.document == doc) { + continue; + } + ok(!key, "key for new tool should not exists in the other browser windows"); + const menuitem = win.document.getElementById("menuitem_" + TOOL_ID); + ok(!menuitem, "menu item should not exists in the other browser window"); + } + + // Test that the tool is built once selected and then test its unregistering. + info("select per-toolbox tool in the opened toolbox."); + gDevTools + .showToolboxForTab(gBrowser.selectedTab, { toolId: TOOL_ID }) + .then(waitForToolInstanceBuild) + .then(testUnregister); +} + +function getAllBrowserWindows() { + return Array.from(Services.wm.getEnumerator("navigator:browser")); +} + +function testUnregister() { + info("remove per-toolbox tool in the opened toolbox."); + toolbox.removeAdditionalTool(TOOL_ID); + + Promise.all([waitForToolInstanceDestroyed]).then(toolboxToolUnregistered); +} + +function toolboxToolUnregistered() { + ok( + !toolbox.hasAdditionalTool(TOOL_ID), + "per-toolbox tool unregistered from the specific toolbox" + ); + + // test that it disappeared from the UI + const doc = toolbox.doc; + const tab = getToolboxTab(doc, TOOL_ID); + ok(!tab, "tool's tab was removed from the toolbox UI"); + + const panel = doc.getElementById("toolbox-panel-" + TOOL_ID); + ok(!panel, "tool's panel was removed from toolbox UI"); + + cleanup(); +} + +function cleanup() { + toolbox.destroy().then(() => { + toolbox = null; + gBrowser.removeCurrentTab(); + finish(); + }); +} diff --git a/devtools/client/framework/test/browser_toolbox_view_source_01.js b/devtools/client/framework/test/browser_toolbox_view_source_01.js new file mode 100644 index 0000000000..f1a0924cf9 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_view_source_01.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that Toolbox#viewSourceInDebugger works when debugger is not + * yet opened. + */ + +var URL = `${URL_ROOT_SSL}doc_viewsource.html`; +var JS_URL = `${URL_ROOT_SSL}code_math.js`; + +async function viewSource() { + const toolbox = await openNewTabAndToolbox(URL); + + await toolbox.viewSourceInDebugger(JS_URL, 2); + + const debuggerPanel = toolbox.getPanel("jsdebugger"); + ok(debuggerPanel, "The debugger panel was opened."); + is(toolbox.currentToolId, "jsdebugger", "The debugger panel was selected."); + + assertSelectedLocationInDebugger(debuggerPanel, 2, undefined); + await closeToolboxAndTab(toolbox); + finish(); +} + +function test() { + viewSource().then(finish, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + finish(); + }); +} diff --git a/devtools/client/framework/test/browser_toolbox_view_source_02.js b/devtools/client/framework/test/browser_toolbox_view_source_02.js new file mode 100644 index 0000000000..25bf0c2717 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_view_source_02.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that Toolbox#viewSourceInDebugger works when debugger is already loaded. + */ + +var URL = `${URL_ROOT_SSL}doc_viewsource.html`; +var JS_URL = `${URL_ROOT_SSL}code_math.js`; + +async function viewSource() { + const toolbox = await openNewTabAndToolbox(URL); + await toolbox.selectTool("jsdebugger"); + + await toolbox.viewSourceInDebugger(JS_URL, 2); + + const debuggerPanel = toolbox.getPanel("jsdebugger"); + ok(debuggerPanel, "The debugger panel was opened."); + is(toolbox.currentToolId, "jsdebugger", "The debugger panel was selected."); + + assertSelectedLocationInDebugger(debuggerPanel, 2, undefined); + + // See Bug 1637793 and Bug 1621337. + // Ideally the debugger should only resolve when the worker targets have been + // retrieved, which should be fixed by Bug 1621337 or a followup. + info("Wait for all pending requests to settle on the DevToolsClient"); + await toolbox.commands.client.waitForRequestsToSettle(); + + await closeToolboxAndTab(toolbox); + finish(); +} + +function test() { + viewSource().then(finish, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + finish(); + }); +} diff --git a/devtools/client/framework/test/browser_toolbox_view_source_03.js b/devtools/client/framework/test/browser_toolbox_view_source_03.js new file mode 100644 index 0000000000..dce9ff8840 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_view_source_03.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that Toolbox#viewSourceInStyleEditor works when style editor is not + * yet opened. + */ + +var URL = `${URL_ROOT_SSL}doc_viewsource.html`; +var CSS_URL = `${URL_ROOT_SSL}doc_theme.css`; + +async function viewSource() { + const toolbox = await openNewTabAndToolbox(URL); + + const fileFound = await toolbox.viewSourceInStyleEditorByURL(CSS_URL, 2); + ok( + fileFound, + "viewSourceInStyleEditorByURL should resolve to true if source found." + ); + + const stylePanel = toolbox.getPanel("styleeditor"); + ok(stylePanel, "The style editor panel was opened."); + is( + toolbox.currentToolId, + "styleeditor", + "The style editor panel was selected." + ); + + const { UI } = stylePanel; + + is( + UI.selectedEditor.styleSheet.href, + CSS_URL, + "The correct source is shown in the style editor." + ); + is( + UI.selectedEditor.sourceEditor.getCursor().line + 1, + 2, + "The correct line is highlighted in the style editor's source editor." + ); + + await closeToolboxAndTab(toolbox); + finish(); +} + +function test() { + viewSource().then(finish, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + finish(); + }); +} diff --git a/devtools/client/framework/test/browser_toolbox_view_source_style_editor_fallback.js b/devtools/client/framework/test/browser_toolbox_view_source_style_editor_fallback.js new file mode 100644 index 0000000000..b895d0a80e --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_view_source_style_editor_fallback.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that Toolbox#viewSourceInStyleEditor does fall back to view-source + */ + +const TEST_URL = `data:text/html,Got no style`; +const CSS_URL = `${URL_ROOT_SSL}doc_theme.css`; + +add_task(async function () { + // start on webconsole since it doesn't have much activity so we're less vulnerable + // to pending promises. + const toolbox = await openNewTabAndToolbox(TEST_URL, "webconsole"); + + const onTabOpen = BrowserTestUtils.waitForNewTab( + gBrowser, + url => url == `view-source:${CSS_URL}`, + true + ); + + info("View source of an existing file that isn't used by the page"); + const fileFound = await toolbox.viewSourceInStyleEditorByURL(CSS_URL, 0); + ok( + !fileFound, + "viewSourceInStyleEditorByURL should resolve to false if source isn't found." + ); + + info("Waiting for view-source tab to open"); + const viewSourceTab = await onTabOpen; + ok(true, "The view source tab was opened"); + await removeTab(viewSourceTab); + + info("Check that the current panel is the console"); + is(toolbox.currentToolId, "webconsole", "Console is still selected"); + + await closeToolboxAndTab(toolbox); +}); diff --git a/devtools/client/framework/test/browser_toolbox_watchedByDevTools.js b/devtools/client/framework/test/browser_toolbox_watchedByDevTools.js new file mode 100644 index 0000000000..a58b57885d --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_watchedByDevTools.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that the "watchedByDevTools" flag is properly handled. + */ +const EXAMPLE_HTTP_URI = + "http://mochi.test:8888/document-builder.sjs?html=
http"; +const EXAMPLE_COM_URI = + "https://example.com/document-builder.sjs?html=
com"; +const EXAMPLE_ORG_URI = + "https://example.org/document-builder.sjs?headers=Cross-Origin-Opener-Policy:same-origin&html=
org
"; + +add_task(async function () { + const tab = await addTab(EXAMPLE_HTTP_URI); + + is( + tab.linkedBrowser.browsingContext.watchedByDevTools, + false, + "watchedByDevTools isn't set when DevTools aren't opened" + ); + + info( + "Open a toolbox for the opened tab and check that watchedByDevTools is set" + ); + await gDevTools.showToolboxForTab(tab, { toolId: "options" }); + + is( + tab.linkedBrowser.browsingContext.watchedByDevTools, + true, + "watchedByDevTools is set after opening a toolbox" + ); + + info( + "Check that watchedByDevTools persist when the tab navigates to a different origin" + ); + await navigateTo(EXAMPLE_COM_URI); + + is( + tab.linkedBrowser.browsingContext.watchedByDevTools, + true, + "watchedByDevTools is still set after navigating to a different origin" + ); + + info( + "Check that watchedByDevTools persist when navigating to a page that creates a new browsing context" + ); + const previousBrowsingContextId = tab.linkedBrowser.browsingContext.id; + await navigateTo(EXAMPLE_ORG_URI); + + isnot( + tab.linkedBrowser.browsingContext.id, + previousBrowsingContextId, + "A new browsing context was created" + ); + + is( + tab.linkedBrowser.browsingContext.watchedByDevTools, + true, + "watchedByDevTools is still set after navigating to a new browsing context" + ); + + info("Check that the flag is reset when the toolbox is closed"); + await gDevTools.closeToolboxForTab(tab); + is( + tab.linkedBrowser.browsingContext.watchedByDevTools, + false, + "watchedByDevTools is reset after closing the toolbox" + ); +}); diff --git a/devtools/client/framework/test/browser_toolbox_window_reload_target.js b/devtools/client/framework/test/browser_toolbox_window_reload_target.js new file mode 100644 index 0000000000..d9a4eb34c1 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_window_reload_target.js @@ -0,0 +1,130 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that pressing various page reload keyboard shortcuts always works when devtools +// has focus, no matter if it's undocked or docked, and whatever the tool selected (this +// is to avoid tools from overriding the page reload shortcuts). +// This test also serves as a safety net checking that tools just don't explode when the +// page is reloaded. +// It is therefore quite long to run. + +requestLongerTimeout(10); +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); + +// allow a context error because it is harmless. This could likely be removed in the next patch because it is a symptom of events coming from the target-list and debugger targets module... +PromiseTestUtils.allowMatchingRejectionsGlobally(/Page has navigated/); + +const TEST_URL = + "data:text/html;charset=utf-8," + + "Test reload" + + "

Testing reload from devtools

"; + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); +const L10N = new LocalizationHelper( + "devtools/client/locales/toolbox.properties" +); + +// Track how many page reloads we've sent to the page. +var reloadsSent = 0; + +add_task(async function () { + await addTab(TEST_URL); + const tab = gBrowser.selectedTab; + const toolIDs = await getSupportedToolIds(tab); + + info( + "Display the toolbox, docked at the bottom, with the first tool selected" + ); + const toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: toolIDs[0], + hostType: Toolbox.HostType.BOTTOM, + }); + + info( + "Listen to page reloads to check that they are indeed sent by the toolbox" + ); + let reloadDetected = 0; + const reloadCounter = msg => { + reloadDetected++; + info("Detected reload #" + reloadDetected); + is( + reloadDetected, + reloadsSent, + "Detected the right number of reloads in the page" + ); + }; + + const removeLoadListener = BrowserTestUtils.addContentEventListener( + gBrowser.selectedBrowser, + "load", + reloadCounter, + {} + ); + + info("Start testing with the toolbox docked"); + // Note that we actually only test 1 tool in docked mode, to cut down on test time. + await testOneTool(toolbox, toolIDs[toolIDs.length - 1]); + + info("Switch to undocked mode"); + await toolbox.switchHost(Toolbox.HostType.WINDOW); + toolbox.win.focus(); + + info("Now test with the toolbox undocked"); + for (const toolID of toolIDs) { + await testOneTool(toolbox, toolID); + } + + info("Switch back to docked mode"); + await toolbox.switchHost(Toolbox.HostType.BOTTOM); + + removeLoadListener(); + + await toolbox.destroy(); + gBrowser.removeCurrentTab(); +}); + +async function testOneTool(toolbox, toolID) { + info(`Select tool ${toolID}`); + await toolbox.selectTool(toolID); + + assertThemeStyleSheet(toolbox, toolID); + + await testReload("toolbox.reload.key", toolbox); + await testReload("toolbox.reload2.key", toolbox); + await testReload("toolbox.forceReload.key", toolbox); + await testReload("toolbox.forceReload2.key", toolbox); +} + +async function testReload(shortcut, toolbox) { + info(`Reload with ${shortcut}`); + + await sendToolboxReloadShortcut(L10N.getStr(shortcut), toolbox); + reloadsSent++; +} + +/** + * As opening all panels is an expensive operation, reuse this test in order + * to add a few assertions around panel's stylesheets. + * Ensure the proper ordering of the theme stylesheet. `global.css` should come + * first if it exists, then the theme. + */ +function assertThemeStyleSheet(toolbox, toolID) { + const iframe = toolbox.doc.getElementById("toolbox-panel-iframe-" + toolID); + const styleSheets = iframe.contentDocument.querySelectorAll( + `link[rel="stylesheet"]` + ); + ok( + !!styleSheets.length, + `The panel ${toolID} should have at least have one stylesheet` + ); + + // In the web console, we have a special case where global.css is registered very first + if (styleSheets[0].href === "chrome://global/skin/global.css") { + is(styleSheets[1].href, "chrome://devtools/skin/light-theme.css"); + } else { + // Otherwise, in all other panels, the theme file is registered very first + is(styleSheets[0].href, "chrome://devtools/skin/light-theme.css"); + } +} diff --git a/devtools/client/framework/test/browser_toolbox_window_reload_target_force.js b/devtools/client/framework/test/browser_toolbox_window_reload_target_force.js new file mode 100644 index 0000000000..d563fb7d37 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_window_reload_target_force.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Simple test page which writes the value of the cache-control header. +const TEST_URL = URL_ROOT + "sjs_cache_controle_header.sjs"; + +const L10N = new LocalizationHelper( + "devtools/client/locales/toolbox.properties" +); + +// Test that "forceReload" shorcuts send requests with the correct cache-control +// header value: no-cache. +add_task(async function () { + await addTab(TEST_URL); + const tab = gBrowser.selectedTab; + + info("Open the toolbox with the inspector selected"); + const toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: "inspector", + }); + + // The VALIDATE_ALWAYS flag isn’t going to be applied when we only revalidate + // the top level document, thus the expectedHeader is empty. + const expectedHeader = Services.prefs.getBoolPref( + "browser.soft_reload.only_force_validate_top_level_document", + false + ) + ? "" + : "max-age=0"; + await testReload("toolbox.reload.key", toolbox, expectedHeader); + await testReload("toolbox.reload2.key", toolbox, expectedHeader); + await testReload("toolbox.forceReload.key", toolbox, "no-cache"); + await testReload("toolbox.forceReload2.key", toolbox, "no-cache"); +}); + +async function testReload(shortcut, toolbox, expectedHeader) { + info(`Reload with ${shortcut}`); + await sendToolboxReloadShortcut(L10N.getStr(shortcut), toolbox); + + info("Retrieve the text content of the test page"); + const textContent = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + function () { + return content.document.body.textContent; + } + ); + + // See sjs_cache_controle_header.sjs + is( + textContent, + "cache-control:" + expectedHeader, + "cache-control header for the page request had the expected value" + ); +} diff --git a/devtools/client/framework/test/browser_toolbox_window_shortcuts.js b/devtools/client/framework/test/browser_toolbox_window_shortcuts.js new file mode 100644 index 0000000000..53cea3a55a --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_window_shortcuts.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var Startup = Cc["@mozilla.org/devtools/startup-clh;1"].getService( + Ci.nsISupports +).wrappedJSObject; +var { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +var toolbox, + toolIDs, + toolShortcuts = [], + idIndex, + modifiedPrefs = []; + +async function test() { + addTab("about:blank").then(async function () { + toolIDs = []; + for (const [id, definition] of gDevTools._tools) { + const shortcut = Startup.KeyShortcuts.filter(s => s.toolId == id)[0]; + if (!shortcut) { + continue; + } + toolIDs.push(id); + toolShortcuts.push(shortcut); + + // Enable disabled tools + const pref = definition.visibilityswitch; + if (pref) { + const prefValue = Services.prefs.getBoolPref(pref, false); + if (!prefValue) { + modifiedPrefs.push(pref); + Services.prefs.setBoolPref(pref, true); + } + } + } + const tab = gBrowser.selectedTab; + idIndex = 0; + gDevTools + .showToolboxForTab(tab, { + toolId: toolIDs[0], + hostType: Toolbox.HostType.WINDOW, + }) + .then(testShortcuts); + }); +} + +function testShortcuts(aToolbox, aIndex) { + if (aIndex === undefined) { + aIndex = 1; + } else if (aIndex == toolIDs.length) { + tidyUp(); + return; + } + + toolbox = aToolbox; + info("Toolbox fired a `ready` event"); + + toolbox.once("select", selectCB); + + const shortcut = toolShortcuts[aIndex]; + const key = shortcut.shortcut; + const toolModifiers = shortcut.modifiers; + const modifiers = { + accelKey: toolModifiers.includes("accel"), + altKey: toolModifiers.includes("alt"), + shiftKey: toolModifiers.includes("shift"), + }; + idIndex = aIndex; + info( + "Testing shortcut for tool " + + aIndex + + ":" + + toolIDs[aIndex] + + " using key " + + key + ); + EventUtils.synthesizeKey(key, modifiers, toolbox.win.parent); +} + +function selectCB(id) { + info("toolbox-select event from " + id); + + is( + toolIDs.indexOf(id), + idIndex, + "Correct tool is selected on pressing the shortcut for " + id + ); + + testShortcuts(toolbox, idIndex + 1); +} + +function tidyUp() { + toolbox.destroy().then(function () { + gBrowser.removeCurrentTab(); + + for (const pref of modifiedPrefs) { + Services.prefs.clearUserPref(pref); + } + toolbox = toolIDs = idIndex = modifiedPrefs = Toolbox = null; + finish(); + }); +} diff --git a/devtools/client/framework/test/browser_toolbox_window_title_changes.js b/devtools/client/framework/test/browser_toolbox_window_title_changes.js new file mode 100644 index 0000000000..176b0b0f65 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_window_title_changes.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +requestLongerTimeout(5); + +var { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +const NAME_1 = ""; +const NAME_2 = "Toolbox test for title update"; +const NAME_3 = NAME_2; +const NAME_4 = "Toolbox test for another title update"; + +const URL_1 = "data:text/plain;charset=UTF-8,abcde"; +const URL_2 = + URL_ROOT_ORG_SSL + "browser_toolbox_window_title_changes_page.html"; +const URL_3 = + URL_ROOT_COM_SSL + "browser_toolbox_window_title_changes_page.html"; +const URL_4 = `https://example.com/document-builder.sjs?html=${NAME_4}

Hello`; + +add_task(async function test() { + await addTab(URL_1); + + const tab = gBrowser.selectedTab; + let toolbox = await gDevTools.showToolboxForTab(tab, { + hostType: Toolbox.HostType.BOTTOM, + }); + await toolbox.selectTool("webconsole"); + + info("Undock toolbox and check title"); + // We have to first switch the host in order to spawn the new top level window + // on which we are going to listen from title change event + await toolbox.switchHost(Toolbox.HostType.WINDOW); + await checkTitle(NAME_1, URL_1, "toolbox undocked"); + + info("switch to different tool and check title again"); + await toolbox.selectTool("jsdebugger"); + await checkTitle(NAME_1, URL_1, "tool changed"); + + info("navigate to different local url and check title"); + + await navigateTo(URL_2); + info("wait for title change"); + await checkTitle(NAME_2, URL_2, "url changed"); + + info("navigate to a real url and check title"); + await navigateTo(URL_3); + + info("wait for title change"); + await checkTitle(NAME_3, URL_3, "url changed"); + + info("navigate to another page on the same domain"); + await navigateTo(URL_4); + await checkTitle(NAME_4, URL_4, "title changed"); + + info( + "destroy toolbox, create new one hosted in a window (with a different tool id), and check title" + ); + // Give the tools a chance to handle the navigation event before + // destroying the toolbox. + await new Promise(resolve => executeSoon(resolve)); + await toolbox.destroy(); + + // After destroying the toolbox, open a new one. + toolbox = await gDevTools.showToolboxForTab(tab, { + hostType: Toolbox.HostType.WINDOW, + }); + toolbox.selectTool("webconsole"); + await checkTitle(NAME_4, URL_4, "toolbox destroyed and recreated"); + + info("clean up"); + await toolbox.destroy(); + gBrowser.removeCurrentTab(); + Services.prefs.clearUserPref("devtools.toolbox.host"); + Services.prefs.clearUserPref("devtools.toolbox.selectedTool"); +}); + +function getExpectedTitle(name, url) { + if (name) { + return `Developer Tools — ${name} — ${url}`; + } + return `Developer Tools — ${url}`; +} + +async function checkTitle(name, url, context) { + info("Check title - " + context); + await waitFor( + () => getToolboxWindowTitle() === getExpectedTitle(name, url), + `Didn't get the expected title ("${getExpectedTitle(name, url)}"`, + 200, + 50 + ); + const expectedTitle = getExpectedTitle(name, url); + is(getToolboxWindowTitle(), expectedTitle, context); +} + +function getToolboxWindowTitle() { + return Services.wm.getMostRecentWindow("devtools:toolbox").document.title; +} diff --git a/devtools/client/framework/test/browser_toolbox_window_title_changes_page.html b/devtools/client/framework/test/browser_toolbox_window_title_changes_page.html new file mode 100644 index 0000000000..8678469ee5 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_window_title_changes_page.html @@ -0,0 +1,10 @@ + + + + + Toolbox test for title update + + + + diff --git a/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js b/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js new file mode 100644 index 0000000000..06905fbd3b --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js @@ -0,0 +1,171 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that the detached devtools window title is not updated when switching + * the selected frame. Also check that frames command button has 'open' + * attribute set when the list of frames is opened. + */ + +var { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); +const URL = + URL_ROOT_SSL + "browser_toolbox_window_title_frame_select_page.html"; +const IFRAME_URL = + URL_ROOT_SSL + "browser_toolbox_window_title_changes_page.html"; +const L10N = new LocalizationHelper( + "devtools/client/locales/toolbox.properties" +); + +/** + * Wait for a given toolbox to get its title updated. + */ +function waitForTitleChange(toolbox) { + return new Promise(resolve => { + toolbox.topWindow.addEventListener("message", function onmessage(event) { + if (event.data.name == "set-host-title") { + toolbox.topWindow.removeEventListener("message", onmessage); + resolve(); + } + }); + }); +} + +add_task(async function () { + Services.prefs.setBoolPref("devtools.command-button-frames.enabled", true); + + await addTab(URL); + const tab = gBrowser.selectedTab; + let toolbox = await gDevTools.showToolboxForTab(tab, { + hostType: Toolbox.HostType.BOTTOM, + }); + + await toolbox.switchHost(Toolbox.HostType.WINDOW); + // Wait for title change event *after* switch host, in order to listen + // for the event on the WINDOW host window, which only exists after switchHost + await waitForTitleChange(toolbox); + + is( + getTitle(), + `Developer Tools — Page title — ${URL}`, + "Devtools title correct after switching to detached window host" + ); + + // Wait for tick to avoid unexpected 'popuphidden' event, which + // blocks the frame popup menu opened below. See also bug 1276873 + await waitForTick(); + + const btn = toolbox.doc.getElementById("command-button-frames"); + + await testShortcutToOpenFrames(btn, toolbox); + + // Open frame menu and wait till it's available on the screen. + // Also check 'aria-expanded' attribute on the command button. + is( + btn.getAttribute("aria-expanded"), + "false", + "The aria-expanded attribute must be set to false" + ); + btn.click(); + + const panel = toolbox.doc.getElementById("command-button-frames-panel"); + ok(panel, "popup panel has created."); + await waitUntil(() => panel.classList.contains("tooltip-visible")); + + is( + btn.getAttribute("aria-expanded"), + "true", + "The aria-expanded attribute must be set to true" + ); + + // Verify that the frame list menu is populated + const menuList = toolbox.doc.getElementById("toolbox-frame-menu"); + const frames = Array.from(menuList.querySelectorAll(".command")); + is(frames.length, 2, "We have both frames in the list"); + + const topFrameBtn = frames.filter( + b => b.querySelector(".label").textContent == URL + )[0]; + const iframeBtn = frames.filter( + b => b.querySelector(".label").textContent == IFRAME_URL + )[0]; + ok(topFrameBtn, "Got top level document in the list"); + ok(iframeBtn, "Got iframe document in the list"); + + // Listen to will-navigate to check if the view is empty + const { resourceCommand } = toolbox.commands; + const { onResource: willNavigate } = + await resourceCommand.waitForNextResource( + resourceCommand.TYPES.DOCUMENT_EVENT, + { + ignoreExistingResources: true, + predicate(resource) { + return resource.name == "will-navigate"; + }, + } + ); + + // Only select the iframe after we are able to select an element from the top + // level document. + const onInspectorReloaded = toolbox.getPanel("inspector").once("reloaded"); + info("Select the iframe"); + iframeBtn.click(); + + // will-navigate isn't emitted in the targetCommand-based iframe picker. + if (!isEveryFrameTargetEnabled()) { + await willNavigate; + } + await onInspectorReloaded; + // wait a bit more in case an eventual title update would happen later + await wait(1000); + + info("Navigation to the iframe is done, the inspector should be back up"); + is( + getTitle(), + `Developer Tools — Page title — ${URL}`, + "Devtools title was not updated after changing inspected frame" + ); + + info("Cleanup toolbox and test preferences."); + await toolbox.destroy(); + toolbox = null; + gBrowser.removeCurrentTab(); + Services.prefs.clearUserPref("devtools.toolbox.host"); + Services.prefs.clearUserPref("devtools.toolbox.selectedTool"); + Services.prefs.clearUserPref("devtools.command-button-frames.enabled"); + finish(); +}); + +function getTitle() { + return Services.wm.getMostRecentWindow("devtools:toolbox").document.title; +} + +async function testShortcutToOpenFrames(btn, toolbox) { + info("Tests if shortcut Alt+Down opens the frames"); + // focus the button so that keyPress can be performed + btn.focus(); + // perform keyPress - Alt+Down + const shortcut = L10N.getStr("toolbox.showFrames.key"); + synthesizeKeyShortcut(shortcut, toolbox.win); + + const panel = toolbox.doc.getElementById("command-button-frames-panel"); + ok(panel, "popup panel has created."); + await waitUntil(() => panel.classList.contains("tooltip-visible")); + + is( + btn.getAttribute("aria-expanded"), + "true", + "The aria-expanded attribute must be set to true" + ); + + // pressing Esc should hide the menu again + EventUtils.sendKey("ESCAPE", toolbox.win); + await waitUntil(() => !panel.classList.contains("tooltip-visible")); + + is( + btn.getAttribute("aria-expanded"), + "false", + "The aria-expanded attribute must be set to false" + ); +} diff --git a/devtools/client/framework/test/browser_toolbox_window_title_frame_select_page.html b/devtools/client/framework/test/browser_toolbox_window_title_frame_select_page.html new file mode 100644 index 0000000000..1eda94a9cf --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_window_title_frame_select_page.html @@ -0,0 +1,11 @@ + + + + + Page title + + + + + diff --git a/devtools/client/framework/test/browser_toolbox_zoom.js b/devtools/client/framework/test/browser_toolbox_zoom.js new file mode 100644 index 0000000000..6db94900f9 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_zoom.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); +const L10N = new LocalizationHelper( + "devtools/client/locales/toolbox.properties" +); + +add_task(async function () { + registerCleanupFunction(function () { + Services.prefs.clearUserPref("devtools.toolbox.zoomValue"); + }); + + // This test assume that zoom value will be default value. i.e. x1.0. + Services.prefs.setCharPref("devtools.toolbox.zoomValue", "1.0"); + await addTab("about:blank"); + const toolbox = await gDevTools.showToolboxForTab(gBrowser.selectedTab, { + toolId: "styleeditor", + hostType: Toolbox.HostType.BOTTOM, + }); + + info("testing zoom keys"); + + testZoomLevel("In", 2, 1.2, toolbox); + testZoomLevel("Out", 3, 0.9, toolbox); + testZoomLevel("Reset", 1, 1, toolbox); + + await toolbox.destroy(); + gBrowser.removeCurrentTab(); +}); + +function testZoomLevel(type, times, expected, toolbox) { + sendZoomKey("toolbox.zoom" + type + ".key", times); + + const zoom = getCurrentZoom(toolbox); + is( + zoom.toFixed(1), + expected.toFixed(1), + "zoom level correct after zoom " + type + ); + + const savedZoom = parseFloat( + Services.prefs.getCharPref("devtools.toolbox.zoomValue") + ); + is( + savedZoom.toFixed(1), + expected.toFixed(1), + "saved zoom level is correct after zoom " + type + ); +} + +function sendZoomKey(shortcut, times) { + for (let i = 0; i < times; i++) { + synthesizeKeyShortcut(L10N.getStr(shortcut)); + } +} + +function getCurrentZoom(toolbox) { + return toolbox.win.browsingContext.fullZoom; +} diff --git a/devtools/client/framework/test/browser_toolbox_zoom_popup.js b/devtools/client/framework/test/browser_toolbox_zoom_popup.js new file mode 100644 index 0000000000..7b764bf703 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_zoom_popup.js @@ -0,0 +1,213 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the popup menu position when zooming in the devtools panel. + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +// Use a simple URL in order to prevent displacing the left position of the +// frames menu. +const TEST_URL = "data:text/html;charset=utf-8, + + + diff --git a/devtools/client/framework/test/doc_cached-resource_iframe.html b/devtools/client/framework/test/doc_cached-resource_iframe.html new file mode 100644 index 0000000000..0fc5bb2263 --- /dev/null +++ b/devtools/client/framework/test/doc_cached-resource_iframe.html @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/devtools/client/framework/test/doc_empty-tab-01.html b/devtools/client/framework/test/doc_empty-tab-01.html new file mode 100644 index 0000000000..28398f7768 --- /dev/null +++ b/devtools/client/framework/test/doc_empty-tab-01.html @@ -0,0 +1,14 @@ + + + + + + + Empty test page 1 + + + + + + diff --git a/devtools/client/framework/test/doc_lazy_tool.html b/devtools/client/framework/test/doc_lazy_tool.html new file mode 100644 index 0000000000..3f1f1b7d01 --- /dev/null +++ b/devtools/client/framework/test/doc_lazy_tool.html @@ -0,0 +1,6 @@ + + + + Lazy tool + + diff --git a/devtools/client/framework/test/doc_textbox_tool.html b/devtools/client/framework/test/doc_textbox_tool.html new file mode 100644 index 0000000000..6f0c32ade0 --- /dev/null +++ b/devtools/client/framework/test/doc_textbox_tool.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/devtools/client/framework/test/doc_theme.css b/devtools/client/framework/test/doc_theme.css new file mode 100644 index 0000000000..5ed6e866a0 --- /dev/null +++ b/devtools/client/framework/test/doc_theme.css @@ -0,0 +1,3 @@ +.theme-test #devtools-theme-box { + color: red !important; +} diff --git a/devtools/client/framework/test/doc_viewsource.html b/devtools/client/framework/test/doc_viewsource.html new file mode 100644 index 0000000000..7094eb87eb --- /dev/null +++ b/devtools/client/framework/test/doc_viewsource.html @@ -0,0 +1,13 @@ + + + + + Toolbox test for View Source methods + + + + + + + diff --git a/devtools/client/framework/test/head.js b/devtools/client/framework/test/head.js new file mode 100644 index 0000000000..2001d5e8c4 --- /dev/null +++ b/devtools/client/framework/test/head.js @@ -0,0 +1,490 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// shared-head.js handles imports, constants, and utility functions +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js", + this +); + +const EventEmitter = require("resource://devtools/shared/event-emitter.js"); + +/** + * Retrieve all tool ids compatible with a target created for the provided tab. + * + * @param {XULTab} tab + * The tab for which we want to get the list of supported toolIds + * @return {Array} array of tool ids + */ +async function getSupportedToolIds(tab) { + info("Getting the entire list of tools supported in this tab"); + + let shouldDestroyToolbox = false; + + // Get the toolbox for this tab, or create one if needed. + let toolbox = gDevTools.getToolboxForTab(tab); + if (!toolbox) { + toolbox = await gDevTools.showToolboxForTab(tab); + shouldDestroyToolbox = true; + } + + const toolIds = gDevTools + .getToolDefinitionArray() + .filter(def => def.isToolSupported(toolbox)) + .map(def => def.id); + + if (shouldDestroyToolbox) { + // Only close the toolbox if it was explicitly created here. + await toolbox.destroy(); + } + + return toolIds; +} + +function toggleAllTools(state) { + for (const [, tool] of gDevTools._tools) { + if (!tool.visibilityswitch) { + continue; + } + if (state) { + Services.prefs.setBoolPref(tool.visibilityswitch, true); + } else { + Services.prefs.clearUserPref(tool.visibilityswitch); + } + } +} + +async function getParentProcessActors(callback) { + const commands = await CommandsFactory.forMainProcess(); + const mainProcessTargetFront = await commands.descriptorFront.getTarget(); + + callback(commands.client, mainProcessTargetFront); +} + +function getSourceActor(aSources, aURL) { + const item = aSources.getItemForAttachment(a => a.source.url === aURL); + return item && item.value; +} + +/** + * Synthesize a keypress from a element, taking into account + * any modifiers. + * @param {Element} el the element to synthesize + */ +function synthesizeKeyElement(el) { + const key = el.getAttribute("key") || el.getAttribute("keycode"); + const mod = {}; + el.getAttribute("modifiers") + .split(" ") + .forEach(m => (mod[m + "Key"] = true)); + info(`Synthesizing: key=${key}, mod=${JSON.stringify(mod)}`); + EventUtils.synthesizeKey(key, mod, el.ownerDocument.defaultView); +} + +/* Check the toolbox host type and prefs to make sure they match the + * expected values + * @param {Toolbox} + * @param {HostType} hostType + * One of {SIDE, BOTTOM, WINDOW} from Toolbox.HostType + * @param {HostType} Optional previousHostType + * The host that will be switched to when calling switchToPreviousHost + */ +function checkHostType(toolbox, hostType, previousHostType) { + is(toolbox.hostType, hostType, "host type is " + hostType); + + const pref = Services.prefs.getCharPref("devtools.toolbox.host"); + is(pref, hostType, "host pref is " + hostType); + + if (previousHostType) { + is( + Services.prefs.getCharPref("devtools.toolbox.previousHost"), + previousHostType, + "The previous host is correct" + ); + } +} + +/** + * Create a new + + + Empty test page 1 + + + + + + diff --git a/devtools/client/framework/test/reload/v2/code_bundle_reload.js b/devtools/client/framework/test/reload/v2/code_bundle_reload.js new file mode 100644 index 0000000000..c21166ba58 --- /dev/null +++ b/devtools/client/framework/test/reload/v2/code_bundle_reload.js @@ -0,0 +1,19 @@ +/******/ (() => { + // webpackBootstrap + /******/ "use strict"; + var __webpack_exports__ = {}; + /*!*****************************!*\ + !*** ./v2/code_reload_2.js ***! + \*****************************/ + /* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + + function f() { + console.log("The second version of the script"); + } + + f(); + + /******/ +})(); +//# sourceMappingURL=code_bundle_reload.js.map diff --git a/devtools/client/framework/test/reload/v2/code_bundle_reload.js.map b/devtools/client/framework/test/reload/v2/code_bundle_reload.js.map new file mode 100644 index 0000000000..d28bd1e30c --- /dev/null +++ b/devtools/client/framework/test/reload/v2/code_bundle_reload.js.map @@ -0,0 +1 @@ +{"version":3,"file":"v2/code_bundle_reload.js","mappings":";;;;;;AAAA;AACA;;AAEa;;AAEb;AACA;AACA;;AAEA","sources":["webpack://code-reload/./v2/code_reload_2.js"],"sourcesContent":["/* Any copyright is dedicated to the Public Domain.\n http://creativecommons.org/publicdomain/zero/1.0/ */\n\n\"use strict\";\n\nfunction f() {\n console.log(\"The second version of the script\");\n}\n\nf();\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/devtools/client/framework/test/reload/v2/code_reload_2.js b/devtools/client/framework/test/reload/v2/code_reload_2.js new file mode 100644 index 0000000000..f4690279b4 --- /dev/null +++ b/devtools/client/framework/test/reload/v2/code_reload_2.js @@ -0,0 +1,10 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function f() { + console.log("The second version of the script"); +} + +f(); diff --git a/devtools/client/framework/test/reload/v2/doc_reload.html b/devtools/client/framework/test/reload/v2/doc_reload.html new file mode 100644 index 0000000000..164e2cd26c --- /dev/null +++ b/devtools/client/framework/test/reload/v2/doc_reload.html @@ -0,0 +1,15 @@ + + + + + + + + Empty test page 2 + + + + + + diff --git a/devtools/client/framework/test/reload/webpack.config.js b/devtools/client/framework/test/reload/webpack.config.js new file mode 100644 index 0000000000..e26b42cd4c --- /dev/null +++ b/devtools/client/framework/test/reload/webpack.config.js @@ -0,0 +1,13 @@ +const path = require("path"); + +module.exports = [1, 2].map(version => { + return { + devtool: "source-map", + mode: "development", + entry: [path.join(__dirname, `/v${version}/code_reload_${version}.js`)], + output: { + path: __dirname, + filename: `v${version}/code_bundle_reload.js`, + }, + }; +}); diff --git a/devtools/client/framework/test/serviceworker.js b/devtools/client/framework/test/serviceworker.js new file mode 100644 index 0000000000..db1b339fe6 --- /dev/null +++ b/devtools/client/framework/test/serviceworker.js @@ -0,0 +1,4 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// empty service worker, always succeed! diff --git a/devtools/client/framework/test/sjs_cache_controle_header.sjs b/devtools/client/framework/test/sjs_cache_controle_header.sjs new file mode 100644 index 0000000000..af58a3fc89 --- /dev/null +++ b/devtools/client/framework/test/sjs_cache_controle_header.sjs @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* exported handleRequest */ + +"use strict"; + +// Simple server that writes a text response displaying the value of the +// cache-control header: +// - if the header is missing, the text will be `cache-control:` +// - if the header is available, the text will be `cache-control:${value}` +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain; charset=utf-8", false); + if (request.hasHeader("cache-control")) { + response.write(`cache-control:${request.getHeader("cache-control")}`); + } else { + response.write(`cache-control:`); + } +} diff --git a/devtools/client/framework/test/test_chrome_page.html b/devtools/client/framework/test/test_chrome_page.html new file mode 100644 index 0000000000..688b9de1d6 --- /dev/null +++ b/devtools/client/framework/test/test_chrome_page.html @@ -0,0 +1,9 @@ + + +Chrome page + diff --git a/devtools/client/framework/test/xpcshell/.eslintrc.js b/devtools/client/framework/test/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..7f6b62a9e5 --- /dev/null +++ b/devtools/client/framework/test/xpcshell/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + extends: "../../../../.eslintrc.xpcshell.js", +}; diff --git a/devtools/client/framework/test/xpcshell/test_tabs_absolute_order.js b/devtools/client/framework/test/xpcshell/test_tabs_absolute_order.js new file mode 100644 index 0000000000..23755d5e8d --- /dev/null +++ b/devtools/client/framework/test/xpcshell/test_tabs_absolute_order.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); + +const TEST_DATA = [ + { + description: "Test for no order in preference", + preferenceOrder: [], + currentTabsOrder: ["T1", "T2", "T3", "T4", "T5"], + dragTarget: "T1", + expectedOrder: ["T1", "T2", "T3", "T4", "T5"], + }, + { + description: "Test for drag a tab to left with hidden tab", + preferenceOrder: ["T1", "T2", "T3", "E1", "T4", "T5"], + currentTabsOrder: ["T1", "T2", "T4", "T3", "T5"], + dragTarget: "T4", + expectedOrder: ["T1", "T2", "T4", "T3", "E1", "T5"], + }, + { + description: "Test for drag a tab to right with hidden tab", + preferenceOrder: ["T1", "T2", "T3", "E1", "T4", "T5"], + currentTabsOrder: ["T1", "T3", "T4", "T2", "T5"], + dragTarget: "T2", + expectedOrder: ["T1", "T3", "E1", "T4", "T2", "T5"], + }, + { + description: + "Test for drag a tab to left end in case hidden tab was left end", + preferenceOrder: ["E1", "T1", "T2", "T3", "T4", "T5"], + currentTabsOrder: ["T4", "T1", "T2", "T3", "T5"], + dragTarget: "T4", + expectedOrder: ["E1", "T4", "T1", "T2", "T3", "T5"], + }, + { + description: + "Test for drag a tab to right end in case hidden tab was right end", + preferenceOrder: ["T1", "T2", "T3", "T4", "T5", "E1"], + currentTabsOrder: ["T2", "T3", "T4", "T5", "T1"], + dragTarget: "T1", + expectedOrder: ["T2", "T3", "T4", "T5", "E1", "T1"], + }, + { + description: "Test for multiple hidden tabs", + preferenceOrder: ["T1", "T2", "E1", "E2", "E3", "E4"], + currentTabsOrder: ["T2", "T1"], + dragTarget: "T1", + expectedOrder: ["T2", "E1", "E2", "E3", "E4", "T1"], + }, +]; + +function run_test() { + const { + toAbsoluteOrder, + } = require("resource://devtools/client/framework/toolbox-tabs-order-manager.js"); + + for (const { + description, + preferenceOrder, + currentTabsOrder, + dragTarget, + expectedOrder, + } of TEST_DATA) { + info(description); + const resultOrder = toAbsoluteOrder( + preferenceOrder, + currentTabsOrder, + dragTarget + ); + equal( + resultOrder.join(","), + expectedOrder.join(","), + "Result should be correct" + ); + } +} diff --git a/devtools/client/framework/test/xpcshell/xpcshell.toml b/devtools/client/framework/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..569a4b1e71 --- /dev/null +++ b/devtools/client/framework/test/xpcshell/xpcshell.toml @@ -0,0 +1,6 @@ +[DEFAULT] +tags = "devtools" +firefox-appdir = "browser" +skip-if = ["os == 'android'"] + +["test_tabs_absolute_order.js"] -- cgit v1.2.3