summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/framework
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/framework')
-rw-r--r--devtools/client/framework/actions/dom-mutation-breakpoints.js140
-rw-r--r--devtools/client/framework/actions/index.js9
-rw-r--r--devtools/client/framework/actions/moz.build12
-rw-r--r--devtools/client/framework/actions/targets.js28
-rw-r--r--devtools/client/framework/browser-menus.js316
-rw-r--r--devtools/client/framework/browser-toolbox/Launcher.jsm392
-rw-r--r--devtools/client/framework/browser-toolbox/README.md37
-rw-r--r--devtools/client/framework/browser-toolbox/moz.build13
-rw-r--r--devtools/client/framework/browser-toolbox/test/.eslintrc.js6
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser.ini31
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox.js25
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_debugger.js148
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_evaluation_context.js73
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_contentframe_inspector.js96
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_inspector.js140
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_print_preview.js58
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_rtl.js29
-rw-r--r--devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_frame.html14
-rw-r--r--devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_page.html16
-rw-r--r--devtools/client/framework/browser-toolbox/test/head.js26
-rw-r--r--devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js192
-rw-r--r--devtools/client/framework/browser-toolbox/window.css48
-rw-r--r--devtools/client/framework/browser-toolbox/window.html23
-rw-r--r--devtools/client/framework/browser-toolbox/window.js293
-rw-r--r--devtools/client/framework/components/DebugTargetErrorPage.css21
-rw-r--r--devtools/client/framework/components/DebugTargetErrorPage.js47
-rw-r--r--devtools/client/framework/components/DebugTargetInfo.js344
-rw-r--r--devtools/client/framework/components/MeatballMenu.js241
-rw-r--r--devtools/client/framework/components/ToolboxController.js202
-rw-r--r--devtools/client/framework/components/ToolboxTab.js110
-rw-r--r--devtools/client/framework/components/ToolboxTabs.js331
-rw-r--r--devtools/client/framework/components/ToolboxToolbar.js501
-rw-r--r--devtools/client/framework/components/moz.build16
-rw-r--r--devtools/client/framework/devtools-browser.js822
-rw-r--r--devtools/client/framework/devtools.js839
-rw-r--r--devtools/client/framework/enable-devtools-popup.js63
-rw-r--r--devtools/client/framework/menu-item.js79
-rw-r--r--devtools/client/framework/menu.js231
-rw-r--r--devtools/client/framework/moz.build50
-rw-r--r--devtools/client/framework/options-panel.css180
-rw-r--r--devtools/client/framework/reducers/dom-mutation-breakpoints.js115
-rw-r--r--devtools/client/framework/reducers/index.js10
-rw-r--r--devtools/client/framework/reducers/moz.build12
-rw-r--r--devtools/client/framework/reducers/targets.js67
-rw-r--r--devtools/client/framework/selection.js315
-rw-r--r--devtools/client/framework/source-map-url-service.js476
-rw-r--r--devtools/client/framework/store-provider.js8
-rw-r--r--devtools/client/framework/store.js13
-rw-r--r--devtools/client/framework/target-from-url.js211
-rw-r--r--devtools/client/framework/target.js119
-rw-r--r--devtools/client/framework/test/.eslintrc.js6
-rw-r--r--devtools/client/framework/test/allocations/browser_allocations_target.ini10
-rw-r--r--devtools/client/framework/test/allocations/browser_allocations_target.js114
-rw-r--r--devtools/client/framework/test/browser-enable-popup-devtools-user.ini18
-rw-r--r--devtools/client/framework/test/browser-enable-popup-new-user.ini17
-rw-r--r--devtools/client/framework/test/browser-telemetry-startup.ini13
-rw-r--r--devtools/client/framework/test/browser.ini166
-rw-r--r--devtools/client/framework/test/browser_about-devtools-toolbox_load.js31
-rw-r--r--devtools/client/framework/test/browser_about-devtools-toolbox_reload.js67
-rw-r--r--devtools/client/framework/test/browser_devtools_api_destroy.js71
-rw-r--r--devtools/client/framework/test/browser_dynamic_tool_enabling.js44
-rw-r--r--devtools/client/framework/test/browser_enable_devtools_popup.js34
-rw-r--r--devtools/client/framework/test/browser_enable_devtools_popup_devtools_user.js33
-rw-r--r--devtools/client/framework/test/browser_enable_devtools_popup_new_user.js30
-rw-r--r--devtools/client/framework/test/browser_front_parentFront.js40
-rw-r--r--devtools/client/framework/test/browser_ignore_toolbox_network_requests.js29
-rw-r--r--devtools/client/framework/test/browser_keybindings_01.js111
-rw-r--r--devtools/client/framework/test/browser_keybindings_02.js68
-rw-r--r--devtools/client/framework/test/browser_keybindings_03.js53
-rw-r--r--devtools/client/framework/test/browser_menu_api.js219
-rw-r--r--devtools/client/framework/test/browser_new_activation_workflow.js74
-rw-r--r--devtools/client/framework/test/browser_source_map-01.js71
-rw-r--r--devtools/client/framework/test/browser_source_map-absolute.js36
-rw-r--r--devtools/client/framework/test/browser_source_map-cross-domain.js41
-rw-r--r--devtools/client/framework/test/browser_source_map-init.js51
-rw-r--r--devtools/client/framework/test/browser_source_map-inline.js42
-rw-r--r--devtools/client/framework/test/browser_source_map-late-script.js52
-rw-r--r--devtools/client/framework/test/browser_source_map-no-race.js43
-rw-r--r--devtools/client/framework/test/browser_source_map-pub-sub.js97
-rw-r--r--devtools/client/framework/test/browser_source_map-reload.js53
-rw-r--r--devtools/client/framework/test/browser_tab_descriptor_fission.js79
-rw-r--r--devtools/client/framework/test/browser_target_cached-front.js24
-rw-r--r--devtools/client/framework/test/browser_target_cached-resource.js48
-rw-r--r--devtools/client/framework/test/browser_target_events.js26
-rw-r--r--devtools/client/framework/test/browser_target_from_url.js160
-rw-r--r--devtools/client/framework/test/browser_target_get-front.js115
-rw-r--r--devtools/client/framework/test/browser_target_listeners.js34
-rw-r--r--devtools/client/framework/test/browser_target_parents.js142
-rw-r--r--devtools/client/framework/test/browser_target_remote.js15
-rw-r--r--devtools/client/framework/test/browser_target_server_compartment.js137
-rw-r--r--devtools/client/framework/test/browser_target_support.js47
-rw-r--r--devtools/client/framework/test/browser_toolbox_browsertoolbox_host.js28
-rw-r--r--devtools/client/framework/test/browser_toolbox_contentpage_contextmenu.js76
-rw-r--r--devtools/client/framework/test/browser_toolbox_dynamic_registration.js88
-rw-r--r--devtools/client/framework/test/browser_toolbox_error_count.js182
-rw-r--r--devtools/client/framework/test/browser_toolbox_error_count_reset_on_navigation.js78
-rw-r--r--devtools/client/framework/test/browser_toolbox_getpanelwhenready.js40
-rw-r--r--devtools/client/framework/test/browser_toolbox_highlight.js112
-rw-r--r--devtools/client/framework/test/browser_toolbox_hosts.js195
-rw-r--r--devtools/client/framework/test/browser_toolbox_hosts_size.js103
-rw-r--r--devtools/client/framework/test/browser_toolbox_hosts_telemetry.js50
-rw-r--r--devtools/client/framework/test/browser_toolbox_keyboard_navigation.js136
-rw-r--r--devtools/client/framework/test/browser_toolbox_keyboard_navigation_notification_box.js49
-rw-r--r--devtools/client/framework/test/browser_toolbox_meatball.js132
-rw-r--r--devtools/client/framework/test/browser_toolbox_options.js558
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_buttons.js220
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_cache-01.js36
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_cache-02.js49
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_cache-03.js60
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_cache.css.sjs10
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_cache.sjs28
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_js.html48
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_js.js131
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_js_iframe.html35
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.html81
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js128
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_frames_button.js63
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_multiple_tabs.js131
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_panel_toggle.js82
-rw-r--r--devtools/client/framework/test/browser_toolbox_races.js93
-rw-r--r--devtools/client/framework/test/browser_toolbox_raise.js91
-rw-r--r--devtools/client/framework/test/browser_toolbox_ready.js19
-rw-r--r--devtools/client/framework/test/browser_toolbox_remoteness_change.js91
-rw-r--r--devtools/client/framework/test/browser_toolbox_select_event.js98
-rw-r--r--devtools/client/framework/test/browser_toolbox_selected_tool_unavailable.js46
-rw-r--r--devtools/client/framework/test/browser_toolbox_selectionchanged_event.js46
-rw-r--r--devtools/client/framework/test/browser_toolbox_show_toolbox_tool_ready.js78
-rw-r--r--devtools/client/framework/test/browser_toolbox_split_console.js85
-rw-r--r--devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js81
-rw-r--r--devtools/client/framework/test/browser_toolbox_target.js57
-rw-r--r--devtools/client/framework/test/browser_toolbox_telemetry_activate_splitconsole.js107
-rw-r--r--devtools/client/framework/test/browser_toolbox_telemetry_close.js64
-rw-r--r--devtools/client/framework/test/browser_toolbox_telemetry_enter.js153
-rw-r--r--devtools/client/framework/test/browser_toolbox_telemetry_exit.js130
-rw-r--r--devtools/client/framework/test/browser_toolbox_telemetry_open_event.js35
-rw-r--r--devtools/client/framework/test/browser_toolbox_textbox_context_menu.js146
-rw-r--r--devtools/client/framework/test/browser_toolbox_theme.js33
-rw-r--r--devtools/client/framework/test/browser_toolbox_theme_registration.js169
-rw-r--r--devtools/client/framework/test/browser_toolbox_toggle.js117
-rw-r--r--devtools/client/framework/test/browser_toolbox_tool_ready.js42
-rw-r--r--devtools/client/framework/test/browser_toolbox_tool_remote_reopen.js106
-rw-r--r--devtools/client/framework/test/browser_toolbox_toolbar_minimum_width.js43
-rw-r--r--devtools/client/framework/test/browser_toolbox_toolbar_overflow.js83
-rw-r--r--devtools/client/framework/test/browser_toolbox_toolbar_overflow_button_visibility.js73
-rw-r--r--devtools/client/framework/test/browser_toolbox_toolbar_reorder_by_dnd.js190
-rw-r--r--devtools/client/framework/test/browser_toolbox_toolbar_reorder_by_width.js106
-rw-r--r--devtools/client/framework/test/browser_toolbox_toolbar_reorder_with_extension.js148
-rw-r--r--devtools/client/framework/test/browser_toolbox_toolbar_reorder_with_hidden_extension.js248
-rw-r--r--devtools/client/framework/test/browser_toolbox_toolbar_reorder_with_secondary_toolbox.js48
-rw-r--r--devtools/client/framework/test/browser_toolbox_tools_per_toolbox_registration.js141
-rw-r--r--devtools/client/framework/test/browser_toolbox_view_source_01.js31
-rw-r--r--devtools/client/framework/test/browser_toolbox_view_source_02.js38
-rw-r--r--devtools/client/framework/test/browser_toolbox_view_source_03.js51
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_reload_target.js110
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_reload_target_force.js46
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_shortcuts.js101
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_title_changes.js128
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_title_changes_page.html10
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_title_frame_select.js147
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_title_frame_select_page.html11
-rw-r--r--devtools/client/framework/test/browser_toolbox_zoom.js65
-rw-r--r--devtools/client/framework/test/browser_toolbox_zoom_popup.js187
-rw-r--r--devtools/client/framework/test/browser_two_tabs.js112
-rw-r--r--devtools/client/framework/test/code_binary_search.coffee18
-rw-r--r--devtools/client/framework/test/code_binary_search.js29
-rw-r--r--devtools/client/framework/test/code_binary_search.map10
-rw-r--r--devtools/client/framework/test/code_binary_search_absolute.js29
-rw-r--r--devtools/client/framework/test/code_binary_search_absolute.map10
-rw-r--r--devtools/client/framework/test/code_bundle_cross_domain.js93
-rw-r--r--devtools/client/framework/test/code_bundle_cross_domain.js.map1
-rw-r--r--devtools/client/framework/test/code_bundle_late_script.js116
-rw-r--r--devtools/client/framework/test/code_bundle_late_script.js.map1
-rw-r--r--devtools/client/framework/test/code_bundle_no_race.js95
-rw-r--r--devtools/client/framework/test/code_bundle_no_race.js.map1
-rw-r--r--devtools/client/framework/test/code_bundle_reload_1.js94
-rw-r--r--devtools/client/framework/test/code_bundle_reload_1.js.map1
-rw-r--r--devtools/client/framework/test/code_bundle_reload_2.js94
-rw-r--r--devtools/client/framework/test/code_bundle_reload_2.js.map1
-rw-r--r--devtools/client/framework/test/code_cross_domain.js19
-rw-r--r--devtools/client/framework/test/code_inline_bundle.js92
-rw-r--r--devtools/client/framework/test/code_inline_original.js14
-rw-r--r--devtools/client/framework/test/code_late_script.js14
-rw-r--r--devtools/client/framework/test/code_math.js7
-rw-r--r--devtools/client/framework/test/code_no_race.js17
-rw-r--r--devtools/client/framework/test/code_reload_1.js16
-rw-r--r--devtools/client/framework/test/code_reload_2.js16
-rw-r--r--devtools/client/framework/test/doc_cached-resource.html15
-rw-r--r--devtools/client/framework/test/doc_cached-resource_iframe.html14
-rw-r--r--devtools/client/framework/test/doc_empty-tab-01.html14
-rw-r--r--devtools/client/framework/test/doc_reload.html15
-rw-r--r--devtools/client/framework/test/doc_theme.css3
-rw-r--r--devtools/client/framework/test/doc_viewsource.html13
-rw-r--r--devtools/client/framework/test/head.js475
-rw-r--r--devtools/client/framework/test/helper_disable_cache.js143
-rw-r--r--devtools/client/framework/test/helper_enable_devtools_popup.js154
-rw-r--r--devtools/client/framework/test/metrics/browser_metrics.ini14
-rw-r--r--devtools/client/framework/test/metrics/browser_metrics_debugger.ini12
-rw-r--r--devtools/client/framework/test/metrics/browser_metrics_debugger.js62
-rw-r--r--devtools/client/framework/test/metrics/browser_metrics_inspector.ini12
-rw-r--r--devtools/client/framework/test/metrics/browser_metrics_inspector.js43
-rw-r--r--devtools/client/framework/test/metrics/browser_metrics_netmonitor.ini12
-rw-r--r--devtools/client/framework/test/metrics/browser_metrics_netmonitor.js90
-rw-r--r--devtools/client/framework/test/metrics/browser_metrics_pool.js116
-rw-r--r--devtools/client/framework/test/metrics/browser_metrics_webconsole.ini12
-rw-r--r--devtools/client/framework/test/metrics/browser_metrics_webconsole.js57
-rw-r--r--devtools/client/framework/test/metrics/head.js189
-rw-r--r--devtools/client/framework/test/node/.eslintrc.js10
-rw-r--r--devtools/client/framework/test/node/README.md22
-rw-r--r--devtools/client/framework/test/node/babel.config.js13
-rw-r--r--devtools/client/framework/test/node/components/__snapshots__/debug-target-info.test.js.snap526
-rw-r--r--devtools/client/framework/test/node/components/debug-target-info.test.js195
-rw-r--r--devtools/client/framework/test/node/jest.config.js13
-rw-r--r--devtools/client/framework/test/node/package.json22
-rw-r--r--devtools/client/framework/test/node/setup.js10
-rw-r--r--devtools/client/framework/test/node/store/targets.test.js141
-rw-r--r--devtools/client/framework/test/node/yarn.lock3144
-rw-r--r--devtools/client/framework/test/serviceworker.js4
-rw-r--r--devtools/client/framework/test/sjs_cache_controle_header.sjs19
-rw-r--r--devtools/client/framework/test/sjs_code_bundle_reload_map.sjs26
-rw-r--r--devtools/client/framework/test/sjs_code_reload.sjs26
-rw-r--r--devtools/client/framework/test/test_chrome_page.html9
-rw-r--r--devtools/client/framework/test/xpcshell/.eslintrc.js6
-rw-r--r--devtools/client/framework/test/xpcshell/test_tabs_absolute_order.js79
-rw-r--r--devtools/client/framework/test/xpcshell/xpcshell.ini6
-rw-r--r--devtools/client/framework/toolbox-context-menu.js119
-rw-r--r--devtools/client/framework/toolbox-host-manager.js290
-rw-r--r--devtools/client/framework/toolbox-hosts.js443
-rw-r--r--devtools/client/framework/toolbox-init.js165
-rw-r--r--devtools/client/framework/toolbox-options.html180
-rw-r--r--devtools/client/framework/toolbox-options.js640
-rw-r--r--devtools/client/framework/toolbox-tabs-order-manager.js285
-rw-r--r--devtools/client/framework/toolbox-window.xhtml16
-rw-r--r--devtools/client/framework/toolbox.js4344
-rw-r--r--devtools/client/framework/toolbox.xhtml41
234 files changed, 30129 insertions, 0 deletions
diff --git a/devtools/client/framework/actions/dom-mutation-breakpoints.js b/devtools/client/framework/actions/dom-mutation-breakpoints.js
new file mode 100644
index 0000000000..a81c3e3d57
--- /dev/null
+++ b/devtools/client/framework/actions/dom-mutation-breakpoints.js
@@ -0,0 +1,140 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+"use strict";
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const {
+ getDOMMutationBreakpoint,
+ getDOMMutationBreakpoints,
+} = require("devtools/client/framework/reducers/dom-mutation-breakpoints");
+
+exports.registerWalkerListeners = registerWalkerListeners;
+function registerWalkerListeners(store, walker) {
+ walker.on("mutations", mutations => handleWalkerMutations(mutations, store));
+}
+
+function handleWalkerMutations(mutations, store) {
+ // If we got BP updates for detach/unload, we want to drop those nodes from
+ // the list of active DOM mutation breakpoints. We explicitly check these
+ // cases because BP updates could also happen due to explicitly API
+ // operations to add/remove bps.
+ const mutationItems = mutations.filter(
+ mutation => mutation.type === "mutationBreakpoint"
+ );
+ if (mutationItems.length > 0) {
+ store.dispatch(updateBreakpointsForMutations(mutationItems));
+ }
+}
+
+exports.createDOMMutationBreakpoint = createDOMMutationBreakpoint;
+function createDOMMutationBreakpoint(nodeFront, mutationType) {
+ assert(typeof nodeFront === "object" && nodeFront);
+ assert(typeof mutationType === "string");
+
+ return async function({ dispatch, getState }) {
+ const walker = nodeFront.walkerFront;
+
+ dispatch({
+ type: "ADD_DOM_MUTATION_BREAKPOINT",
+ nodeFront,
+ mutationType,
+ });
+
+ await walker.setMutationBreakpoints(nodeFront, {
+ [mutationType]: true,
+ });
+ };
+}
+
+exports.deleteDOMMutationBreakpoint = deleteDOMMutationBreakpoint;
+function deleteDOMMutationBreakpoint(nodeFront, mutationType) {
+ assert(typeof nodeFront === "object" && nodeFront);
+ assert(typeof mutationType === "string");
+
+ return async function({ dispatch, getState }) {
+ const walker = nodeFront.walkerFront;
+ await walker.setMutationBreakpoints(nodeFront, {
+ [mutationType]: false,
+ });
+
+ dispatch({
+ type: "REMOVE_DOM_MUTATION_BREAKPOINT",
+ nodeFront,
+ mutationType,
+ });
+ };
+}
+
+function updateBreakpointsForMutations(mutationItems) {
+ return async function({ dispatch, getState }) {
+ const removedNodeFronts = [];
+ const changedNodeFronts = new Set();
+
+ for (const { target: nodeFront, mutationReason } of mutationItems) {
+ switch (mutationReason) {
+ case "api":
+ changedNodeFronts.add(nodeFront);
+ break;
+ default:
+ console.error(
+ "Unexpected mutation reason",
+ mutationReason,
+ ", removing"
+ );
+ // Fall Through
+ case "detach":
+ case "unload":
+ removedNodeFronts.push(nodeFront);
+ break;
+ }
+ }
+
+ if (removedNodeFronts.length > 0) {
+ dispatch({
+ type: "REMOVE_DOM_MUTATION_BREAKPOINTS_FOR_FRONTS",
+ nodeFronts: removedNodeFronts,
+ });
+ }
+ if (changedNodeFronts.size > 0) {
+ const enabledStates = [];
+ for (const {
+ id,
+ nodeFront,
+ mutationType,
+ enabled,
+ } of getDOMMutationBreakpoints(getState())) {
+ if (changedNodeFronts.has(nodeFront)) {
+ const bpEnabledOnFront = nodeFront.mutationBreakpoints[mutationType];
+ if (bpEnabledOnFront !== enabled) {
+ // Sync the bp state from the front into the store.
+ enabledStates.push([id, bpEnabledOnFront]);
+ }
+ }
+ }
+
+ dispatch({
+ type: "SET_DOM_MUTATION_BREAKPOINTS_ENABLED_STATE",
+ enabledStates,
+ });
+ }
+ };
+}
+
+exports.toggleDOMMutationBreakpointState = toggleDOMMutationBreakpointState;
+function toggleDOMMutationBreakpointState(id, enabled) {
+ assert(typeof id === "string");
+ assert(typeof enabled === "boolean");
+
+ return async function({ dispatch, getState }) {
+ const bp = getDOMMutationBreakpoint(getState(), id);
+ if (!bp) {
+ throw new Error(`No DOM mutation BP with ID ${id}`);
+ }
+
+ const walker = bp.nodeFront.getParent();
+ await walker.setMutationBreakpoints(bp.nodeFront, {
+ [bp.mutationType]: enabled,
+ });
+ };
+}
diff --git a/devtools/client/framework/actions/index.js b/devtools/client/framework/actions/index.js
new file mode 100644
index 0000000000..e7098ad2f9
--- /dev/null
+++ b/devtools/client/framework/actions/index.js
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.exports = {
+ ...require("devtools/client/framework/actions/dom-mutation-breakpoints"),
+ ...require("devtools/client/framework/actions/targets"),
+};
diff --git a/devtools/client/framework/actions/moz.build b/devtools/client/framework/actions/moz.build
new file mode 100644
index 0000000000..53f955faa6
--- /dev/null
+++ b/devtools/client/framework/actions/moz.build
@@ -0,0 +1,12 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "dom-mutation-breakpoints.js",
+ "index.js",
+ "targets.js",
+)
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "Framework")
diff --git a/devtools/client/framework/actions/targets.js b/devtools/client/framework/actions/targets.js
new file mode 100644
index 0000000000..7e5580d835
--- /dev/null
+++ b/devtools/client/framework/actions/targets.js
@@ -0,0 +1,28 @@
+/* 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";
+
+function registerTarget(targetFront) {
+ return { type: "REGISTER_TARGET", targetFront };
+}
+
+function unregisterTarget(targetFront) {
+ return { type: "UNREGISTER_TARGET", targetFront };
+}
+
+/**
+ *
+ * @param {String} targetActorID: The actorID of the target we want to select.
+ */
+function selectTarget(targetActorID) {
+ return function({ dispatch, getState }) {
+ dispatch({ type: "SELECT_TARGET", targetActorID });
+ };
+}
+
+module.exports = {
+ registerTarget,
+ unregisterTarget,
+ selectTarget,
+};
diff --git a/devtools/client/framework/browser-menus.js b/devtools/client/framework/browser-menus.js
new file mode 100644
index 0000000000..1b2b2aaea0
--- /dev/null
+++ b/devtools/client/framework/browser-menus.js
@@ -0,0 +1,316 @@
+/* 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";
+
+/**
+ * This module inject dynamically menu items into browser UI.
+ *
+ * Menu definitions are fetched from:
+ * - devtools/client/menus for top level entires
+ * - devtools/client/definitions for tool-specifics entries
+ */
+
+const { Cu } = require("chrome");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const MENUS_L10N = new LocalizationHelper(
+ "devtools/client/locales/menus.properties"
+);
+
+loader.lazyRequireGetter(
+ this,
+ "gDevTools",
+ "devtools/client/framework/devtools",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "gDevToolsBrowser",
+ "devtools/client/framework/devtools-browser",
+ true
+);
+loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
+
+let telemetry = null;
+
+// Keep list of inserted DOM Elements in order to remove them on unload
+// Maps browser xul document => list of DOM Elements
+const FragmentsCache = new Map();
+
+function l10n(key) {
+ return MENUS_L10N.getStr(key);
+}
+
+/**
+ * Create a xul:menuitem element
+ *
+ * @param {HTMLDocument} doc
+ * The document to which menus are to be added.
+ * @param {String} id
+ * Element id.
+ * @param {String} label
+ * Menu label.
+ * @param {String} accesskey (optional)
+ * Access key of the menuitem, used as shortcut while opening the menu.
+ * @param {Boolean} isCheckbox (optional)
+ * If true, the menuitem will act as a checkbox and have an optional
+ * tick on its left.
+ *
+ * @return XULMenuItemElement
+ */
+function createMenuItem({ doc, id, label, accesskey, isCheckbox }) {
+ const menuitem = doc.createXULElement("menuitem");
+ menuitem.id = id;
+ menuitem.setAttribute("label", label);
+ if (accesskey) {
+ menuitem.setAttribute("accesskey", accesskey);
+ }
+ if (isCheckbox) {
+ menuitem.setAttribute("type", "checkbox");
+ menuitem.setAttribute("autocheck", "false");
+ }
+ return menuitem;
+}
+
+/**
+ * Add a menu entry for a tool definition
+ *
+ * @param {Object} toolDefinition
+ * Tool definition of the tool to add a menu entry.
+ * @param {HTMLDocument} doc
+ * The document to which the tool menu item is to be added.
+ */
+function createToolMenuElements(toolDefinition, doc) {
+ const id = toolDefinition.id;
+ const menuId = "menuitem_" + id;
+
+ // Prevent multiple entries for the same tool.
+ if (doc.getElementById(menuId)) {
+ return;
+ }
+
+ const oncommand = async function(id, event) {
+ try {
+ const window = event.target.ownerDocument.defaultView;
+ await gDevToolsBrowser.selectToolCommand(window, id, Cu.now());
+ sendEntryPointTelemetry(window);
+ } catch (e) {
+ console.error(`Exception while opening ${id}: ${e}\n${e.stack}`);
+ }
+ }.bind(null, id);
+
+ const menuitem = createMenuItem({
+ doc,
+ id: "menuitem_" + id,
+ label: toolDefinition.menuLabel || toolDefinition.label,
+ accesskey: toolDefinition.accesskey,
+ });
+ // Refer to the key in order to display the key shortcut at menu ends
+ // This <key> element is being created by devtools/client/devtools-startup.js
+ menuitem.setAttribute("key", "key_" + id);
+ menuitem.addEventListener("command", oncommand);
+
+ return {
+ menuitem,
+ };
+}
+
+/**
+ * Send entry point telemetry explaining how the devtools were launched when
+ * launched from the System Menu.. This functionality also lives inside
+ * `devtools/startup/devtools-startup.js` but that codepath is only used the
+ * first time a toolbox is opened for a tab.
+ */
+function sendEntryPointTelemetry(window) {
+ if (!telemetry) {
+ telemetry = new Telemetry();
+ }
+
+ telemetry.addEventProperty(window, "open", "tools", null, "shortcut", "");
+
+ telemetry.addEventProperty(
+ window,
+ "open",
+ "tools",
+ null,
+ "entrypoint",
+ "SystemMenu"
+ );
+}
+
+/**
+ * Create xul menuitem, key elements for a given tool.
+ * And then insert them into browser DOM.
+ *
+ * @param {HTMLDocument} doc
+ * The document to which the tool is to be registered.
+ * @param {Object} toolDefinition
+ * Tool definition of the tool to register.
+ * @param {Object} prevDef
+ * The tool definition after which the tool menu item is to be added.
+ */
+function insertToolMenuElements(doc, toolDefinition, prevDef) {
+ const { menuitem } = createToolMenuElements(toolDefinition, doc);
+
+ let ref;
+ if (prevDef) {
+ const menuitem = doc.getElementById("menuitem_" + prevDef.id);
+ ref = menuitem?.nextSibling ? menuitem.nextSibling : null;
+ } else {
+ ref = doc.getElementById("menu_devtools_separator");
+ }
+
+ if (ref) {
+ ref.parentNode.insertBefore(menuitem, ref);
+ }
+}
+exports.insertToolMenuElements = insertToolMenuElements;
+
+/**
+ * Remove a tool's menuitem from a window
+ *
+ * @param {string} toolId
+ * Id of the tool to add a menu entry for
+ * @param {HTMLDocument} doc
+ * The document to which the tool menu item is to be removed from
+ */
+function removeToolFromMenu(toolId, doc) {
+ const key = doc.getElementById("key_" + toolId);
+ if (key) {
+ key.remove();
+ }
+
+ const menuitem = doc.getElementById("menuitem_" + toolId);
+ if (menuitem) {
+ menuitem.remove();
+ }
+}
+exports.removeToolFromMenu = removeToolFromMenu;
+
+/**
+ * Add all tools to the developer tools menu of a window.
+ *
+ * @param {HTMLDocument} doc
+ * The document to which the tool items are to be added.
+ */
+function addAllToolsToMenu(doc) {
+ const fragKeys = doc.createDocumentFragment();
+ const fragMenuItems = doc.createDocumentFragment();
+
+ for (const toolDefinition of gDevTools.getToolDefinitionArray()) {
+ if (!toolDefinition.inMenu) {
+ continue;
+ }
+
+ const elements = createToolMenuElements(toolDefinition, doc);
+
+ if (!elements) {
+ continue;
+ }
+
+ if (elements.key) {
+ fragKeys.appendChild(elements.key);
+ }
+ fragMenuItems.appendChild(elements.menuitem);
+ }
+
+ const mps = doc.getElementById("menu_devtools_separator");
+ if (mps) {
+ mps.parentNode.insertBefore(fragMenuItems, mps);
+ }
+}
+
+/**
+ * Add global menus that are not panel specific.
+ *
+ * @param {HTMLDocument} doc
+ * The document to which menus are to be added.
+ */
+function addTopLevelItems(doc) {
+ const menuItems = doc.createDocumentFragment();
+
+ const { menuitems } = require("devtools/client/menus");
+ for (const item of menuitems) {
+ if (item.separator) {
+ const separator = doc.createXULElement("menuseparator");
+ separator.id = item.id;
+ menuItems.appendChild(separator);
+ } else {
+ const { id, l10nKey } = item;
+
+ // Create a <menuitem>
+ const menuitem = createMenuItem({
+ doc,
+ id,
+ label: l10n(l10nKey + ".label"),
+ accesskey: l10n(l10nKey + ".accesskey"),
+ isCheckbox: item.checkbox,
+ });
+ menuitem.addEventListener("command", item.oncommand);
+ menuItems.appendChild(menuitem);
+
+ if (item.keyId) {
+ menuitem.setAttribute("key", "key_" + item.keyId);
+ }
+ }
+ }
+
+ // Cache all nodes before insertion to be able to remove them on unload
+ const nodes = [];
+ for (const node of menuItems.children) {
+ nodes.push(node);
+ }
+ FragmentsCache.set(doc, nodes);
+
+ const menu = doc.getElementById("menuWebDeveloperPopup");
+ menu.appendChild(menuItems);
+
+ // There is still "Page Source" menuitem hardcoded into browser.xhtml. Instead
+ // of manually inserting everything around it, move it to the expected
+ // position.
+ const pageSource = doc.getElementById("menu_pageSource");
+ const endSeparator = doc.getElementById("devToolsEndSeparator");
+ menu.insertBefore(pageSource, endSeparator);
+}
+
+/**
+ * Remove global menus that are not panel specific.
+ *
+ * @param {HTMLDocument} doc
+ * The document to which menus are to be added.
+ */
+function removeTopLevelItems(doc) {
+ const nodes = FragmentsCache.get(doc);
+ if (!nodes) {
+ return;
+ }
+ FragmentsCache.delete(doc);
+ for (const node of nodes) {
+ node.remove();
+ }
+}
+
+/**
+ * Add menus to a browser document
+ *
+ * @param {HTMLDocument} doc
+ * The document to which menus are to be added.
+ */
+exports.addMenus = function(doc) {
+ addTopLevelItems(doc);
+
+ addAllToolsToMenu(doc);
+};
+
+/**
+ * Remove menus from a browser document
+ *
+ * @param {HTMLDocument} doc
+ * The document to which menus are to be removed.
+ */
+exports.removeMenus = function(doc) {
+ // We only remove top level entries. Per-tool entries are removed while
+ // unregistering each tool.
+ removeTopLevelItems(doc);
+};
diff --git a/devtools/client/framework/browser-toolbox/Launcher.jsm b/devtools/client/framework/browser-toolbox/Launcher.jsm
new file mode 100644
index 0000000000..f7e8c0f4b9
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/Launcher.jsm
@@ -0,0 +1,392 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const BROWSER_TOOLBOX_WINDOW_URL =
+ "chrome://devtools/content/framework/browser-toolbox/window.html";
+const CHROME_DEBUGGER_PROFILE_NAME = "chrome_debugger_profile";
+
+const { require, DevToolsLoader } = ChromeUtils.import(
+ "resource://devtools/shared/Loader.jsm"
+);
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "Subprocess",
+ "resource://gre/modules/Subprocess.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "AppConstants",
+ "resource://gre/modules/AppConstants.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "FileUtils",
+ "resource://gre/modules/FileUtils.jsm"
+);
+
+XPCOMUtils.defineLazyGetter(this, "Telemetry", function() {
+ return require("devtools/client/shared/telemetry");
+});
+XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() {
+ return require("devtools/shared/event-emitter");
+});
+
+const Services = require("Services");
+
+const EXPORTED_SYMBOLS = ["BrowserToolboxLauncher"];
+
+var processes = new Set();
+
+/**
+ * Constructor for creating a process that will hold a chrome toolbox.
+ *
+ * @param function onClose [optional]
+ * A function called when the process stops running.
+ * @param function onRun [optional]
+ * A function called when the process starts running.
+ * @param boolean overwritePreferences [optional]
+ * Set to force overwriting the toolbox profile's preferences with the
+ * current set of preferences.
+ */
+function BrowserToolboxLauncher(
+ onClose,
+ onRun,
+ overwritePreferences,
+ binaryPath
+) {
+ const emitter = new EventEmitter();
+ this.on = emitter.on.bind(emitter);
+ this.off = emitter.off.bind(emitter);
+ this.once = emitter.once.bind(emitter);
+ // Forward any events to the shared emitter.
+ this.emit = function(...args) {
+ emitter.emit(...args);
+ BrowserToolboxLauncher.emit(...args);
+ };
+
+ if (onClose) {
+ this.once("close", onClose);
+ }
+ if (onRun) {
+ this.once("run", onRun);
+ }
+
+ this._telemetry = new Telemetry();
+
+ this.close = this.close.bind(this);
+ Services.obs.addObserver(this.close, "quit-application");
+ this._initServer();
+ this._initProfile(overwritePreferences);
+ this._create(binaryPath);
+
+ processes.add(this);
+}
+
+EventEmitter.decorate(BrowserToolboxLauncher);
+
+/**
+ * Initializes and starts a chrome toolbox process.
+ * @return object
+ */
+BrowserToolboxLauncher.init = function(
+ onClose,
+ onRun,
+ overwritePreferences,
+ binaryPath
+) {
+ if (
+ !Services.prefs.getBoolPref("devtools.chrome.enabled") ||
+ !Services.prefs.getBoolPref("devtools.debugger.remote-enabled")
+ ) {
+ console.error("Could not start Browser Toolbox, you need to enable it.");
+ return null;
+ }
+ return new BrowserToolboxLauncher(
+ onClose,
+ onRun,
+ overwritePreferences,
+ binaryPath
+ );
+};
+
+/**
+ * Figure out if there are any open Browser Toolboxes that'll need to be restored.
+ * @return bool
+ */
+BrowserToolboxLauncher.getBrowserToolboxSessionState = function() {
+ return processes.size !== 0;
+};
+
+BrowserToolboxLauncher.prototype = {
+ /**
+ * Initializes the devtools server.
+ */
+ _initServer: function() {
+ if (this.devToolsServer) {
+ dumpn("The chrome toolbox server is already running.");
+ return;
+ }
+
+ dumpn("Initializing the chrome toolbox server.");
+
+ // Create a separate loader instance, so that we can be sure to receive a
+ // separate instance of the DebuggingServer from the rest of the devtools.
+ // This allows us to safely use the tools against even the actors and
+ // DebuggingServer itself, especially since we can mark this loader as
+ // invisible to the debugger (unlike the usual loader settings).
+ this.loader = new DevToolsLoader({
+ invisibleToDebugger: true,
+ });
+ const { DevToolsServer } = this.loader.require(
+ "devtools/server/devtools-server"
+ );
+ const { SocketListener } = this.loader.require(
+ "devtools/shared/security/socket"
+ );
+ this.devToolsServer = DevToolsServer;
+ dumpn("Created a separate loader instance for the DevToolsServer.");
+
+ this.devToolsServer.init();
+ // We mainly need a root actor and target actors for opening a toolbox, even
+ // against chrome/content. But the "no auto hide" button uses the
+ // preference actor, so also register the browser actors.
+ this.devToolsServer.registerAllActors();
+ this.devToolsServer.allowChromeProcess = true;
+ dumpn("initialized and added the browser actors for the DevToolsServer.");
+
+ const chromeDebuggingWebSocket = Services.prefs.getBoolPref(
+ "devtools.debugger.chrome-debugging-websocket"
+ );
+ const socketOptions = {
+ portOrPath: -1,
+ webSocket: chromeDebuggingWebSocket,
+ };
+ const listener = new SocketListener(this.devToolsServer, socketOptions);
+ listener.open();
+ this.listener = listener;
+ this.port = listener.port;
+
+ if (!this.port) {
+ throw new Error("No devtools server port");
+ }
+
+ dumpn("Finished initializing the chrome toolbox server.");
+ dump(
+ `DevTools Server for Browser Toolbox listening on port: ${this.port}\n`
+ );
+ },
+
+ /**
+ * Initializes a profile for the remote debugger process.
+ */
+ _initProfile(overwritePreferences) {
+ dumpn("Initializing the chrome toolbox user profile.");
+
+ const debuggingProfileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ debuggingProfileDir.append(CHROME_DEBUGGER_PROFILE_NAME);
+ try {
+ debuggingProfileDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ } catch (ex) {
+ if (ex.result === Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
+ if (!overwritePreferences) {
+ this._dbgProfilePath = debuggingProfileDir.path;
+ return;
+ }
+ // Fall through and copy the current set of prefs to the profile.
+ } else {
+ dumpn("Error trying to create a profile directory, failing.");
+ dumpn("Error: " + (ex.message || ex));
+ return;
+ }
+ }
+
+ this._dbgProfilePath = debuggingProfileDir.path;
+
+ // We would like to copy prefs into this new profile...
+ const prefsFile = debuggingProfileDir.clone();
+ prefsFile.append("prefs.js");
+ // ... but unfortunately, when we run tests, it seems the starting profile
+ // clears out the prefs file before re-writing it, and in practice the
+ // file is empty when we get here. So just copying doesn't work in that
+ // case.
+ // We could force a sync pref flush and then copy it... but if we're doing
+ // that, we might as well just flush directly to the new profile, which
+ // always works:
+ Services.prefs.savePrefFile(prefsFile);
+
+ dumpn(
+ "Finished creating the chrome toolbox user profile at: " +
+ this._dbgProfilePath
+ );
+ },
+
+ /**
+ * Creates and initializes the profile & process for the remote debugger.
+ */
+ _create: function(binaryPath) {
+ dumpn("Initializing chrome debugging process.");
+
+ let command = Services.dirsvc.get("XREExeF", Ci.nsIFile).path;
+ let profilePath = this._dbgProfilePath;
+
+ if (binaryPath) {
+ command = binaryPath;
+ profilePath = FileUtils.getDir("TmpD", ["browserToolboxProfile"], true)
+ .path;
+ }
+
+ dumpn("Running chrome debugging process.");
+ const args = [
+ "-no-remote",
+ "-foreground",
+ "-profile",
+ profilePath,
+ "-chrome",
+ BROWSER_TOOLBOX_WINDOW_URL,
+ ];
+
+ const isBrowserToolboxFission = Services.prefs.getBoolPref(
+ "devtools.browsertoolbox.fission",
+ false
+ );
+ const isInputContextEnabled = Services.prefs.getBoolPref(
+ "devtools.webconsole.input.context",
+ false
+ );
+ const environment = {
+ // Will be read by the Browser Toolbox Firefox instance to update the
+ // devtools.browsertoolbox.fission pref on the Browser Toolbox profile.
+ MOZ_BROWSER_TOOLBOX_FISSION_PREF: isBrowserToolboxFission ? "1" : "0",
+ // Similar, but for the WebConsole input context dropdown.
+ MOZ_BROWSER_TOOLBOX_INPUT_CONTEXT: isInputContextEnabled ? "1" : "0",
+ // Disable safe mode for the new process in case this was opened via the
+ // keyboard shortcut.
+ MOZ_DISABLE_SAFE_MODE_KEY: "1",
+ MOZ_BROWSER_TOOLBOX_PORT: String(this.port),
+ MOZ_HEADLESS: null,
+ };
+
+ // During local development, incremental builds can trigger the main process
+ // to clear its startup cache with the "flag file" .purgecaches, but this
+ // file is removed during app startup time, so we aren't able to know if it
+ // was present in order to also clear the child profile's startup cache as
+ // well.
+ //
+ // As an approximation of "isLocalBuild", check for an unofficial build.
+ if (!AppConstants.MOZILLA_OFFICIAL) {
+ args.push("-purgecaches");
+ }
+
+ dump(`Starting Browser Toolbox ${command} ${args.join(" ")}\n`);
+ this._dbgProcessPromise = Subprocess.call({
+ command,
+ arguments: args,
+ environmentAppend: true,
+ stderr: "stdout",
+ environment,
+ }).then(
+ proc => {
+ this._dbgProcess = proc;
+
+ // jsbrowserdebugger is not connected with a toolbox so we pass -1 as the
+ // toolbox session id.
+ this._telemetry.toolOpened("jsbrowserdebugger", -1, this);
+
+ dumpn("Chrome toolbox is now running...");
+ this.emit("run", this);
+
+ proc.stdin.close();
+ const dumpPipe = async pipe => {
+ let data = await pipe.readString();
+ while (data) {
+ dump("> " + data);
+ data = await pipe.readString();
+ }
+ };
+ dumpPipe(proc.stdout);
+
+ proc.wait().then(() => this.close());
+
+ return proc;
+ },
+ err => {
+ console.log(
+ `Error loading Browser Toolbox: ${command} ${args.join(" ")}`,
+ err
+ );
+ }
+ );
+ },
+
+ /**
+ * Closes the remote debugging server and kills the toolbox process.
+ */
+ close: async function() {
+ if (this.closed) {
+ return;
+ }
+
+ this.closed = true;
+
+ dumpn("Cleaning up the chrome debugging process.");
+
+ Services.obs.removeObserver(this.close, "quit-application");
+
+ this._dbgProcess.stdout.close();
+ await this._dbgProcess.kill();
+
+ // jsbrowserdebugger is not connected with a toolbox so we pass -1 as the
+ // toolbox session id.
+ this._telemetry.toolClosed("jsbrowserdebugger", -1, this);
+
+ if (this.listener) {
+ this.listener.close();
+ }
+
+ if (this.devToolsServer) {
+ this.devToolsServer.destroy();
+ this.devToolsServer = null;
+ }
+
+ dumpn("Chrome toolbox is now closed...");
+ this.emit("close", this);
+ processes.delete(this);
+
+ this._dbgProcess = null;
+ if (this.loader) {
+ this.loader.destroy();
+ }
+ this.loader = null;
+ this._telemetry = null;
+ },
+};
+
+/**
+ * Helper method for debugging.
+ * @param string
+ */
+function dumpn(str) {
+ if (wantLogging) {
+ dump("DBG-FRONTEND: " + str + "\n");
+ }
+}
+
+var wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+const prefObserver = {
+ observe: (...args) => {
+ wantLogging = Services.prefs.getBoolPref(args.pop());
+ },
+};
+Services.prefs.addObserver("devtools.debugger.log", prefObserver);
+const unloadObserver = function(subject) {
+ if (subject.wrappedJSObject == require("@loader/unload")) {
+ Services.prefs.removeObserver("devtools.debugger.log", prefObserver);
+ Services.obs.removeObserver(unloadObserver, "devtools:loader:destroy");
+ }
+};
+Services.obs.addObserver(unloadObserver, "devtools:loader:destroy");
diff --git a/devtools/client/framework/browser-toolbox/README.md b/devtools/client/framework/browser-toolbox/README.md
new file mode 100644
index 0000000000..fee09b2dd9
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/README.md
@@ -0,0 +1,37 @@
+# Browser Toolbox
+
+## Introduction
+
+The Browser Toolbox spawns a toolbox in a new dedicated Firefox instance to debug the currently running Firefox. This new instance runs in a distinct process.
+
+To enable it, you must first flip two preferences in the DevTools Options panel (F1):
+- Enable browser chrome and add-on debugging toolboxes
+- Enable remote debugging
+
+You can either start it via a keyboard shortcut (CmdOrCtrl+Alt+Shift+I) or via the Tools > Web Developer > Browser Toolbox menu item.
+
+When describing the setup used by the Browser Toolbox, we will refer to those two distinct Firefox instances as:
+- the target Firefox: this is the current instance, that we want to debug
+- the client Firefox: this is the new instance that will only run the Browser Toolbox window
+
+## Browser Toolbox Architecture
+
+The startup sequence of the browser toolbox begins in the target Firefox.
+
+`browser-toolbox/Launcher.jsm` will be first reponsible for creating a remote DevToolsServer. This new DevToolsServer runs in the parent process but is separated from any existing DevTools DevToolsServer that spawned earlier for regular DevTools usage. Thanks to this, we will be able to debug files loaded in those regular DevToolsServers used for content toolboxes, about:debugging, ...
+
+Then we need to start the client Firefox. To do that, `browser-toolbox/Launcher.jsm` creates a profile that will be a copy of the current profile loaded in the target Firefox, so that all user preferences can be automatically ported over. As a reminder both client and target Firefox will run simultaneously, so they can't use the same profile.
+
+This new profile is stored inside the folder of the target profile, in a `chrome_debugger_profile` folder. So the next time the Browser Toolbox opens this for profile, it will be reused.
+
+Once the profile is ready (or if it was already there), `browser-toolbox/Launcher.jsm` spawns a new Firefox instance with a few additional parameters, most importantly `-chrome chrome://devtools/content/framework/browser-toolbox/window.html`.
+
+This way Firefox will load `browser-toolbox/window.html` instead of the regular browser window. Most of the logic is then handled by `browser-toolbox/window.js` which will connect to the remote server opened on the target Firefox and will then load a toolbox connected to this server.
+
+## Debugging the Browser Toolbox
+
+Note that you can open a Browser Toolbox from the Browser Toolbox. Simply reuse the same shortcut as the one you used to open the first Browser Toolbox, but this time while the Browser Toolbox window is focused.
+
+Another Browser Toolbox will spawn, this time debugging the first Browser Toolbox Firefox instance. If you are curious about how this is done, `browser-toolbox/window.js` simply loads `browser-toolbox/Launcher.jsm` and requests to open a new Browser Toolbox.
+
+This will open yet another Firefox instance, running in another process. And a new `chrome_debugger_profile` folder will be created inside the existing Browser Toolbox profile (which as explained in the previous section, is already in a `chrome_debugger_profile` folder under the target Firefox profile).
diff --git a/devtools/client/framework/browser-toolbox/moz.build b/devtools/client/framework/browser-toolbox/moz.build
new file mode 100644
index 0000000000..35954c1f2b
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+BROWSER_CHROME_MANIFESTS += [
+ "test/browser.ini",
+]
+
+DevToolsModules(
+ "Launcher.jsm",
+)
diff --git a/devtools/client/framework/browser-toolbox/test/.eslintrc.js b/devtools/client/framework/browser-toolbox/test/.eslintrc.js
new file mode 100644
index 0000000000..2eba290f7d
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "../../../../.eslintrc.mochitests.js",
+};
diff --git a/devtools/client/framework/browser-toolbox/test/browser.ini b/devtools/client/framework/browser-toolbox/test/browser.ini
new file mode 100644
index 0000000000..867ab4a77f
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser.ini
@@ -0,0 +1,31 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+# UNTIL Bug 1591064 IS FIXED ALL NEW TESTS SHOULD BE SKIPPED ON ASAN
+skip-if = asan # Bug 1591064
+support-files =
+ doc_browser_toolbox_fission_contentframe_inspector_frame.html
+ doc_browser_toolbox_fission_contentframe_inspector_page.html
+ head.js
+ helpers-browser-toolbox.js
+ !/devtools/client/debugger/test/mochitest/head.js
+ !/devtools/client/debugger/test/mochitest/helpers.js
+ !/devtools/client/debugger/test/mochitest/helpers/context.js
+ !/devtools/client/inspector/test/shared-head.js
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/shared-redux-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+ !/devtools/client/shared/test/test-actor.js
+# This is far from ideal. https://bugzilla.mozilla.org/show_bug.cgi?id=1565279
+# covers removing this pref flip.
+prefs =
+ security.allow_unsafe_parent_loads=true
+
+
+[browser_browser_toolbox.js]
+[browser_browser_toolbox_debugger.js]
+[browser_browser_toolbox_evaluation_context.js]
+[browser_browser_toolbox_fission_contentframe_inspector.js]
+[browser_browser_toolbox_fission_inspector.js]
+[browser_browser_toolbox_print_preview.js]
+[browser_browser_toolbox_rtl.js]
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox.js
new file mode 100644
index 0000000000..9fab969c4b
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PromiseTestUtils.jsm"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+add_task(async function() {
+ const ToolboxTask = await initBrowserToolboxTask();
+ await ToolboxTask.importFunctions({});
+
+ const hasCloseButton = await ToolboxTask.spawn(null, async () => {
+ /* global gToolbox */
+ return !!gToolbox.doc.getElementById("toolbox-close");
+ });
+ ok(!hasCloseButton, "Browser toolbox doesn't have a close button");
+
+ await ToolboxTask.destroy();
+});
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_debugger.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_debugger.js
new file mode 100644
index 0000000000..c8ad9b224f
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_debugger.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test asserts that the new debugger works from the browser toolbox process
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PromiseTestUtils.jsm"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+// On debug test runner, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+const { fetch } = require("devtools/shared/DevToolsUtils");
+
+const debuggerHeadURL =
+ CHROME_URL_ROOT + "../../../debugger/test/mochitest/head.js";
+const helpersURL =
+ CHROME_URL_ROOT + "../../../debugger/test/mochitest/helpers.js";
+const helpersContextURL =
+ CHROME_URL_ROOT + "../../../debugger/test/mochitest/helpers/context.js";
+
+add_task(async function runTest() {
+ const s = Cu.Sandbox("http://mozilla.org");
+
+ // Use a unique id for the fake script name in order to be able to run
+ // this test more than once. That's because the Sandbox is not immediately
+ // destroyed and so the debugger would display only one file but not necessarily
+ // connected to the latest sandbox.
+ const id = new Date().getTime();
+
+ // Pass a fake URL to evalInSandbox. If we just pass a filename,
+ // Debugger is going to fail and only display root folder (`/`) listing.
+ // But it won't try to fetch this url and use sandbox content as expected.
+ const testUrl = `http://mozilla.org/browser-toolbox-test-${id}.js`;
+ Cu.evalInSandbox(
+ "(" +
+ function() {
+ this.plop = function plop() {
+ return 1;
+ };
+ } +
+ ").call(this)",
+ s,
+ "1.8",
+ testUrl,
+ 0
+ );
+
+ // Execute the function every second in order to trigger the breakpoint
+ const interval = setInterval(s.plop, 1000);
+
+ let { content: debuggerHead } = await fetch(debuggerHeadURL);
+
+ // Also include the debugger helpers which are separated from debugger's head to be
+ // reused in other modules.
+ const { content: debuggerHelpers } = await fetch(helpersURL);
+ const { content: debuggerContextHelpers } = await fetch(helpersContextURL);
+ debuggerHead = debuggerHead + debuggerContextHelpers + debuggerHelpers;
+
+ // We remove its import of shared-head, which isn't available in browser toolbox process
+ // And isn't needed thanks to testHead's symbols
+ debuggerHead = debuggerHead.replace(
+ /Services.scriptloader.loadSubScript[^\)]*\);/g,
+ ""
+ );
+
+ const ToolboxTask = await initBrowserToolboxTask({
+ enableBrowserToolboxFission: true,
+ });
+ await ToolboxTask.importFunctions({
+ // head.js uses this method
+ registerCleanupFunction: () => {},
+ waitUntil,
+ });
+ await ToolboxTask.importScript(debuggerHead);
+
+ await ToolboxTask.spawn(`"${testUrl}"`, async _testUrl => {
+ /* global createDebuggerContext, waitForSources,
+ waitForPaused, addBreakpoint, assertPausedLocation, stepIn,
+ findSource, removeBreakpoint, resume, selectSource */
+ const { Services } = ChromeUtils.import(
+ "resource://gre/modules/Services.jsm"
+ );
+
+ Services.prefs.clearUserPref("devtools.debugger.tabs");
+ Services.prefs.clearUserPref("devtools.debugger.pending-selected-location");
+
+ info("Waiting for debugger load");
+ /* global gToolbox */
+ await gToolbox.selectTool("jsdebugger");
+ const dbg = createDebuggerContext(gToolbox);
+ const window = dbg.win;
+ const document = window.document;
+
+ await waitForSources(dbg, _testUrl);
+
+ info("Loaded, selecting the test script to debug");
+ // First expand the main thread
+ const mainThread = [...document.querySelectorAll(".tree-node")].find(
+ node => {
+ return node.querySelector(".label").textContent.trim() == "Main Thread";
+ }
+ );
+ mainThread.querySelector(".arrow").click();
+
+ // Then expand the domain
+ const domain = [...document.querySelectorAll(".tree-node")].find(node => {
+ return node.querySelector(".label").textContent.trim() == "mozilla.org";
+ });
+ const arrow = domain.querySelector(".arrow");
+ arrow.click();
+
+ const fileName = _testUrl.match(/browser-toolbox-test.*\.js/)[0];
+
+ // And finally the expected source
+ let script = [...document.querySelectorAll(".tree-node")].find(node => {
+ return node.textContent.includes(fileName);
+ });
+ script = script.querySelector(".node");
+ script.click();
+
+ const onPaused = waitForPaused(dbg);
+ await selectSource(dbg, fileName);
+ await addBreakpoint(dbg, fileName, 2);
+
+ await onPaused;
+
+ assertPausedLocation(dbg, fileName, 2);
+
+ await stepIn(dbg);
+
+ assertPausedLocation(dbg, fileName, 3);
+
+ // Remove the breakpoint before resuming in order to prevent hitting the breakpoint
+ // again during test closing.
+ const source = findSource(dbg, fileName);
+ await removeBreakpoint(dbg, source.id, 2);
+
+ await resume(dbg);
+ });
+
+ clearInterval(interval);
+
+ await ToolboxTask.destroy();
+});
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_evaluation_context.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_evaluation_context.js
new file mode 100644
index 0000000000..7aec01a72a
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_evaluation_context.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PromiseTestUtils.jsm"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+// This test is used to test fission-like features via the Browser Toolbox:
+// - computed view is correct when selecting an element in a remote frame
+
+add_task(async function() {
+ // Forces the Browser Toolbox to open on the console by default
+ await pushPref("devtools.browsertoolbox.panel", "webconsole");
+ await pushPref("devtools.webconsole.input.context", true);
+
+ // Open the test *before* opening the Browser toolbox in order to have the right target title.
+ // Once created, the target won't update its title, and so would be "New Tab", instead of "Test tab"
+ const tab = await addTab(`data:text/html,<title>Test tab</title>`);
+
+ const ToolboxTask = await initBrowserToolboxTask({
+ enableBrowserToolboxFission: true,
+ });
+
+ const tabProcessID =
+ tab.linkedBrowser.browsingContext.currentWindowGlobal.osPid;
+
+ await ToolboxTask.spawn(tabProcessID, async processID => {
+ /* global gToolbox */
+ const { hud } = await gToolbox.getPanel("webconsole");
+
+ const evaluationContextSelectorButton = hud.ui.outputNode.querySelector(
+ ".webconsole-evaluation-selector-button"
+ );
+
+ is(
+ !!evaluationContextSelectorButton,
+ true,
+ "The evaluation context selector is visible"
+ );
+ is(
+ evaluationContextSelectorButton.innerText,
+ "Top",
+ "The button has the expected 'Top' text"
+ );
+
+ // Note that the context menu is in the top level chrome document (browser-toolbox.xhtml)
+ // instead of webconsole.xhtml.
+ const labels = hud.chromeWindow.document.querySelectorAll(
+ "#webconsole-console-evaluation-context-selector-menu-list li .label"
+ );
+ const labelTexts = Array.from(labels).map(item => item.textContent);
+
+ is(
+ labelTexts.includes(`Content Process (pid ${processID})`),
+ true,
+ `${processID} content process visible in the execution context (${labelTexts})`
+ );
+
+ is(
+ labelTexts.includes(`Test tab`),
+ true,
+ `Test tab is visible in the execution context (${labelTexts})`
+ );
+ });
+
+ await ToolboxTask.destroy();
+});
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_contentframe_inspector.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_contentframe_inspector.js
new file mode 100644
index 0000000000..3c7b3122e4
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_contentframe_inspector.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PromiseTestUtils.jsm"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+/* import-globals-from ../../../inspector/test/shared-head.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this
+);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+/**
+ * Check that different-site iframes can be expanded in the Omniscient Browser
+ * Toolbox. The test is supposed to run successfully with or without fission.
+ * Pass --enable-fission to ./mach test to enable fission when running this
+ * test locally.
+ */
+add_task(async function() {
+ const ToolboxTask = await initBrowserToolboxTask({
+ enableBrowserToolboxFission: true,
+ });
+ await ToolboxTask.importFunctions({
+ selectNodeFront,
+ });
+
+ const tab = await addTab(
+ `http://example.com/browser/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_page.html`
+ );
+
+ // Set a custom attribute on the tab's browser, in order to easily select it in the markup view
+ tab.linkedBrowser.setAttribute("test-tab", "true");
+
+ const testAttribute = await ToolboxTask.spawn(null, async () => {
+ /* global gToolbox */
+ const inspector = await gToolbox.selectTool("inspector");
+ const onSidebarSelect = inspector.sidebar.once("select");
+ inspector.sidebar.select("computedview");
+ await onSidebarSelect;
+
+ info("Select the browser element for the content page");
+ const browserFront = await selectNodeFront(
+ inspector,
+ inspector.walker,
+ 'browser[remote="true"][test-tab]'
+ );
+ const browserTarget = await browserFront.connectToRemoteFrame();
+ const browserWalker = (await browserTarget.getFront("inspector")).walker;
+
+ info("Select the iframe element in the content page");
+ const iframeFront = await selectNodeFront(
+ inspector,
+ browserWalker,
+ "iframe"
+ );
+
+ // With Fission, the iframe is a remoteFrame and will have a new dedicated
+ // target front. Without Fission, the iframe is in scope of the browser
+ // target front, so we will simply reuse browserTarget.
+ const iframeTarget = iframeFront.remoteFrame
+ ? await iframeFront.connectToRemoteFrame()
+ : browserTarget;
+ const iframeWalker = (await iframeTarget.getFront("inspector")).walker;
+
+ // We need to use the iframe's document node front as the root node of the
+ // next query, because in non-fission mode "iframeWalker" is actually the
+ // browserWalker and the simple selector "#inside-iframe" is not enough to
+ // find the node across iframes.
+ const { nodes } = await iframeWalker.children(iframeFront);
+ const iframeDocFront = nodes.find(n => n.nodeType === Node.DOCUMENT_NODE);
+
+ info("Select the test element nested in the remote iframe");
+ const nodeFront = await selectNodeFront(
+ inspector,
+ iframeWalker,
+ "#inside-iframe",
+ iframeDocFront
+ );
+ return nodeFront.getAttribute("test-attribute");
+ });
+
+ is(
+ testAttribute,
+ "fission",
+ "Could successfully read attribute on a node inside a remote iframe"
+ );
+
+ await ToolboxTask.destroy();
+});
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_inspector.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_inspector.js
new file mode 100644
index 0000000000..096ff7e4f0
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_inspector.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PromiseTestUtils.jsm"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+// This test is used to test fission-like features via the Browser Toolbox:
+// - computed view is correct when selecting an element in a remote frame
+
+add_task(async function() {
+ // Forces the Browser Toolbox to open on the inspector by default
+ await pushPref("devtools.browsertoolbox.panel", "inspector");
+
+ const ToolboxTask = await initBrowserToolboxTask({
+ enableBrowserToolboxFission: true,
+ });
+ await ToolboxTask.importFunctions({
+ selectNodeFront,
+ });
+
+ // Open the tab *after* opening the Browser Toolbox in order to force creating the remote frames
+ // late and exercise frame target watching code.
+ const tab = await addTab(
+ `data:text/html,<div id="my-div" style="color: red">Foo</div><div id="second-div" style="color: blue">Foo</div>`
+ );
+ // Set a custom attribute on the tab's browser, in order to easily select it in the markup view
+ tab.linkedBrowser.setAttribute("test-tab", "true");
+
+ const color = await ToolboxTask.spawn(null, async () => {
+ /* global gToolbox */
+ const inspector = gToolbox.getPanel("inspector");
+ const onSidebarSelect = inspector.sidebar.once("select");
+ inspector.sidebar.select("computedview");
+ await onSidebarSelect;
+
+ const browser = await selectNodeFront(
+ inspector,
+ inspector.walker,
+ 'browser[remote="true"][test-tab]'
+ );
+ const browserTarget = await browser.connectToRemoteFrame();
+ const walker = (await browserTarget.getFront("inspector")).walker;
+ await selectNodeFront(inspector, walker, "#my-div");
+
+ const view = inspector.getPanel("computedview").computedView;
+ function getProperty(name) {
+ const propertyViews = view.propertyViews;
+ for (const propView of propertyViews) {
+ if (propView.name == name) {
+ return propView;
+ }
+ }
+ return null;
+ }
+ const prop = getProperty("color");
+ return prop.valueNode.textContent;
+ });
+
+ is(
+ color,
+ "rgb(255, 0, 0)",
+ "The color property of the <div> within a tab isn't red"
+ );
+
+ await ToolboxTask.spawn(null, async () => {
+ const onPickerStarted = gToolbox.nodePicker.once("picker-started");
+
+ // Wait until the inspector front was initialized in the target that
+ // contains the element we want to pick (#second-div).
+ // Otherwise, even if the picker is "started", the corresponding WalkerActor
+ // might not be listening to the correct pick events (WalkerActor::pick)
+ const onPickerReady = new Promise(resolve => {
+ gToolbox.nodePicker.on(
+ "inspector-front-ready-for-picker",
+ async function onFrontReady(walker) {
+ if (await walker.querySelector(walker.rootNode, "#second-div")) {
+ gToolbox.nodePicker.off(
+ "inspector-front-ready-for-picker",
+ onFrontReady
+ );
+ resolve();
+ }
+ }
+ );
+ });
+
+ gToolbox.nodePicker.start();
+ await onPickerStarted;
+ await onPickerReady;
+
+ const inspector = gToolbox.getPanel("inspector");
+
+ // Save the promises for later tasks, in order to start listening
+ // *before* hovering the element and wait for resolution *after* hovering.
+ this.onPickerStopped = gToolbox.nodePicker.once("picker-stopped");
+ this.onInspectorUpdated = inspector.once("inspector-updated");
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#second-div",
+ {},
+ tab.linkedBrowser
+ );
+
+ const secondColor = await ToolboxTask.spawn(null, async () => {
+ info(" # Waiting for picker stop");
+ await this.onPickerStopped;
+ info(" # Waiting for inspector-updated");
+ await this.onInspectorUpdated;
+
+ const inspector = gToolbox.getPanel("inspector");
+ const view = inspector.getPanel("computedview").computedView;
+ function getProperty(name) {
+ const propertyViews = view.propertyViews;
+ for (const propView of propertyViews) {
+ if (propView.name == name) {
+ return propView;
+ }
+ }
+ return null;
+ }
+ const prop = getProperty("color");
+ return prop.valueNode.textContent;
+ });
+
+ is(
+ secondColor,
+ "rgb(0, 0, 255)",
+ "The color property of the <div> within a tab isn't blue"
+ );
+
+ await ToolboxTask.destroy();
+});
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_print_preview.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_print_preview.js
new file mode 100644
index 0000000000..c1c624e907
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_print_preview.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PromiseTestUtils.jsm"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+// Test that the MultiProcessBrowserToolbox can be opened when print preview is
+// started, and can select elements in the print preview document.
+add_task(async function() {
+ // Forces the Browser Toolbox to open on the inspector by default
+ await pushPref("devtools.browsertoolbox.panel", "inspector");
+
+ // Force the modal print preview, otherwise the printPreview command will not
+ // open the expected UI.
+ await pushPref("print.tab_modal.enabled", true);
+
+ // Open the tab *after* opening the Browser Toolbox in order to force creating the remote frames
+ // late and exercise frame target watching code.
+ await addTab(`data:text/html,<div id="test-div">PRINT PREVIEW TEST</div>`);
+
+ info("Start the print preview for the current tab");
+ document.getElementById("cmd_printPreview").doCommand();
+
+ const ToolboxTask = await initBrowserToolboxTask({
+ enableBrowserToolboxFission: true,
+ });
+ await ToolboxTask.importFunctions({
+ selectNodeFront,
+ });
+
+ const hasCloseButton = await ToolboxTask.spawn(null, async () => {
+ /* global gToolbox */
+ const inspector = gToolbox.getPanel("inspector");
+
+ info("Select the printpreview document in the markup view");
+ const browser = await selectNodeFront(
+ inspector,
+ inspector.walker,
+ 'browser[printpreview="true"]'
+ );
+ const browserTarget = await browser.connectToRemoteFrame();
+ const walker = (await browserTarget.getFront("inspector")).walker;
+
+ info("Select the #test-div in the print preview document");
+ await selectNodeFront(inspector, walker, "#test-div");
+ return !!gToolbox.doc.getElementById("toolbox-close");
+ });
+ ok(!hasCloseButton, "Browser toolbox doesn't have a close button");
+
+ await ToolboxTask.destroy();
+});
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_rtl.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_rtl.js
new file mode 100644
index 0000000000..0706e7e5ca
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_rtl.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PromiseTestUtils.jsm"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+// Test that DevTools panels are rendered in "rtl" (right-to-left) in the Browser Toolbox.
+add_task(async function() {
+ await pushPref("intl.l10n.pseudo", "bidi");
+
+ const ToolboxTask = await initBrowserToolboxTask();
+ await ToolboxTask.importFunctions({});
+
+ const dir = await ToolboxTask.spawn(null, async () => {
+ /* global gToolbox */
+ const inspector = await gToolbox.selectTool("inspector");
+ return inspector.panelDoc.dir;
+ });
+ is(dir, "rtl", "Inspector panel has the expected direction");
+
+ await ToolboxTask.destroy();
+});
diff --git a/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_frame.html b/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_frame.html
new file mode 100644
index 0000000000..1f365cc17f
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_frame.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Frame for browser_browser_toolbox_fission_contentframe_inspector.js</title>
+ </head>
+
+ <body>
+ <div id="inside-iframe" test-attribute="fission">Inside iframe</div>
+ </body>
+</html>
diff --git a/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_page.html b/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_page.html
new file mode 100644
index 0000000000..67ee09ceed
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_page.html
@@ -0,0 +1,16 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Frame for browser_browser_toolbox_fission_contentframe_inspector.js</title>
+ </head>
+
+ <body>
+ <!-- Here we use example.org, while the embedder is loaded with example.com (.org vs .com)
+ This ensures this frame will be a remote frame when fission is enabled. -->
+ <iframe src="http://example.org/browser/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_frame.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/framework/browser-toolbox/test/head.js b/devtools/client/framework/browser-toolbox/test/head.js
new file mode 100644
index 0000000000..4601d6b1d5
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/head.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from ../../../shared/test/shared-head.js */
+/* import-globals-from ../../../shared/test/telemetry-test-helpers.js */
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+/* import-globals-from helpers-browser-toolbox.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js",
+ this
+);
+
+async function selectNodeFront(inspector, walker, selector, rootNode) {
+ rootNode = rootNode || walker.rootNode;
+ const nodeFront = await walker.querySelector(rootNode, selector);
+ const updated = inspector.once("inspector-updated");
+ inspector.selection.setNodeFront(nodeFront);
+ await updated;
+ return nodeFront;
+}
diff --git a/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js b/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js
new file mode 100644
index 0000000000..da2364dec6
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js
@@ -0,0 +1,192 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-unused-vars, no-undef */
+
+"use strict";
+
+const { BrowserToolboxLauncher } = ChromeUtils.import(
+ "resource://devtools/client/framework/browser-toolbox/Launcher.jsm"
+);
+const { DevToolsClient } = require("devtools/client/devtools-client");
+
+/**
+ * Open up a browser toolbox and return a ToolboxTask object for interacting
+ * with it. ToolboxTask has the following methods:
+ *
+ * importFunctions(object)
+ *
+ * The object contains functions from this process which should be defined in
+ * the global evaluation scope of the toolbox. The toolbox cannot load testing
+ * files directly.
+ *
+ * spawn(arg, function)
+ *
+ * Invoke the given function and argument within the global evaluation scope
+ * of the toolbox. The evaluation scope predefines the name "gToolbox" for the
+ * toolbox itself.
+ *
+ * destroy()
+ *
+ * Destroy the browser toolbox and make sure it exits cleanly.
+ *
+ * @param {Object}:
+ * - {Boolean} enableBrowserToolboxFission: pass true to enable the OBT.
+ * - {Boolean} enableContentMessages: pass true to log content messages
+ * in the Console.
+ */
+async function initBrowserToolboxTask({
+ enableBrowserToolboxFission,
+ enableContentMessages,
+} = {}) {
+ if (AppConstants.ASAN) {
+ ok(
+ false,
+ "ToolboxTask cannot be used on ASAN builds. This test should be skipped (Bug 1591064)."
+ );
+ }
+
+ await pushPref("devtools.chrome.enabled", true);
+ await pushPref("devtools.debugger.remote-enabled", true);
+ await pushPref("devtools.browsertoolbox.enable-test-server", true);
+ await pushPref("devtools.debugger.prompt-connection", false);
+
+ if (enableBrowserToolboxFission) {
+ await pushPref("devtools.browsertoolbox.fission", true);
+ }
+
+ // This rejection seems to affect all tests using the browser toolbox.
+ ChromeUtils.import(
+ "resource://testing-common/PromiseTestUtils.jsm"
+ ).PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+ const process = await new Promise(onRun => {
+ BrowserToolboxLauncher.init(null, onRun, /* overwritePreferences */ true);
+ });
+ ok(true, "Browser toolbox started\n");
+ is(
+ BrowserToolboxLauncher.getBrowserToolboxSessionState(),
+ true,
+ "Has session state"
+ );
+
+ // The port of the DevToolsServer installed in the toolbox process is fixed.
+ // See browser-toolbox-window.js
+ let transport;
+ while (true) {
+ try {
+ transport = await DevToolsClient.socketConnect({
+ host: "localhost",
+ port: 6001,
+ webSocket: false,
+ });
+ break;
+ } catch (e) {
+ await waitForTime(100);
+ }
+ }
+ ok(true, "Got transport");
+
+ const client = new DevToolsClient(transport);
+ await client.connect();
+
+ ok(true, "Connected");
+
+ const descriptorFront = await client.mainRoot.getMainProcess();
+ const target = await descriptorFront.getTarget();
+ const consoleFront = await target.getFront("console");
+ const preferenceFront = await client.mainRoot.getFront("preference");
+
+ if (enableContentMessages) {
+ await preferenceFront.setBoolPref(
+ "devtools.browserconsole.contentMessages",
+ true
+ );
+ }
+
+ await importFunctions({
+ info: msg => dump(msg + "\n"),
+ is: (a, b, description) => {
+ let msg =
+ "'" + JSON.stringify(a) + "' is equal to '" + JSON.stringify(b) + "'";
+ if (description) {
+ msg += " - " + description;
+ }
+ if (a !== b) {
+ msg = "FAILURE: " + msg;
+ dump(msg + "\n");
+ throw new Error(msg);
+ } else {
+ msg = "SUCCESS: " + msg;
+ dump(msg + "\n");
+ }
+ },
+ ok: (a, description) => {
+ let msg = "'" + JSON.stringify(a) + "' is true";
+ if (description) {
+ msg += " - " + description;
+ }
+ if (!a) {
+ msg = "FAILURE: " + msg;
+ dump(msg + "\n");
+ throw new Error(msg);
+ } else {
+ msg = "SUCCESS: " + msg;
+ dump(msg + "\n");
+ }
+ },
+ });
+
+ async function spawn(arg, fn) {
+ const rv = await consoleFront.evaluateJSAsync(`(${fn})(${arg})`, {
+ mapped: { await: true },
+ });
+ if (rv.exception) {
+ throw new Error(`ToolboxTask.spawn failure: ${rv.exception.message}`);
+ } else if (rv.topLevelAwaitRejected) {
+ throw new Error(`ToolboxTask.spawn await rejected`);
+ }
+ return rv.result;
+ }
+
+ async function importFunctions(functions) {
+ for (const [key, fn] of Object.entries(functions)) {
+ await consoleFront.evaluateJSAsync(`this.${key} = ${fn}`);
+ }
+ }
+
+ async function importScript(script) {
+ const response = await consoleFront.evaluateJSAsync(script);
+ if (response.hasException) {
+ ok(
+ false,
+ "ToolboxTask.spawn exception while importing script: " +
+ response.exceptionMessage
+ );
+ }
+ }
+
+ async function destroy() {
+ const closePromise = process._dbgProcess.wait();
+ consoleFront.evaluateJSAsync("gToolbox.destroy()");
+
+ const { exitCode } = await closePromise;
+ ok(true, "Browser toolbox process closed");
+
+ is(exitCode, 0, "The remote debugger process died cleanly");
+
+ is(
+ BrowserToolboxLauncher.getBrowserToolboxSessionState(),
+ false,
+ "No session state after closing"
+ );
+
+ await client.close();
+ }
+
+ return {
+ importFunctions,
+ importScript,
+ spawn,
+ destroy,
+ };
+}
diff --git a/devtools/client/framework/browser-toolbox/window.css b/devtools/client/framework/browser-toolbox/window.css
new file mode 100644
index 0000000000..4fb43b2cbb
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/window.css
@@ -0,0 +1,48 @@
+/* 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/. */
+
+body {
+ padding: 0;
+ margin: 0;
+ display: flex;
+ height: 100vh;
+}
+
+/**
+ * The main content of the BrowserToolbox runs within an iframe.
+ */
+.devtools-toolbox-browsertoolbox-iframe {
+ border: 0;
+ width: 100%;
+}
+
+/**
+ * Status message shows connection (to the backend) info messages.
+ */
+#status-message-container {
+ width: calc(100% - 10px);
+ font-family: var(--monospace-font-family);
+ padding: 5px;
+ color: FieldText;
+ background-color: Field;
+}
+
+/**
+ * Helper for hiding/showing the status message.
+ */
+#status-message-container[hidden="true"] {
+ display: none;
+}
+
+#status-message-title {
+ font-size: 14px;
+ font-weight: bold;
+}
+
+#status-message {
+ font-size: 12px;
+ width: 100%;
+ height: 200px;
+ overflow: auto;
+}
diff --git a/devtools/client/framework/browser-toolbox/window.html b/devtools/client/framework/browser-toolbox/window.html
new file mode 100644
index 0000000000..767c7b61ef
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/window.html
@@ -0,0 +1,23 @@
+<!-- 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/. -->
+<!DOCTYPE html>
+<html id="devtools-toolbox-window"
+ windowtype="devtools:toolbox"
+ width="900" height="350"
+ persist="screenX screenY width height sizemode">
+ <head>
+ <link rel="stylesheet" href="chrome://global/skin/global.css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/common.css"/>
+ <link rel="stylesheet" href="chrome://devtools/content/framework/browser-toolbox/window.css"/>
+ <script src="chrome://devtools/content/framework/browser-toolbox/window.js"></script>
+ <script src="chrome://global/content/viewSourceUtils.js"></script>
+ <script src="chrome://browser/content/utilityOverlay.js"></script>
+ </head>
+ <body>
+ <div id="status-message-container" hidden="true">
+ <div id="status-message-title"></div>
+ <pre id="status-message"></pre>
+ </div>
+ </body>
+</html>
diff --git a/devtools/client/framework/browser-toolbox/window.js b/devtools/client/framework/browser-toolbox/window.js
new file mode 100644
index 0000000000..99560cbde9
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/window.js
@@ -0,0 +1,293 @@
+/* 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";
+
+var { loader, require, DevToolsLoader } = ChromeUtils.import(
+ "resource://devtools/shared/Loader.jsm"
+);
+
+// Require this module to setup core modules
+loader.require("devtools/client/framework/devtools-browser");
+
+var { gDevTools } = require("devtools/client/framework/devtools");
+var { Toolbox } = require("devtools/client/framework/toolbox");
+var Services = require("Services");
+var { DevToolsClient } = require("devtools/client/devtools-client");
+var { PrefsHelper } = require("devtools/client/shared/prefs");
+const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/toolbox.properties"
+);
+loader.lazyImporter(
+ this,
+ "BrowserToolboxLauncher",
+ "resource://devtools/client/framework/browser-toolbox/Launcher.jsm"
+);
+
+// Timeout to wait before we assume that a connect() timed out without an error.
+// In milliseconds. (With the Debugger pane open, this has been reported to last
+// more than 10 seconds!)
+const STATUS_REVEAL_TIME = 15000;
+
+/**
+ * Shortcuts for accessing various debugger preferences.
+ */
+var Prefs = new PrefsHelper("devtools.debugger", {
+ chromeDebuggingHost: ["Char", "chrome-debugging-host"],
+ chromeDebuggingWebSocket: ["Bool", "chrome-debugging-websocket"],
+});
+
+var gToolbox, gClient, gShortcuts;
+
+function appendStatusMessage(msg) {
+ const statusMessage = document.getElementById("status-message");
+ statusMessage.textContent += msg + "\n";
+ if (msg.stack) {
+ statusMessage.textContent += msg.stack + "\n";
+ }
+}
+
+function toggleStatusMessage(visible = true) {
+ const statusMessageContainer = document.getElementById(
+ "status-message-container"
+ );
+ if (visible) {
+ statusMessageContainer.removeAttribute("hidden");
+ } else {
+ statusMessageContainer.setAttribute("hidden", "true");
+ }
+}
+
+function revealStatusMessage() {
+ toggleStatusMessage(true);
+}
+
+function hideStatusMessage() {
+ toggleStatusMessage(false);
+}
+
+var connect = async function() {
+ // Initiate the connection
+ const env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+
+ // MOZ_BROWSER_TOOLBOX_FISSION_PREF is set by the target Firefox instance
+ // before opening the Browser Toolbox.
+ // If "devtools.browsertoolbox.fission" is true, the variable is set to "1",
+ // otherwise it is set to "0".
+ Services.prefs.setBoolPref(
+ "devtools.browsertoolbox.fission",
+ env.get("MOZ_BROWSER_TOOLBOX_FISSION_PREF") === "1"
+ );
+ // Similar, but for the WebConsole input context dropdown.
+ Services.prefs.setBoolPref(
+ "devtools.webconsole.input.context",
+ env.get("MOZ_BROWSER_TOOLBOX_INPUT_CONTEXT") === "1"
+ );
+
+ const port = env.get("MOZ_BROWSER_TOOLBOX_PORT");
+
+ // A port needs to be passed in from the environment, for instance:
+ // MOZ_BROWSER_TOOLBOX_PORT=6080 ./mach run -chrome \
+ // chrome://devtools/content/framework/browser-toolbox/window.html
+ if (!port) {
+ throw new Error(
+ "Must pass a port in an env variable with MOZ_BROWSER_TOOLBOX_PORT"
+ );
+ }
+
+ const host = Prefs.chromeDebuggingHost;
+ const webSocket = Prefs.chromeDebuggingWebSocket;
+ appendStatusMessage(`Connecting to ${host}:${port}, ws: ${webSocket}`);
+ const transport = await DevToolsClient.socketConnect({
+ host,
+ port,
+ webSocket,
+ });
+ gClient = new DevToolsClient(transport);
+ appendStatusMessage("Start protocol client for connection");
+ await gClient.connect();
+
+ appendStatusMessage("Get root form for toolbox");
+ const mainProcessDescriptor = await gClient.mainRoot.getMainProcess();
+ const mainProcessTargetFront = await mainProcessDescriptor.getTarget();
+ await openToolbox(mainProcessTargetFront);
+};
+
+// Certain options should be toggled since we can assume chrome debugging here
+function setPrefDefaults() {
+ Services.prefs.setBoolPref("devtools.inspector.showUserAgentStyles", true);
+ Services.prefs.setBoolPref(
+ "devtools.performance.ui.show-platform-data",
+ true
+ );
+ Services.prefs.setBoolPref(
+ "devtools.inspector.showAllAnonymousContent",
+ true
+ );
+ Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true);
+ Services.prefs.setBoolPref("devtools.console.stdout.chrome", true);
+ Services.prefs.setBoolPref(
+ "devtools.command-button-noautohide.enabled",
+ true
+ );
+ Services.prefs.setBoolPref("devtools.performance.new-panel-enabled", false);
+ Services.prefs.setBoolPref("layout.css.emulate-moz-box-with-flex", false);
+
+ Services.prefs.setBoolPref("devtools.performance.enabled", false);
+}
+
+window.addEventListener(
+ "load",
+ async function() {
+ gShortcuts = new KeyShortcuts({ window });
+ gShortcuts.on("CmdOrCtrl+W", onCloseCommand);
+ gShortcuts.on("CmdOrCtrl+Alt+Shift+I", onDebugBrowserToolbox);
+
+ const statusMessageContainer = document.getElementById(
+ "status-message-title"
+ );
+ statusMessageContainer.textContent = L10N.getStr(
+ "browserToolbox.statusMessage"
+ );
+
+ setPrefDefaults();
+
+ // Reveal status message if connecting is slow or if an error occurs.
+ const delayedStatusReveal = setTimeout(
+ revealStatusMessage,
+ STATUS_REVEAL_TIME
+ );
+ try {
+ await connect();
+ clearTimeout(delayedStatusReveal);
+ hideStatusMessage();
+ } catch (e) {
+ clearTimeout(delayedStatusReveal);
+ appendStatusMessage(e);
+ revealStatusMessage();
+ console.error(e);
+ }
+ },
+ { once: true }
+);
+
+function onCloseCommand(event) {
+ window.close();
+}
+
+/**
+ * Open a Browser toolbox debugging the current browser toolbox
+ *
+ * This helps debugging the browser toolbox code, especially the code
+ * running in the parent process. i.e. frontend code.
+ */
+function onDebugBrowserToolbox() {
+ BrowserToolboxLauncher.init();
+}
+
+async function openToolbox(targetFront) {
+ const form = targetFront.targetForm;
+ appendStatusMessage(
+ `Create toolbox target: ${JSON.stringify({ form }, null, 2)}`
+ );
+
+ // Remember the last panel that was used inside of this profile.
+ // But if we are testing, then it should always open the debugger panel.
+ const selectedTool = Services.prefs.getCharPref(
+ "devtools.browsertoolbox.panel",
+ Services.prefs.getCharPref("devtools.toolbox.selectedTool", "jsdebugger")
+ );
+
+ const toolboxOptions = { doc: document };
+ appendStatusMessage(`Show toolbox with ${selectedTool} selected`);
+
+ gToolbox = await gDevTools.showToolbox(
+ targetFront,
+ selectedTool,
+ Toolbox.HostType.BROWSERTOOLBOX,
+ toolboxOptions
+ );
+
+ bindToolboxHandlers();
+ gToolbox.raise();
+
+ // Enable some testing features if the browser toolbox test pref is set.
+ if (
+ Services.prefs.getBoolPref(
+ "devtools.browsertoolbox.enable-test-server",
+ false
+ )
+ ) {
+ // setup a server so that the test can evaluate messages in this process.
+ installTestingServer();
+ }
+}
+
+function installTestingServer() {
+ // Install a DevToolsServer in this process and inform the server of its
+ // location. Tests operating on the browser toolbox run in the server
+ // (the firefox parent process) and can connect to this new server using
+ // initBrowserToolboxTask(), allowing them to evaluate scripts here.
+
+ const testLoader = new DevToolsLoader({
+ invisibleToDebugger: true,
+ });
+ const { DevToolsServer } = testLoader.require(
+ "devtools/server/devtools-server"
+ );
+ const { SocketListener } = testLoader.require(
+ "devtools/shared/security/socket"
+ );
+
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+ DevToolsServer.allowChromeProcess = true;
+
+ // Use a fixed port which initBrowserToolboxTask can look for.
+ const socketOptions = { portOrPath: 6001 };
+ const listener = new SocketListener(DevToolsServer, socketOptions);
+ listener.open();
+}
+
+async function bindToolboxHandlers() {
+ gToolbox.once("destroyed", quitApp);
+ window.addEventListener("unload", onUnload);
+
+ if (Services.appinfo.OS == "Darwin") {
+ // Badge the dock icon to differentiate this process from the main application
+ // process.
+ updateBadgeText(false);
+
+ gToolbox.on("toolbox-paused", () => updateBadgeText(true));
+ gToolbox.on("toolbox-resumed", () => updateBadgeText(false));
+ }
+}
+
+function updateBadgeText(paused) {
+ const dockSupport = Cc["@mozilla.org/widget/macdocksupport;1"].getService(
+ Ci.nsIMacDockSupport
+ );
+ dockSupport.badgeText = paused ? "▐▐ " : " ▶";
+}
+
+function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ gToolbox.destroy();
+}
+
+function quitApp() {
+ const quit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ Services.obs.notifyObservers(quit, "quit-application-requested");
+
+ const shouldProceed = !quit.data;
+ if (shouldProceed) {
+ Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
+ }
+}
diff --git a/devtools/client/framework/components/DebugTargetErrorPage.css b/devtools/client/framework/components/DebugTargetErrorPage.css
new file mode 100644
index 0000000000..ffac30cece
--- /dev/null
+++ b/devtools/client/framework/components/DebugTargetErrorPage.css
@@ -0,0 +1,21 @@
+/* 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/. */
+
+.error-page {
+ --base-unit: 4px; /* from photon */
+
+ padding: calc(var(--base-unit) * 4);
+ font-size: 15px; /* from photon */
+ min-height: 100vh;
+}
+
+.error-page__title {
+ margin: 0;
+ font-size: 36px; /* from photon */
+ font-weight: 200; /* from photon */
+}
+
+.error-page__details {
+ font-family: monospace;
+}
diff --git a/devtools/client/framework/components/DebugTargetErrorPage.js b/devtools/client/framework/components/DebugTargetErrorPage.js
new file mode 100644
index 0000000000..e3247456b2
--- /dev/null
+++ b/devtools/client/framework/components/DebugTargetErrorPage.js
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+/**
+ * This component is displayed when the about:devtools-toolbox fails to load
+ * properly due to wrong parameters or debug targets that don't exist.
+ */
+class DebugTargetErrorPage extends PureComponent {
+ static get propTypes() {
+ return {
+ errorMessage: PropTypes.string.isRequired,
+ L10N: PropTypes.object.isRequired,
+ };
+ }
+
+ render() {
+ const { errorMessage, L10N } = this.props;
+
+ return dom.article(
+ {
+ className: "error-page qa-error-page",
+ },
+ dom.h1(
+ {
+ className: "error-page__title",
+ },
+ L10N.getStr("toolbox.debugTargetErrorPage.title")
+ ),
+ dom.p({}, L10N.getStr("toolbox.debugTargetErrorPage.description")),
+ dom.output(
+ {
+ className: "error-page__details",
+ },
+ errorMessage
+ )
+ );
+ }
+}
+
+module.exports = DebugTargetErrorPage;
diff --git a/devtools/client/framework/components/DebugTargetInfo.js b/devtools/client/framework/components/DebugTargetInfo.js
new file mode 100644
index 0000000000..781ae30cf6
--- /dev/null
+++ b/devtools/client/framework/components/DebugTargetInfo.js
@@ -0,0 +1,344 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const Services = require("Services");
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const {
+ CONNECTION_TYPES,
+ DEBUG_TARGET_TYPES,
+} = require("devtools/client/shared/remote-debugging/constants");
+
+/**
+ * This is header that should be displayed on top of the toolbox when using
+ * about:devtools-toolbox.
+ */
+class DebugTargetInfo extends PureComponent {
+ static get propTypes() {
+ return {
+ debugTargetData: PropTypes.shape({
+ connectionType: PropTypes.oneOf(Object.values(CONNECTION_TYPES))
+ .isRequired,
+ runtimeInfo: PropTypes.shape({
+ deviceName: PropTypes.string,
+ icon: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ version: PropTypes.string.isRequired,
+ }).isRequired,
+ targetType: PropTypes.oneOf(Object.values(DEBUG_TARGET_TYPES))
+ .isRequired,
+ }).isRequired,
+ L10N: PropTypes.object.isRequired,
+ toolbox: PropTypes.object.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = { urlValue: props.toolbox.target.url };
+
+ this.onChange = this.onChange.bind(this);
+ this.onFocus = this.onFocus.bind(this);
+ this.onSubmit = this.onSubmit.bind(this);
+ }
+
+ componentDidMount() {
+ this.updateTitle();
+ }
+
+ updateTitle() {
+ const { L10N, debugTargetData, toolbox } = this.props;
+ const title = toolbox.target.name;
+ const targetTypeStr = L10N.getStr(
+ this.getAssetsForDebugTargetType().l10nId
+ );
+
+ const { connectionType } = debugTargetData;
+ if (connectionType === CONNECTION_TYPES.THIS_FIREFOX) {
+ toolbox.doc.title = L10N.getFormatStr(
+ "toolbox.debugTargetInfo.tabTitleLocal",
+ targetTypeStr,
+ title
+ );
+ } else {
+ const connectionTypeStr = L10N.getStr(
+ this.getAssetsForConnectionType().l10nId
+ );
+ toolbox.doc.title = L10N.getFormatStr(
+ "toolbox.debugTargetInfo.tabTitleRemote",
+ connectionTypeStr,
+ targetTypeStr,
+ title
+ );
+ }
+ }
+
+ getRuntimeText() {
+ const { debugTargetData, L10N } = this.props;
+ const { name, version } = debugTargetData.runtimeInfo;
+ const { connectionType } = debugTargetData;
+
+ return connectionType === CONNECTION_TYPES.THIS_FIREFOX
+ ? L10N.getFormatStr(
+ "toolbox.debugTargetInfo.runtimeLabel.thisFirefox",
+ version
+ )
+ : L10N.getFormatStr(
+ "toolbox.debugTargetInfo.runtimeLabel",
+ name,
+ version
+ );
+ }
+
+ getAssetsForConnectionType() {
+ const { connectionType } = this.props.debugTargetData;
+
+ switch (connectionType) {
+ case CONNECTION_TYPES.USB:
+ return {
+ image: "chrome://devtools/skin/images/aboutdebugging-usb-icon.svg",
+ l10nId: "toolbox.debugTargetInfo.connection.usb",
+ };
+ case CONNECTION_TYPES.NETWORK:
+ return {
+ image: "chrome://devtools/skin/images/aboutdebugging-globe-icon.svg",
+ l10nId: "toolbox.debugTargetInfo.connection.network",
+ };
+ default:
+ return {};
+ }
+ }
+
+ getAssetsForDebugTargetType() {
+ const { targetType } = this.props.debugTargetData;
+
+ // TODO: https://bugzilla.mozilla.org/show_bug.cgi?id=1520723
+ // Show actual favicon (currently toolbox.target.activeTab.favicon
+ // is unpopulated)
+ const favicon = "chrome://devtools/skin/images/globe.svg";
+
+ switch (targetType) {
+ case DEBUG_TARGET_TYPES.EXTENSION:
+ return {
+ image: "chrome://devtools/skin/images/debugging-addons.svg",
+ l10nId: "toolbox.debugTargetInfo.targetType.extension",
+ };
+ case DEBUG_TARGET_TYPES.PROCESS:
+ return {
+ image: "chrome://devtools/skin/images/settings.svg",
+ l10nId: "toolbox.debugTargetInfo.targetType.process",
+ };
+ case DEBUG_TARGET_TYPES.TAB:
+ return {
+ image: favicon,
+ l10nId: "toolbox.debugTargetInfo.targetType.tab",
+ };
+ case DEBUG_TARGET_TYPES.WORKER:
+ return {
+ image: "chrome://devtools/skin/images/debugging-workers.svg",
+ l10nId: "toolbox.debugTargetInfo.targetType.worker",
+ };
+ default:
+ return {};
+ }
+ }
+
+ onChange({ target }) {
+ this.setState({ urlValue: target.value });
+ }
+
+ onFocus({ target }) {
+ target.select();
+ }
+
+ onSubmit(event) {
+ event.preventDefault();
+ let url = this.state.urlValue;
+
+ if (!url || !url.length) {
+ return;
+ }
+
+ try {
+ // Get the URL from the fixup service:
+ const flags = Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS;
+ const uriInfo = Services.uriFixup.getFixupURIInfo(url, flags);
+ url = uriInfo.fixedURI.spec;
+ } catch (ex) {
+ // The getFixupURIInfo service will throw an error if a malformed URI is
+ // produced from the input.
+ console.error(ex);
+ }
+
+ this.props.toolbox.target.navigateTo({ url });
+ }
+
+ shallRenderConnection() {
+ const { connectionType } = this.props.debugTargetData;
+ const renderableTypes = [CONNECTION_TYPES.USB, CONNECTION_TYPES.NETWORK];
+
+ return renderableTypes.includes(connectionType);
+ }
+
+ renderConnection() {
+ const { connectionType } = this.props.debugTargetData;
+ const { image, l10nId } = this.getAssetsForConnectionType();
+
+ return dom.span(
+ {
+ className: "iconized-label qa-connection-info",
+ },
+ dom.img({ src: image, alt: `${connectionType} icon` }),
+ this.props.L10N.getStr(l10nId)
+ );
+ }
+
+ renderRuntime() {
+ if (!this.props.debugTargetData.runtimeInfo) {
+ // Skip the runtime render if no runtimeInfo is available.
+ // Runtime info is retrieved from the remote-client-manager, which might not be
+ // setup if about:devtools-toolbox was not opened from about:debugging.
+ return null;
+ }
+
+ const { icon, deviceName } = this.props.debugTargetData.runtimeInfo;
+
+ return dom.span(
+ {
+ className: "iconized-label qa-runtime-info",
+ },
+ dom.img({ src: icon, className: "channel-icon qa-runtime-icon" }),
+ dom.b({ className: "devtools-ellipsis-text" }, this.getRuntimeText()),
+ dom.span({ className: "devtools-ellipsis-text" }, deviceName)
+ );
+ }
+
+ renderTargetTitle() {
+ const title = this.props.toolbox.target.name;
+
+ const { image, l10nId } = this.getAssetsForDebugTargetType();
+
+ return dom.span(
+ {
+ className: "iconized-label debug-target-title",
+ },
+ dom.img({ src: image, alt: this.props.L10N.getStr(l10nId) }),
+ title
+ ? dom.b({ className: "devtools-ellipsis-text qa-target-title" }, title)
+ : null
+ );
+ }
+
+ renderTargetURI() {
+ const url = this.props.toolbox.target.url;
+ const { targetType } = this.props.debugTargetData;
+ const isURLEditable = targetType === DEBUG_TARGET_TYPES.TAB;
+
+ return dom.span(
+ {
+ key: url,
+ className: "debug-target-url",
+ },
+ isURLEditable
+ ? this.renderTargetInput(url)
+ : dom.span(
+ { className: "debug-target-url-readonly devtools-ellipsis-text" },
+ url
+ )
+ );
+ }
+
+ renderTargetInput(url) {
+ return dom.form(
+ {
+ className: "debug-target-url-form",
+ onSubmit: this.onSubmit,
+ },
+ dom.input({
+ className: "devtools-textinput debug-target-url-input",
+ onChange: this.onChange,
+ onFocus: this.onFocus,
+ defaultValue: url,
+ })
+ );
+ }
+
+ renderNavigationButton(detail) {
+ const { L10N } = this.props;
+
+ return dom.button(
+ {
+ className: `iconized-label navigation-button ${detail.className}`,
+ onClick: detail.onClick,
+ title: L10N.getStr(detail.l10nId),
+ },
+ dom.img({
+ src: detail.icon,
+ alt: L10N.getStr(detail.l10nId),
+ })
+ );
+ }
+
+ renderNavigation() {
+ const { debugTargetData } = this.props;
+ const { targetType } = debugTargetData;
+
+ if (targetType !== DEBUG_TARGET_TYPES.TAB) {
+ return null;
+ }
+
+ const items = [];
+
+ if (this.props.toolbox.target.traits.navigation) {
+ items.push(
+ this.renderNavigationButton({
+ className: "qa-back-button",
+ icon: "chrome://browser/skin/back.svg",
+ l10nId: "toolbox.debugTargetInfo.back",
+ onClick: () => this.props.toolbox.target.goBack(),
+ }),
+ this.renderNavigationButton({
+ className: "qa-forward-button",
+ icon: "chrome://browser/skin/forward.svg",
+ l10nId: "toolbox.debugTargetInfo.forward",
+ onClick: () => this.props.toolbox.target.goForward(),
+ })
+ );
+ }
+
+ items.push(
+ this.renderNavigationButton({
+ className: "qa-reload-button",
+ icon: "chrome://browser/skin/reload.svg",
+ l10nId: "toolbox.debugTargetInfo.reload",
+ onClick: () => this.props.toolbox.target.reload(),
+ })
+ );
+
+ return dom.div(
+ {
+ className: "debug-target-navigation",
+ },
+ ...items
+ );
+ }
+
+ render() {
+ return dom.header(
+ {
+ className: "debug-target-info qa-debug-target-info",
+ },
+ this.shallRenderConnection() ? this.renderConnection() : null,
+ this.renderRuntime(),
+ this.renderTargetTitle(),
+ this.renderNavigation(),
+ this.renderTargetURI()
+ );
+ }
+}
+
+module.exports = DebugTargetInfo;
diff --git a/devtools/client/framework/components/MeatballMenu.js b/devtools/client/framework/components/MeatballMenu.js
new file mode 100644
index 0000000000..52043740d0
--- /dev/null
+++ b/devtools/client/framework/components/MeatballMenu.js
@@ -0,0 +1,241 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {
+ PureComponent,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { hr } = dom;
+
+loader.lazyGetter(this, "MenuItem", function() {
+ return createFactory(
+ require("devtools/client/shared/components/menu/MenuItem")
+ );
+});
+loader.lazyGetter(this, "MenuList", function() {
+ return createFactory(
+ require("devtools/client/shared/components/menu/MenuList")
+ );
+});
+
+loader.lazyRequireGetter(
+ this,
+ "openDocLink",
+ "devtools/client/shared/link",
+ true
+);
+loader.lazyRequireGetter(this, "assert", "devtools/shared/DevToolsUtils", true);
+
+const openDevToolsDocsLink = () => {
+ openDocLink(
+ "https://developer.mozilla.org/docs/Tools?utm_source=devtools&utm_medium=tabbar-menu"
+ );
+};
+
+const openCommunityLink = () => {
+ openDocLink(
+ "https://discourse.mozilla.org/c/devtools?utm_source=devtools&utm_medium=tabbar-menu"
+ );
+};
+
+class MeatballMenu extends PureComponent {
+ static get propTypes() {
+ return {
+ // The id of the currently selected tool, e.g. "inspector"
+ currentToolId: PropTypes.string,
+
+ // List of possible docking options.
+ hostTypes: PropTypes.arrayOf(
+ PropTypes.shape({
+ position: PropTypes.string.isRequired,
+ switchHost: PropTypes.func.isRequired,
+ })
+ ),
+
+ // Current docking type. Typically one of the position values in
+ // |hostTypes| but this is not always the case (e.g. for "browsertoolbox").
+ currentHostType: PropTypes.string,
+
+ // Is the split console currently visible?
+ isSplitConsoleActive: PropTypes.bool,
+
+ // Are we disabling the behavior where pop-ups are automatically closed
+ // when clicking outside them?
+ //
+ // This is a tri-state value that may be true/false or undefined where
+ // undefined means that the option is not relevant in this context
+ // (i.e. we're not in a browser toolbox).
+ disableAutohide: PropTypes.bool,
+
+ // Function to turn the options panel on / off.
+ toggleOptions: PropTypes.func.isRequired,
+
+ // Function to turn the split console on / off.
+ toggleSplitConsole: PropTypes.func,
+
+ // Function to turn the disable pop-up autohide behavior on / off.
+ toggleNoAutohide: PropTypes.func,
+
+ // Localization interface.
+ L10N: PropTypes.object.isRequired,
+
+ // Callback function that will be invoked any time the component contents
+ // update in such a way that its bounding box might change.
+ onResize: PropTypes.func,
+ };
+ }
+
+ componentDidUpdate(prevProps) {
+ if (!this.props.onResize) {
+ return;
+ }
+
+ // We are only expecting the following kinds of dynamic changes when a popup
+ // is showing:
+ //
+ // - The "Disable pop-up autohide" menu item being added after the Browser
+ // Toolbox is connected.
+ // - The split console label changing between "Show Split Console" and "Hide
+ // Split Console".
+ // - The "Show/Hide Split Console" entry being added removed or removed.
+ //
+ // The latter two cases are only likely to be noticed when "Disable pop-up
+ // autohide" is active, but for completeness we handle them here.
+ const didChange =
+ typeof this.props.disableAutohide !== typeof prevProps.disableAutohide ||
+ this.props.currentToolId !== prevProps.currentToolId ||
+ this.props.isSplitConsoleActive !== prevProps.isSplitConsoleActive;
+
+ if (didChange) {
+ this.props.onResize();
+ }
+ }
+
+ render() {
+ const items = [];
+
+ // Dock options
+ for (const hostType of this.props.hostTypes) {
+ // This is more verbose than it needs to be but lets us easily search for
+ // l10n entities.
+ let l10nkey;
+ switch (hostType.position) {
+ case "window":
+ l10nkey = "toolbox.meatballMenu.dock.separateWindow.label";
+ break;
+
+ case "bottom":
+ l10nkey = "toolbox.meatballMenu.dock.bottom.label";
+ break;
+
+ case "left":
+ l10nkey = "toolbox.meatballMenu.dock.left.label";
+ break;
+
+ case "right":
+ l10nkey = "toolbox.meatballMenu.dock.right.label";
+ break;
+
+ default:
+ assert(false, `Unexpected hostType.position: ${hostType.position}`);
+ break;
+ }
+
+ items.push(
+ MenuItem({
+ id: `toolbox-meatball-menu-dock-${hostType.position}`,
+ key: `dock-${hostType.position}`,
+ label: this.props.L10N.getStr(l10nkey),
+ onClick: hostType.switchHost,
+ checked: hostType.position === this.props.currentHostType,
+ className: "iconic",
+ })
+ );
+ }
+
+ if (items.length) {
+ items.push(hr({ key: "dock-separator" }));
+ }
+
+ // Split console
+ if (this.props.currentToolId !== "webconsole") {
+ const l10nkey = this.props.isSplitConsoleActive
+ ? "toolbox.meatballMenu.hideconsole.label"
+ : "toolbox.meatballMenu.splitconsole.label";
+ items.push(
+ MenuItem({
+ id: "toolbox-meatball-menu-splitconsole",
+ key: "splitconsole",
+ label: this.props.L10N.getStr(l10nkey),
+ accelerator: "Esc",
+ onClick: this.props.toggleSplitConsole,
+ className: "iconic",
+ })
+ );
+ }
+
+ // Disable pop-up autohide
+ //
+ // If |disableAutohide| is undefined, it means this feature is not available
+ // in this context.
+ if (typeof this.props.disableAutohide !== "undefined") {
+ items.push(
+ MenuItem({
+ id: "toolbox-meatball-menu-noautohide",
+ key: "noautohide",
+ label: this.props.L10N.getStr(
+ "toolbox.meatballMenu.noautohide.label"
+ ),
+ type: "checkbox",
+ checked: this.props.disableAutohide,
+ onClick: this.props.toggleNoAutohide,
+ className: "iconic",
+ })
+ );
+ }
+
+ // Settings
+ items.push(
+ MenuItem({
+ id: "toolbox-meatball-menu-settings",
+ key: "settings",
+ label: this.props.L10N.getStr("toolbox.meatballMenu.settings.label"),
+ accelerator: this.props.L10N.getStr("toolbox.help.key"),
+ onClick: this.props.toggleOptions,
+ className: "iconic",
+ })
+ );
+
+ items.push(hr({ key: "docs-separator" }));
+
+ // Getting started
+ items.push(
+ MenuItem({
+ id: "toolbox-meatball-menu-documentation",
+ key: "documentation",
+ label: this.props.L10N.getStr(
+ "toolbox.meatballMenu.documentation.label"
+ ),
+ onClick: openDevToolsDocsLink,
+ })
+ );
+
+ // Give feedback
+ items.push(
+ MenuItem({
+ id: "toolbox-meatball-menu-community",
+ key: "community",
+ label: this.props.L10N.getStr("toolbox.meatballMenu.community.label"),
+ onClick: openCommunityLink,
+ })
+ );
+
+ return MenuList({ id: "toolbox-meatball-menu" }, items);
+ }
+}
+
+module.exports = MeatballMenu;
diff --git a/devtools/client/framework/components/ToolboxController.js b/devtools/client/framework/components/ToolboxController.js
new file mode 100644
index 0000000000..de0aa58cb4
--- /dev/null
+++ b/devtools/client/framework/components/ToolboxController.js
@@ -0,0 +1,202 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const ToolboxToolbar = createFactory(
+ require("devtools/client/framework/components/ToolboxToolbar")
+);
+const ELEMENT_PICKER_ID = "command-button-pick";
+
+/**
+ * This component serves as a state controller for the toolbox React component. It's a
+ * thin layer for translating events and state of the outside world into the React update
+ * cycle. This solution was used to keep the amount of code changes to a minimimum while
+ * adapting the existing codebase to start using React.
+ */
+class ToolboxController extends Component {
+ constructor(props, context) {
+ super(props, context);
+
+ // See the ToolboxToolbar propTypes for documentation on each of these items in
+ // state, and for the definitions of the props that are expected to be passed in.
+ this.state = {
+ focusedButton: ELEMENT_PICKER_ID,
+ toolboxButtons: [],
+ visibleToolboxButtonCount: 0,
+ currentToolId: null,
+ highlightedTools: new Set(),
+ panelDefinitions: [],
+ hostTypes: [],
+ currentHostType: undefined,
+ areDockOptionsEnabled: true,
+ canCloseToolbox: true,
+ isSplitConsoleActive: false,
+ disableAutohide: undefined,
+ canRender: false,
+ buttonIds: [],
+ checkedButtonsUpdated: () => {
+ this.forceUpdate();
+ },
+ };
+
+ this.setFocusedButton = this.setFocusedButton.bind(this);
+ this.setToolboxButtons = this.setToolboxButtons.bind(this);
+ this.setCurrentToolId = this.setCurrentToolId.bind(this);
+ this.highlightTool = this.highlightTool.bind(this);
+ this.unhighlightTool = this.unhighlightTool.bind(this);
+ this.setHostTypes = this.setHostTypes.bind(this);
+ this.setCurrentHostType = this.setCurrentHostType.bind(this);
+ this.setDockOptionsEnabled = this.setDockOptionsEnabled.bind(this);
+ this.setCanCloseToolbox = this.setCanCloseToolbox.bind(this);
+ this.setIsSplitConsoleActive = this.setIsSplitConsoleActive.bind(this);
+ this.setDisableAutohide = this.setDisableAutohide.bind(this);
+ this.setCanRender = this.setCanRender.bind(this);
+ this.setPanelDefinitions = this.setPanelDefinitions.bind(this);
+ this.updateButtonIds = this.updateButtonIds.bind(this);
+ this.updateFocusedButton = this.updateFocusedButton.bind(this);
+ this.setDebugTargetData = this.setDebugTargetData.bind(this);
+ }
+
+ shouldComponentUpdate() {
+ return this.state.canRender;
+ }
+
+ componentWillUnmount() {
+ this.state.toolboxButtons.forEach(button => {
+ button.off("updatechecked", this.state.checkedButtonsUpdated);
+ });
+ }
+
+ /**
+ * The button and tab ids must be known in order to be able to focus left and right
+ * using the arrow keys.
+ */
+ updateButtonIds() {
+ const { toolboxButtons, panelDefinitions, canCloseToolbox } = this.state;
+
+ // This is a little gnarly, but go through all of the state and extract the IDs.
+ this.setState({
+ buttonIds: [
+ ...toolboxButtons
+ .filter(btn => btn.isInStartContainer)
+ .map(({ id }) => id),
+ ...panelDefinitions.map(({ id }) => id),
+ ...toolboxButtons
+ .filter(btn => !btn.isInStartContainer)
+ .map(({ id }) => id),
+ canCloseToolbox ? "toolbox-close" : null,
+ ].filter(id => id),
+ });
+
+ this.updateFocusedButton();
+ }
+
+ updateFocusedButton() {
+ this.setFocusedButton(this.state.focusedButton);
+ }
+
+ setFocusedButton(focusedButton) {
+ const { buttonIds } = this.state;
+
+ focusedButton =
+ focusedButton && buttonIds.includes(focusedButton)
+ ? focusedButton
+ : buttonIds[0];
+ if (this.state.focusedButton !== focusedButton) {
+ this.setState({
+ focusedButton,
+ });
+ }
+ }
+
+ setCurrentToolId(currentToolId) {
+ this.setState({ currentToolId }, () => {
+ // Also set the currently focused button to this tool.
+ this.setFocusedButton(currentToolId);
+ });
+ }
+
+ setCanRender() {
+ this.setState({ canRender: true }, this.updateButtonIds);
+ }
+
+ highlightTool(highlightedTool) {
+ const { highlightedTools } = this.state;
+ highlightedTools.add(highlightedTool);
+ this.setState({ highlightedTools });
+ }
+
+ unhighlightTool(id) {
+ const { highlightedTools } = this.state;
+ if (highlightedTools.has(id)) {
+ highlightedTools.delete(id);
+ this.setState({ highlightedTools });
+ }
+ }
+
+ setDockOptionsEnabled(areDockOptionsEnabled) {
+ this.setState({ areDockOptionsEnabled });
+ }
+
+ setHostTypes(hostTypes) {
+ this.setState({ hostTypes });
+ }
+
+ setCurrentHostType(currentHostType) {
+ this.setState({ currentHostType });
+ }
+
+ setCanCloseToolbox(canCloseToolbox) {
+ this.setState({ canCloseToolbox }, this.updateButtonIds);
+ }
+
+ setIsSplitConsoleActive(isSplitConsoleActive) {
+ this.setState({ isSplitConsoleActive });
+ }
+
+ setDisableAutohide(disableAutohide) {
+ this.setState({ disableAutohide });
+ }
+
+ setPanelDefinitions(panelDefinitions) {
+ this.setState({ panelDefinitions }, this.updateButtonIds);
+ }
+
+ get panelDefinitions() {
+ return this.state.panelDefinitions;
+ }
+
+ setToolboxButtons(toolboxButtons) {
+ // Listen for updates of the checked attribute.
+ this.state.toolboxButtons.forEach(button => {
+ button.off("updatechecked", this.state.checkedButtonsUpdated);
+ });
+ toolboxButtons.forEach(button => {
+ button.on("updatechecked", this.state.checkedButtonsUpdated);
+ });
+
+ const visibleToolboxButtonCount = toolboxButtons.filter(
+ button => button.isVisible
+ ).length;
+
+ this.setState(
+ { toolboxButtons, visibleToolboxButtonCount },
+ this.updateButtonIds
+ );
+ }
+
+ setDebugTargetData(data) {
+ this.setState({ debugTargetData: data });
+ }
+
+ render() {
+ return ToolboxToolbar(Object.assign({}, this.props, this.state));
+ }
+}
+
+module.exports = ToolboxController;
diff --git a/devtools/client/framework/components/ToolboxTab.js b/devtools/client/framework/components/ToolboxTab.js
new file mode 100644
index 0000000000..60f36e3107
--- /dev/null
+++ b/devtools/client/framework/components/ToolboxTab.js
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Component } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { img, button, span } = dom;
+
+class ToolboxTab extends Component {
+ // See toolbox-toolbar propTypes for details on the props used here.
+ static get propTypes() {
+ return {
+ currentToolId: PropTypes.string,
+ focusButton: PropTypes.func,
+ focusedButton: PropTypes.string,
+ highlightedTools: PropTypes.object.isRequired,
+ panelDefinition: PropTypes.object,
+ selectTool: PropTypes.func,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.renderIcon = this.renderIcon.bind(this);
+ }
+
+ renderIcon(definition) {
+ const { icon } = definition;
+ if (!icon) {
+ return [];
+ }
+ return [
+ img({
+ alt: "",
+ src: icon,
+ }),
+ ];
+ }
+
+ render() {
+ const {
+ panelDefinition,
+ currentToolId,
+ highlightedTools,
+ selectTool,
+ focusedButton,
+ focusButton,
+ } = this.props;
+ const {
+ id,
+ extensionId,
+ tooltip,
+ label,
+ iconOnly,
+ badge,
+ } = panelDefinition;
+ const isHighlighted = id === currentToolId;
+
+ const className = [
+ "devtools-tab",
+ currentToolId === id ? "selected" : "",
+ highlightedTools.has(id) ? "highlighted" : "",
+ iconOnly ? "devtools-tab-icon-only" : "",
+ ].join(" ");
+
+ return button(
+ {
+ className,
+ id: `toolbox-tab-${id}`,
+ "data-id": id,
+ "data-extension-id": extensionId,
+ title: tooltip,
+ type: "button",
+ "aria-pressed": currentToolId === id ? "true" : "false",
+ tabIndex: focusedButton === id ? "0" : "-1",
+ onFocus: () => focusButton(id),
+ onMouseDown: () => selectTool(id, "tab_switch"),
+ onKeyDown: evt => {
+ if (evt.key === "Enter" || evt.key === " ") {
+ selectTool(id, "tab_switch");
+ }
+ },
+ },
+ span({
+ className: "devtools-tab-line",
+ }),
+ ...this.renderIcon(panelDefinition),
+ iconOnly
+ ? null
+ : span(
+ {
+ className: "devtools-tab-label",
+ },
+ label,
+ badge && !isHighlighted
+ ? span(
+ {
+ className: "devtools-tab-badge",
+ },
+ badge
+ )
+ : null
+ )
+ );
+ }
+}
+
+module.exports = ToolboxTab;
diff --git a/devtools/client/framework/components/ToolboxTabs.js b/devtools/client/framework/components/ToolboxTabs.js
new file mode 100644
index 0000000000..435a7f0368
--- /dev/null
+++ b/devtools/client/framework/components/ToolboxTabs.js
@@ -0,0 +1,331 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {
+ Component,
+ createFactory,
+ createRef,
+} = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const {
+ ToolboxTabsOrderManager,
+} = require("devtools/client/framework/toolbox-tabs-order-manager");
+
+const { div } = dom;
+
+const ToolboxTab = createFactory(
+ require("devtools/client/framework/components/ToolboxTab")
+);
+
+loader.lazyGetter(this, "MenuButton", function() {
+ return createFactory(
+ require("devtools/client/shared/components/menu/MenuButton")
+ );
+});
+loader.lazyGetter(this, "MenuItem", function() {
+ return createFactory(
+ require("devtools/client/shared/components/menu/MenuItem")
+ );
+});
+loader.lazyGetter(this, "MenuList", function() {
+ return createFactory(
+ require("devtools/client/shared/components/menu/MenuList")
+ );
+});
+
+// 26px is chevron devtools button width.(i.e. tools-chevronmenu)
+const CHEVRON_BUTTON_WIDTH = 26;
+
+class ToolboxTabs extends Component {
+ // See toolbox-toolbar propTypes for details on the props used here.
+ static get propTypes() {
+ return {
+ currentToolId: PropTypes.string,
+ focusButton: PropTypes.func,
+ focusedButton: PropTypes.string,
+ highlightedTools: PropTypes.object,
+ panelDefinitions: PropTypes.array,
+ selectTool: PropTypes.func,
+ toolbox: PropTypes.object,
+ visibleToolboxButtonCount: PropTypes.number.isRequired,
+ L10N: PropTypes.object,
+ onTabsOrderUpdated: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ // Array of overflowed tool id.
+ overflowedTabIds: [],
+ };
+
+ this.wrapperEl = createRef();
+
+ // Map with tool Id and its width size. This lifecycle is out of React's
+ // lifecycle. If a tool is registered, ToolboxTabs will add target tool id
+ // to this map. ToolboxTabs will never remove tool id from this cache.
+ this._cachedToolTabsWidthMap = new Map();
+
+ this._resizeTimerId = null;
+ this.resizeHandler = this.resizeHandler.bind(this);
+
+ const { toolbox, onTabsOrderUpdated, panelDefinitions } = props;
+ this._tabsOrderManager = new ToolboxTabsOrderManager(
+ toolbox,
+ onTabsOrderUpdated,
+ panelDefinitions
+ );
+ }
+
+ componentDidMount() {
+ window.addEventListener("resize", this.resizeHandler);
+ this.updateCachedToolTabsWidthMap();
+ this.updateOverflowedTabs();
+ }
+
+ componentWillUpdate(nextProps, nextState) {
+ if (this.shouldUpdateToolboxTabs(this.props, nextProps)) {
+ // Force recalculate and render in this cycle if panel definition has
+ // changed or selected tool has changed.
+ nextState.overflowedTabIds = [];
+ }
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (this.shouldUpdateToolboxTabs(prevProps, this.props)) {
+ this.updateCachedToolTabsWidthMap();
+ this.updateOverflowedTabs();
+ this._tabsOrderManager.setCurrentPanelDefinitions(
+ this.props.panelDefinitions
+ );
+ }
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener("resize", this.resizeHandler);
+ window.cancelIdleCallback(this._resizeTimerId);
+ this._tabsOrderManager.destroy();
+ }
+
+ /**
+ * Check if two array of ids are the same or not.
+ */
+ equalToolIdArray(prevPanels, nextPanels) {
+ if (prevPanels.length !== nextPanels.length) {
+ return false;
+ }
+
+ // Check panel definitions even if both of array size is same.
+ // For example, the case of changing the tab's order.
+ return prevPanels.join("-") === nextPanels.join("-");
+ }
+
+ /**
+ * Return true if we should update the overflowed tabs.
+ */
+ shouldUpdateToolboxTabs(prevProps, nextProps) {
+ if (
+ prevProps.currentToolId !== nextProps.currentToolId ||
+ prevProps.visibleToolboxButtonCount !==
+ nextProps.visibleToolboxButtonCount
+ ) {
+ return true;
+ }
+
+ const prevPanels = prevProps.panelDefinitions.map(def => def.id);
+ const nextPanels = nextProps.panelDefinitions.map(def => def.id);
+ return !this.equalToolIdArray(prevPanels, nextPanels);
+ }
+
+ /**
+ * Update the Map of tool id and tool tab width.
+ */
+ updateCachedToolTabsWidthMap() {
+ const utils = window.windowUtils;
+ // Force a reflow before calling getBoundingWithoutFlushing on each tab.
+ this.wrapperEl.current.clientWidth;
+
+ for (const tab of this.wrapperEl.current.querySelectorAll(
+ ".devtools-tab"
+ )) {
+ const tabId = tab.id.replace("toolbox-tab-", "");
+ if (!this._cachedToolTabsWidthMap.has(tabId)) {
+ const rect = utils.getBoundsWithoutFlushing(tab);
+ this._cachedToolTabsWidthMap.set(tabId, rect.width);
+ }
+ }
+ }
+
+ /**
+ * Update the overflowed tab array from currently displayed tool tab.
+ * If calculated result is the same as the current overflowed tab array, this
+ * function will not update state.
+ */
+ updateOverflowedTabs() {
+ const toolboxWidth = parseInt(
+ getComputedStyle(this.wrapperEl.current).width,
+ 10
+ );
+ const { currentToolId } = this.props;
+ const enabledTabs = this.props.panelDefinitions.map(def => def.id);
+ let sumWidth = 0;
+ const visibleTabs = [];
+
+ for (const id of enabledTabs) {
+ const width = this._cachedToolTabsWidthMap.get(id);
+ sumWidth += width;
+ if (sumWidth <= toolboxWidth) {
+ visibleTabs.push(id);
+ } else {
+ sumWidth = sumWidth - width + CHEVRON_BUTTON_WIDTH;
+
+ // If toolbox can't display the Chevron, remove the last tool tab.
+ if (sumWidth > toolboxWidth) {
+ const removeTabId = visibleTabs.pop();
+ sumWidth -= this._cachedToolTabsWidthMap.get(removeTabId);
+ }
+ break;
+ }
+ }
+
+ // If the selected tab is in overflowed tabs, insert it into a visible
+ // toolbox.
+ if (
+ !visibleTabs.includes(currentToolId) &&
+ enabledTabs.includes(currentToolId)
+ ) {
+ const selectedToolWidth = this._cachedToolTabsWidthMap.get(currentToolId);
+ while (
+ sumWidth + selectedToolWidth > toolboxWidth &&
+ visibleTabs.length > 0
+ ) {
+ const removingToolId = visibleTabs.pop();
+ const removingToolWidth = this._cachedToolTabsWidthMap.get(
+ removingToolId
+ );
+ sumWidth -= removingToolWidth;
+ }
+
+ // If toolbox width is narrow, toolbox display only chevron menu.
+ // i.e. All tool tabs will overflow.
+ if (sumWidth + selectedToolWidth <= toolboxWidth) {
+ visibleTabs.push(currentToolId);
+ }
+ }
+
+ const willOverflowTabs = enabledTabs.filter(
+ id => !visibleTabs.includes(id)
+ );
+ if (!this.equalToolIdArray(this.state.overflowedTabIds, willOverflowTabs)) {
+ this.setState({ overflowedTabIds: willOverflowTabs });
+ }
+ }
+
+ resizeHandler(evt) {
+ window.cancelIdleCallback(this._resizeTimerId);
+ this._resizeTimerId = window.requestIdleCallback(
+ () => {
+ this.updateOverflowedTabs();
+ },
+ { timeout: 100 }
+ );
+ }
+
+ renderToolsChevronMenuList() {
+ const { panelDefinitions, selectTool } = this.props;
+
+ const items = [];
+ for (const { id, label, icon } of panelDefinitions) {
+ if (this.state.overflowedTabIds.includes(id)) {
+ items.push(
+ MenuItem({
+ key: id,
+ id: "tools-chevron-menupopup-" + id,
+ label,
+ type: "checkbox",
+ onClick: () => {
+ selectTool(id, "tab_switch");
+ },
+ icon,
+ })
+ );
+ }
+ }
+
+ return MenuList({ id: "tools-chevron-menupopup" }, items);
+ }
+
+ /**
+ * Render a button to access overflowed tools, displayed only when the toolbar
+ * presents an overflow.
+ */
+ renderToolsChevronButton() {
+ const { toolbox } = this.props;
+
+ return MenuButton(
+ {
+ id: "tools-chevron-menu-button",
+ menuId: "tools-chevron-menu-button-panel",
+ className: "devtools-tabbar-button tools-chevron-menu",
+ toolboxDoc: toolbox.doc,
+ },
+ this.renderToolsChevronMenuList()
+ );
+ }
+
+ /**
+ * Render all of the tabs, based on the panel definitions and builds out
+ * a toolbox tab for each of them. Will render the chevron button if the
+ * container has an overflow.
+ */
+ render() {
+ const {
+ currentToolId,
+ focusButton,
+ focusedButton,
+ highlightedTools,
+ panelDefinitions,
+ selectTool,
+ } = this.props;
+
+ const tabs = panelDefinitions.map(panelDefinition => {
+ // Don't display overflowed tab.
+ if (!this.state.overflowedTabIds.includes(panelDefinition.id)) {
+ return ToolboxTab({
+ key: panelDefinition.id,
+ currentToolId,
+ focusButton,
+ focusedButton,
+ highlightedTools,
+ panelDefinition,
+ selectTool,
+ });
+ }
+ return null;
+ });
+
+ return div(
+ {
+ className: "toolbox-tabs-wrapper",
+ ref: this.wrapperEl,
+ },
+ div(
+ {
+ className: "toolbox-tabs",
+ onMouseDown: e => this._tabsOrderManager.onMouseDown(e),
+ },
+ tabs,
+ this.state.overflowedTabIds.length > 0
+ ? this.renderToolsChevronButton()
+ : null
+ )
+ );
+ }
+}
+
+module.exports = ToolboxTabs;
diff --git a/devtools/client/framework/components/ToolboxToolbar.js b/devtools/client/framework/components/ToolboxToolbar.js
new file mode 100644
index 0000000000..20eb5f5832
--- /dev/null
+++ b/devtools/client/framework/components/ToolboxToolbar.js
@@ -0,0 +1,501 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { div, button } = dom;
+
+const DebugTargetInfo = createFactory(
+ require("devtools/client/framework/components/DebugTargetInfo")
+);
+const MenuButton = createFactory(
+ require("devtools/client/shared/components/menu/MenuButton")
+);
+const ToolboxTabs = createFactory(
+ require("devtools/client/framework/components/ToolboxTabs")
+);
+
+loader.lazyGetter(this, "MeatballMenu", function() {
+ return createFactory(
+ require("devtools/client/framework/components/MeatballMenu")
+ );
+});
+loader.lazyGetter(this, "MenuItem", function() {
+ return createFactory(
+ require("devtools/client/shared/components/menu/MenuItem")
+ );
+});
+loader.lazyGetter(this, "MenuList", function() {
+ return createFactory(
+ require("devtools/client/shared/components/menu/MenuList")
+ );
+});
+
+loader.lazyRequireGetter(
+ this,
+ "getUnicodeUrl",
+ "devtools/client/shared/unicode-url",
+ true
+);
+
+/**
+ * This is the overall component for the toolbox toolbar. It is designed to not know how
+ * the state is being managed, and attempts to be as pure as possible. The
+ * ToolboxController component controls the changing state, and passes in everything as
+ * props.
+ */
+class ToolboxToolbar extends Component {
+ static get propTypes() {
+ return {
+ // The currently focused item (for arrow keyboard navigation)
+ // This ID determines the tabindex being 0 or -1.
+ focusedButton: PropTypes.string,
+ // List of command button definitions.
+ toolboxButtons: PropTypes.array,
+ // The id of the currently selected tool, e.g. "inspector"
+ currentToolId: PropTypes.string,
+ // An optionally highlighted tools, e.g. "inspector" (used by ToolboxTabs
+ // component).
+ highlightedTools: PropTypes.instanceOf(Set),
+ // List of tool panel definitions (used by ToolboxTabs component).
+ panelDefinitions: PropTypes.array,
+ // List of possible docking options.
+ hostTypes: PropTypes.arrayOf(
+ PropTypes.shape({
+ position: PropTypes.string.isRequired,
+ switchHost: PropTypes.func.isRequired,
+ })
+ ),
+ // Current docking type. Typically one of the position values in
+ // |hostTypes| but this is not always the case (e.g. for "browsertoolbox").
+ currentHostType: PropTypes.string,
+ // Are docking options enabled? They are not enabled in certain situations
+ // like when the toolbox is opened in a tab.
+ areDockOptionsEnabled: PropTypes.bool,
+ // Do we need to add UI for closing the toolbox? We don't when the
+ // toolbox is undocked, for example.
+ canCloseToolbox: PropTypes.bool,
+ // Is the split console currently visible?
+ isSplitConsoleActive: PropTypes.bool,
+ // Are we disabling the behavior where pop-ups are automatically closed
+ // when clicking outside them?
+ //
+ // This is a tri-state value that may be true/false or undefined where
+ // undefined means that the option is not relevant in this context
+ // (i.e. we're not in a browser toolbox).
+ disableAutohide: PropTypes.bool,
+ // Function to turn the options panel on / off.
+ toggleOptions: PropTypes.func.isRequired,
+ // Function to turn the split console on / off.
+ toggleSplitConsole: PropTypes.func,
+ // Function to turn the disable pop-up autohide behavior on / off.
+ toggleNoAutohide: PropTypes.func,
+ // Function to completely close the toolbox.
+ closeToolbox: PropTypes.func,
+ // Keep a record of what button is focused.
+ focusButton: PropTypes.func,
+ // Hold off displaying the toolbar until enough information is ready for
+ // it to render nicely.
+ canRender: PropTypes.bool,
+ // Localization interface.
+ L10N: PropTypes.object.isRequired,
+ // The devtools toolbox
+ toolbox: PropTypes.object,
+ // Call back function to detect tabs order updated.
+ onTabsOrderUpdated: PropTypes.func.isRequired,
+ // Count of visible toolbox buttons which is used by ToolboxTabs component
+ // to recognize that the visibility of toolbox buttons were changed.
+ // Because in the component we cannot compare the visibility since the
+ // button definition instance in toolboxButtons will be unchanged.
+ visibleToolboxButtonCount: PropTypes.number,
+ // Data to show debug target info, if needed
+ debugTargetData: PropTypes.shape({
+ runtimeInfo: PropTypes.object.isRequired,
+ targetType: PropTypes.string.isRequired,
+ }),
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.hideMenu = this.hideMenu.bind(this);
+ this.createFrameList = this.createFrameList.bind(this);
+ this.highlightFrame = this.highlightFrame.bind(this);
+ this.clickFrameButton = this.clickFrameButton.bind(this);
+ }
+
+ componentDidMount() {
+ this.props.toolbox.on("panel-changed", this.hideMenu);
+ }
+
+ componentWillUnmount() {
+ this.props.toolbox.off("panel-changed", this.hideMenu);
+ }
+
+ hideMenu() {
+ if (this.refs.meatballMenuButton) {
+ this.refs.meatballMenuButton.hideMenu();
+ }
+
+ if (this.refs.frameMenuButton) {
+ this.refs.frameMenuButton.hideMenu();
+ }
+ }
+
+ /**
+ * A little helper function to call renderToolboxButtons for buttons at the start
+ * of the toolbox.
+ */
+ renderToolboxButtonsStart() {
+ return this.renderToolboxButtons(true);
+ }
+
+ /**
+ * A little helper function to call renderToolboxButtons for buttons at the end
+ * of the toolbox.
+ */
+ renderToolboxButtonsEnd() {
+ return this.renderToolboxButtons(false);
+ }
+
+ /**
+ * Render all of the tabs, this takes in a list of toolbox button states. These are plain
+ * objects that have all of the relevant information needed to render the button.
+ * See Toolbox.prototype._createButtonState in devtools/client/framework/toolbox.js for
+ * documentation on this object.
+ *
+ * @param {String} focusedButton - The id of the focused button.
+ * @param {Array} toolboxButtons - Array of objects that define the command buttons.
+ * @param {Function} focusButton - Keep a record of the currently focused button.
+ * @param {boolean} isStart - Render either the starting buttons, or ending buttons.
+ */
+ renderToolboxButtons(isStart) {
+ const { focusedButton, toolboxButtons, focusButton } = this.props;
+ const visibleButtons = toolboxButtons.filter(command => {
+ const { isVisible, isInStartContainer } = command;
+ return isVisible && (isStart ? isInStartContainer : !isInStartContainer);
+ });
+
+ if (visibleButtons.length === 0) {
+ return null;
+ }
+
+ // The RDM button, if present, should always go last
+ const rdmIndex = visibleButtons.findIndex(
+ button => button.id === "command-button-responsive"
+ );
+ if (rdmIndex !== -1 && rdmIndex !== visibleButtons.length - 1) {
+ const rdm = visibleButtons.splice(rdmIndex, 1)[0];
+ visibleButtons.push(rdm);
+ }
+
+ const renderedButtons = visibleButtons.map(command => {
+ const {
+ id,
+ description,
+ disabled,
+ onClick,
+ isChecked,
+ className: buttonClass,
+ onKeyDown,
+ } = command;
+
+ // If button is frame button, create menu button in order to
+ // use the doorhanger menu.
+ if (id === "command-button-frames") {
+ return this.renderFrameButton(command);
+ }
+
+ if (id === "command-button-errorcount") {
+ return this.renderErrorIcon(command);
+ }
+
+ return button({
+ id,
+ title: description,
+ disabled,
+ className: `devtools-tabbar-button command-button ${buttonClass ||
+ ""} ${isChecked ? "checked" : ""}`,
+ onClick: event => {
+ onClick(event);
+ focusButton(id);
+ },
+ onFocus: () => focusButton(id),
+ tabIndex: id === focusedButton ? "0" : "-1",
+ onKeyDown: event => {
+ onKeyDown(event);
+ },
+ });
+ });
+
+ // Add the appropriate separator, if needed.
+ const children = renderedButtons;
+ if (renderedButtons.length) {
+ if (isStart) {
+ children.push(this.renderSeparator());
+ // For the end group we add a separator *before* the RDM button if it
+ // exists, but only if it is not the only button.
+ } else if (rdmIndex !== -1 && renderedButtons.length > 1) {
+ children.splice(children.length - 1, 0, this.renderSeparator());
+ }
+ }
+
+ return div(
+ { id: `toolbox-buttons-${isStart ? "start" : "end"}` },
+ ...children
+ );
+ }
+
+ renderFrameButton(command) {
+ const { id, isChecked, disabled, description } = command;
+
+ const { toolbox } = this.props;
+
+ return MenuButton(
+ {
+ id,
+ disabled,
+ menuId: id + "-panel",
+ toolboxDoc: toolbox.doc,
+ className: `devtools-tabbar-button command-button ${
+ isChecked ? "checked" : ""
+ }`,
+ ref: "frameMenuButton",
+ title: description,
+ onCloseButton: async () => {
+ // Only try to unhighlight if the inspectorFront has been created already
+ const inspectorFront = toolbox.target.getCachedFront("inspector");
+ if (inspectorFront) {
+ const highlighter = toolbox.getHighlighter();
+ await highlighter.unhighlight();
+ }
+ },
+ },
+ this.createFrameList
+ );
+ }
+
+ renderErrorIcon(command) {
+ let { errorCount, id } = command;
+
+ if (!errorCount) {
+ return null;
+ }
+
+ if (errorCount > 99) {
+ errorCount = "99+";
+ }
+
+ return button(
+ {
+ id,
+ className: "devtools-tabbar-button command-button toolbox-error",
+ onClick: () => {
+ if (this.props.currentToolId !== "webconsole") {
+ this.props.toolbox.openSplitConsole();
+ }
+ },
+ title:
+ this.props.currentToolId !== "webconsole"
+ ? this.props.L10N.getStr("toolbox.errorCountButton.tooltip")
+ : null,
+ },
+ errorCount
+ );
+ }
+
+ clickFrameButton(event) {
+ const { toolbox } = this.props;
+ toolbox.onSelectFrame(event.target.id);
+ }
+
+ highlightFrame(id) {
+ if (!id) {
+ return;
+ }
+
+ const { toolbox } = this.props;
+ toolbox.onHighlightFrame(id);
+ }
+
+ createFrameList() {
+ const { toolbox } = this.props;
+ if (toolbox.frameMap.size < 1) {
+ return null;
+ }
+
+ const items = [];
+ toolbox.frameMap.forEach((frame, index) => {
+ const label = toolbox.target.isWebExtension
+ ? toolbox.target.getExtensionPathName(frame.url)
+ : getUnicodeUrl(frame.url);
+ items.push(
+ MenuItem({
+ id: frame.id.toString(),
+ key: "toolbox-frame-key-" + frame.id,
+ label,
+ checked: frame.id === toolbox.selectedFrameId,
+ onClick: this.clickFrameButton,
+ })
+ );
+ });
+
+ return MenuList(
+ {
+ id: "toolbox-frame-menu",
+ onHighlightedChildChange: this.highlightFrame,
+ },
+ items
+ );
+ }
+
+ /**
+ * Render a separator.
+ */
+ renderSeparator() {
+ return div({ className: "devtools-separator" });
+ }
+
+ /**
+ * Render the toolbox control buttons. The following props are expected:
+ *
+ * @param {string} props.focusedButton
+ * The id of the focused button.
+ * @param {string} props.currentToolId
+ * The id of the currently selected tool, e.g. "inspector".
+ * @param {Object[]} props.hostTypes
+ * Array of host type objects.
+ * @param {string} props.hostTypes[].position
+ * Position name.
+ * @param {Function} props.hostTypes[].switchHost
+ * Function to switch the host.
+ * @param {string} props.currentHostType
+ * The current docking configuration.
+ * @param {boolean} props.areDockOptionsEnabled
+ * They are not enabled in certain situations like when the toolbox is
+ * in a tab.
+ * @param {boolean} props.canCloseToolbox
+ * Do we need to add UI for closing the toolbox? We don't when the
+ * toolbox is undocked, for example.
+ * @param {boolean} props.isSplitConsoleActive
+ * Is the split console currently visible?
+ * toolbox is undocked, for example.
+ * @param {boolean|undefined} props.disableAutohide
+ * Are we disabling the behavior where pop-ups are automatically
+ * closed when clicking outside them?
+ * (Only defined for the browser toolbox.)
+ * @param {Function} props.selectTool
+ * Function to select a tool based on its id.
+ * @param {Function} props.toggleOptions
+ * Function to turn the options panel on / off.
+ * @param {Function} props.toggleSplitConsole
+ * Function to turn the split console on / off.
+ * @param {Function} props.toggleNoAutohide
+ * Function to turn the disable pop-up autohide behavior on / off.
+ * @param {Function} props.closeToolbox
+ * Completely close the toolbox.
+ * @param {Function} props.focusButton
+ * Keep a record of the currently focused button.
+ * @param {Object} props.L10N
+ * Localization interface.
+ * @param {Object} props.toolbox
+ * The devtools toolbox. Used by the MenuButton component to display
+ * the menu popup.
+ * @param {Object} refs
+ * The components refs object. Used to keep a reference to the MenuButton
+ * for the meatball menu so that we can tell it to resize its contents
+ * when they change.
+ */
+ renderToolboxControls() {
+ const {
+ focusedButton,
+ canCloseToolbox,
+ closeToolbox,
+ focusButton,
+ L10N,
+ toolbox,
+ } = this.props;
+
+ const meatballMenuButtonId = "toolbox-meatball-menu-button";
+
+ const meatballMenuButton = MenuButton(
+ {
+ id: meatballMenuButtonId,
+ menuId: meatballMenuButtonId + "-panel",
+ toolboxDoc: toolbox.doc,
+ onFocus: () => focusButton(meatballMenuButtonId),
+ className: "devtools-tabbar-button",
+ title: L10N.getStr("toolbox.meatballMenu.button.tooltip"),
+ tabIndex: focusedButton === meatballMenuButtonId ? "0" : "-1",
+ ref: "meatballMenuButton",
+ },
+ MeatballMenu({
+ ...this.props,
+ hostTypes: this.props.areDockOptionsEnabled ? this.props.hostTypes : [],
+ onResize: () => {
+ this.refs.meatballMenuButton.resizeContent();
+ },
+ })
+ );
+
+ const closeButtonId = "toolbox-close";
+
+ const closeButton = canCloseToolbox
+ ? button({
+ id: closeButtonId,
+ onFocus: () => focusButton(closeButtonId),
+ className: "devtools-tabbar-button",
+ title: L10N.getStr("toolbox.closebutton.tooltip"),
+ onClick: () => closeToolbox(),
+ tabIndex: focusedButton === "toolbox-close" ? "0" : "-1",
+ })
+ : null;
+
+ return div({ id: "toolbox-controls" }, meatballMenuButton, closeButton);
+ }
+
+ /**
+ * The render function is kept fairly short for maintainability. See the individual
+ * render functions for how each of the sections is rendered.
+ */
+ render() {
+ const { L10N, debugTargetData, toolbox } = this.props;
+ const classnames = ["devtools-tabbar"];
+ const startButtons = this.renderToolboxButtonsStart();
+ const endButtons = this.renderToolboxButtonsEnd();
+
+ if (!startButtons) {
+ classnames.push("devtools-tabbar-has-start");
+ }
+ if (!endButtons) {
+ classnames.push("devtools-tabbar-has-end");
+ }
+
+ const toolbar = this.props.canRender
+ ? div(
+ {
+ className: classnames.join(" "),
+ },
+ startButtons,
+ ToolboxTabs(this.props),
+ endButtons,
+ this.renderToolboxControls()
+ )
+ : div({ className: classnames.join(" ") });
+
+ const debugTargetInfo = debugTargetData
+ ? DebugTargetInfo({ debugTargetData, L10N, toolbox })
+ : null;
+
+ return div({}, debugTargetInfo, toolbar);
+ }
+}
+
+module.exports = ToolboxToolbar;
diff --git a/devtools/client/framework/components/moz.build b/devtools/client/framework/components/moz.build
new file mode 100644
index 0000000000..9499fa6294
--- /dev/null
+++ b/devtools/client/framework/components/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/.
+
+
+DevToolsModules(
+ "DebugTargetErrorPage.js",
+ "DebugTargetInfo.js",
+ "MeatballMenu.js",
+ "ToolboxController.js",
+ "ToolboxTab.js",
+ "ToolboxTabs.js",
+ "ToolboxToolbar.js",
+)
diff --git a/devtools/client/framework/devtools-browser.js b/devtools/client/framework/devtools-browser.js
new file mode 100644
index 0000000000..b461a1b234
--- /dev/null
+++ b/devtools/client/framework/devtools-browser.js
@@ -0,0 +1,822 @@
+/* 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";
+
+/**
+ * This is the main module loaded in Firefox desktop that handles browser
+ * windows and coordinates devtools around each window.
+ *
+ * This module is loaded lazily by devtools-clhandler.js, once the first
+ * browser window is ready (i.e. fired browser-delayed-startup-finished event)
+ **/
+
+const { Cc, Ci } = require("chrome");
+const Services = require("Services");
+const { gDevTools } = require("devtools/client/framework/devtools");
+
+// Load target and toolbox lazily as they need gDevTools to be fully initialized
+loader.lazyRequireGetter(
+ this,
+ "TargetFactory",
+ "devtools/client/framework/target",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "Toolbox",
+ "devtools/client/framework/toolbox",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "DevToolsServer",
+ "devtools/server/devtools-server",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "DevToolsClient",
+ "devtools/client/devtools-client",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "BrowserMenus",
+ "devtools/client/framework/browser-menus"
+);
+loader.lazyRequireGetter(
+ this,
+ "appendStyleSheet",
+ "devtools/client/shared/stylesheet-utils",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "ResponsiveUIManager",
+ "devtools/client/responsive/manager"
+);
+loader.lazyRequireGetter(
+ this,
+ "toggleEnableDevToolsPopup",
+ "devtools/client/framework/enable-devtools-popup",
+ true
+);
+loader.lazyImporter(
+ this,
+ "BrowserToolboxLauncher",
+ "resource://devtools/client/framework/browser-toolbox/Launcher.jsm"
+);
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/toolbox.properties"
+);
+
+const BROWSER_STYLESHEET_URL = "chrome://devtools/skin/devtools-browser.css";
+
+// XXX: This could also be moved to DevToolsStartup, which is the first
+// "entry point" for DevTools shortcuts and forwards the events
+// devtools-browser.
+const DEVTOOLS_F12_DISABLED_PREF = "devtools.experiment.f12.shortcut_disabled";
+/**
+ * gDevToolsBrowser exposes functions to connect the gDevTools instance with a
+ * Firefox instance.
+ */
+var gDevToolsBrowser = (exports.gDevToolsBrowser = {
+ /**
+ * A record of the windows whose menus we altered, so we can undo the changes
+ * as the window is closed
+ */
+ _trackedBrowserWindows: new Set(),
+
+ /**
+ * WeakMap keeping track of the devtools-browser stylesheets loaded in the various
+ * tracked windows.
+ */
+ _browserStyleSheets: new WeakMap(),
+
+ /**
+ * This function is for the benefit of Tools:DevToolbox in
+ * browser/base/content/browser-sets.inc and should not be used outside
+ * of there
+ */
+ // used by browser-sets.inc, command
+ async toggleToolboxCommand(gBrowser, startTime) {
+ const target = await TargetFactory.forTab(gBrowser.selectedTab);
+ const toolbox = gDevTools.getToolbox(target);
+
+ // If a toolbox exists, using toggle from the Main window :
+ // - should close a docked toolbox
+ // - should focus a windowed toolbox
+ const isDocked = toolbox && toolbox.hostType != Toolbox.HostType.WINDOW;
+ if (isDocked) {
+ gDevTools.closeToolbox(target);
+ } else {
+ gDevTools.showToolbox(target, null, null, null, startTime);
+ }
+ },
+
+ /**
+ * This function ensures the right commands are enabled in a window,
+ * depending on their relevant prefs. It gets run when a window is registered,
+ * or when any of the devtools prefs change.
+ */
+ updateCommandAvailability(win) {
+ const doc = win.document;
+
+ function toggleMenuItem(id, isEnabled) {
+ const cmd = doc.getElementById(id);
+ if (isEnabled) {
+ cmd.removeAttribute("disabled");
+ cmd.removeAttribute("hidden");
+ } else {
+ cmd.setAttribute("disabled", "true");
+ cmd.setAttribute("hidden", "true");
+ }
+ }
+
+ // Enable Browser Toolbox?
+ const chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled");
+ const devtoolsRemoteEnabled = Services.prefs.getBoolPref(
+ "devtools.debugger.remote-enabled"
+ );
+ const remoteEnabled = chromeEnabled && devtoolsRemoteEnabled;
+ toggleMenuItem("menu_browserToolbox", remoteEnabled);
+ toggleMenuItem(
+ "menu_browserContentToolbox",
+ remoteEnabled && win.gMultiProcessBrowser
+ );
+ },
+
+ /**
+ * This function makes sure that the "devtoolstheme" attribute is set on the browser
+ * window to make it possible to change colors on elements in the browser (like the
+ * splitter between the toolbox and web content).
+ */
+ updateDevtoolsThemeAttribute(win) {
+ // Set an attribute on root element of each window to make it possible
+ // to change colors based on the selected devtools theme.
+ let devtoolsTheme = Services.prefs.getCharPref("devtools.theme");
+ if (devtoolsTheme != "dark") {
+ devtoolsTheme = "light";
+ }
+
+ // Style the splitter between the toolbox and page content. This used to
+ // set the attribute on the browser's root node but that regressed tpaint:
+ // bug 1331449.
+ win.document
+ .getElementById("appcontent")
+ .setAttribute("devtoolstheme", devtoolsTheme);
+ },
+
+ observe(subject, topic, prefName) {
+ switch (topic) {
+ case "browser-delayed-startup-finished":
+ this._registerBrowserWindow(subject);
+ break;
+ case "nsPref:changed":
+ if (prefName.endsWith("enabled")) {
+ for (const win of this._trackedBrowserWindows) {
+ this.updateCommandAvailability(win);
+ }
+ }
+ if (prefName === "devtools.theme") {
+ for (const win of this._trackedBrowserWindows) {
+ this.updateDevtoolsThemeAttribute(win);
+ }
+ }
+ break;
+ case "quit-application":
+ gDevToolsBrowser.destroy({ shuttingDown: true });
+ break;
+ case "devtools:loader:destroy":
+ // This event is fired when the devtools loader unloads, which happens
+ // only when the add-on workflow ask devtools to be reloaded.
+ if (subject.wrappedJSObject == require("@loader/unload")) {
+ gDevToolsBrowser.destroy({ shuttingDown: false });
+ }
+ break;
+ }
+ },
+
+ _prefObserverRegistered: false,
+
+ ensurePrefObserver() {
+ if (!this._prefObserverRegistered) {
+ this._prefObserverRegistered = true;
+ Services.prefs.addObserver("devtools.", this);
+ }
+ },
+
+ /**
+ * This function is for the benefit of Tools:{toolId} commands,
+ * triggered from the WebDeveloper menu and keyboard shortcuts.
+ *
+ * selectToolCommand's behavior:
+ * - if the current page is about:devtools-toolbox
+ * we select the targeted tool
+ * - if the toolbox is closed,
+ * we open the toolbox and select the tool
+ * - if the toolbox is open, and the targeted tool is not selected,
+ * we select it
+ * - if the toolbox is open, and the targeted tool is selected,
+ * and the host is NOT a window, we close the toolbox
+ * - if the toolbox is open, and the targeted tool is selected,
+ * and the host is a window, we raise the toolbox window
+ *
+ * Used when: - registering a new tool
+ * - new xul window, to add menu items
+ */
+ async selectToolCommand(win, toolId, startTime) {
+ if (gDevToolsBrowser._isAboutDevtoolsToolbox(win)) {
+ const toolbox = gDevToolsBrowser._getAboutDevtoolsToolbox(win);
+ toolbox.selectTool(toolId, "key_shortcut");
+ return;
+ }
+
+ const target = await TargetFactory.forTab(win.gBrowser.selectedTab);
+ const toolbox = gDevTools.getToolbox(target);
+ const toolDefinition = gDevTools.getToolDefinition(toolId);
+
+ if (
+ toolbox &&
+ (toolbox.currentToolId == toolId ||
+ (toolId == "webconsole" && toolbox.splitConsole))
+ ) {
+ toolbox.fireCustomKey(toolId);
+
+ if (
+ toolDefinition.preventClosingOnKey ||
+ toolbox.hostType == Toolbox.HostType.WINDOW
+ ) {
+ if (!toolDefinition.preventRaisingOnKey) {
+ toolbox.raise();
+ }
+ } else {
+ toolbox.destroy();
+ }
+ gDevTools.emit("select-tool-command", toolId);
+ } else {
+ gDevTools
+ .showToolbox(
+ target,
+ toolId,
+ null,
+ null,
+ startTime,
+ undefined,
+ !toolDefinition.preventRaisingOnKey
+ )
+ .then(newToolbox => {
+ newToolbox.fireCustomKey(toolId);
+ gDevTools.emit("select-tool-command", toolId);
+ });
+ }
+ },
+
+ /**
+ * Called by devtools/client/devtools-startup.js when a key shortcut is pressed
+ *
+ * @param {Window} window
+ * The top level browser window from which the key shortcut is pressed.
+ * @param {Object} key
+ * Key object describing the key shortcut being pressed. It comes
+ * from devtools-startup.js's KeyShortcuts array. The useful fields here
+ * are:
+ * - `toolId` used to identify a toolbox's panel like inspector or webconsole,
+ * - `id` used to identify any other key shortcuts like about:debugging
+ * @param {Number} startTime
+ * Optional, indicates the time at which the key event fired. This is a
+ * `Cu.now()` timing.
+ */
+ async onKeyShortcut(window, key, startTime) {
+ // Avoid to open devtools when the about:devtools-toolbox page is showing
+ // on the window now.
+ if (
+ gDevToolsBrowser._isAboutDevtoolsToolbox(window) &&
+ (key.id === "toggleToolbox" || key.id === "toggleToolboxF12")
+ ) {
+ return;
+ }
+
+ // If this is a toolbox's panel key shortcut, delegate to selectToolCommand
+ if (key.toolId) {
+ await gDevToolsBrowser.selectToolCommand(window, key.toolId, startTime);
+ return;
+ }
+ // Otherwise implement all other key shortcuts individually here
+ switch (key.id) {
+ case "toggleToolbox":
+ await gDevToolsBrowser.toggleToolboxCommand(window.gBrowser, startTime);
+ break;
+ case "toggleToolboxF12":
+ // See Bug 1630228. F12 is responsible for most of the accidental usage
+ // of DevTools. The preference here is used as part of an experiment to
+ // disable the F12 shortcut by default.
+ const isF12Disabled = Services.prefs.getBoolPref(
+ DEVTOOLS_F12_DISABLED_PREF,
+ false
+ );
+
+ if (isF12Disabled) {
+ toggleEnableDevToolsPopup(window.document, startTime);
+ } else {
+ await gDevToolsBrowser.toggleToolboxCommand(
+ window.gBrowser,
+ startTime
+ );
+ }
+ break;
+ case "browserToolbox":
+ BrowserToolboxLauncher.init();
+ break;
+ case "browserConsole":
+ const {
+ BrowserConsoleManager,
+ } = require("devtools/client/webconsole/browser-console-manager");
+ BrowserConsoleManager.openBrowserConsoleOrFocus();
+ break;
+ case "responsiveDesignMode":
+ ResponsiveUIManager.toggle(window, window.gBrowser.selectedTab, {
+ trigger: "shortcut",
+ });
+ break;
+ }
+ },
+
+ /**
+ * Open a tab on "about:debugging", optionally pre-select a given tab.
+ */
+ // Used by browser-sets.inc, command
+ openAboutDebugging(gBrowser, hash) {
+ const url = "about:debugging" + (hash ? "#" + hash : "");
+ gBrowser.selectedTab = gBrowser.addTrustedTab(url);
+ },
+
+ async _getContentProcessTarget(processId) {
+ // Create a DevToolsServer in order to connect locally to it
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+ DevToolsServer.allowChromeProcess = true;
+
+ const transport = DevToolsServer.connectPipe();
+ const client = new DevToolsClient(transport);
+
+ await client.connect();
+ const targetDescriptor = await client.mainRoot.getProcess(processId);
+ const target = await targetDescriptor.getTarget();
+ // Ensure closing the connection in order to cleanup
+ // the devtools client and also the server created in the
+ // content process
+ target.on("close", () => {
+ client.close();
+ });
+ return target;
+ },
+
+ /**
+ * Open the Browser Content Toolbox for the provided gBrowser instance.
+ * Returns a promise that resolves with a toolbox instance. If no content process is
+ * available, the promise will be rejected and a message will be displayed to the user.
+ *
+ * Used by menus.js
+ */
+ openContentProcessToolbox(gBrowser) {
+ const { childCount } = Services.ppmm;
+ // Get the process message manager for the current tab
+ const mm = gBrowser.selectedBrowser.messageManager.processMessageManager;
+ let processId = null;
+ for (let i = 1; i < childCount; i++) {
+ const child = Services.ppmm.getChildAt(i);
+ if (child == mm) {
+ processId = mm.osPid;
+ break;
+ }
+ }
+ if (processId) {
+ return this._getContentProcessTarget(processId)
+ .then(target => {
+ // Display a new toolbox in a new window
+ return gDevTools.showToolbox(target, null, Toolbox.HostType.WINDOW);
+ })
+ .catch(e => {
+ console.error(
+ "Exception while opening the browser content toolbox:",
+ e
+ );
+ });
+ }
+
+ const msg = L10N.getStr("toolbox.noContentProcessForTab.message");
+ Services.prompt.alert(null, "", msg);
+ return Promise.reject(msg);
+ },
+
+ /**
+ * Open a window-hosted toolbox to debug the worker associated to the provided
+ * worker actor.
+ *
+ * @param {WorkerDescriptorFront} workerDescriptorFront
+ * descriptor front of the worker to debug
+ * @param {String} toolId (optional)
+ * The id of the default tool to show
+ */
+ async openWorkerToolbox(workerDescriptorFront, toolId) {
+ await gDevTools.showToolbox(
+ workerDescriptorFront,
+ toolId,
+ Toolbox.HostType.WINDOW
+ );
+ },
+
+ /**
+ * Add the devtools-browser stylesheet to browser window's document. Returns a promise.
+ *
+ * @param {Window} win
+ * The window on which the stylesheet should be added.
+ * @return {Promise} promise that resolves when the stylesheet is loaded (or rejects
+ * if it fails to load).
+ */
+ loadBrowserStyleSheet: function(win) {
+ if (this._browserStyleSheets.has(win)) {
+ return Promise.resolve();
+ }
+
+ const doc = win.document;
+ const { styleSheet, loadPromise } = appendStyleSheet(
+ doc,
+ BROWSER_STYLESHEET_URL
+ );
+ this._browserStyleSheets.set(win, styleSheet);
+ return loadPromise;
+ },
+
+ /**
+ * Add this DevTools's presence to a browser window's document
+ *
+ * @param {HTMLDocument} doc
+ * The document to which devtools should be hooked to.
+ */
+ _registerBrowserWindow(win) {
+ if (gDevToolsBrowser._trackedBrowserWindows.has(win)) {
+ return;
+ }
+ gDevToolsBrowser._trackedBrowserWindows.add(win);
+
+ BrowserMenus.addMenus(win.document);
+
+ this.updateCommandAvailability(win);
+ this.updateDevtoolsThemeAttribute(win);
+ this.ensurePrefObserver();
+ win.addEventListener("unload", this);
+
+ const tabContainer = win.gBrowser.tabContainer;
+ tabContainer.addEventListener("TabSelect", this);
+ },
+
+ /**
+ * Hook the JS debugger tool to the "Debug Script" button of the slow script
+ * dialog.
+ */
+ setSlowScriptDebugHandler() {
+ const debugService = Cc["@mozilla.org/dom/slow-script-debug;1"].getService(
+ Ci.nsISlowScriptDebug
+ );
+
+ async function slowScriptDebugHandler(tab, callback) {
+ const target = await TargetFactory.forTab(tab);
+
+ gDevTools.showToolbox(target, "jsdebugger").then(toolbox => {
+ const threadFront = toolbox.threadFront;
+
+ // Break in place, which means resuming the debuggee thread and pausing
+ // right before the next step happens.
+ switch (threadFront.state) {
+ case "paused":
+ // When the debugger is already paused.
+ threadFront.resumeThenPause();
+ callback();
+ break;
+ case "attached":
+ // When the debugger is already open.
+ threadFront.interrupt().then(() => {
+ threadFront.resumeThenPause();
+ callback();
+ });
+ break;
+ case "resuming":
+ // The debugger is newly opened.
+ threadFront.once("resumed", () => {
+ threadFront.interrupt().then(() => {
+ threadFront.resumeThenPause();
+ callback();
+ });
+ });
+ break;
+ default:
+ throw Error(
+ "invalid thread front state in slow script debug handler: " +
+ threadFront.state
+ );
+ }
+ });
+ }
+
+ debugService.activationHandler = function(window) {
+ const chromeWindow = window.browsingContext.topChromeWindow;
+
+ let setupFinished = false;
+ slowScriptDebugHandler(chromeWindow.gBrowser.selectedTab, () => {
+ setupFinished = true;
+ });
+
+ // Don't return from the interrupt handler until the debugger is brought
+ // up; no reason to continue executing the slow script.
+ const utils = window.windowUtils;
+ utils.enterModalState();
+ Services.tm.spinEventLoopUntil(() => {
+ return setupFinished;
+ });
+ utils.leaveModalState();
+ };
+
+ debugService.remoteActivationHandler = function(browser, callback) {
+ const chromeWindow = browser.ownerDocument.defaultView;
+ const tab = chromeWindow.gBrowser.getTabForBrowser(browser);
+ chromeWindow.gBrowser.selected = tab;
+
+ slowScriptDebugHandler(tab, function() {
+ callback.finishDebuggerStartup();
+ }).catch(console.error);
+ };
+ },
+
+ /**
+ * Unset the slow script debug handler.
+ */
+ unsetSlowScriptDebugHandler() {
+ const debugService = Cc["@mozilla.org/dom/slow-script-debug;1"].getService(
+ Ci.nsISlowScriptDebug
+ );
+ debugService.activationHandler = undefined;
+ },
+
+ /**
+ * Add the menuitem for a tool to all open browser windows.
+ *
+ * @param {object} toolDefinition
+ * properties of the tool to add
+ */
+ _addToolToWindows(toolDefinition) {
+ // No menu item or global shortcut is required for options panel.
+ if (!toolDefinition.inMenu) {
+ return;
+ }
+
+ // Skip if the tool is disabled.
+ try {
+ if (
+ toolDefinition.visibilityswitch &&
+ !Services.prefs.getBoolPref(toolDefinition.visibilityswitch)
+ ) {
+ return;
+ }
+ } catch (e) {
+ // Prevent breaking everything if the pref doesn't exists.
+ }
+
+ // We need to insert the new tool in the right place, which means knowing
+ // the tool that comes before the tool that we're trying to add
+ const allDefs = gDevTools.getToolDefinitionArray();
+ let prevDef;
+ for (const def of allDefs) {
+ if (!def.inMenu) {
+ continue;
+ }
+ if (def === toolDefinition) {
+ break;
+ }
+ prevDef = def;
+ }
+
+ for (const win of gDevToolsBrowser._trackedBrowserWindows) {
+ BrowserMenus.insertToolMenuElements(
+ win.document,
+ toolDefinition,
+ prevDef
+ );
+ // If we are on a page where devtools menu items are hidden such as
+ // about:devtools-toolbox, we need to call _updateMenuItems to update the
+ // visibility of the newly created menu item.
+ gDevToolsBrowser._updateMenuItems(win);
+ }
+
+ if (toolDefinition.id === "jsdebugger") {
+ gDevToolsBrowser.setSlowScriptDebugHandler();
+ }
+ },
+
+ hasToolboxOpened(win) {
+ const tab = win.gBrowser.selectedTab;
+ for (const [target] of gDevTools._toolboxes) {
+ if (target.localTab == tab) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Update developer tools menu items and the "Toggle Tools" checkbox. This is
+ * called when a toolbox is created or destroyed.
+ */
+ _updateMenu() {
+ for (const win of gDevToolsBrowser._trackedBrowserWindows) {
+ gDevToolsBrowser._updateMenuItems(win);
+ }
+ },
+
+ /**
+ * Update developer tools menu items and the "Toggle Tools" checkbox of XULWindow.
+ *
+ * @param {XULWindow} win
+ */
+ _updateMenuItems(win) {
+ const menu = win.document.getElementById("menu_devToolbox");
+
+ // Hide the "Toggle Tools" menu item if we are on about:devtools-toolbox.
+ const isAboutDevtoolsToolbox = gDevToolsBrowser._isAboutDevtoolsToolbox(
+ win
+ );
+ if (isAboutDevtoolsToolbox) {
+ menu.setAttribute("hidden", "true");
+ } else {
+ menu.removeAttribute("hidden");
+ }
+
+ // Add a checkmark for the "Toggle Tools" menu item if a toolbox is already opened.
+ const hasToolbox = gDevToolsBrowser.hasToolboxOpened(win);
+ if (hasToolbox) {
+ menu.setAttribute("checked", "true");
+ } else {
+ menu.removeAttribute("checked");
+ }
+ },
+
+ /**
+ * Check whether the window is showing about:devtools-toolbox page or not.
+ *
+ * @param {XULWindow} win
+ * @return {boolean} true: about:devtools-toolbox is showing
+ * false: otherwise
+ */
+ _isAboutDevtoolsToolbox(win) {
+ const currentURI = win.gBrowser.currentURI;
+ return (
+ currentURI.scheme === "about" &&
+ currentURI.filePath === "devtools-toolbox"
+ );
+ },
+
+ /**
+ * Retrieve the Toolbox instance loaded in the current page if the page is
+ * about:devtools-toolbox, null otherwise.
+ *
+ * @param {XULWindow} win
+ * The chrome window containing about:devtools-toolbox. Will match
+ * toolbox.topWindow.
+ * @return {Toolbox} The toolbox instance loaded in about:devtools-toolbox
+ *
+ */
+ _getAboutDevtoolsToolbox(win) {
+ if (!gDevToolsBrowser._isAboutDevtoolsToolbox(win)) {
+ return null;
+ }
+ return gDevTools.getToolboxes().find(toolbox => toolbox.topWindow === win);
+ },
+
+ /**
+ * Remove the menuitem for a tool to all open browser windows.
+ *
+ * @param {string} toolId
+ * id of the tool to remove
+ */
+ _removeToolFromWindows(toolId) {
+ for (const win of gDevToolsBrowser._trackedBrowserWindows) {
+ BrowserMenus.removeToolFromMenu(toolId, win.document);
+ }
+
+ if (toolId === "jsdebugger") {
+ gDevToolsBrowser.unsetSlowScriptDebugHandler();
+ }
+ },
+
+ /**
+ * Called on browser unload to remove menu entries, toolboxes and event
+ * listeners from the closed browser window.
+ *
+ * @param {XULWindow} win
+ * The window containing the menu entry
+ */
+ _forgetBrowserWindow(win) {
+ if (!gDevToolsBrowser._trackedBrowserWindows.has(win)) {
+ return;
+ }
+ gDevToolsBrowser._trackedBrowserWindows.delete(win);
+ win.removeEventListener("unload", this);
+
+ BrowserMenus.removeMenus(win.document);
+
+ // Destroy toolboxes for closed window
+ for (const [target, toolbox] of gDevTools._toolboxes) {
+ if (target.localTab && target.localTab.ownerDocument.defaultView == win) {
+ toolbox.destroy();
+ }
+ }
+
+ const styleSheet = this._browserStyleSheets.get(win);
+ if (styleSheet) {
+ styleSheet.remove();
+ this._browserStyleSheets.delete(win);
+ }
+
+ const tabContainer = win.gBrowser.tabContainer;
+ tabContainer.removeEventListener("TabSelect", this);
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "TabSelect":
+ gDevToolsBrowser._updateMenu();
+ break;
+ case "unload":
+ // top-level browser window unload
+ gDevToolsBrowser._forgetBrowserWindow(event.target.defaultView);
+ break;
+ }
+ },
+
+ /**
+ * Either the DevTools Loader has been destroyed by the add-on contribution
+ * workflow, or firefox is shutting down.
+
+ * @param {boolean} shuttingDown
+ * True if firefox is currently shutting down. We may prevent doing
+ * some cleanups to speed it up. Otherwise everything need to be
+ * cleaned up in order to be able to load devtools again.
+ */
+ destroy({ shuttingDown }) {
+ Services.prefs.removeObserver("devtools.", gDevToolsBrowser);
+ Services.obs.removeObserver(
+ gDevToolsBrowser,
+ "browser-delayed-startup-finished"
+ );
+ Services.obs.removeObserver(gDevToolsBrowser, "quit-application");
+ Services.obs.removeObserver(gDevToolsBrowser, "devtools:loader:destroy");
+
+ for (const win of gDevToolsBrowser._trackedBrowserWindows) {
+ gDevToolsBrowser._forgetBrowserWindow(win);
+ }
+
+ // Remove scripts loaded in content process to support the Browser Content Toolbox.
+ DevToolsServer.removeContentServerScript();
+
+ gDevTools.destroy({ shuttingDown });
+ },
+});
+
+// Handle all already registered tools,
+gDevTools
+ .getToolDefinitionArray()
+ .forEach(def => gDevToolsBrowser._addToolToWindows(def));
+// and the new ones.
+gDevTools.on("tool-registered", function(toolId) {
+ const toolDefinition = gDevTools._tools.get(toolId);
+ // If the tool has been registered globally, add to all the
+ // available windows.
+ if (toolDefinition) {
+ gDevToolsBrowser._addToolToWindows(toolDefinition);
+ }
+});
+
+gDevTools.on("tool-unregistered", function(toolId) {
+ gDevToolsBrowser._removeToolFromWindows(toolId);
+});
+
+gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenu);
+gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenu);
+
+Services.obs.addObserver(gDevToolsBrowser, "quit-application");
+Services.obs.addObserver(gDevToolsBrowser, "browser-delayed-startup-finished");
+// Watch for module loader unload. Fires when the tools are reloaded.
+Services.obs.addObserver(gDevToolsBrowser, "devtools:loader:destroy");
+
+// Fake end of browser window load event for all already opened windows
+// that is already fully loaded.
+for (const win of Services.wm.getEnumerator(gDevTools.chromeWindowType)) {
+ if (win.gBrowserInit?.delayedStartupFinished) {
+ gDevToolsBrowser._registerBrowserWindow(win);
+ }
+}
diff --git a/devtools/client/framework/devtools.js b/devtools/client/framework/devtools.js
new file mode 100644
index 0000000000..81f32b9be1
--- /dev/null
+++ b/devtools/client/framework/devtools.js
@@ -0,0 +1,839 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Cu } = require("chrome");
+const Services = require("Services");
+
+const {
+ DevToolsShim,
+} = require("chrome://devtools-startup/content/DevToolsShim.jsm");
+
+loader.lazyRequireGetter(
+ this,
+ "TargetFactory",
+ "devtools/client/framework/target",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "ToolboxHostManager",
+ "devtools/client/framework/toolbox-host-manager",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "BrowserConsoleManager",
+ "devtools/client/webconsole/browser-console-manager",
+ true
+);
+loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
+loader.lazyImporter(
+ this,
+ "BrowserToolboxLauncher",
+ "resource://devtools/client/framework/browser-toolbox/Launcher.jsm"
+);
+
+const {
+ defaultTools: DefaultTools,
+ defaultThemes: DefaultThemes,
+} = require("devtools/client/definitions");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {
+ getTheme,
+ setTheme,
+ addThemeObserver,
+ removeThemeObserver,
+} = require("devtools/client/shared/theme");
+
+const FORBIDDEN_IDS = new Set(["toolbox", ""]);
+const MAX_ORDINAL = 99;
+
+/**
+ * DevTools is a class that represents a set of developer tools, it holds a
+ * set of tools and keeps track of open toolboxes in the browser.
+ */
+function DevTools() {
+ this._tools = new Map(); // Map<toolId, tool>
+ this._themes = new Map(); // Map<themeId, theme>
+ this._toolboxes = new Map(); // Map<target, toolbox>
+ // List of toolboxes that are still in process of creation
+ this._creatingToolboxes = new Map(); // Map<target, toolbox Promise>
+
+ EventEmitter.decorate(this);
+ this._telemetry = new Telemetry();
+ this._telemetry.setEventRecordingEnabled(true);
+
+ // Listen for changes to the theme pref.
+ this._onThemeChanged = this._onThemeChanged.bind(this);
+ addThemeObserver(this._onThemeChanged);
+
+ // This is important step in initialization codepath where we are going to
+ // start registering all default tools and themes: create menuitems, keys, emit
+ // related events.
+ this.registerDefaults();
+
+ // Register this DevTools instance on the DevToolsShim, which is used by non-devtools
+ // code to interact with DevTools.
+ DevToolsShim.register(this);
+}
+
+DevTools.prototype = {
+ // The windowtype of the main window, used in various tools. This may be set
+ // to something different by other gecko apps.
+ chromeWindowType: "navigator:browser",
+
+ registerDefaults() {
+ // Ensure registering items in the sorted order (getDefault* functions
+ // return sorted lists)
+ this.getDefaultTools().forEach(definition => this.registerTool(definition));
+ this.getDefaultThemes().forEach(definition =>
+ this.registerTheme(definition)
+ );
+ },
+
+ unregisterDefaults() {
+ for (const definition of this.getToolDefinitionArray()) {
+ this.unregisterTool(definition.id);
+ }
+ for (const definition of this.getThemeDefinitionArray()) {
+ this.unregisterTheme(definition.id);
+ }
+ },
+
+ /**
+ * Register a new developer tool.
+ *
+ * A definition is a light object that holds different information about a
+ * developer tool. This object is not supposed to have any operational code.
+ * See it as a "manifest".
+ * The only actual code lives in the build() function, which will be used to
+ * start an instance of this tool.
+ *
+ * Each toolDefinition has the following properties:
+ * - id: Unique identifier for this tool (string|required)
+ * - visibilityswitch: Property name to allow us to hide this tool from the
+ * DevTools Toolbox.
+ * A falsy value indicates that it cannot be hidden.
+ * - icon: URL pointing to a graphic which will be used as the src for an
+ * 16x16 img tag (string|required)
+ * - url: URL pointing to a XUL/XHTML document containing the user interface
+ * (string|required)
+ * - label: Localized name for the tool to be displayed to the user
+ * (string|required)
+ * - hideInOptions: Boolean indicating whether or not this tool should be
+ shown in toolbox options or not. Defaults to false.
+ * (boolean)
+ * - build: Function that takes an iframe, which has been populated with the
+ * markup from |url|, and also the toolbox containing the panel.
+ * And returns an instance of ToolPanel (function|required)
+ */
+ registerTool(toolDefinition) {
+ const toolId = toolDefinition.id;
+
+ if (!toolId || FORBIDDEN_IDS.has(toolId)) {
+ throw new Error("Invalid definition.id");
+ }
+
+ // Make sure that additional tools will always be able to be hidden.
+ // When being called from main.js, defaultTools has not yet been exported.
+ // But, we can assume that in this case, it is a default tool.
+ if (!DefaultTools.includes(toolDefinition)) {
+ toolDefinition.visibilityswitch = "devtools." + toolId + ".enabled";
+ }
+
+ this._tools.set(toolId, toolDefinition);
+
+ this.emit("tool-registered", toolId);
+ },
+
+ /**
+ * Removes all tools that match the given |toolId|
+ * Needed so that add-ons can remove themselves when they are deactivated
+ *
+ * @param {string|object} tool
+ * Definition or the id of the tool to unregister. Passing the
+ * tool id should be avoided as it is a temporary measure.
+ * @param {boolean} isQuitApplication
+ * true to indicate that the call is due to app quit, so we should not
+ * cause a cascade of costly events
+ */
+ unregisterTool(tool, isQuitApplication) {
+ let toolId = null;
+ if (typeof tool == "string") {
+ toolId = tool;
+ tool = this._tools.get(tool);
+ } else {
+ const { Deprecated } = require("resource://gre/modules/Deprecated.jsm");
+ Deprecated.warning(
+ "Deprecation WARNING: gDevTools.unregisterTool(tool) is " +
+ "deprecated. You should unregister a tool using its toolId: " +
+ "gDevTools.unregisterTool(toolId)."
+ );
+ toolId = tool.id;
+ }
+ this._tools.delete(toolId);
+
+ if (!isQuitApplication) {
+ this.emit("tool-unregistered", toolId);
+ }
+ },
+
+ /**
+ * Sorting function used for sorting tools based on their ordinals.
+ */
+ ordinalSort(d1, d2) {
+ const o1 = typeof d1.ordinal == "number" ? d1.ordinal : MAX_ORDINAL;
+ const o2 = typeof d2.ordinal == "number" ? d2.ordinal : MAX_ORDINAL;
+ return o1 - o2;
+ },
+
+ getDefaultTools() {
+ return DefaultTools.sort(this.ordinalSort);
+ },
+
+ getAdditionalTools() {
+ const tools = [];
+ for (const [, value] of this._tools) {
+ if (!DefaultTools.includes(value)) {
+ tools.push(value);
+ }
+ }
+ return tools.sort(this.ordinalSort);
+ },
+
+ getDefaultThemes() {
+ return DefaultThemes.sort(this.ordinalSort);
+ },
+
+ /**
+ * Get a tool definition if it exists and is enabled.
+ *
+ * @param {string} toolId
+ * The id of the tool to show
+ *
+ * @return {ToolDefinition|null} tool
+ * The ToolDefinition for the id or null.
+ */
+ getToolDefinition(toolId) {
+ const tool = this._tools.get(toolId);
+ if (!tool) {
+ return null;
+ } else if (!tool.visibilityswitch) {
+ return tool;
+ }
+
+ const enabled = Services.prefs.getBoolPref(tool.visibilityswitch, true);
+
+ return enabled ? tool : null;
+ },
+
+ /**
+ * Allow ToolBoxes to get at the list of tools that they should populate
+ * themselves with.
+ *
+ * @return {Map} tools
+ * A map of the the tool definitions registered in this instance
+ */
+ getToolDefinitionMap() {
+ const tools = new Map();
+
+ for (const [id, definition] of this._tools) {
+ if (this.getToolDefinition(id)) {
+ tools.set(id, definition);
+ }
+ }
+
+ return tools;
+ },
+
+ /**
+ * Tools have an inherent ordering that can't be represented in a Map so
+ * getToolDefinitionArray provides an alternative representation of the
+ * definitions sorted by ordinal value.
+ *
+ * @return {Array} tools
+ * A sorted array of the tool definitions registered in this instance
+ */
+ getToolDefinitionArray() {
+ const definitions = [];
+
+ for (const [id, definition] of this._tools) {
+ if (this.getToolDefinition(id)) {
+ definitions.push(definition);
+ }
+ }
+
+ return definitions.sort(this.ordinalSort);
+ },
+
+ /**
+ * Returns the name of the current theme for devtools.
+ *
+ * @return {string} theme
+ * The name of the current devtools theme.
+ */
+ getTheme() {
+ return getTheme();
+ },
+
+ /**
+ * Called when the developer tools theme changes.
+ */
+ _onThemeChanged() {
+ this.emit("theme-changed", getTheme());
+ },
+
+ /**
+ * Register a new theme for developer tools toolbox.
+ *
+ * A definition is a light object that holds various information about a
+ * theme.
+ *
+ * Each themeDefinition has the following properties:
+ * - id: Unique identifier for this theme (string|required)
+ * - label: Localized name for the theme to be displayed to the user
+ * (string|required)
+ * - stylesheets: Array of URLs pointing to a CSS document(s) containing
+ * the theme style rules (array|required)
+ * - classList: Array of class names identifying the theme within a document.
+ * These names are set to document element when applying
+ * the theme (array|required)
+ * - onApply: Function that is executed by the framework when the theme
+ * is applied. The function takes the current iframe window
+ * and the previous theme id as arguments (function)
+ * - onUnapply: Function that is executed by the framework when the theme
+ * is unapplied. The function takes the current iframe window
+ * and the new theme id as arguments (function)
+ */
+ registerTheme(themeDefinition) {
+ const themeId = themeDefinition.id;
+
+ if (!themeId) {
+ throw new Error("Invalid theme id");
+ }
+
+ if (this._themes.get(themeId)) {
+ throw new Error("Theme with the same id is already registered");
+ }
+
+ this._themes.set(themeId, themeDefinition);
+
+ this.emit("theme-registered", themeId);
+ },
+
+ /**
+ * Removes an existing theme from the list of registered themes.
+ * Needed so that add-ons can remove themselves when they are deactivated
+ *
+ * @param {string|object} theme
+ * Definition or the id of the theme to unregister.
+ */
+ unregisterTheme(theme) {
+ let themeId = null;
+ if (typeof theme == "string") {
+ themeId = theme;
+ theme = this._themes.get(theme);
+ } else {
+ themeId = theme.id;
+ }
+
+ const currTheme = getTheme();
+
+ // Note that we can't check if `theme` is an item
+ // of `DefaultThemes` as we end up reloading definitions
+ // module and end up with different theme objects
+ const isCoreTheme = DefaultThemes.some(t => t.id === themeId);
+
+ // Reset the theme if an extension theme that's currently applied
+ // is being removed.
+ // Ignore shutdown since addons get disabled during that time.
+ if (
+ !Services.startup.shuttingDown &&
+ !isCoreTheme &&
+ theme.id == currTheme
+ ) {
+ setTheme("light");
+
+ this.emit("theme-unregistered", theme);
+ }
+
+ this._themes.delete(themeId);
+ },
+
+ /**
+ * Get a theme definition if it exists.
+ *
+ * @param {string} themeId
+ * The id of the theme
+ *
+ * @return {ThemeDefinition|null} theme
+ * The ThemeDefinition for the id or null.
+ */
+ getThemeDefinition(themeId) {
+ const theme = this._themes.get(themeId);
+ if (!theme) {
+ return null;
+ }
+ return theme;
+ },
+
+ /**
+ * Get map of registered themes.
+ *
+ * @return {Map} themes
+ * A map of the the theme definitions registered in this instance
+ */
+ getThemeDefinitionMap() {
+ const themes = new Map();
+
+ for (const [id, definition] of this._themes) {
+ if (this.getThemeDefinition(id)) {
+ themes.set(id, definition);
+ }
+ }
+
+ return themes;
+ },
+
+ /**
+ * Get registered themes definitions sorted by ordinal value.
+ *
+ * @return {Array} themes
+ * A sorted array of the theme definitions registered in this instance
+ */
+ getThemeDefinitionArray() {
+ const definitions = [];
+
+ for (const [id, definition] of this._themes) {
+ if (this.getThemeDefinition(id)) {
+ definitions.push(definition);
+ }
+ }
+
+ return definitions.sort(this.ordinalSort);
+ },
+
+ /**
+ * Called from SessionStore.jsm in mozilla-central when saving the current state.
+ *
+ * @param {Object} state
+ * A SessionStore state object that gets modified by reference
+ */
+ saveDevToolsSession: function(state) {
+ state.browserConsole = BrowserConsoleManager.getBrowserConsoleSessionState();
+ state.browserToolbox = BrowserToolboxLauncher.getBrowserToolboxSessionState();
+ },
+
+ /**
+ * Restore the devtools session state as provided by SessionStore.
+ */
+ restoreDevToolsSession: async function({ browserConsole, browserToolbox }) {
+ if (browserToolbox) {
+ BrowserToolboxLauncher.init();
+ }
+
+ if (browserConsole && !BrowserConsoleManager.getBrowserConsole()) {
+ await BrowserConsoleManager.toggleBrowserConsole();
+ }
+ },
+
+ /**
+ * Boolean, true, if we never opened a toolbox.
+ * Used to implement the telemetry tracking toolbox opening.
+ */
+ _firstShowToolbox: true,
+
+ /**
+ * Show a Toolbox for a target (either by creating a new one, or if a toolbox
+ * already exists for the target, by bring to the front the existing one)
+ * If |toolId| is specified then the displayed toolbox will have the
+ * specified tool selected.
+ * If |hostType| is specified then the toolbox will be displayed using the
+ * specified HostType.
+ *
+ * @param {Target} target
+ * The target the toolbox will debug
+ * @param {string} toolId
+ * The id of the tool to show
+ * @param {Toolbox.HostType} hostType
+ * The type of host (bottom, window, left, right)
+ * @param {object} hostOptions
+ * Options for host specifically
+ * @param {Number} startTime
+ * Optional, indicates the time at which the user event related to this toolbox
+ * opening started. This is a `Cu.now()` timing.
+ * @param {string} reason
+ * Reason the tool was opened
+ * @param {boolean} shouldRaiseToolbox
+ * Whether we need to raise the toolbox or not.
+ *
+ * @return {Toolbox} toolbox
+ * The toolbox that was opened
+ */
+ async showToolbox(
+ target,
+ toolId,
+ hostType,
+ hostOptions,
+ startTime,
+ reason = "toolbox_show",
+ shouldRaiseToolbox = true
+ ) {
+ let toolbox = this._toolboxes.get(target);
+
+ if (toolbox) {
+ if (hostType != null && toolbox.hostType != hostType) {
+ await toolbox.switchHost(hostType);
+ }
+
+ if (toolId != null) {
+ // selectTool will either select the tool if not currently selected, or wait for
+ // the tool to be loaded if needed.
+ await toolbox.selectTool(toolId, reason);
+ }
+
+ if (shouldRaiseToolbox) {
+ toolbox.raise();
+ }
+ } else {
+ // As toolbox object creation is async, we have to be careful about races
+ // Check for possible already in process of loading toolboxes before
+ // actually trying to create a new one.
+ const promise = this._creatingToolboxes.get(target);
+ if (promise) {
+ return promise;
+ }
+ const toolboxPromise = this.createToolbox(
+ target,
+ toolId,
+ hostType,
+ hostOptions
+ );
+ this._creatingToolboxes.set(target, toolboxPromise);
+ toolbox = await toolboxPromise;
+ this._creatingToolboxes.delete(target);
+
+ if (startTime) {
+ this.logToolboxOpenTime(toolbox, startTime);
+ }
+ this._firstShowToolbox = false;
+ }
+
+ // We send the "enter" width here to ensure it is always sent *after*
+ // the "open" event.
+ const width = Math.ceil(toolbox.win.outerWidth / 50) * 50;
+ const panelName = this.makeToolIdHumanReadable(
+ toolId || toolbox.defaultToolId
+ );
+ this._telemetry.addEventProperty(
+ toolbox,
+ "enter",
+ panelName,
+ null,
+ "width",
+ width
+ );
+
+ return toolbox;
+ },
+
+ /**
+ * Log telemetry related to toolbox opening.
+ * Two distinct probes are logged. One for cold startup, when we open the very first
+ * toolbox. This one includes devtools framework loading. And a second one for all
+ * subsequent toolbox opening, which should all be faster.
+ * These two probes are indexed by Tool ID.
+ *
+ * @param {String} toolbox
+ * Toolbox instance.
+ * @param {Number} startTime
+ * Indicates the time at which the user event related to the toolbox
+ * opening started. This is a `Cu.now()` timing.
+ */
+ logToolboxOpenTime(toolbox, startTime) {
+ const toolId = toolbox.currentToolId || toolbox.defaultToolId;
+ const delay = Cu.now() - startTime;
+ const panelName = this.makeToolIdHumanReadable(toolId);
+
+ const telemetryKey = this._firstShowToolbox
+ ? "DEVTOOLS_COLD_TOOLBOX_OPEN_DELAY_MS"
+ : "DEVTOOLS_WARM_TOOLBOX_OPEN_DELAY_MS";
+ this._telemetry.getKeyedHistogramById(telemetryKey).add(toolId, delay);
+
+ const browserWin = toolbox.topWindow;
+ this._telemetry.addEventProperty(
+ browserWin,
+ "open",
+ "tools",
+ null,
+ "first_panel",
+ panelName
+ );
+ },
+
+ makeToolIdHumanReadable(toolId) {
+ if (/^[0-9a-fA-F]{40}_temporary-addon/.test(toolId)) {
+ return "temporary-addon";
+ }
+
+ let matches = toolId.match(
+ /^_([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})_/
+ );
+ if (matches && matches.length === 2) {
+ return matches[1];
+ }
+
+ matches = toolId.match(/^_?(.*)-\d+-\d+-devtools-panel$/);
+ if (matches && matches.length === 2) {
+ return matches[1];
+ }
+
+ return toolId;
+ },
+
+ async createToolbox(target, toolId, hostType, hostOptions) {
+ const manager = new ToolboxHostManager(target, hostType, hostOptions);
+
+ const toolbox = await manager.create(toolId);
+
+ this._toolboxes.set(target, toolbox);
+
+ this.emit("toolbox-created", toolbox);
+
+ toolbox.once("destroy", () => {
+ this.emit("toolbox-destroy", toolbox);
+ });
+
+ toolbox.once("destroyed", () => {
+ this._toolboxes.delete(target);
+ this.emit("toolbox-destroyed", toolbox);
+ });
+ // If the document navigates to another process, the current target will be
+ // destroyed in favor of a new one. So acknowledge this swap here.
+ toolbox.on("switch-target", newTarget => {
+ this._toolboxes.delete(target);
+ this._toolboxes.set(newTarget, toolbox);
+ target = newTarget;
+ });
+
+ await toolbox.open();
+ this.emit("toolbox-ready", toolbox);
+
+ return toolbox;
+ },
+
+ /**
+ * Return the toolbox for a given target.
+ *
+ * @param {object} target
+ * Target value e.g. the target that owns this toolbox
+ *
+ * @return {Toolbox} toolbox
+ * The toolbox that is debugging the given target
+ */
+ getToolbox(target) {
+ return this._toolboxes.get(target);
+ },
+
+ /**
+ * Close the toolbox for a given target
+ *
+ * @return promise
+ * This promise will resolve to false if no toolbox was found
+ * associated to the target. true, if the toolbox was successfully
+ * closed.
+ */
+ async closeToolbox(target) {
+ let toolbox = await this._creatingToolboxes.get(target);
+ if (!toolbox) {
+ toolbox = this._toolboxes.get(target);
+ }
+ if (!toolbox) {
+ return false;
+ }
+ await toolbox.destroy();
+ return true;
+ },
+
+ /**
+ * Wrapper on TargetFactory.forTab, constructs a Target for the provided tab.
+ *
+ * @param {XULTab} tab
+ * The tab to use in creating a new target.
+ *
+ * @return {Target} A target object
+ */
+ getTargetForTab: function(tab) {
+ return TargetFactory.forTab(tab);
+ },
+
+ /**
+ * Compatibility layer for web-extensions. Used by DevToolsShim for
+ * browser/components/extensions/ext-devtools.js
+ *
+ * web-extensions need to use dedicated instances of Target and cannot reuse the
+ * cached instances managed by DevTools target factory.
+ */
+ createDescriptorForTab: function(tab) {
+ return TargetFactory.createDescriptorForTab(tab);
+ },
+
+ /**
+ * Compatibility layer for web-extensions. Used by DevToolsShim for
+ * browser/components/extensions/ext-devtools-inspectedWindow.js
+ */
+ createWebExtensionInspectedWindowFront: function(tabTarget) {
+ return tabTarget.getFront("webExtensionInspectedWindow");
+ },
+
+ /**
+ * Compatibility layer for web-extensions. Used by DevToolsShim for
+ * toolkit/components/extensions/ext-c-toolkit.js
+ */
+ openBrowserConsole: function() {
+ const {
+ BrowserConsoleManager,
+ } = require("devtools/client/webconsole/browser-console-manager");
+ BrowserConsoleManager.openBrowserConsoleOrFocus();
+ },
+
+ /**
+ * Called from the DevToolsShim, used by nsContextMenu.js.
+ *
+ * @param {XULTab} tab
+ * The browser tab on which inspect node was used.
+ * @param {ElementIdentifier} domReference
+ * Identifier generated by ContentDOMReference. It is a unique pair of
+ * BrowsingContext ID and a numeric ID.
+ * @param {Number} startTime
+ * Optional, indicates the time at which the user event related to this node
+ * inspection started. This is a `Cu.now()` timing.
+ * @return {Promise} a promise that resolves when the node is selected in the inspector
+ * markup view.
+ */
+ async inspectNode(tab, domReference, startTime) {
+ const target = await TargetFactory.forTab(tab);
+
+ const toolbox = await gDevTools.showToolbox(
+ target,
+ "inspector",
+ null,
+ null,
+ startTime,
+ "inspect_dom"
+ );
+ const inspector = toolbox.getCurrentPanel();
+
+ const nodeFront = await inspector.inspectorFront.getNodeActorFromContentDomReference(
+ domReference
+ );
+ if (!nodeFront) {
+ return;
+ }
+
+ // "new-node-front" tells us when the node has been selected, whether the
+ // browser is remote or not.
+ const onNewNode = inspector.selection.once("new-node-front");
+ // Select the final node
+ inspector.selection.setNodeFront(nodeFront, {
+ reason: "browser-context-menu",
+ });
+
+ await onNewNode;
+ // Now that the node has been selected, wait until the inspector is
+ // fully updated.
+ await inspector.once("inspector-updated");
+ },
+
+ /**
+ * Called from the DevToolsShim, used by nsContextMenu.js.
+ *
+ * @param {XULTab} tab
+ * The browser tab on which inspect accessibility was used.
+ * @param {ElementIdentifier} domReference
+ * Identifier generated by ContentDOMReference. It is a unique pair of
+ * BrowsingContext ID and a numeric ID.
+ * @param {Number} startTime
+ * Optional, indicates the time at which the user event related to this
+ * node inspection started. This is a `Cu.now()` timing.
+ * @return {Promise} a promise that resolves when the accessible object is
+ * selected in the accessibility inspector.
+ */
+ async inspectA11Y(tab, domReference, startTime) {
+ const target = await TargetFactory.forTab(tab);
+
+ const toolbox = await gDevTools.showToolbox(
+ target,
+ "accessibility",
+ null,
+ null,
+ startTime
+ );
+ const inspectorFront = await toolbox.target.getFront("inspector");
+ const nodeFront = await inspectorFront.getNodeActorFromContentDomReference(
+ domReference
+ );
+ if (!nodeFront) {
+ return;
+ }
+
+ // Select the accessible object in the panel and wait for the event that
+ // tells us it has been done.
+ const a11yPanel = toolbox.getCurrentPanel();
+ const onSelected = a11yPanel.once("new-accessible-front-selected");
+ a11yPanel.selectAccessibleForNode(nodeFront, "browser-context-menu");
+ await onSelected;
+ },
+
+ /**
+ * Either the DevTools Loader has been destroyed or firefox is shutting down.
+ * @param {boolean} shuttingDown
+ * True if firefox is currently shutting down. We may prevent doing
+ * some cleanups to speed it up. Otherwise everything need to be
+ * cleaned up in order to be able to load devtools again.
+ */
+ destroy({ shuttingDown }) {
+ // Do not cleanup everything during firefox shutdown.
+ if (!shuttingDown) {
+ for (const [, toolbox] of this._toolboxes) {
+ toolbox.destroy();
+ }
+ }
+
+ for (const [key] of this.getToolDefinitionMap()) {
+ this.unregisterTool(key, true);
+ }
+
+ gDevTools.unregisterDefaults();
+
+ removeThemeObserver(this._onThemeChanged);
+
+ // Do not unregister devtools from the DevToolsShim if the destroy is caused by an
+ // application shutdown. For instance SessionStore needs to save the Browser Toolbox
+ // state on shutdown.
+ if (!shuttingDown) {
+ // Notify the DevToolsShim that DevTools are no longer available, particularly if
+ // the destroy was caused by disabling/removing DevTools.
+ DevToolsShim.unregister();
+ }
+
+ // Cleaning down the toolboxes: i.e.
+ // for (let [target, toolbox] of this._toolboxes) toolbox.destroy();
+ // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow
+ },
+
+ /**
+ * Returns the array of the existing toolboxes.
+ *
+ * @return {Array<Toolbox>}
+ * An array of toolboxes.
+ */
+ getToolboxes() {
+ return Array.from(this._toolboxes.values());
+ },
+};
+
+const gDevTools = (exports.gDevTools = new DevTools());
diff --git a/devtools/client/framework/enable-devtools-popup.js b/devtools/client/framework/enable-devtools-popup.js
new file mode 100644
index 0000000000..f8be5a7cd5
--- /dev/null
+++ b/devtools/client/framework/enable-devtools-popup.js
@@ -0,0 +1,63 @@
+/* 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";
+
+loader.lazyGetter(this, "telemetry", () => {
+ const Telemetry = require("devtools/client/shared/telemetry");
+ return new Telemetry();
+});
+
+// This session id will be initialized the first time the popup is displayed.
+let telemetrySessionId = null;
+
+/**
+ * Helper dedicated to toggle a popup triggered by pressing F12 if DevTools have
+ * never been opened by the user.
+ *
+ * This popup should be anchored below the main hamburger menu of Firefox,
+ * which contains the Web Developer menu.
+ *
+ * This is part of the OFF12 experiment which tries to disable F12 by default to
+ * reduce accidental usage of DevTools and increase retention of non DevTools
+ * users.
+ */
+exports.toggleEnableDevToolsPopup = function(doc) {
+ // The popup is initially wrapped in a template tag to avoid loading
+ // resources on startup. Unwrap it the first time we show the notification.
+ const panelWrapper = doc.getElementById("wrapper-enable-devtools-popup");
+ if (panelWrapper) {
+ panelWrapper.replaceWith(panelWrapper.content);
+ }
+
+ const popup = doc.getElementById("enable-devtools-popup");
+
+ // Use the icon of the Firefox menu in order to be aligned with the
+ // position of the hamburger menu.
+ const anchor = doc
+ .getElementById("PanelUI-menu-button")
+ .querySelector(".toolbarbutton-icon");
+
+ const isVisible = popup.state === "open";
+ if (isVisible) {
+ popup.hidePopup();
+ } else {
+ if (!telemetrySessionId) {
+ telemetrySessionId = parseInt(telemetry.msSinceProcessStart(), 10);
+ }
+
+ popup.openPopup(anchor, "bottomcenter topright");
+ telemetry.recordEvent("f12_popup_displayed", "tools", null, {
+ session_id: telemetrySessionId,
+ });
+ }
+};
+
+/**
+ * If a session id was already generated here for telemetry, expose it so that
+ * the toolbox can use it as its own session id.
+ */
+exports.getF12SessionId = function() {
+ return telemetrySessionId;
+};
diff --git a/devtools/client/framework/menu-item.js b/devtools/client/framework/menu-item.js
new file mode 100644
index 0000000000..dcfb12f93b
--- /dev/null
+++ b/devtools/client/framework/menu-item.js
@@ -0,0 +1,79 @@
+/* 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";
+
+/**
+ * A partial implementation of the MenuItem API provided by electron:
+ * https://github.com/electron/electron/blob/master/docs/api/menu-item.md.
+ *
+ * Missing features:
+ * - id String - Unique within a single menu. If defined then it can be used
+ * as a reference to this item by the position attribute.
+ * - role String - Define the action of the menu item; when specified the
+ * click property will be ignored
+ * - sublabel String
+ * - accelerator Accelerator
+ * - position String - This field allows fine-grained definition of the
+ * specific location within a given menu.
+ *
+ * Implemented features:
+ * @param Object options
+ * String accelerator
+ * Text that appears beside the menu label to indicate the shortcut key
+ * (accelerator key) to use to invoke the command.
+ * Unlike the Electron API, this is a label only and does not actually
+ * register a handler for the key.
+ * String accesskey [non-standard]
+ * A single character used as the shortcut key. This should be one of the
+ * characters that appears in the label.
+ * Function click
+ * Will be called with click(menuItem, browserWindow) when the menu item
+ * is clicked
+ * String type
+ * Can be normal, separator, submenu, checkbox or radio
+ * String label
+ * String image
+ * Boolean enabled
+ * If false, the menu item will be greyed out and unclickable.
+ * Boolean checked
+ * Should only be specified for checkbox or radio type menu items.
+ * Menu submenu
+ * Should be specified for submenu type menu items. If submenu is specified,
+ * the type: 'submenu' can be omitted. If the value is not a Menu then it
+ * will be automatically converted to one using Menu.buildFromTemplate.
+ * Boolean visible
+ * If false, the menu item will be entirely hidden.
+ */
+function MenuItem({
+ accelerator = null,
+ accesskey = null,
+ l10nID = null,
+ checked = false,
+ click = () => {},
+ disabled = false,
+ hover = () => {},
+ id = null,
+ label = "",
+ image = null,
+ submenu = null,
+ type = "normal",
+ visible = true,
+} = {}) {
+ this.accelerator = accelerator;
+ this.accesskey = accesskey;
+ this.l10nID = l10nID;
+ this.checked = checked;
+ this.click = click;
+ this.disabled = disabled;
+ this.hover = hover;
+ this.id = id;
+ this.label = label;
+ this.image = image;
+ this.submenu = submenu;
+ this.type = type;
+ this.visible = visible;
+}
+
+module.exports = MenuItem;
diff --git a/devtools/client/framework/menu.js b/devtools/client/framework/menu.js
new file mode 100644
index 0000000000..184b36099c
--- /dev/null
+++ b/devtools/client/framework/menu.js
@@ -0,0 +1,231 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { getCurrentZoom } = require("devtools/shared/layout/utils");
+
+/**
+ * A partial implementation of the Menu API provided by electron:
+ * https://github.com/electron/electron/blob/master/docs/api/menu.md.
+ *
+ * Extra features:
+ * - Emits an 'open' and 'close' event when the menu is opened/closed
+
+ * @param String id (non standard)
+ * Needed so tests can confirm the XUL implementation is working
+ */
+function Menu({ id = null } = {}) {
+ this.menuitems = [];
+ this.id = id;
+
+ Object.defineProperty(this, "items", {
+ get() {
+ return this.menuitems;
+ },
+ });
+
+ EventEmitter.decorate(this);
+}
+
+/**
+ * Add an item to the end of the Menu
+ *
+ * @param {MenuItem} menuItem
+ */
+Menu.prototype.append = function(menuItem) {
+ this.menuitems.push(menuItem);
+};
+
+/**
+ * Remove all items from the Menu
+ */
+Menu.prototype.clear = function() {
+ this.menuitems = [];
+};
+
+/**
+ * Add an item to a specified position in the menu
+ *
+ * @param {int} pos
+ * @param {MenuItem} menuItem
+ */
+Menu.prototype.insert = function(pos, menuItem) {
+ throw Error("Not implemented");
+};
+
+/**
+ * Show the Menu next to the provided target. Anchor point is bottom-left.
+ *
+ * @param {Element} target
+ * The element to use as anchor.
+ * @param {Document} doc
+ * The document that should own the popup.
+ */
+Menu.prototype.popupAtTarget = function(target, doc) {
+ const zoom = getCurrentZoom(doc);
+
+ const rect = target.getBoundingClientRect();
+ const defaultView = target.ownerDocument.defaultView;
+ const x = rect.left + defaultView.mozInnerScreenX;
+ const y = rect.bottom + defaultView.mozInnerScreenY;
+
+ this.popup(x * zoom, y * zoom, doc);
+};
+
+/**
+ * Show the Menu at a specified location on the screen
+ *
+ * Missing features:
+ * - browserWindow - BrowserWindow (optional) - Default is null.
+ * - positioningItem Number - (optional) OS X
+ *
+ * @param {int} screenX
+ * @param {int} screenY
+ * @param {Document} doc
+ * The document that should own the context menu.
+ */
+Menu.prototype.popup = function(screenX, screenY, doc) {
+ // The context-menu will be created in the topmost window to preserve keyboard
+ // navigation (see Bug 1543940).
+ // Keep a reference on the window owning the menu to hide the popup on unload.
+ const win = doc.defaultView;
+ doc = DevToolsUtils.getTopWindow(doc.defaultView).document;
+
+ let popupset = doc.querySelector("popupset");
+ if (!popupset) {
+ popupset = doc.createXULElement("popupset");
+ doc.documentElement.appendChild(popupset);
+ }
+ // See bug 1285229, on Windows, opening the same popup multiple times in a
+ // row ends up duplicating the popup. The newly inserted popup doesn't
+ // dismiss the old one. So remove any previously displayed popup before
+ // opening a new one.
+ let popup = popupset.querySelector('menupopup[menu-api="true"]');
+ if (popup) {
+ popup.hidePopup();
+ }
+
+ popup = doc.createXULElement("menupopup");
+ popup.setAttribute("menu-api", "true");
+ popup.setAttribute("consumeoutsideclicks", "false");
+ popup.setAttribute("incontentshell", "false");
+
+ if (this.id) {
+ popup.id = this.id;
+ }
+ this._createMenuItems(popup);
+
+ // The context menu will be created in the topmost chrome window. Hide it manually when
+ // the owner document is unloaded.
+ const onWindowUnload = () => popup.hidePopup();
+ win.addEventListener("unload", onWindowUnload);
+
+ // Remove the menu from the DOM once it's hidden.
+ popup.addEventListener("popuphidden", e => {
+ if (e.target === popup) {
+ win.removeEventListener("unload", onWindowUnload);
+ popup.remove();
+ this.emit("close");
+ }
+ });
+
+ popup.addEventListener("popupshown", e => {
+ if (e.target === popup) {
+ this.emit("open");
+ }
+ });
+
+ popupset.appendChild(popup);
+ popup.openPopupAtScreen(screenX, screenY, true);
+};
+
+Menu.prototype._createMenuItems = function(parent) {
+ const doc = parent.ownerDocument;
+ this.menuitems.forEach(item => {
+ if (!item.visible) {
+ return;
+ }
+
+ if (item.submenu) {
+ const menupopup = doc.createXULElement("menupopup");
+ menupopup.setAttribute("incontentshell", "false");
+
+ item.submenu._createMenuItems(menupopup);
+
+ const menu = doc.createXULElement("menu");
+ menu.appendChild(menupopup);
+ applyItemAttributesToNode(item, menu);
+ parent.appendChild(menu);
+ } else if (item.type === "separator") {
+ const menusep = doc.createXULElement("menuseparator");
+ parent.appendChild(menusep);
+ } else {
+ const menuitem = doc.createXULElement("menuitem");
+ applyItemAttributesToNode(item, menuitem);
+
+ menuitem.addEventListener("command", () => {
+ item.click();
+ });
+ menuitem.addEventListener("DOMMenuItemActive", () => {
+ item.hover();
+ });
+
+ parent.appendChild(menuitem);
+ }
+ });
+};
+
+Menu.getMenuElementById = function(id, doc) {
+ const menuDoc = DevToolsUtils.getTopWindow(doc.defaultView).document;
+ return menuDoc.getElementById(id);
+};
+
+Menu.setApplicationMenu = () => {
+ throw Error("Not implemented");
+};
+
+Menu.sendActionToFirstResponder = () => {
+ throw Error("Not implemented");
+};
+
+Menu.buildFromTemplate = () => {
+ throw Error("Not implemented");
+};
+
+function applyItemAttributesToNode(item, node) {
+ if (item.l10nID) {
+ node.setAttribute("data-l10n-id", item.l10nID);
+ } else {
+ node.setAttribute("label", item.label);
+ if (item.accelerator) {
+ node.setAttribute("acceltext", item.accelerator);
+ }
+ if (item.accesskey) {
+ node.setAttribute("accesskey", item.accesskey);
+ }
+ }
+ if (item.type === "checkbox") {
+ node.setAttribute("type", "checkbox");
+ }
+ if (item.type === "radio") {
+ node.setAttribute("type", "radio");
+ }
+ if (item.disabled) {
+ node.setAttribute("disabled", "true");
+ }
+ if (item.checked) {
+ node.setAttribute("checked", "true");
+ }
+ if (item.image) {
+ node.setAttribute("image", item.image);
+ }
+ if (item.id) {
+ node.id = item.id;
+ }
+}
+
+module.exports = Menu;
diff --git a/devtools/client/framework/moz.build b/devtools/client/framework/moz.build
new file mode 100644
index 0000000000..66ffdfd79b
--- /dev/null
+++ b/devtools/client/framework/moz.build
@@ -0,0 +1,50 @@
+# -*- 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 += [
+ "test/allocations/browser_allocations_target.ini",
+ "test/browser-enable-popup-devtools-user.ini",
+ "test/browser-enable-popup-new-user.ini",
+ "test/browser-telemetry-startup.ini",
+ "test/browser.ini",
+ "test/metrics/browser_metrics.ini",
+ "test/metrics/browser_metrics_debugger.ini",
+ "test/metrics/browser_metrics_inspector.ini",
+ "test/metrics/browser_metrics_netmonitor.ini",
+ "test/metrics/browser_metrics_webconsole.ini",
+]
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.ini"]
+
+DIRS += [
+ "actions",
+ "browser-toolbox",
+ "components",
+ "reducers",
+]
+
+DevToolsModules(
+ "browser-menus.js",
+ "devtools-browser.js",
+ "devtools.js",
+ "enable-devtools-popup.js",
+ "menu-item.js",
+ "menu.js",
+ "selection.js",
+ "source-map-url-service.js",
+ "store-provider.js",
+ "store.js",
+ "target-from-url.js",
+ "target.js",
+ "toolbox-context-menu.js",
+ "toolbox-host-manager.js",
+ "toolbox-hosts.js",
+ "toolbox-options.js",
+ "toolbox-tabs-order-manager.js",
+ "toolbox.js",
+)
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "Framework")
diff --git a/devtools/client/framework/options-panel.css b/devtools/client/framework/options-panel.css
new file mode 100644
index 0000000000..bb45827768
--- /dev/null
+++ b/devtools/client/framework/options-panel.css
@@ -0,0 +1,180 @@
+/* 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/. */
+:root{
+ user-select: none;
+}
+
+.theme-light {
+ --experimental-background: #E0EEFF;
+ --experimental-color: #436286;
+}
+
+.theme-dark {
+ --experimental-background: #436286;
+ --experimental-color: #E0EEFF;
+}
+
+#options-panel-container {
+ overflow: auto;
+}
+
+#options-panel {
+ display: block;
+}
+
+.options-vertical-pane {
+ display: inline;
+ float: inline-start;
+}
+
+.options-vertical-pane {
+ margin: 5px;
+ width: calc(100%/3 - 10px);
+ min-width: 320px;
+ padding-inline-start: 5px;
+ box-sizing: border-box;
+}
+
+/* Snap to 50% width once there is not room for 3 columns anymore.
+ This prevents having 2 columns showing in a row, but taking up
+ only ~66% of the available space. */
+@media (max-width: 1000px) {
+ .options-vertical-pane {
+ width: calc(100%/2 - 10px);
+ }
+}
+
+.options-vertical-pane fieldset {
+ border: none;
+}
+
+.options-vertical-pane fieldset legend {
+ font-size: 1.4rem;
+ margin-inline-start: -15px;
+ margin-bottom: 3px;
+ cursor: default;
+}
+
+.options-vertical-pane fieldset + fieldset {
+ margin-top: 1rem;
+}
+
+.options-groupbox {
+ margin-inline-start: 15px;
+ padding: 2px;
+}
+
+.options-groupbox label {
+ display: flex;
+ padding: 4px 0;
+ align-items: center;
+}
+
+/* Add padding for label of select inputs in order to
+ align it with surrounding checkboxes */
+.options-groupbox label span:first-child {
+ padding-inline-start: 5px;
+}
+
+.options-groupbox label span + select {
+ margin-inline-start: 4px;
+}
+
+.options-groupbox.horizontal-options-groupbox label {
+ display: inline-flex;
+ align-items: flex-end;
+}
+
+.options-groupbox.horizontal-options-groupbox label + label {
+ margin-inline-start: 4px;
+}
+
+.options-groupbox > * {
+ padding: 2px;
+}
+
+.options-citation-label {
+ display: inline-block;
+ font-size: 1rem;
+ font-style: italic;
+ /* To align it with the checkbox */
+ padding: 4px 0 0;
+ padding-inline-end: 4px;
+}
+
+#devtools-sourceeditor-keybinding-select {
+ min-width: 130px;
+}
+
+#devtools-sourceeditor-tabsize-select {
+ min-width: 80px;
+}
+
+#screenshot-options legend::after {
+ content: "";
+ display: inline-block;
+ background-image: url("chrome://devtools/skin/images/command-screenshot.svg");
+ width: 16px;
+ height: 16px;
+ vertical-align: sub;
+ margin-inline-start: 5px;
+ -moz-context-properties: fill;
+ fill: var(--theme-toolbar-color);
+ opacity: 0.6;
+}
+
+.deprecation-notice::before {
+ background-image: url("chrome://global/skin/icons/warning.svg");
+ content: '';
+ display: inline-block;
+ flex-shrink: 0;
+ height: 15px;
+ margin-inline-end: 5px;
+ width: 15px;
+}
+
+.deprecation-notice {
+ align-items: center;
+ background-color: var(--theme-warning-background);
+ color: var(--theme-warning-color);
+ display: flex;
+ margin-inline-start: 8px;
+ outline: var(--theme-warning-background) solid 4px;
+}
+
+.deprecation-notice a {
+ color: currentColor;
+}
+.deprecation-notice a:hover{
+ text-decoration: underline;
+}
+
+.experimental-notice::before {
+ mask-image: url("chrome://devtools/skin/images/filter-small.svg");
+ mask-size: 15px;
+ transform: scaleY(-1);
+ background-color: var(--experimental-color);
+ display: inline-block;
+ content: "";
+ flex-shrink: 0;
+ height: 15px;
+ margin-inline-end: 5px;
+ width: 15px;
+}
+
+.experimental-notice {
+ background-color: var(--experimental-background);
+ color: var(--experimental-color);
+ outline: var(--experimental-background) solid 4px;
+ align-items: center;
+ display: flex;
+ margin-inline-start: 8px;
+}
+
+.experimental-notice a {
+ color: currentColor;
+}
+.experimental-notice a:hover{
+ text-decoration: underline;
+}
diff --git a/devtools/client/framework/reducers/dom-mutation-breakpoints.js b/devtools/client/framework/reducers/dom-mutation-breakpoints.js
new file mode 100644
index 0000000000..c47472caeb
--- /dev/null
+++ b/devtools/client/framework/reducers/dom-mutation-breakpoints.js
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+"use strict";
+
+const initialReducerState = {
+ counter: 1,
+ breakpoints: [],
+};
+
+exports.reducer = domMutationBreakpointReducer;
+function domMutationBreakpointReducer(state = initialReducerState, action) {
+ switch (action.type) {
+ case "ADD_DOM_MUTATION_BREAKPOINT":
+ const hasExistingBp = state.breakpoints.some(
+ bp =>
+ bp.nodeFront === action.nodeFront &&
+ bp.mutationType === action.mutationType
+ );
+
+ if (hasExistingBp) {
+ break;
+ }
+
+ state = {
+ ...state,
+ counter: state.counter + 1,
+ breakpoints: [
+ ...state.breakpoints,
+ {
+ id: `${state.counter}`,
+ nodeFront: action.nodeFront,
+ mutationType: action.mutationType,
+ enabled: true,
+ },
+ ],
+ };
+ break;
+ case "REMOVE_DOM_MUTATION_BREAKPOINT":
+ for (const [index, bp] of state.breakpoints.entries()) {
+ if (
+ bp.nodeFront === action.nodeFront &&
+ bp.mutationType === action.mutationType
+ ) {
+ state = {
+ ...state,
+ breakpoints: [
+ ...state.breakpoints.slice(0, index),
+ ...state.breakpoints.slice(index + 1),
+ ],
+ };
+ break;
+ }
+ }
+ break;
+ case "REMOVE_DOM_MUTATION_BREAKPOINTS_FOR_FRONTS": {
+ const { nodeFronts } = action;
+ const nodeFrontSet = new Set(nodeFronts);
+
+ const breakpoints = state.breakpoints.filter(
+ bp => !nodeFrontSet.has(bp.nodeFront)
+ );
+
+ // Since we might not have made any actual changes, we verify first
+ // to avoid unnecessary changes in the state.
+ if (state.breakpoints.length !== breakpoints.length) {
+ state = {
+ ...state,
+ breakpoints,
+ };
+ }
+ break;
+ }
+
+ case "SET_DOM_MUTATION_BREAKPOINTS_ENABLED_STATE": {
+ const { enabledStates } = action;
+ const toUpdateById = new Map(enabledStates);
+
+ const breakpoints = state.breakpoints.map(bp => {
+ const newBpState = toUpdateById.get(bp.id);
+ if (typeof newBpState === "boolean" && newBpState !== bp.enabled) {
+ bp = {
+ ...bp,
+ enabled: newBpState,
+ };
+ }
+
+ return bp;
+ });
+
+ // Since we might not have made any actual changes, we verify first
+ // to avoid unnecessary changes in the state.
+ if (state.breakpoints.some((bp, i) => breakpoints[i] !== bp)) {
+ state = {
+ ...state,
+ breakpoints,
+ };
+ }
+ break;
+ }
+ }
+ return state;
+}
+
+exports.getDOMMutationBreakpoints = getDOMMutationBreakpoints;
+function getDOMMutationBreakpoints(state) {
+ return state.domMutationBreakpoints.breakpoints;
+}
+
+exports.getDOMMutationBreakpoint = getDOMMutationBreakpoint;
+function getDOMMutationBreakpoint(state, id) {
+ return (
+ state.domMutationBreakpoints.breakpoints.find(v => v.id === id) || null
+ );
+}
diff --git a/devtools/client/framework/reducers/index.js b/devtools/client/framework/reducers/index.js
new file mode 100644
index 0000000000..86c4604131
--- /dev/null
+++ b/devtools/client/framework/reducers/index.js
@@ -0,0 +1,10 @@
+/* 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";
+
+module.exports = {
+ domMutationBreakpoints: require("devtools/client/framework/reducers/dom-mutation-breakpoints")
+ .reducer,
+ targets: require("devtools/client/framework/reducers/targets").reducer,
+};
diff --git a/devtools/client/framework/reducers/moz.build b/devtools/client/framework/reducers/moz.build
new file mode 100644
index 0000000000..53f955faa6
--- /dev/null
+++ b/devtools/client/framework/reducers/moz.build
@@ -0,0 +1,12 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "dom-mutation-breakpoints.js",
+ "index.js",
+ "targets.js",
+)
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "Framework")
diff --git a/devtools/client/framework/reducers/targets.js b/devtools/client/framework/reducers/targets.js
new file mode 100644
index 0000000000..2a0f3c4c36
--- /dev/null
+++ b/devtools/client/framework/reducers/targets.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+"use strict";
+
+const initialReducerState = {
+ // Array of targetFront
+ targets: [],
+ // The selected targetFront instance
+ selected: null,
+};
+
+exports.reducer = targetsReducer;
+function targetsReducer(state = initialReducerState, action) {
+ switch (action.type) {
+ case "SELECT_TARGET": {
+ const { targetActorID } = action;
+
+ if (state.selected?.actorID === targetActorID) {
+ return state;
+ }
+
+ const selectedTarget = state.targets.find(
+ target => target.actorID === targetActorID
+ );
+
+ // It's possible that the target reducer is missing a target
+ // e.g. workers, remote iframes, etc. (Bug 1594754)
+ if (!selectedTarget) {
+ return state;
+ }
+
+ return { ...state, selected: selectedTarget };
+ }
+
+ case "REGISTER_TARGET": {
+ return {
+ ...state,
+ targets: [...state.targets, action.targetFront],
+ };
+ }
+
+ case "UNREGISTER_TARGET": {
+ const targets = state.targets.filter(
+ target => target !== action.targetFront
+ );
+
+ let { selected } = state;
+ if (selected === action.targetFront) {
+ selected = null;
+ }
+
+ return { ...state, targets, selected };
+ }
+ }
+ return state;
+}
+
+exports.getToolboxTargets = getToolboxTargets;
+function getToolboxTargets(state) {
+ return state.targets.targets;
+}
+
+exports.getSelectedTarget = getSelectedTarget;
+function getSelectedTarget(state) {
+ return state.targets.selected;
+}
diff --git a/devtools/client/framework/selection.js b/devtools/client/framework/selection.js
new file mode 100644
index 0000000000..efafce359e
--- /dev/null
+++ b/devtools/client/framework/selection.js
@@ -0,0 +1,315 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+
+loader.lazyRequireGetter(
+ this,
+ "nodeConstants",
+ "devtools/shared/dom-node-constants"
+);
+
+/**
+ * Selection is a singleton belonging to the Toolbox that manages the current selected
+ * NodeFront. In addition, it provides some helpers about the context of the selected
+ * node.
+ *
+ * API
+ *
+ * new Selection()
+ * destroy()
+ * nodeFront (readonly)
+ * setNodeFront(node, origin="unknown")
+ *
+ * Helpers:
+ *
+ * window
+ * document
+ * isRoot()
+ * isNode()
+ * isHTMLNode()
+ *
+ * Check the nature of the node:
+ *
+ * isElementNode()
+ * isAttributeNode()
+ * isTextNode()
+ * isCDATANode()
+ * isEntityRefNode()
+ * isEntityNode()
+ * isProcessingInstructionNode()
+ * isCommentNode()
+ * isDocumentNode()
+ * isDocumentTypeNode()
+ * isDocumentFragmentNode()
+ * isNotationNode()
+ *
+ * Events:
+ * "new-node-front" when the inner node changed
+ * "attribute-changed" when an attribute is changed
+ * "detached-front" when the node (or one of its parents) is removed from
+ * the document
+ * "reparented" when the node (or one of its parents) is moved under
+ * a different node
+ */
+function Selection() {
+ EventEmitter.decorate(this);
+
+ // The WalkerFront is dynamic and is always set to the selected NodeFront's WalkerFront.
+ this._walker = null;
+ // A single node front can be represented twice on the client when the node is a slotted
+ // element. It will be displayed once as a direct child of the host element, and once as
+ // a child of a slot in the "shadow DOM". The latter is called the slotted version.
+ this._isSlotted = false;
+
+ this._onMutations = this._onMutations.bind(this);
+ this.setNodeFront = this.setNodeFront.bind(this);
+}
+
+Selection.prototype = {
+ _onMutations: function(mutations) {
+ let attributeChange = false;
+ let pseudoChange = false;
+ let detached = false;
+ let parentNode = null;
+
+ for (const m of mutations) {
+ if (!attributeChange && m.type == "attributes") {
+ attributeChange = true;
+ }
+ if (m.type == "childList") {
+ if (!detached && !this.isConnected()) {
+ if (this.isNode()) {
+ parentNode = m.target;
+ }
+ detached = true;
+ }
+ }
+ if (m.type == "pseudoClassLock") {
+ pseudoChange = true;
+ }
+ }
+
+ // Fire our events depending on what changed in the mutations array
+ if (attributeChange) {
+ this.emit("attribute-changed");
+ }
+ if (pseudoChange) {
+ this.emit("pseudoclass");
+ }
+ if (detached) {
+ this.emit("detached-front", parentNode);
+ }
+ },
+
+ destroy: function() {
+ this.setWalker();
+ },
+
+ setWalker: function(walker = null) {
+ if (this._walker) {
+ this._walker.off("mutations", this._onMutations);
+ }
+ this._walker = walker;
+ if (this._walker) {
+ this._walker.on("mutations", this._onMutations);
+ }
+ },
+
+ /**
+ * Update the currently selected node-front.
+ *
+ * @param {NodeFront} nodeFront
+ * The NodeFront being selected.
+ * @param {Object} (optional)
+ * - {String} reason: Reason that triggered the selection, will be fired with
+ * the "new-node-front" event.
+ * - {Boolean} isSlotted: Is the selection representing the slotted version of
+ * the node.
+ */
+ setNodeFront: function(
+ nodeFront,
+ { reason = "unknown", isSlotted = false } = {}
+ ) {
+ this.reason = reason;
+
+ // If an inlineTextChild text node is being set, then set it's parent instead.
+ const parentNode = nodeFront && nodeFront.parentNode();
+ if (nodeFront && parentNode && parentNode.inlineTextChild === nodeFront) {
+ nodeFront = parentNode;
+ }
+
+ if (this._nodeFront == null && nodeFront == null) {
+ // Avoid to notify multiple "unselected" events with a null/undefined nodeFront
+ // (e.g. once when the webpage start to navigate away from the current webpage,
+ // and then again while the new page is being loaded).
+ return;
+ }
+
+ this._isSlotted = isSlotted;
+ this._nodeFront = nodeFront;
+
+ if (nodeFront) {
+ this.setWalker(nodeFront.walkerFront);
+ } else {
+ this.setWalker();
+ }
+
+ this.emit("new-node-front", nodeFront, this.reason);
+ },
+
+ get nodeFront() {
+ return this._nodeFront;
+ },
+
+ isRoot: function() {
+ return (
+ this.isNode() && this.isConnected() && this._nodeFront.isDocumentElement
+ );
+ },
+
+ isNode: function() {
+ return !!this._nodeFront;
+ },
+
+ isConnected: function() {
+ let node = this._nodeFront;
+ if (!node || node.isDestroyed()) {
+ return false;
+ }
+
+ while (node) {
+ if (node === this._walker.rootNode) {
+ return true;
+ }
+ node = node.parentOrHost();
+ }
+ return false;
+ },
+
+ isHTMLNode: function() {
+ const xhtmlNs = "http://www.w3.org/1999/xhtml";
+ return this.isNode() && this.nodeFront.namespaceURI == xhtmlNs;
+ },
+
+ // Node type
+
+ isElementNode: function() {
+ return (
+ this.isNode() && this.nodeFront.nodeType == nodeConstants.ELEMENT_NODE
+ );
+ },
+
+ isPseudoElementNode: function() {
+ return this.isNode() && this.nodeFront.isPseudoElement;
+ },
+
+ isAnonymousNode: function() {
+ return this.isNode() && this.nodeFront.isAnonymous;
+ },
+
+ isAttributeNode: function() {
+ return (
+ this.isNode() && this.nodeFront.nodeType == nodeConstants.ATTRIBUTE_NODE
+ );
+ },
+
+ isTextNode: function() {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.TEXT_NODE;
+ },
+
+ isCDATANode: function() {
+ return (
+ this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.CDATA_SECTION_NODE
+ );
+ },
+
+ isEntityRefNode: function() {
+ return (
+ this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.ENTITY_REFERENCE_NODE
+ );
+ },
+
+ isEntityNode: function() {
+ return (
+ this.isNode() && this.nodeFront.nodeType == nodeConstants.ENTITY_NODE
+ );
+ },
+
+ isProcessingInstructionNode: function() {
+ return (
+ this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE
+ );
+ },
+
+ isCommentNode: function() {
+ return (
+ this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE
+ );
+ },
+
+ isDocumentNode: function() {
+ return (
+ this.isNode() && this.nodeFront.nodeType == nodeConstants.DOCUMENT_NODE
+ );
+ },
+
+ /**
+ * @returns true if the selection is the <body> HTML element.
+ */
+ isBodyNode: function() {
+ return (
+ this.isHTMLNode() &&
+ this.isConnected() &&
+ this.nodeFront.nodeName === "BODY"
+ );
+ },
+
+ /**
+ * @returns true if the selection is the <head> HTML element.
+ */
+ isHeadNode: function() {
+ return (
+ this.isHTMLNode() &&
+ this.isConnected() &&
+ this.nodeFront.nodeName === "HEAD"
+ );
+ },
+
+ isDocumentTypeNode: function() {
+ return (
+ this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE
+ );
+ },
+
+ isDocumentFragmentNode: function() {
+ return (
+ this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.DOCUMENT_FRAGMENT_NODE
+ );
+ },
+
+ isNotationNode: function() {
+ return (
+ this.isNode() && this.nodeFront.nodeType == nodeConstants.NOTATION_NODE
+ );
+ },
+
+ isSlotted: function() {
+ return this._isSlotted;
+ },
+
+ isShadowRootNode: function() {
+ return this.isNode() && this.nodeFront.isShadowRoot;
+ },
+};
+
+module.exports = Selection;
diff --git a/devtools/client/framework/source-map-url-service.js b/devtools/client/framework/source-map-url-service.js
new file mode 100644
index 0000000000..06f6b417fb
--- /dev/null
+++ b/devtools/client/framework/source-map-url-service.js
@@ -0,0 +1,476 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const Services = require("Services");
+const SOURCE_MAP_PREF = "devtools.source-map.client-service.enabled";
+
+/**
+ * A simple service to track source actors and keep a mapping between
+ * original URLs and objects holding the source or style actor's ID
+ * (which is used as a cookie by the devtools-source-map service) and
+ * the source map URL.
+ *
+ * @param {object} toolbox
+ * The toolbox.
+ * @param {SourceMapService} sourceMapService
+ * The devtools-source-map functions
+ */
+class SourceMapURLService {
+ constructor(toolbox, sourceMapService) {
+ this._toolbox = toolbox;
+ this._sourceMapService = sourceMapService;
+
+ this._prefValue = Services.prefs.getBoolPref(SOURCE_MAP_PREF);
+ this._pendingIDSubscriptions = new Map();
+ this._pendingURLSubscriptions = new Map();
+ this._urlToIDMap = new Map();
+ this._mapsById = new Map();
+ this._sourcesLoading = null;
+ this._onResourceAvailable = this._onResourceAvailable.bind(this);
+ this._runningCallback = false;
+
+ this._syncPrevValue = this._syncPrevValue.bind(this);
+ this._clearAllState = this._clearAllState.bind(this);
+
+ this._target.on("will-navigate", this._clearAllState);
+
+ Services.prefs.addObserver(SOURCE_MAP_PREF, this._syncPrevValue);
+ }
+
+ get _target() {
+ return this._toolbox.target;
+ }
+
+ destroy() {
+ this._clearAllState();
+ this._target.off("will-navigate", this._clearAllState);
+ Services.prefs.removeObserver(SOURCE_MAP_PREF, this._syncPrevValue);
+ }
+
+ /**
+ * Subscribe to notifications about the original location of a given
+ * generated location, as it may not be known at this time, may become
+ * available at some unknown time in the future, or may change from one
+ * location to another.
+ *
+ * @param {string} id The actor ID of the source.
+ * @param {number} line The line number in the source.
+ * @param {number} column The column number in the source.
+ * @param {Function} callback A callback that may eventually be passed an
+ * an object with url/line/column properties specifying a location in
+ * the original file, or null if no particular original location could
+ * be found. The callback will run synchronously if the location is
+ * already know to the URL service.
+ *
+ * @return {Function} A function to call to remove this subscription. The
+ * "callback" argument is guaranteed to never run once unsubscribed.
+ */
+ subscribeByID(id, line, column, callback) {
+ this._ensureAllSourcesPopulated();
+
+ let pending = this._pendingIDSubscriptions.get(id);
+ if (!pending) {
+ pending = new Set();
+ this._pendingIDSubscriptions.set(id, pending);
+ }
+ const entry = {
+ line,
+ column,
+ callback,
+ unsubscribed: false,
+ owner: pending,
+ };
+ pending.add(entry);
+
+ const map = this._mapsById.get(id);
+ if (map) {
+ this._flushPendingIDSubscriptionsToMapQueries(map);
+ }
+
+ return () => {
+ entry.unsubscribed = true;
+ entry.owner.delete(entry);
+ };
+ }
+
+ /**
+ * Subscribe to notifications about the original location of a given
+ * generated location, as it may not be known at this time, may become
+ * available at some unknown time in the future, or may change from one
+ * location to another.
+ *
+ * @param {string} id The actor ID of the source.
+ * @param {number} line The line number in the source.
+ * @param {number} column The column number in the source.
+ * @param {Function} callback A callback that may eventually be passed an
+ * an object with url/line/column properties specifying a location in
+ * the original file, or null if no particular original location could
+ * be found. The callback will run synchronously if the location is
+ * already know to the URL service.
+ *
+ * @return {Function} A function to call to remove this subscription. The
+ * "callback" argument is guaranteed to never run once unsubscribed.
+ */
+ subscribeByURL(url, line, column, callback) {
+ this._ensureAllSourcesPopulated();
+
+ let pending = this._pendingURLSubscriptions.get(url);
+ if (!pending) {
+ pending = new Set();
+ this._pendingURLSubscriptions.set(url, pending);
+ }
+ const entry = {
+ line,
+ column,
+ callback,
+ unsubscribed: false,
+ owner: pending,
+ };
+ pending.add(entry);
+
+ const id = this._urlToIDMap.get(url);
+ if (id) {
+ this._convertPendingURLSubscriptionsToID(url, id);
+ const map = this._mapsById.get(id);
+ if (map) {
+ this._flushPendingIDSubscriptionsToMapQueries(map);
+ }
+ }
+
+ return () => {
+ entry.unsubscribed = true;
+ entry.owner.delete(entry);
+ };
+ }
+
+ /**
+ * Subscribe generically based on either an ID or a URL.
+ *
+ * In an ideal world we'd always know which of these to use, but there are
+ * still cases where end up with a mixture of both, so this is provided as
+ * a helper. If you can specifically use one of these, please do that
+ * instead however.
+ */
+ subscribeByLocation({ id, url, line, column }, callback) {
+ if (id) {
+ return this.subscribeByID(id, line, column, callback);
+ }
+
+ return this.subscribeByURL(url, line, column, callback);
+ }
+
+ /**
+ * Tell the URL service than some external entity has registered a sourcemap
+ * in the worker for one of the source files.
+ *
+ * @param {string} id The actor ID of the source that had the map registered.
+ */
+ async newSourceMapCreated(id) {
+ await this._ensureAllSourcesPopulated();
+
+ const map = this._mapsById.get(id);
+ if (!map) {
+ // State could have been cleared.
+ return;
+ }
+
+ map.loaded = Promise.resolve();
+ for (const query of map.queries.values()) {
+ query.action = null;
+ query.result = null;
+ if (this._prefValue) {
+ this._dispatchQuery(query);
+ }
+ }
+ }
+
+ _syncPrevValue() {
+ this._prefValue = Services.prefs.getBoolPref(SOURCE_MAP_PREF);
+
+ for (const map of this._mapsById.values()) {
+ for (const query of map.queries.values()) {
+ this._ensureSubscribersSynchronized(query);
+ }
+ }
+ }
+
+ _clearAllState() {
+ this._sourceMapService.clearSourceMaps();
+ this._pendingIDSubscriptions.clear();
+ this._pendingURLSubscriptions.clear();
+ this._urlToIDMap.clear();
+
+ const { resourceWatcher } = this._toolbox;
+ try {
+ resourceWatcher.unwatchResources(
+ [resourceWatcher.TYPES.STYLESHEET, resourceWatcher.TYPES.SOURCE],
+ { onAvailable: this._onResourceAvailable }
+ );
+ } catch (e) {
+ // If unwatchResources is called before finishing process of watchResources,
+ // it throws an error during stopping listener.
+ }
+
+ this._sourcesLoading = null;
+ }
+
+ _onNewJavascript(source) {
+ const { url, actor: id, sourceMapBaseURL, sourceMapURL } = source;
+
+ this._onNewSource(id, url, sourceMapURL, sourceMapBaseURL);
+ }
+
+ _onNewStyleSheet(sheet) {
+ const {
+ href,
+ nodeHref,
+ sourceMapBaseURL,
+ sourceMapURL,
+ resourceId: id,
+ } = sheet;
+ const url = href || nodeHref;
+
+ this._onNewSource(id, url, sourceMapURL, sourceMapBaseURL);
+ }
+
+ _onNewSource(id, url, sourceMapURL, sourceMapBaseURL) {
+ this._urlToIDMap.set(url, id);
+ this._convertPendingURLSubscriptionsToID(url, id);
+
+ let map = this._mapsById.get(id);
+ if (!map) {
+ map = {
+ id,
+ url,
+ sourceMapURL,
+ sourceMapBaseURL,
+ loaded: null,
+ queries: new Map(),
+ };
+ this._mapsById.set(id, map);
+ } else if (
+ map.id !== id &&
+ map.url !== url &&
+ map.sourceMapURL !== sourceMapURL &&
+ map.sourceMapBaseURL !== sourceMapBaseURL
+ ) {
+ console.warn(
+ `Attempted to load populate sourcemap for source ${id} multiple times`
+ );
+ }
+
+ this._flushPendingIDSubscriptionsToMapQueries(map);
+ }
+
+ _buildQuery(map, line, column) {
+ const key = `${line}:${column}`;
+ let query = map.queries.get(key);
+ if (!query) {
+ query = {
+ map,
+ line,
+ column,
+ subscribers: new Set(),
+ action: null,
+ result: null,
+ mostRecentEmitted: null,
+ };
+ map.queries.set(key, query);
+ }
+ return query;
+ }
+
+ _dispatchQuery(query, newSubscribers = null) {
+ if (!this._prefValue) {
+ throw new Error("This function should only be called if the pref is on.");
+ }
+
+ if (!query.action) {
+ const { map } = query;
+
+ // Call getOriginalURLs to make sure the source map has been
+ // fetched. We don't actually need the result of this though.
+ if (!map.loaded) {
+ map.loaded = this._sourceMapService.getOriginalURLs({
+ id: map.id,
+ url: map.url,
+ sourceMapBaseURL: map.sourceMapBaseURL,
+ sourceMapURL: map.sourceMapURL,
+ });
+ }
+
+ const action = (async () => {
+ let result = null;
+ try {
+ await map.loaded;
+
+ const position = await this._sourceMapService.getOriginalLocation({
+ sourceId: map.id,
+ line: query.line,
+ column: query.column,
+ });
+ if (position && position.sourceId !== map.id) {
+ result = {
+ url: position.sourceUrl,
+ line: position.line,
+ column: position.column,
+ };
+ }
+ } finally {
+ // If this action was dispatched and then the file was pretty-printed
+ // we want to ignore the result since the query has restarted.
+ if (action === query.action) {
+ // It is important that we consistently set the query result and
+ // trigger the subscribers here in order to maintain the invariant
+ // that if 'result' is truthy, then the subscribers will have run.
+ const position = result;
+ query.result = { position };
+ this._ensureSubscribersSynchronized(query);
+ }
+ }
+ })();
+ query.action = action;
+ }
+
+ this._ensureSubscribersSynchronized(query);
+ }
+
+ _ensureSubscribersSynchronized(query) {
+ // Synchronize the subscribers with the pref-disabled state if they need it.
+ if (!this._prefValue) {
+ if (query.mostRecentEmitted) {
+ query.mostRecentEmitted = null;
+ this._dispatchSubscribers(null, query.subscribers);
+ }
+ return;
+ }
+
+ // Synchronize the subscribers with the newest computed result if they
+ // need it.
+ const { result } = query;
+ if (result && query.mostRecentEmitted !== result.position) {
+ query.mostRecentEmitted = result.position;
+ this._dispatchSubscribers(result.position, query.subscribers);
+ }
+ }
+
+ _dispatchSubscribers(position, subscribers) {
+ // We copy the subscribers before iterating because something could be
+ // removed while we're calling the callbacks, which is also why we check
+ // the 'unsubscribed' flag.
+ for (const subscriber of Array.from(subscribers)) {
+ if (subscriber.unsubscribed) {
+ continue;
+ }
+
+ if (this._runningCallback) {
+ console.error(
+ "The source map url service does not support reentrant subscribers."
+ );
+ continue;
+ }
+
+ try {
+ this._runningCallback = true;
+
+ const { callback } = subscriber;
+ callback(position ? { ...position } : null);
+ } catch (err) {
+ console.error("Error in source map url service subscriber", err);
+ } finally {
+ this._runningCallback = false;
+ }
+ }
+ }
+
+ _flushPendingIDSubscriptionsToMapQueries(map) {
+ const subscriptions = this._pendingIDSubscriptions.get(map.id);
+ if (!subscriptions || subscriptions.size === 0) {
+ return;
+ }
+ this._pendingIDSubscriptions.delete(map.id);
+
+ for (const entry of subscriptions) {
+ const query = this._buildQuery(map, entry.line, entry.column);
+
+ const { subscribers } = query;
+
+ entry.owner = subscribers;
+ subscribers.add(entry);
+
+ if (query.mostRecentEmitted) {
+ // Maintain the invariant that if a query has emitted a value, then
+ // _all_ subscribers will have received that value.
+ this._dispatchSubscribers(query.mostRecentEmitted, [entry]);
+ }
+
+ if (this._prefValue) {
+ this._dispatchQuery(query);
+ }
+ }
+ }
+
+ _ensureAllSourcesPopulated() {
+ if (!this._prefValue) {
+ return null;
+ }
+ if (this._target.isWorkerTarget) {
+ return;
+ }
+
+ if (!this._sourcesLoading) {
+ const { resourceWatcher } = this._toolbox;
+ const { STYLESHEET, SOURCE } = resourceWatcher.TYPES;
+
+ this._sourcesLoading = resourceWatcher.watchResources(
+ [STYLESHEET, SOURCE],
+ {
+ onAvailable: this._onResourceAvailable,
+ }
+ );
+ }
+
+ return this._sourcesLoading;
+ }
+
+ waitForSourcesLoading() {
+ if (this._sourcesLoading) {
+ return this._sourcesLoading;
+ }
+ return Promise.resolve();
+ }
+
+ _onResourceAvailable(resources) {
+ const { resourceWatcher } = this._toolbox;
+ const { STYLESHEET, SOURCE } = resourceWatcher.TYPES;
+ for (const resource of resources) {
+ if (resource.resourceType == STYLESHEET) {
+ this._onNewStyleSheet(resource);
+ } else if (resource.resourceType == SOURCE) {
+ this._onNewJavascript(resource);
+ }
+ }
+ }
+
+ _convertPendingURLSubscriptionsToID(url, id) {
+ const urlSubscriptions = this._pendingURLSubscriptions.get(url);
+ if (!urlSubscriptions) {
+ return;
+ }
+ this._pendingURLSubscriptions.delete(url);
+
+ let pending = this._pendingIDSubscriptions.get(id);
+ if (!pending) {
+ pending = new Set();
+ this._pendingIDSubscriptions.set(id, pending);
+ }
+ for (const entry of urlSubscriptions) {
+ entry.owner = pending;
+ pending.add(entry);
+ }
+ }
+}
+
+exports.SourceMapURLService = SourceMapURLService;
diff --git a/devtools/client/framework/store-provider.js b/devtools/client/framework/store-provider.js
new file mode 100644
index 0000000000..301ce91133
--- /dev/null
+++ b/devtools/client/framework/store-provider.js
@@ -0,0 +1,8 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { createProvider } = require("devtools/client/shared/vendor/react-redux");
+
+module.exports = createProvider("toolbox-store");
diff --git a/devtools/client/framework/store.js b/devtools/client/framework/store.js
new file mode 100644
index 0000000000..dbdd9066fa
--- /dev/null
+++ b/devtools/client/framework/store.js
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const createStore = require("devtools/client/shared/redux/create-store");
+const reducers = require("devtools/client/framework/reducers/index");
+
+exports.createToolboxStore = () =>
+ createStore(reducers, {
+ // Uncomment this for logging in tests.
+ // shouldLog: true,
+ });
diff --git a/devtools/client/framework/target-from-url.js b/devtools/client/framework/target-from-url.js
new file mode 100644
index 0000000000..34dc3bfb8d
--- /dev/null
+++ b/devtools/client/framework/target-from-url.js
@@ -0,0 +1,211 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { DevToolsServer } = require("devtools/server/devtools-server");
+const { DevToolsClient } = require("devtools/client/devtools-client");
+const {
+ remoteClientManager,
+} = require("devtools/client/shared/remote-debugging/remote-client-manager");
+
+/**
+ * Construct a Target for a given URL object having various query parameters:
+ *
+ * - host, port & ws: See the documentation for clientFromURL
+ *
+ * - type: tab, process, window
+ * {String} The type of target to connect to.
+ *
+ * If type == "tab":
+ * - id:
+ * {Number} the tab outerWindowID
+ * - chrome: Optional
+ * {Boolean} Force the creation of a chrome target. Gives more privileges to
+ * the target actor. Allows chrome execution in the webconsole and see chrome
+ * files in the debugger. (handy when contributing to firefox)
+ *
+ * If type == "process":
+ * - id:
+ * {Number} the process id to debug. Default to 0, which is the parent process.
+ *
+ * If type == "window":
+ * - id:
+ * {Number} the window outerWindowID
+ *
+ * @param {URL} url
+ * The url to fetch query params from.
+ *
+ * @return A target object
+ */
+exports.targetFromURL = async function targetFromURL(url) {
+ const client = await clientFromURL(url);
+ const params = url.searchParams;
+
+ // Clients retrieved from the remote-client-manager are already connected.
+ const isCachedClient = params.get("remoteId");
+ if (!isCachedClient) {
+ // Connect any other client.
+ await client.connect();
+ }
+
+ const id = params.get("id");
+ const type = params.get("type");
+ const chrome = params.has("chrome");
+
+ let target;
+ try {
+ target = await _targetFromURL(client, id, type, chrome);
+ } catch (e) {
+ if (!isCachedClient) {
+ // If the client was not cached, then the client was created here. If the target
+ // creation failed, we should close the client.
+ await client.close();
+ }
+ throw e;
+ }
+
+ // If this isn't a cached client, it means that we just created a new client
+ // in `clientFromURL` and we have to destroy it at some point.
+ // In such case, force the Target to destroy the client as soon as it gets
+ // destroyed. This typically happens only for about:debugging toolboxes
+ // opened for local Firefox's targets.
+ target.shouldCloseClient = !isCachedClient;
+
+ return target;
+};
+
+async function _targetFromURL(client, id, type, chrome) {
+ if (!type) {
+ throw new Error("targetFromURL, missing type parameter");
+ }
+
+ let front;
+ if (type === "tab") {
+ // Fetch target for a remote tab
+ id = parseInt(id, 10);
+ if (isNaN(id)) {
+ throw new Error(
+ `targetFromURL, wrong tab id '${id}', should be a number`
+ );
+ }
+ try {
+ const tabDescriptor = await client.mainRoot.getTab({ outerWindowID: id });
+ front = await tabDescriptor.getTarget();
+ } catch (ex) {
+ if (ex.message.startsWith("Protocol error (noTab)")) {
+ throw new Error(
+ `targetFromURL, tab with outerWindowID '${id}' doesn't exist`
+ );
+ }
+ throw ex;
+ }
+ } else if (type === "extension") {
+ const addonDescriptor = await client.mainRoot.getAddon({ id });
+
+ if (!addonDescriptor) {
+ throw new Error(`targetFromURL, extension with id '${id}' doesn't exist`);
+ }
+
+ front = await addonDescriptor.getTarget();
+ } else if (type === "worker") {
+ front = await client.mainRoot.getWorker(id);
+
+ if (!front) {
+ throw new Error(
+ `targetFromURL, worker with actor id '${id}' doesn't exist`
+ );
+ }
+ } else if (type == "process") {
+ // Fetch target for a remote chrome actor
+ DevToolsServer.allowChromeProcess = true;
+ try {
+ id = parseInt(id, 10);
+ if (isNaN(id)) {
+ id = 0;
+ }
+ const frontDescriptor = await client.mainRoot.getProcess(id);
+ front = await frontDescriptor.getTarget(id);
+ } catch (ex) {
+ if (ex.error == "noProcess") {
+ throw new Error(`targetFromURL, process with id '${id}' doesn't exist`);
+ }
+ throw ex;
+ }
+ } else if (type == "window") {
+ // Fetch target for a remote window actor
+ DevToolsServer.allowChromeProcess = true;
+ try {
+ id = parseInt(id, 10);
+ if (isNaN(id)) {
+ throw new Error("targetFromURL, window requires id parameter");
+ }
+ front = await client.mainRoot.getWindow({
+ outerWindowID: id,
+ });
+ } catch (ex) {
+ if (ex.error == "notFound") {
+ throw new Error(`targetFromURL, window with id '${id}' doesn't exist`);
+ }
+ throw ex;
+ }
+ } else {
+ throw new Error(`targetFromURL, unsupported type '${type}' parameter`);
+ }
+
+ // Allows to spawn a chrome enabled target for any context
+ // (handy to debug chrome stuff in a content process)
+ if (chrome) {
+ front.forceChrome();
+ }
+
+ return front;
+}
+
+/**
+ * Create a DevToolsClient for a given URL object having various query parameters:
+ *
+ * host:
+ * {String} The hostname or IP address to connect to.
+ * port:
+ * {Number} The TCP port to connect to, to use with `host` argument.
+ * remoteId:
+ * {String} Remote client id, for runtimes from the remote-client-manager
+ * ws:
+ * {Boolean} If true, connect via websocket instead of regular TCP connection.
+ *
+ * @param {URL} url
+ * The url to fetch query params from.
+ * @return a promise that resolves a DevToolsClient object
+ */
+async function clientFromURL(url) {
+ const params = url.searchParams;
+
+ // If a remote id was provided we should already have a connected client available.
+ const remoteId = params.get("remoteId");
+ if (remoteId) {
+ const client = remoteClientManager.getClientByRemoteId(remoteId);
+ if (!client) {
+ throw new Error(`Could not find client with remote id: ${remoteId}`);
+ }
+ return client;
+ }
+
+ const host = params.get("host");
+ const port = params.get("port");
+ const webSocket = !!params.get("ws");
+
+ let transport;
+ if (port) {
+ transport = await DevToolsClient.socketConnect({ host, port, webSocket });
+ } else {
+ // Setup a server if we don't have one already running
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+ transport = DevToolsServer.connectPipe();
+ }
+ return new DevToolsClient(transport);
+}
+
+exports.clientFromURL = clientFromURL;
diff --git a/devtools/client/framework/target.js b/devtools/client/framework/target.js
new file mode 100644
index 0000000000..844ca02a19
--- /dev/null
+++ b/devtools/client/framework/target.js
@@ -0,0 +1,119 @@
+/* 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";
+
+loader.lazyRequireGetter(
+ this,
+ "DevToolsServer",
+ "devtools/server/devtools-server",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "DevToolsClient",
+ "devtools/client/devtools-client",
+ true
+);
+
+const targets = new WeakMap();
+
+/**
+ * Functions for creating Targets
+ */
+exports.TargetFactory = {
+ /**
+ * Construct a Target. The target will be cached for each Tab so that we create only
+ * one per tab.
+ *
+ * @param {XULTab} tab
+ * The tab to use in creating a new target.
+ * @param {DevToolsClient} client
+ * Optional client to fetch the target actor from.
+ *
+ * @return A target object
+ */
+ forTab: async function(tab, client) {
+ let target = targets.get(tab);
+ if (target) {
+ return target;
+ }
+
+ const promise = this._createTargetForTab(tab, client);
+
+ // Immediately set the target's promise in cache to prevent race
+ targets.set(tab, promise);
+ target = await promise;
+ // Then replace the promise with the target object
+ targets.set(tab, target);
+ target.once("close", () => {
+ targets.delete(tab);
+ });
+ return target;
+ },
+
+ async _createTargetForTab(tab, client) {
+ const tabDescriptor = await this.createDescriptorForTab(tab, client);
+ return tabDescriptor.getTarget();
+ },
+
+ /**
+ * Create a tab target descriptor for the given tab.
+ *
+ * This will automatically:
+ * - if no client is passed, spawn a DevToolsServer in the parent process,
+ * and create a DevToolsClient and connect it to this local DevToolsServer,
+ * - call RootActor's `getTab` request to retrieve the FrameTargetActor's form,
+ * - instantiate a Target instance.
+ *
+ * @param {XULTab} tab
+ * The tab to use in creating a new target.
+ * @param {DevToolsClient} client
+ * Optional client to fetch the target actor from.
+ *
+ * @return {TabDescriptorFront} The tab descriptor for the provided tab.
+ */
+ async createDescriptorForTab(tab, client) {
+ function createLocalServer() {
+ // Since a remote protocol connection will be made, let's start the
+ // DevToolsServer here, once and for all tools.
+ DevToolsServer.init();
+
+ // When connecting to a local tab, we only need the root actor.
+ // Then we are going to call frame-connector.js' connectToFrame and talk
+ // directly with actors living in the child process.
+ // We also need browser actors for actor registry which enabled addons
+ // to register custom actors.
+ // TODO: the comment and implementation are out of sync here. See Bug 1420134.
+ DevToolsServer.registerAllActors();
+ // Enable being able to get child process actors
+ DevToolsServer.allowChromeProcess = true;
+ }
+
+ function createLocalClient() {
+ createLocalServer();
+ return new DevToolsClient(DevToolsServer.connectPipe());
+ }
+
+ if (!client) {
+ client = createLocalClient();
+
+ // Connect the local client to the local server
+ await client.connect();
+ }
+
+ return client.mainRoot.getTab({ tab });
+ },
+
+ /**
+ * Creating a target for a tab that is being closed is a problem because it
+ * allows a leak as a result of coming after the close event which normally
+ * clears things up. This function allows us to ask if there is a known
+ * target for a tab without creating a target
+ * @return true/false
+ */
+ isKnownTab: function(tab) {
+ return targets.has(tab);
+ },
+};
diff --git a/devtools/client/framework/test/.eslintrc.js b/devtools/client/framework/test/.eslintrc.js
new file mode 100644
index 0000000000..3d0bd99e1b
--- /dev/null
+++ b/devtools/client/framework/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "../../../.eslintrc.mochitests.js",
+};
diff --git a/devtools/client/framework/test/allocations/browser_allocations_target.ini b/devtools/client/framework/test/allocations/browser_allocations_target.ini
new file mode 100644
index 0000000000..9ce7087739
--- /dev/null
+++ b/devtools/client/framework/test/allocations/browser_allocations_target.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ !/devtools/shared/test-helpers/allocation-tracker.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]
+skip-if = os != 'linux' || debug || asan # Results should be platform agnostic - only run on linux64-opt
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..2429f9d06d
--- /dev/null
+++ b/devtools/client/framework/test/allocations/browser_allocations_target.js
@@ -0,0 +1,114 @@
+const TEST_URL =
+ "data:text/html;charset=UTF-8,<div>Target allocations test</div>";
+
+// Load the tracker very first in order to ensure tracking all objects created by DevTools.
+// Loader.jsm shouldn't be loaded, not any other DevTools module until an explicit user action.
+let tracker;
+{
+ const { DevToolsLoader } = ChromeUtils.import(
+ "resource://devtools/shared/Loader.jsm"
+ );
+ const loader = new DevToolsLoader({
+ invisibleToDebugger: true,
+ });
+ const { allocationTracker } = loader.require(
+ "chrome://mochitests/content/browser/devtools/shared/test-helpers/allocation-tracker"
+ );
+ tracker = allocationTracker({ watchDevToolsGlobals: true });
+}
+
+// So that PERFHERDER data can be extracted from the logs.
+SimpleTest.requestCompleteLog();
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+
+const { TargetFactory } = require("devtools/client/framework/target");
+
+async function doGC() {
+ // In order to get stable results, we really have to do 3 GC attempts
+ // *and* do wait for 1s between each GC.
+ const numCycles = 3;
+ for (let i = 0; i < numCycles; i++) {
+ Cu.forceGC();
+ Cu.forceCC();
+ await new Promise(resolve => Cu.schedulePreciseShrinkingGC(resolve));
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ }
+}
+
+async function addTab(url) {
+ const tab = BrowserTestUtils.addTab(gBrowser, url);
+ gBrowser.selectedTab = tab;
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ return tab;
+}
+
+async function testScript(tab) {
+ const target = await TargetFactory.forTab(tab);
+ await target.attach();
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ await target.destroy();
+}
+
+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);
+
+ // Do a first pass of GC, to ensure all to-be-freed objects from the first run
+ // are really freed.
+ await doGC();
+
+ // Then, record what was already allocated, which should not be declared
+ // as potential leaks. For ex, there is all the modules already loaded
+ // in the main DevTools loader.
+ const totalBefore = tracker.stillAllocatedObjects();
+
+ // Now, run the test script. This time, we record this run.
+ await testScript(tab);
+
+ // After that, re-do some GCs in order to free all what is to-be-freed.
+ await doGC();
+
+ // Ensure that Memory API didn't ran out of buffers
+ ok(!tracker.overflowed, "Allocation were all recorded");
+
+ // And finally, retrieve the number of objects that are still allocated.
+ const totalAfter = tracker.stillAllocatedObjects();
+
+ gBrowser.removeTab(tab);
+
+ const PERFHERDER_DATA = {
+ framework: {
+ name: "devtools",
+ },
+ suites: [
+ {
+ name: "total-after-gc",
+ value: totalAfter - totalBefore,
+ subtests: [
+ {
+ name: "before",
+ value: totalBefore,
+ },
+ {
+ name: "after",
+ value: totalAfter - totalBefore,
+ },
+ ],
+ },
+ ],
+ };
+ info("PERFHERDER_DATA: " + JSON.stringify(PERFHERDER_DATA));
+ ok(true, "Test succeeded");
+});
diff --git a/devtools/client/framework/test/browser-enable-popup-devtools-user.ini b/devtools/client/framework/test/browser-enable-popup-devtools-user.ini
new file mode 100644
index 0000000000..b446ae5c66
--- /dev/null
+++ b/devtools/client/framework/test/browser-enable-popup-devtools-user.ini
@@ -0,0 +1,18 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ helper_enable_devtools_popup.js
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+prefs =
+ devtools.experiment.f12.shortcut_disabled=true
+ devtools.selfxss.count=5
+
+# This test checks the interaction between devtools.selfxss.count and
+# devtools.experiment.f12.shortcut_disabled which is handled by DevToolsStartup
+# and therefore needs to run in isolation, with preferences set before starting
+# the browser.
+[browser_enable_devtools_popup_devtools_user.js]
+skip-if = verify # This test is only valid with a new browser instance.
diff --git a/devtools/client/framework/test/browser-enable-popup-new-user.ini b/devtools/client/framework/test/browser-enable-popup-new-user.ini
new file mode 100644
index 0000000000..c10536f6cd
--- /dev/null
+++ b/devtools/client/framework/test/browser-enable-popup-new-user.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ helper_enable_devtools_popup.js
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+prefs =
+ devtools.experiment.f12.shortcut_disabled=true
+ devtools.selfxss.count=0
+
+# This test checks the interaction between devtools.selfxss.count and
+# devtools.experiment.f12.shortcut_disabled which is handled by DevToolsStartup
+# and therefore needs to run in isolation, with preferences set before starting
+# the browser.
+[browser_enable_devtools_popup_new_user.js]
diff --git a/devtools/client/framework/test/browser-telemetry-startup.ini b/devtools/client/framework/test/browser-telemetry-startup.ini
new file mode 100644
index 0000000000..dd6d52b1cc
--- /dev/null
+++ b/devtools/client/framework/test/browser-telemetry-startup.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.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 ini file will ensure a
+# new browser instance is created just for this test.
+[browser_toolbox_telemetry_open_event.js]
diff --git a/devtools/client/framework/test/browser.ini b/devtools/client/framework/test/browser.ini
new file mode 100644
index 0000000000..2f0921e3f1
--- /dev/null
+++ b/devtools/client/framework/test/browser.ini
@@ -0,0 +1,166 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ 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_bundle_reload_1.js
+ code_bundle_reload_1.js.map
+ code_bundle_reload_2.js
+ code_bundle_reload_2.js.map
+ code_cross_domain.js
+ code_inline_bundle.js
+ code_inline_original.js
+ code_math.js
+ code_no_race.js
+ code_reload_1.js
+ code_reload_2.js
+ doc_cached-resource.html
+ doc_cached-resource_iframe.html
+ doc_empty-tab-01.html
+ doc_reload.html
+ head.js
+ helper_disable_cache.js
+ helper_enable_devtools_popup.js
+ doc_theme.css
+ doc_viewsource.html
+ browser_toolbox_options_enable_serviceworkers_testing.html
+ serviceworker.js
+ sjs_cache_controle_header.sjs
+ sjs_code_reload.sjs
+ sjs_code_bundle_reload_map.sjs
+ test_chrome_page.html
+ !/devtools/client/debugger/test/mochitest/head.js
+ !/devtools/client/debugger/test/mochitest/helpers.js
+ !/devtools/client/debugger/test/mochitest/helpers/context.js
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/shared-redux-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+# This is far from ideal. https://bugzilla.mozilla.org/show_bug.cgi?id=1565279
+# covers removing this pref flip.
+prefs =
+ security.allow_unsafe_parent_loads=true
+
+[browser_about-devtools-toolbox_load.js]
+[browser_about-devtools-toolbox_reload.js]
+[browser_devtools_api_destroy.js]
+[browser_dynamic_tool_enabling.js]
+[browser_enable_devtools_popup.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-no-race.js]
+[browser_source_map-pub-sub.js]
+[browser_source_map-reload.js]
+skip-if = fission # Disable frequent fission intermittents Bug 1675020
+[browser_source_map-late-script.js]
+[browser_tab_descriptor_fission.js]
+[browser_target_from_url.js]
+[browser_target_cached-front.js]
+[browser_target_cached-resource.js]
+[browser_target_events.js]
+[browser_target_parents.js]
+[browser_target_remote.js]
+[browser_target_support.js]
+[browser_target_get-front.js]
+[browser_target_listeners.js]
+[browser_target_server_compartment.js]
+[browser_toolbox_browsertoolbox_host.js]
+[browser_toolbox_contentpage_contextmenu.js]
+[browser_toolbox_dynamic_registration.js]
+[browser_toolbox_error_count_reset_on_navigation.js]
+[browser_toolbox_error_count.js]
+[browser_toolbox_getpanelwhenready.js]
+[browser_toolbox_highlight.js]
+[browser_toolbox_hosts.js]
+[browser_toolbox_hosts_size.js]
+[browser_toolbox_hosts_telemetry.js]
+[browser_toolbox_keyboard_navigation.js]
+[browser_toolbox_keyboard_navigation_notification_box.js]
+[browser_toolbox_meatball.js]
+[browser_toolbox_options.js]
+[browser_toolbox_options_multiple_tabs.js]
+[browser_toolbox_options_disable_buttons.js]
+[browser_toolbox_options_disable_cache-01.js]
+[browser_toolbox_options_disable_cache-02.js]
+[browser_toolbox_options_disable_cache-03.js]
+[browser_toolbox_options_disable_js.js]
+[browser_toolbox_options_enable_serviceworkers_testing.js]
+skip-if = verify || fission # Disable frequent fission intermittents Bug 1675020
+[browser_toolbox_options_frames_button.js]
+[browser_toolbox_options_panel_toggle.js]
+[browser_toolbox_raise.js]
+disabled=Bug 962258
+[browser_toolbox_races.js]
+[browser_toolbox_ready.js]
+[browser_toolbox_remoteness_change.js]
+run-if = e10s
+[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_target.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]
+[browser_toolbox_toolbar_overflow.js]
+[browser_toolbox_toolbar_overflow_button_visibility.js]
+[browser_toolbox_toolbar_reorder_by_dnd.js]
+[browser_toolbox_toolbar_reorder_by_width.js]
+[browser_toolbox_toolbar_reorder_with_extension.js]
+[browser_toolbox_toolbar_reorder_with_hidden_extension.js]
+[browser_toolbox_toolbar_reorder_with_secondary_toolbox.js]
+[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_window_reload_target.js]
+[browser_toolbox_window_reload_target_force.js]
+[browser_toolbox_window_shortcuts.js]
+[browser_toolbox_window_title_changes.js]
+skip-if = fission
+[browser_toolbox_window_title_frame_select.js]
+fail-if = fission
+[browser_toolbox_zoom.js]
+[browser_toolbox_zoom_popup.js]
+fail-if = a11y_checks # bug 1687737 tools-chevron-menu-button is not accessible
+[browser_two_tabs.js]
+# We want these tests to run for mochitest-dt as well, so we include them here:
+[../../../../browser/base/content/test/static/browser_parsable_css.js]
+skip-if = debug || asan || (os == 'linux' && bits == 32) # no point in running on both opt and debug, and will likely intermittently timeout on debug
+[../../../../browser/base/content/test/static/browser_all_files_referenced.js]
+skip-if = debug || asan || bits == 32 || ccov # no point in running on both opt and debug, and will likely intermittently timeout on debug, Bug 1598726
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..f29a5b095b
--- /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..8cbd21f570
--- /dev/null
+++ b/devtools/client/framework/test/browser_about-devtools-toolbox_reload.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that 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("devtools/client/shared/remote-debugging/remote-client-manager");
+ remoteClientManager.setClient(
+ "this-firefox",
+ "this-firefox",
+ devToolsClient,
+ {}
+ );
+ registerCleanupFunction(() => {
+ remoteClientManager.removeAllClients();
+ });
+
+ info("Create a dummy target tab");
+ const targetTab = await addTab("data:text/html,somehtml");
+
+ const { tab } = await openAboutToolbox({
+ id: targetTab.linkedBrowser.outerWindowID,
+ remoteId: "this-firefox-this-firefox",
+ type: "tab",
+ });
+
+ info("Reload about:devtools-toolbox page");
+ const 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("devtools/client/devtools-client");
+ const { DevToolsServer } = require("devtools/server/devtools-server");
+ 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_devtools_api_destroy.js b/devtools/client/framework/test/browser_devtools_api_destroy.js
new file mode 100644
index 0000000000..d04ab084fe
--- /dev/null
+++ b/devtools/client/framework/test/browser_devtools_api_destroy.js
@@ -0,0 +1,71 @@
+/* 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",
+ isTargetSupported: () => true,
+ url: "about:blank",
+ label: "someLabel",
+ build: function(iframeWindow, toolbox) {
+ return new Promise(resolve => {
+ executeSoon(() => {
+ resolve({
+ target: toolbox.target,
+ toolbox: toolbox,
+ isReady: true,
+ destroy: function() {},
+ });
+ });
+ });
+ },
+ };
+
+ gDevTools.registerTool(toolDefinition);
+
+ const collectedEvents = [];
+
+ const target = await TargetFactory.forTab(aTab);
+ gDevTools.showToolbox(target, 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_enable_devtools_popup.js b/devtools/client/framework/test/browser_enable_devtools_popup.js
new file mode 100644
index 0000000000..0133783ef6
--- /dev/null
+++ b/devtools/client/framework/test/browser_enable_devtools_popup.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper_enable_devtools_popup.js */
+loadHelperScript("helper_enable_devtools_popup.js");
+
+const TEST_URL =
+ "data:text/html,<html><head><title>Test Disable F12 experiment</title></head><body>" +
+ "<h1>Disable F12 experiment</h1></body></html>";
+
+// Test the basic behavior of the enable devtools popup depending on the value
+// of the devtools.experiment.f12.shortcut_disabled preference
+add_task(async function testWithF12Disabled() {
+ await pushPref("devtools.experiment.f12.shortcut_disabled", true);
+
+ const tab = await addTab(TEST_URL);
+ await new Promise(done => waitForFocus(done));
+
+ await checkF12IsDisabled(tab);
+ const toolbox = await openDevToolsWithInspectorKey(tab);
+ await closeDevToolsWithF12(tab, toolbox);
+ await checkF12IsEnabled(tab);
+});
+
+add_task(async function testWithF12Enabled() {
+ await pushPref("devtools.experiment.f12.shortcut_disabled", false);
+
+ const tab = await addTab(TEST_URL);
+ await new Promise(done => waitForFocus(done));
+
+ await checkF12IsEnabled(tab);
+});
diff --git a/devtools/client/framework/test/browser_enable_devtools_popup_devtools_user.js b/devtools/client/framework/test/browser_enable_devtools_popup_devtools_user.js
new file mode 100644
index 0000000000..6b43bce05a
--- /dev/null
+++ b/devtools/client/framework/test/browser_enable_devtools_popup_devtools_user.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper_enable_devtools_popup.js */
+loadHelperScript("helper_enable_devtools_popup.js");
+
+const TEST_URL =
+ "data:text/html,<html><head><title>Test Disable F12 experiment</title></head><body>" +
+ "<h1>Disable F12 experiment</h1></body></html>";
+
+// See the corresponding browser-enable-popup-devtools-user.ini for the
+// initialization of the prefs:
+// - devtools.experiment.f12.shortcut_disabled -> true
+// - devtools.selfxss.count -> 5
+//
+// Those prefs are set in the browser ini to run before DevToolsStartup.jsm init
+// logic. We expect devtools.selfxss.count to force shortcut_disabled to false.
+add_task(async function() {
+ const tab = await addTab(TEST_URL);
+ await new Promise(done => waitForFocus(done));
+
+ // With the shortcut initially disabled and the selfxss pref at 5, we expect
+ // the user to be considered as a devtools user and F12 to be immediately
+ // enabled.
+ await checkF12IsEnabled(tab);
+
+ const isF12Disabled = Services.prefs.getBoolPref(
+ "devtools.experiment.f12.shortcut_disabled"
+ );
+ ok(!isF12Disabled, "The F12 disabled preference has been correctly flipped");
+});
diff --git a/devtools/client/framework/test/browser_enable_devtools_popup_new_user.js b/devtools/client/framework/test/browser_enable_devtools_popup_new_user.js
new file mode 100644
index 0000000000..dca3cd8e93
--- /dev/null
+++ b/devtools/client/framework/test/browser_enable_devtools_popup_new_user.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper_enable_devtools_popup.js */
+loadHelperScript("helper_enable_devtools_popup.js");
+
+const TEST_URL =
+ "data:text/html,<html><head><title>Test Disable F12 experiment</title></head><body>" +
+ "<h1>Disable F12 experiment</h1></body></html>";
+
+// See the corresponding browser-enable-popup-devtools-user.ini for the
+// initialization of the prefs:
+// - devtools.experiment.f12.shortcut_disabled -> true
+// - devtools.selfxss.count -> 0
+//
+// Those prefs are set in the browser ini to run before DevToolsStartup.jsm init
+// logic. We expect devtools.selfxss.count to force shortcut_disabled to false.
+add_task(async function() {
+ const tab = await addTab(TEST_URL);
+ await new Promise(done => waitForFocus(done));
+
+ // With the shortcut initially disabled and the selfxss pref at 0, we expect
+ // the shortcut to be effectively disabled.
+ await checkF12IsDisabled(tab);
+ const toolbox = await openDevToolsWithInspectorKey(tab);
+ await closeDevToolsWithF12(tab, toolbox);
+ await checkF12IsEnabled(tab);
+});
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..1e13a792da
--- /dev/null
+++ b/devtools/client/framework/test/browser_front_parentFront.js
@@ -0,0 +1,40 @@
+/* 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,<div id="test"></div>`;
+
+add_task(async function() {
+ const tab = await addTab(TEST_URL);
+ const target = await TargetFactory.forTab(tab);
+ await target.attach();
+
+ 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..5f01a00ea7
--- /dev/null
+++ b/devtools/client/framework/test/browser_ignore_toolbox_network_requests.js
@@ -0,0 +1,29 @@
+/* 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 target = await TargetFactory.forTab(tab);
+ let toolbox = await gDevTools.showToolbox(target, "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 = target = 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..1a6eca82a7
--- /dev/null
+++ b/devtools/client/framework/test/browser_keybindings_01.js
@@ -0,0 +1,111 @@
+/* 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,<html><head><title>Test for the " +
+ "highlighter keybindings</title></head><body>" +
+ "<h1>Keybindings!</h1></body></html>";
+
+const {
+ gDevToolsBrowser,
+} = require("devtools/client/framework/devtools-browser");
+ChromeUtils.defineModuleGetter(
+ this,
+ "AppConstants",
+ "resource://gre/modules/AppConstants.jsm"
+);
+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: modifiers,
+ modifierOpt: {
+ shiftKey: modifiers.match("shift"),
+ ctrlKey: modifiers.match("ctrl"),
+ altKey: modifiers.match("alt"),
+ metaKey: modifiers.match("meta"),
+ accelKey: modifiers.match("accel"),
+ },
+ synthesizeKey: function() {
+ 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..55a5816cd5
--- /dev/null
+++ b/devtools/client/framework/test/browser_keybindings_02.js
@@ -0,0 +1,68 @@
+/* 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("devtools/client/framework/toolbox");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+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 target = await TargetFactory.forTab(tab);
+ const toolbox = await gDevTools.showToolbox(target, "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..2deab58c6a
--- /dev/null
+++ b/devtools/client/framework/test/browser_keybindings_03.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the 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("devtools/client/framework/toolbox");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+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 target = await TargetFactory.forTab(tab);
+ const toolbox = await gDevTools.showToolbox(target, "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..be08410e5f
--- /dev/null
+++ b/devtools/client/framework/test/browser_menu_api.js
@@ -0,0 +1,219 @@
+/* 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("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+
+add_task(async function() {
+ info("Create a test tab and open the toolbox");
+ const tab = await addTab(URL);
+ const target = await TargetFactory.forTab(tab);
+ const toolbox = await gDevTools.showToolbox(target, "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);
+
+ ok(toolbox.topDoc.querySelector("#menu-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");
+ EventUtils.synthesizeMouseAtCenter(menuItems[0], {}, toolbox.topWindow);
+ 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: submenu,
+ })
+ );
+ menu.append(
+ new MenuItem({
+ label: "Submenu parent with attributes",
+ id: "submenu-parent-with-attrs",
+ submenu: submenu,
+ accesskey: "A",
+ disabled: true,
+ })
+ );
+
+ menu.popup(0, 0, toolbox.doc);
+ ok(toolbox.topDoc.querySelector("#menu-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");
+
+ 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");
+ EventUtils.synthesizeMouseAtCenter(subMenuItems[0], {}, toolbox.topWindow);
+
+ 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..db9aa184be
--- /dev/null
+++ b/devtools/client/framework/test/browser_new_activation_workflow.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests devtools API
+
+var toolbox, target;
+
+function test() {
+ addTab("about:blank").then(async function(aTab) {
+ target = await TargetFactory.forTab(gBrowser.selectedTab);
+ loadWebConsole(aTab).then(function() {
+ console.log("loaded");
+ });
+ });
+}
+
+function loadWebConsole(aTab) {
+ ok(gDevTools, "gDevTools exists");
+
+ return gDevTools.showToolbox(target, "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.
+ target = await TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "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;
+ target = 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..fc2b8c485e
--- /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.import(
+ "resource://testing-common/PromiseTestUtils.jsm"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/this\.worker is null/);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/Component not initialized/);
+
+// Empty page
+const PAGE_URL = `${URL_ROOT}doc_empty-tab-01.html`;
+const JS_URL = `${URL_ROOT}code_binary_search.js`;
+const COFFEE_URL = `${URL_ROOT}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..fc1826bd74
--- /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.import(
+ "resource://testing-common/PromiseTestUtils.jsm"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/this\.worker is null/);
+
+// Empty page
+const PAGE_URL = `${URL_ROOT}doc_empty-tab-01.html`;
+const JS_URL = `${URL_ROOT}code_binary_search_absolute.js`;
+const ORIGINAL_URL = `${URL_ROOT}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..3d671d512b
--- /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,
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Empty test page to test cross domain source map</title>
+ </head>
+
+ <body>
+ <script src="${JS_URL}"></script>
+ </body>
+
+</html>`;
+
+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..6a0b6d9fed
--- /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 + "code_bundle_no_race.js";
+
+const PAGE_URL = `data:text/html,
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Empty test page to test race case</title>
+ </head>
+
+ <body>
+ <script src="${JS_URL}"></script>
+ </body>
+
+</html>`;
+
+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.target.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..a63f3b8cd0
--- /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.import(
+ "resource://testing-common/PromiseTestUtils.jsm"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/this\.worker is null/);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/Component not initialized/);
+
+const TEST_ROOT = "http://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..a08c1c4f47
--- /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}doc_empty-tab-01.html`;
+const JS_URL = URL_ROOT + "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..a5c4ab796c
--- /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,
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Empty test page to test race case</title>
+ </head>
+
+ <body>
+ <script src="${JS_URL}"></script>
+ </body>
+
+</html>`;
+
+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..4e05f353c7
--- /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,
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ </head>
+ <body>
+ <script src="${JS_URL}"></script>
+ </body>
+</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..8937c555ac
--- /dev/null
+++ b/devtools/client/framework/test/browser_source_map-reload.js
@@ -0,0 +1,53 @@
+/* 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 = URL_ROOT + "doc_empty-tab-01.html";
+const PAGE_URL = URL_ROOT + "doc_reload.html";
+const JS_URL = URL_ROOT + "sjs_code_reload.sjs";
+
+const ORIGINAL_URL_1 = "webpack:///code_reload_1.js";
+const ORIGINAL_URL_2 = "webpack:///code_reload_2.js";
+
+const GENERATED_LINE = 86;
+const ORIGINAL_LINE = 13;
+
+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");
+
+ // Reload the page. The sjs ensures that a different source file
+ // will be loaded.
+ sourceSeen = waitForSourceLoad(toolbox, JS_URL);
+ await refreshTab();
+ 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();
+ finish();
+});
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..e5df56e45f
--- /dev/null
+++ b/devtools/client/framework/test/browser_tab_descriptor_fission.js
@@ -0,0 +1,79 @@
+/* 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 =
+ "http://example.com/document-builder.sjs?html=<div id=com>com";
+const EXAMPLE_NET_URI =
+ "http://example.net/document-builder.sjs?html=<div id=net>net";
+
+add_task(async function() {
+ const tab = await addTab(EXAMPLE_COM_URI);
+ const target = await TargetFactory.forTab(tab);
+ const toolbox = await gDevTools.showToolbox(target);
+ const client = target.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");
+
+ is(
+ target.descriptorFront,
+ tabDescriptor,
+ "The toolbox target descriptor is the same as the descriptor returned by list 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_NET_URI);
+
+ info("Call list tabs again to update the tab descriptor forms");
+ await client.mainRoot.listTabs();
+
+ is(
+ decodeURIComponent(tabDescriptor.url),
+ EXAMPLE_NET_URI,
+ "The existing descriptor now points to the new URI"
+ );
+
+ const newTarget = toolbox.target;
+ const newTabDescriptor = newTarget.descriptorFront;
+ is(
+ newTabDescriptor,
+ tabDescriptor,
+ "The same tab descriptor instance is reused after navigating"
+ );
+
+ if (isFissionEnabled()) {
+ is(
+ comTabTarget.actorID,
+ null,
+ "With Fission, example.com target front is destroyed"
+ );
+ ok(
+ comTabTarget != newTarget,
+ "With Fission, a new target was created for example.net"
+ );
+ } else {
+ is(
+ comTabTarget,
+ newTarget,
+ "Without Fission, the example.com target is reused"
+ );
+ }
+});
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..dc2a86faa6
--- /dev/null
+++ b/devtools/client/framework/test/browser_target_cached-front.js
@@ -0,0 +1,24 @@
+/* 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 TargetFactory.forTab(gBrowser.selectedTab);
+ await target.attach();
+
+ info("Cached front when getFront has not been called");
+ let getCachedFront = target.getCachedFront("performance");
+ ok(!getCachedFront, "no front exists");
+
+ info("Cached front when getFront has been called but has not finished");
+ const asyncFront = target.getFront("performance");
+ getCachedFront = target.getCachedFront("performance");
+ ok(!getCachedFront, "no front exists");
+
+ info("Cached front when getFront has been called and has finished");
+ const front = await asyncFront;
+ getCachedFront = target.getCachedFront("performance");
+ 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..c9addcf621
--- /dev/null
+++ b/devtools/client/framework/test/browser_target_cached-resource.js
@@ -0,0 +1,48 @@
+/* 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 ResourceWatcher addeed listeners.
+// Test whether that feature works correctly or not.
+const TEST_URI =
+ "http://example.com/browser/devtools/client/framework/test/doc_cached-resource.html";
+const PARENT_MESSAGE = "Hello from parent";
+const CHILD_MESSAGE = "Hello from child";
+
+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(
+ findMessage(hud, PARENT_MESSAGE),
+ "Message from parent doument is in console"
+ );
+ ok(
+ findMessage(hud, CHILD_MESSAGE),
+ "Message from child doument is in console"
+ );
+
+ info("Clear the messages");
+ hud.ui.window.document.querySelector(".devtools-clear-icon").click();
+ await waitUntil(() => !findMessage(hud, PARENT_MESSAGE));
+
+ info("Reload the browsing page");
+ await navigateTo(TEST_URI);
+
+ info("Check the messages after reloading");
+ await waitUntil(
+ () => findMessage(hud, PARENT_MESSAGE) && findMessage(hud, CHILD_MESSAGE)
+ );
+ ok(true, "All messages are shown correctly");
+});
+
+function findMessage(hud, text, selector = ".message") {
+ const messages = hud.ui.outputNode.querySelectorAll(selector);
+ return Array.prototype.find.call(messages, el =>
+ el.textContent.includes(text)
+ );
+}
diff --git a/devtools/client/framework/test/browser_target_events.js b/devtools/client/framework/test/browser_target_events.js
new file mode 100644
index 0000000000..d72a0b1708
--- /dev/null
+++ b/devtools/client/framework/test/browser_target_events.js
@@ -0,0 +1,26 @@
+/* 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 TargetFactory.forTab(gBrowser.selectedTab);
+ await target.attach();
+ is(target.localTab, gBrowser.selectedTab, "Target linked to the right tab.");
+
+ const willNavigate = once(target, "will-navigate");
+ const navigate = once(target, "navigate");
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.location = "data:text/html,<meta charset='utf8'/>test navigation";
+ });
+ await willNavigate;
+ ok(true, "will-navigate event received");
+ await navigate;
+ ok(true, "navigate event received");
+
+ const close = once(target, "close");
+ gBrowser.removeCurrentTab();
+ await close;
+ ok(true, "close event received");
+});
diff --git a/devtools/client/framework/test/browser_target_from_url.js b/devtools/client/framework/test/browser_target_from_url.js
new file mode 100644
index 0000000000..d9fa11e3d0
--- /dev/null
+++ b/devtools/client/framework/test/browser_target_from_url.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI =
+ "data:text/html;charset=utf-8," + "<p>browser_target-from-url.js</p>";
+
+const { DevToolsLoader } = ChromeUtils.import(
+ "resource://devtools/shared/Loader.jsm"
+);
+const { targetFromURL } = require("devtools/client/framework/target-from-url");
+
+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, chrome = false) {
+ is(target.url, url);
+ is(target.isLocalTab, false);
+ is(target.chrome, chrome);
+ is(target.isBrowsingContext, true);
+}
+
+add_task(async function() {
+ const tab = await addTab(TEST_URI);
+ const browser = tab.linkedBrowser;
+ let target;
+
+ info("Test invalid type");
+ try {
+ await targetFromURL(new URL("http://foo?type=x"));
+ ok(false, "Shouldn't pass");
+ } catch (e) {
+ is(e.message, "targetFromURL, unsupported type 'x' parameter");
+ }
+
+ info("Test browser window");
+ let windowId = window.docShell.outerWindowID;
+ target = await targetFromURL(
+ new URL("http://foo?type=window&id=" + windowId)
+ );
+ is(target.url, window.location.href);
+ is(target.isLocalTab, false);
+ is(target.chrome, true);
+ is(target.isBrowsingContext, true);
+ await target.client.close();
+
+ info("Test tab");
+ windowId = browser.outerWindowID;
+ target = await targetFromURL(new URL("http://foo?type=tab&id=" + windowId));
+ assertTarget(target, TEST_URI);
+ await target.client.close();
+
+ info("Test tab with chrome privileges");
+ target = await targetFromURL(
+ new URL("http://foo?type=tab&id=" + windowId + "&chrome")
+ );
+ assertTarget(target, TEST_URI, true);
+ await target.client.close();
+
+ info("Test invalid tab id");
+ try {
+ await targetFromURL(new URL("http://foo?type=tab&id=10000"));
+ ok(false, "Shouldn't pass");
+ } catch (e) {
+ is(
+ e.message,
+ "targetFromURL, tab with outerWindowID '10000' doesn't exist"
+ );
+ }
+
+ info("Test parent process");
+ target = await targetFromURL(new URL("http://foo?type=process"));
+ const topWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ assertTarget(target, topWindow.location.href, true);
+ await target.client.close();
+
+ 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("devtools/server/devtools-server");
+ const { SocketListener } = loader.require("devtools/shared/security/socket");
+
+ 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 target = await targetFromURL(
+ new URL("http://foo?type=process&host=127.0.0.1&port=" + port)
+ );
+ const topWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ assertTarget(target, topWindow.location.href, true);
+
+ const settings = target.client._transport.connectionSettings;
+ is(settings.host, "127.0.0.1");
+ is(parseInt(settings.port, 10), port);
+ is(settings.webSocket, false);
+
+ await target.client.close();
+
+ teardownDevToolsServer(server);
+}
+
+async function testRemoteWebSocket() {
+ info("Test remote process via WebSocket Connection");
+
+ const server = await setupDevToolsServer(true);
+
+ const { port } = server.listener;
+ const target = await targetFromURL(
+ new URL("http://foo?type=process&host=127.0.0.1&port=" + port + "&ws=true")
+ );
+ const topWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ assertTarget(target, topWindow.location.href, true);
+
+ const settings = target.client._transport.connectionSettings;
+ is(settings.host, "127.0.0.1");
+ is(parseInt(settings.port, 10), port);
+ is(settings.webSocket, true);
+ await target.client.close();
+
+ teardownDevToolsServer(server);
+}
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..b5146b971f
--- /dev/null
+++ b/devtools/client/framework/test/browser_target_get-front.js
@@ -0,0 +1,115 @@
+/* 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 TargetFactory.forTab(tab);
+ await target.attach();
+
+ const tab2 = await addTab("about:blank");
+ const target2 = await TargetFactory.forTab(tab2);
+ await target2.attach();
+
+ 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("performance");
+ const asyncFront2 = target.getFront("performance");
+
+ 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 TargetFactory.forTab(tab);
+ await target.attach();
+
+ // do not wait for the front to finish loading
+ target.getFront("performance");
+
+ 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..1e6189c5b5
--- /dev/null
+++ b/devtools/client/framework/test/browser_target_listeners.js
@@ -0,0 +1,34 @@
+/* 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 TargetFactory.forTab(gBrowser.selectedTab);
+ await target.attach();
+
+ 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_parents.js b/devtools/client/framework/test/browser_target_parents.js
new file mode 100644
index 0000000000..1f038b7260
--- /dev/null
+++ b/devtools/client/framework/test/browser_target_parents.js
@@ -0,0 +1,142 @@
+/* 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("devtools/client/devtools-client");
+const { DevToolsServer } = require("devtools/server/devtools-server");
+
+const TEST_URL = `data:text/html;charset=utf-8,<div id="test"></div>`;
+
+// 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();
+
+ await testGetTargetWithConcurrentCalls(tabDescriptors, tabTarget => {
+ // Tab Target is attached when it has a console front.
+ return !!tabTarget.getCachedFront("console");
+ });
+
+ 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 => {
+ // Content Process Target is attached when it has a console front.
+ return !!processTarget.getCachedFront("console");
+ });
+
+ 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();
+ is(
+ addonFront.descriptorFront,
+ addonDescriptorFront,
+ "Got the correct descriptorFront from 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();
+ await Promise.all(
+ workers.map(workerTargetFront => {
+ is(
+ workerTargetFront.descriptorFront,
+ null,
+ "For now, worker target don't have descriptor fronts (see bug 1573779)"
+ );
+ })
+ );
+
+ 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 => {
+ is(
+ target.descriptorFront,
+ descriptor,
+ "Got the correct descriptorFront from the frame 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..f4e8a36b5a
--- /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("close", () => {
+ ok(true, "Target was closed");
+ 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..66e42040ab
--- /dev/null
+++ b/devtools/client/framework/test/browser_target_server_compartment.js
@@ -0,0 +1,137 @@
+/* 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 target = await TargetFactory.forTab(tab);
+ await target.attach();
+
+ const threadFront = await target.attachThread();
+
+ const { sources } = await threadFront.getSources();
+ 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 target.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 { DevToolsLoader } = ChromeUtils.import(
+ "resource://devtools/shared/Loader.jsm"
+ );
+ const customLoader = new DevToolsLoader({
+ invisibleToDebugger: true,
+ });
+ const { DevToolsServer } = customLoader.require(
+ "devtools/server/devtools-server"
+ );
+ const { DevToolsClient } = require("devtools/client/devtools-client");
+
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+ DevToolsServer.allowChromeProcess = true;
+
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ await client.connect();
+
+ 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 targetDescriptor = await client.mainRoot.getMainProcess();
+ const target = await targetDescriptor.getTarget();
+ await target.attach();
+
+ const threadFront = await target.attachThread();
+ const { sources } = await threadFront.getSources();
+ 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"
+ );
+
+ await target.destroy();
+
+ // 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 client.close();
+
+ // As we create the loader and server manually, we also destroy them manually here:
+ await DevToolsServer.destroy();
+ await customLoader.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..0abec544d8
--- /dev/null
+++ b/devtools/client/framework/test/browser_target_support.js
@@ -0,0 +1,47 @@
+/* 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) {
+ await target.attach();
+
+ is(
+ target.hasActor("inspector"),
+ true,
+ "target.hasActor() true when actor exists."
+ );
+ is(
+ target.hasActor("storage"),
+ 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("close", () => {
+ ok(true, "Target was closed");
+ finish();
+ });
+ client.close();
+}
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..46835aec0f
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_browsertoolbox_host.js
@@ -0,0 +1,28 @@
+/* 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("devtools/client/framework/toolbox");
+
+ const tab = await addTab(TEST_URL);
+ const target = await TargetFactory.forTab(tab);
+ const options = { doc: document };
+ const toolbox = await gDevTools.showToolbox(
+ target,
+ null,
+ Toolbox.HostType.BROWSERTOOLBOX,
+ 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..050817b21b
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_contentpage_contextmenu.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = "data:text/html;charset=utf8,<div>test content context menu</div>";
+
+/**
+ * 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 target = await TargetFactory.forTab(tab);
+ const toolbox = await gDevTools.showToolbox(target, "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");
+ EventUtils.sendKey("ESCAPE", toolbox.win);
+ await onContextMenuHidden;
+
+ await toolbox.destroy();
+}
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..a5e2438cbd
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_dynamic_registration.js
@@ -0,0 +1,88 @@
+/* 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 => {
+ const target = await TargetFactory.forTab(tab);
+ gDevTools.showToolbox(target).then(testRegister);
+ });
+}
+
+function testRegister(aToolbox) {
+ toolbox = aToolbox;
+ gDevTools.once("tool-registered", toolRegistered);
+
+ gDevTools.registerTool({
+ id: "testTool",
+ label: "Test Tool",
+ inMenu: true,
+ isTargetSupported: () => true,
+ build: function() {},
+ });
+}
+
+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..17809862b0
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_error_count.js
@@ -0,0 +1,182 @@
+/* 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
+const TEST_URI = `http://example.org/document-builder.sjs?html=<meta charset=utf8></meta>
+<script>
+ console.error("Cache Error1");
+ console.exception(false, "Cache Exception");
+ console.warn("Cache warning");
+ console.assert(false, "Cache assert");
+ cache.unknown.access
+</script><body>`;
+
+const { Toolbox } = require("devtools/client/framework/toolbox");
+
+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 webconsoleDoc = toolbox.getCurrentPanel().hud.ui.window.document;
+ // wait until all error messages are displayed in the console
+ await waitFor(
+ () =>
+ webconsoleDoc.querySelectorAll(".message.error").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(
+ () => webconsoleDoc.querySelectorAll(".message.error").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(
+ () =>
+ webconsoleDoc.querySelectorAll(".message.error").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"
+ );
+ tab.linkedBrowser.reload();
+
+ // (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(
+ () =>
+ webconsoleDoc.querySelectorAll(".message.error").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(
+ () =>
+ webconsoleDoc.querySelectorAll(".message.error").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();
+});
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..f4f5a5e993
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_error_count_reset_on_navigation.js
@@ -0,0 +1,78 @@
+/* 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=<meta charset=utf8></meta>
+<script>
+ console.error("Cache Error1");
+ console.exception(false, "Cache Exception");
+ console.warn("Cache warning");
+ console.assert(false, "Cache assert");
+ cache.unknown.access
+</script>`;
+
+const { Toolbox } = require("devtools/client/framework/toolbox");
+
+add_task(async function() {
+ // 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"
+ );
+ tab.linkedBrowser.reload();
+
+ 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"
+ );
+ 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_getpanelwhenready.js b/devtools/client/framework/test/browser_toolbox_getpanelwhenready.js
new file mode 100644
index 0000000000..328153f586
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_getpanelwhenready.js
@@ -0,0 +1,40 @@
+/* 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);
+ const target = await TargetFactory.forTab(tab);
+ toolbox = await gDevTools.showToolbox(target);
+
+ 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..162a2478b2
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_highlight.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { Toolbox } = require("devtools/client/framework/toolbox");
+
+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);
+
+ const target = await TargetFactory.forTab(gBrowser.selectedTab);
+ toolbox = await gDevTools.showToolbox(
+ target,
+ TOOL_ID_1,
+ 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(
+ 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(
+ 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(
+ !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..5c6a3385ef
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_hosts.js
@@ -0,0 +1,195 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { Toolbox } = require("devtools/client/framework/toolbox");
+var { LEFT, RIGHT, BOTTOM, WINDOW } = Toolbox.HostType;
+var toolbox, target;
+
+const URL =
+ "data:text/html;charset=utf8,test for opening toolbox in different hosts";
+
+add_task(async function runTest() {
+ info("Create a test tab and open the toolbox");
+ const tab = await addTab(URL);
+ target = await TargetFactory.forTab(tab);
+ toolbox = await gDevTools.showToolbox(target, "webconsole");
+
+ await runHostTests(gBrowser);
+ await toolbox.destroy();
+
+ toolbox = target = 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;
+ target = await TargetFactory.forTab(tab);
+ toolbox = await gDevTools.showToolbox(target, "webconsole");
+
+ await runHostTests(browser);
+ await toolbox.destroy();
+
+ toolbox = target = 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();
+ target = await TargetFactory.forTab(browser.selectedTab);
+ toolbox = await gDevTools.showToolbox(target);
+}
+
+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..5f7fe34222
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_hosts_size.js
@@ -0,0 +1,103 @@
+/* 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";
+
+var { Toolbox } = require("devtools/client/framework/toolbox");
+
+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 target = await TargetFactory.forTab(tab);
+ const toolbox = await gDevTools.showToolbox(target);
+
+ 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"
+ );
+
+ 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"
+ );
+
+ // on shutdown, the sidebar width will be set to the clientWidth of the iframe
+ const expectedWidth = iframe.clientWidth;
+ 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"
+ );
+ 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 target = await TargetFactory.forTab(tab);
+ const toolbox = await gDevTools.showToolbox(target);
+
+ 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");
+
+ 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");
+
+ await cleanup(toolbox);
+});
+
+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..3815de7397
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_hosts_telemetry.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from head.js */
+
+"use strict";
+
+const { Toolbox } = require("devtools/client/framework/toolbox");
+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 target = await TargetFactory.forTab(tab);
+ const toolbox = await gDevTools.showToolbox(target, "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..7daf7056cf
--- /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..3bf13e9a85
--- /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 = `<link rel="stylesheet" type="text/css" href="http://mochi.test:1234/invalid.port">`;
+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..b8ed729ea5
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_meatball.js
@@ -0,0 +1,132 @@
+/* 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("devtools/client/shared/focus");
+const { Toolbox } = require("devtools/client/framework/toolbox");
+
+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"
+ );
+ ok(
+ 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..3ecff98958
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options.js
@@ -0,0 +1,558 @@
+/* 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 { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/toolbox.properties"
+);
+const { PrefObserver } = require("devtools/client/shared/prefs");
+
+add_task(async function() {
+ const URL =
+ "data:text/html;charset=utf8,test for dynamically registering " +
+ "and unregistering tools";
+ registerNewTool();
+ const tab = await addTab(URL);
+ const target = await TargetFactory.forTab(tab);
+ toolbox = await gDevTools.showToolbox(target);
+
+ 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",
+ isTargetSupported: () => 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",
+ isTargetSupported: () => 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 > 0,
+ "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..9d5e9a608a
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js
@@ -0,0 +1,220 @@
+/* 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 += '<iframe src="data:text/plain,iframe"></iframe>';
+// The error count button is only shown if there are errors on the page
+TEST_URL += '<script>console.error("err")</script>';
+
+var modifiedPrefs = [];
+registerCleanupFunction(() => {
+ for (const pref of modifiedPrefs) {
+ Services.prefs.clearUserPref(pref);
+ }
+});
+
+add_task(async function test() {
+ const tab = await addTab(TEST_URL);
+ const target = await TargetFactory.forTab(tab);
+ let toolbox = await gDevTools.showToolbox(target);
+ 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 Fission Preferences etc...)
+ const target = await TargetFactory.forTab(gBrowser.selectedTab);
+ const toolbarButtons = toolbox.toolbarButtons.filter(tool =>
+ tool.isTargetSupported(target)
+ );
+
+ 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."
+ );
+ }
+ } 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
+ );
+ }
+}
+
+async function testPrefsAreRespectedWhenReopeningToolbox() {
+ const target = await TargetFactory.forTab(gBrowser.selectedTab);
+ info("Closing toolbox to test after reopening");
+ await gDevTools.closeToolbox(target);
+
+ const tabTarget = await TargetFactory.forTab(gBrowser.selectedTab);
+ const toolbox = await gDevTools.showToolbox(tabTarget);
+ 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) {
+ const isChecked = waitUntil(() => button.classList.contains("checked"));
+
+ button.click();
+ await isChecked;
+ ok(
+ button.classList.contains("checked"),
+ `Button for ${toolboxButton} can be toggled on`
+ );
+
+ const isUnchecked = waitUntil(
+ () => !button.classList.contains("checked")
+ );
+ button.click();
+ await isUnchecked;
+ ok(
+ !button.classList.contains("checked"),
+ `Button for ${toolboxButton} can be 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..50b4966f6d
--- /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..466ff9e362
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_cache-02.js
@@ -0,0 +1,49 @@
+/* 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.showToolbox(tabs[2].target, "options");
+ await checkCacheEnabled(tabs[2], false);
+
+ // Close toolbox in tab 2 and ensure the cache is enabled again
+ await tabs[2].toolbox.destroy();
+ tabs[2].target = await TargetFactory.forTab(tabs[2].tab);
+ await checkCacheEnabled(tabs[2], true);
+
+ // Open toolbox in tab 2 and ensure the cache is then disabled.
+ tabs[2].toolbox = await gDevTools.showToolbox(tabs[2].target, "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..32f361e842
--- /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 = `<html>
+ <head>
+ <meta charset="utf-8"/>
+ <link href="${TEST_CSS}" rel="stylesheet" type="text/css"/>
+ </head>
+ <body></body>
+</html>`;
+
+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 = 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..59012aec88
--- /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..c6c336981a
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_cache.sjs
@@ -0,0 +1,28 @@
+ /* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ let Etag = '"4d881ab-b03-435f0a0f9ef00"';
+ let IfNoneMatch = request.hasHeader("If-None-Match")
+ ? request.getHeader("If-None-Match")
+ : "";
+
+ let guid = 'xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ let r = Math.random() * 16 | 0;
+ let v = c === "x" ? r : (r & 0x3 | 0x8);
+
+ return v.toString(16);
+ });
+
+ let page = "<!DOCTYPE html><html><body><h1>" + guid + "</h1></body></html>";
+
+ 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..766c034e4c
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_js.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>browser_toolbox_options_disablejs.html</title>
+ <meta charset="UTF-8">
+ <style>
+ div {
+ width: 260px;
+ height: 24px;
+ border: 1px solid #000;
+ margin-top: 10px;
+ }
+
+ iframe {
+ height: 90px;
+ border: 1px solid #000;
+ }
+
+ h1 {
+ font-size: 20px
+ }
+ </style>
+ <script type="application/javascript">
+ /* exported log */
+ function log(msg) {
+ const output = document.getElementById("output");
+
+ // eslint-disable-next-line no-unsanitized/property
+ output.innerHTML = msg;
+ }
+ </script>
+ </head>
+ <body>
+ <h1>Test in page</h1>
+ <input id="logJSEnabled"
+ type="button"
+ value="Log JS Enabled"
+ onclick="log('JavaScript Enabled')"/>
+ <input id="logJSDisabled"
+ type="button"
+ value="Log JS Disabled"
+ onclick="log('JavaScript Disabled')"/>
+ <br>
+ <div id="output">No output</div>
+ <h1>Test in iframe</h1>
+ <iframe src="browser_toolbox_options_disable_js_iframe.html"></iframe>
+ </body>
+</html>
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..590ab264e0
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_js.js
@@ -0,0 +1,131 @@
+/* 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 + "browser_toolbox_options_disable_js.html";
+
+add_task(async function() {
+ const tab = await addTab(TEST_URI);
+ const target = await TargetFactory.forTab(tab);
+ const toolbox = await gDevTools.showToolbox(target);
+
+ await toolbox.selectTool("options");
+ ok(true, "Toolbox selected via selectTool method");
+
+ await testJSEnabled();
+ await testJSEnabledIframe();
+
+ // Disable JS.
+ await toggleJS(toolbox);
+
+ 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 docShell.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 } = toolbox.target.configureOptions;
+ is(
+ javascriptEnabled,
+ !cbx.checked,
+ "BrowsingContextTargetFront's configureOptions is correct before the toggle"
+ );
+
+ const browserLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ cbx.click();
+ await browserLoaded;
+
+ ({ javascriptEnabled } = toolbox.target.configureOptions);
+ is(
+ javascriptEnabled,
+ !cbx.checked,
+ "BrowsingContextTargetFront's configureOptions 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();
+
+ ok(
+ 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();
+ ok(
+ 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..709972f023
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_js_iframe.html
@@ -0,0 +1,35 @@
+<html>
+ <head>
+ <title>browser_toolbox_options_disablejs.html</title>
+ <meta charset="UTF-8">
+ <style>
+ div {
+ width: 260px;
+ height: 24px;
+ border: 1px solid #000;
+ margin-top: 10px;
+ }
+ </style>
+ <script type="application/javascript">
+ /* exported log */
+ function log(msg) {
+ const output = document.getElementById("output");
+
+ // eslint-disable-next-line no-unsanitized/property
+ output.innerHTML = msg;
+ }
+ </script>
+ </head>
+ <body>
+ <input id="logJSEnabled"
+ type="button"
+ value="Log JS Enabled"
+ onclick="log('JavaScript Enabled')"/>
+ <input id="logJSDisabled"
+ type="button"
+ value="Log JS Disabled"
+ onclick="log('JavaScript Disabled')"/>
+ <br>
+ <div id="output">No output</div>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>browser_toolbox_options_enable_serviceworkers_testing.html</title>
+ <meta charset="UTF-8">
+ </head>
+ <body>
+ <h1>SW-test</h1>
+ <script>
+ function register() {
+ return Promise.resolve().then(function() {
+ // While ServiceWorkerContainer.register() returns a promise, it's
+ // still wrapped with a .then() because navigator.serviceWorker is not
+ // defined in insecure contexts unless service worker testing is
+ // enabled, so dereferencing it would throw a ReferenceError (which
+ // is then caught in the .catch() clause).
+ return window.navigator.serviceWorker.register("serviceworker.js");
+ }).then(registration => {
+ return {success: true};
+ }).catch(error => {
+ return {success: false};
+ });
+ }
+
+ function unregister() {
+ return Promise.resolve().then(function() {
+ return window.navigator.serviceWorker.getRegistration();
+ }).then(registration => {
+ return registration.unregister().then(result => {
+ return {success: !!result};
+ });
+ }).catch(_ => {
+ return {success: false};
+ });
+ }
+
+ function iframeRegisterAndUnregister() {
+ var frame = window.document.createElement("iframe");
+ var promise = new Promise(function(resolve, reject) {
+ frame.addEventListener("load", function() {
+ Promise.resolve().then(_ => {
+ return frame.contentWindow.navigator.serviceWorker.register("serviceworker.js");
+ }).then(swr => {
+ return swr.unregister();
+ }).then(_ => {
+ frame.remove();
+ resolve({success: true});
+ }).catch(error => {
+ resolve({success: false});
+ });
+ }, {once: true});
+ });
+ frame.src = "browser_toolbox_options_enabled_serviceworkers_testing.html";
+ window.document.body.appendChild(frame);
+ return promise;
+ }
+
+ window.addEventListener("message", function(event) {
+ var response;
+ switch (event.data) {
+ case "devtools:sw-test:register": {
+ response = register();
+ break;
+ }
+ case "devtools:sw-test:unregister": {
+ response = unregister();
+ break;
+ }
+ case "devtools:sw-test:iframe:register-and-unregister": {
+ response = iframeRegisterAndUnregister();
+ break;
+ }
+ }
+ response.then(data => {
+ event.ports[0].postMessage(data);
+ event.ports[0].close();
+ });
+ });
+ </script>
+ </body>
+</html>
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..554a569491
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js
@@ -0,0 +1,128 @@
+/* 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.
+
+const TEST_URI =
+ URL_ROOT + "browser_toolbox_options_enable_serviceworkers_testing.html";
+
+const ELEMENT_ID = "devtools-enable-serviceWorkersTesting";
+
+var toolbox;
+
+function test() {
+ // Note: Pref dom.serviceWorkers.testing.enabled is false since we are testing
+ // the same capabilities are enabled with the devtool pref.
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", false],
+ ],
+ },
+ init
+ );
+}
+
+function init() {
+ addTab(TEST_URI).then(async tab => {
+ const target = await TargetFactory.forTab(tab);
+ gDevTools.showToolbox(target).then(testSelectTool);
+ });
+}
+
+function testSelectTool(aToolbox) {
+ toolbox = aToolbox;
+ toolbox.once("options-selected", start);
+ toolbox.selectTool("options");
+}
+
+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");
+}
+
+function testRegisterFails(data) {
+ is(data.success, false, "Register should fail with security error");
+ return promise.resolve();
+}
+
+function toggleServiceWorkersTestingCheckbox() {
+ const panel = toolbox.getCurrentPanel();
+ const cbx = panel.panelDoc.getElementById(ELEMENT_ID);
+
+ cbx.scrollIntoView();
+
+ if (cbx.checked) {
+ info("Clearing checkbox to disable service workers testing");
+ } else {
+ info("Checking checkbox to enable service workers testing");
+ }
+
+ cbx.click();
+
+ return promise.resolve();
+}
+
+function reload() {
+ const promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.location.reload(false);
+ });
+ return promise;
+}
+
+function testRegisterSuccesses(data) {
+ is(data.success, true, "Register should success");
+ return promise.resolve();
+}
+
+function start() {
+ register()
+ .then(testRegisterFails)
+ .then(toggleServiceWorkersTestingCheckbox)
+ .then(reload)
+ .then(register)
+ .then(testRegisterSuccesses)
+ .then(unregister)
+ .then(registerAndUnregisterInFrame)
+ .then(testRegisterSuccesses)
+ // Workers should be turned back off when we closes the toolbox
+ .then(toolbox.destroy.bind(toolbox))
+ .then(reload)
+ .then(register)
+ .then(testRegisterFails)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ })
+ .then(finishUp);
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+ toolbox = null;
+ finish();
+}
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..e23565a2c6
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_frames_button.js
@@ -0,0 +1,63 @@
+/* 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_URL_FRAMES =
+ TEST_URL + '<iframe src="data:text/plain,iframe"></iframe>';
+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);
+ const target = await TargetFactory.forTab(tab);
+
+ info("Open the toolbox on the Options panel");
+ const toolbox = await gDevTools.showToolbox(target, "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();
+
+ framesButton = doc.getElementById("command-button-frames");
+ ok(framesButton, "Frames button is rendered.");
+ 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.");
+
+ 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 waitUntil(() => {
+ framesButton = doc.getElementById("command-button-frames");
+ return framesButton && !framesButton.disabled;
+ });
+
+ 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..cbb3243d69
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_multiple_tabs.js
@@ -0,0 +1,131 @@
+/* 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 target = await TargetFactory.forTab(tab);
+ const toolbox = await gDevTools.showToolbox(target);
+ 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"}`
+ );
+ await 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..0204708c2c
--- /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("devtools/client/framework/toolbox");
+
+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_races.js b/devtools/client/framework/test/browser_toolbox_races.js
new file mode 100644
index 0000000000..267aea1cb1
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_races.js
@@ -0,0 +1,93 @@
+/* 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("devtools/client/framework/devtools-browser");
+
+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 created = 0,
+ ready = 0,
+ destroy = 0,
+ destroyed = 0;
+ const onCreated = () => {
+ created++;
+ };
+ const onReady = () => {
+ ready++;
+ };
+ const onDestroy = () => {
+ destroy++;
+ };
+ const onDestroyed = () => {
+ destroyed++;
+ };
+ gDevTools.on("toolbox-created", onCreated);
+ 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.
+
+ // Toggle the toolbox at least 3 times.
+ info("Trying to toggle the toolbox 3 times");
+ while (created < 3) {
+ // Sent multiple event to try to race the code during toolbox creation and destruction
+ toggle();
+ toggle();
+ toggle();
+
+ // Release the event loop to let a chance to actually create or destroy the toolbox!
+ await wait(50);
+ }
+ info("Toggled the toolbox 3 times");
+
+ // Now wait for the 3rd toolbox to be fully ready before closing it.
+ // We close the last toolbox manually, out of the first while() loop to
+ // avoid races and be sure we end up we no toolbox and waited for all the
+ // requests to be done.
+ while (ready != 3) {
+ await wait(100);
+ }
+ toggle();
+ while (destroyed != 3) {
+ await wait(100);
+ }
+
+ is(created, 3, "right number of created events");
+ is(ready, 3, "right number of ready events");
+ is(destroy, 3, "right number of destroy events");
+ is(destroyed, 3, "right number of destroyed events");
+
+ gDevTools.off("toolbox-created", onCreated);
+ gDevTools.off("toolbox-ready", onReady);
+ gDevTools.off("toolbox-destroy", onDestroy);
+ gDevTools.off("toolbox-destroyed", onDestroyed);
+
+ 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..945b1dfb47
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_raise.js
@@ -0,0 +1,91 @@
+/* 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("devtools/client/framework/toolbox");
+
+var toolbox, tab1, tab2;
+
+function test() {
+ addTab(TEST_URL).then(async tab => {
+ tab2 = BrowserTestUtils.addTab(gBrowser);
+ const target = await TargetFactory.forTab(tab);
+ gDevTools
+ .showToolbox(target)
+ .then(testBottomHost, console.error)
+ .catch(console.error);
+ });
+}
+
+function testBottomHost(aToolbox) {
+ toolbox = aToolbox;
+
+ // switch to another tab and test toolbox.raise()
+ gBrowser.selectedTab = tab2;
+ executeSoon(function() {
+ is(
+ gBrowser.selectedTab,
+ tab2,
+ "Correct tab is selected before calling raise"
+ );
+ toolbox.raise();
+ executeSoon(function() {
+ is(
+ gBrowser.selectedTab,
+ tab1,
+ "Correct tab was selected after calling raise"
+ );
+
+ toolbox
+ .switchHost(Toolbox.HostType.WINDOW)
+ .then(testWindowHost)
+ .catch(console.error);
+ });
+ });
+}
+
+function testWindowHost() {
+ // Make sure toolbox is not focused.
+ window.addEventListener("focus", onFocus, true);
+
+ // Need to wait for focus as otherwise window.focus() is overridden by
+ // toolbox window getting focused first on Linux and Mac.
+ const onToolboxFocus = () => {
+ toolbox.win.parent.removeEventListener("focus", onToolboxFocus, true);
+ info("focusing main window.");
+ window.focus();
+ };
+ // Need to wait for toolbox window to get focus.
+ toolbox.win.parent.addEventListener("focus", onToolboxFocus, true);
+}
+
+function onFocus() {
+ info("Main window is focused before calling toolbox.raise()");
+ window.removeEventListener("focus", onFocus, true);
+
+ // Check if toolbox window got focus.
+ const onToolboxFocusAgain = () => {
+ toolbox.win.parent.removeEventListener("focus", onToolboxFocusAgain);
+ ok(
+ true,
+ "Toolbox window is the focused window after calling toolbox.raise()"
+ );
+ cleanup();
+ };
+ toolbox.win.parent.addEventListener("focus", onToolboxFocusAgain);
+
+ // Now raise toolbox.
+ toolbox.raise();
+}
+
+function cleanup() {
+ Services.prefs.setCharPref("devtools.toolbox.host", Toolbox.HostType.BOTTOM);
+
+ toolbox.destroy().then(function() {
+ toolbox = null;
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
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..5c89defd70
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_ready.js
@@ -0,0 +1,19 @@
+/* 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 target = await TargetFactory.forTab(tab);
+
+ const toolbox = await gDevTools.showToolbox(target, "webconsole");
+ ok(toolbox.isReady, "toolbox isReady is set");
+ ok(toolbox.threadFront, "toolbox has a thread front");
+
+ const toolbox2 = await gDevTools.showToolbox(toolbox.target, 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..a100910662
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_remoteness_change.js
@@ -0,0 +1,91 @@
+/* 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('<div id="remote-page">foo</div>');
+
+add_task(async function() {
+ // Test twice.
+ // Once without target switching, where the toolbox closes and reopens
+ // And a second time, with target switching, where the toolbox stays open
+ await navigateBetweenProcesses(false);
+ await navigateBetweenProcesses(true);
+});
+
+async function navigateBetweenProcesses(enableTargetSwitching) {
+ info(
+ `Testing navigation between processes ${
+ enableTargetSwitching ? "with" : "without"
+ } target switching`
+ );
+ await pushPref("devtools.target-switching.enabled", enableTargetSwitching);
+
+ 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"
+ );
+
+ let toolbox = await openToolboxForTab(tab);
+
+ const onToolboxDestroyed = toolbox.once("destroyed");
+ const onToolboxCreated = gDevTools.once("toolbox-created");
+ const onToolboxSwitchedToTarget = toolbox.targetList.once("switched-target");
+
+ info("Navigate to a URL supporting remote process");
+ if (enableTargetSwitching) {
+ await navigateTo(URL_2);
+ } else {
+ // `navigateTo` except the toolbox to be kept open.
+ // So, fallback to BrowserTestUtils helpers in this test when
+ // the target-switching preference is turned off.
+ const onBrowserLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ BrowserTestUtils.loadURI(tab.linkedBrowser, URL_2);
+ await onBrowserLoaded;
+ }
+
+ is(
+ tab.linkedBrowser.getAttribute("remote"),
+ "true",
+ "Navigated to a data: URI and switching to remote"
+ );
+
+ if (enableTargetSwitching) {
+ info("Waiting for the toolbox to be switched to the new target");
+ await onToolboxSwitchedToTarget;
+ } else {
+ info("Waiting for the toolbox to be destroyed");
+ await onToolboxDestroyed;
+
+ info("Waiting for a new toolbox to be created");
+ toolbox = await onToolboxCreated;
+
+ info("Waiting for the new toolbox to be ready");
+ await toolbox.once("ready");
+ }
+
+ 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._closed, "The client is closed after closing the toolbox");
+}
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..5bb888e3d1
--- /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..b813525a3d
--- /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",
+ isTargetSupported: () => true,
+ visibilityswitch: "devtools.test-tool.enabled",
+ url: "about:blank",
+ label: "someLabel",
+ build: (iframeWindow, toolbox) => {
+ return {
+ target: toolbox.target,
+ toolbox: toolbox,
+ isReady: true,
+ destroy: () => {},
+ panelDoc: iframeWindow.document,
+ };
+ },
+};
+
+add_task(async function() {
+ gDevTools.registerTool(testToolDefinition);
+ let tab = await addTab("about:blank");
+ let target = await TargetFactory.forTab(tab);
+
+ let toolbox = await gDevTools.showToolbox(target, testToolDefinition.id);
+ is(toolbox.currentToolId, "testTool", "test-tool was selected");
+ await toolbox.destroy();
+
+ // Make the previously selected tool unavailable.
+ testToolDefinition.isTargetSupported = () => false;
+
+ target = await TargetFactory.forTab(tab);
+ toolbox = await gDevTools.showToolbox(target);
+ is(toolbox.currentToolId, "webconsole", "web console was selected");
+
+ await toolbox.destroy();
+ gDevTools.unregisterTool(testToolDefinition.id);
+ tab = toolbox = target = 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..0f0b2b4a9f
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_selectionchanged_event.js
@@ -0,0 +1,46 @@
+/* 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,<body><div></div></body>";
+
+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(undefined, {
+ reason: "browser-context-menu",
+ });
+
+ await onClearSelectionChanged;
+
+ is(
+ inspector.selection.nodeFront,
+ undefined,
+ "The selection is undefined 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..0917d0f3b3
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_show_toolbox_tool_ready.js
@@ -0,0 +1,78 @@
+/* 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);
+ this.emit("ready");
+ 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() {
+ // We have to disable CSP for this test otherwise the CSP of
+ // about:devtools-toolbox will block the data: url.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.csp.enable", false],
+ ["dom.security.skip_about_page_has_csp_assert", true],
+ ],
+ });
+
+ info(
+ "Registering a tool with an input field and making sure the context menu works"
+ );
+
+ gDevTools.registerTool({
+ id: lazyToolId,
+ isTargetSupported: () => true,
+ url: `data:text/html;charset=utf8,Lazy tool`,
+ label: "Lazy",
+ build: function(iframeWindow, toolbox) {
+ this.panel = new LazyDevToolsPanel(iframeWindow, toolbox);
+ return this.panel.open();
+ },
+ });
+
+ const toolbox = await openNewTabAndToolbox(URL, "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.showToolbox(toolbox.target, 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..0fa3f569bb
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_split_console.js
@@ -0,0 +1,85 @@
+/* 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);
+ const target = await TargetFactory.forTab(tab);
+ gToolbox = await gDevTools.showToolbox(target, "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..047842ce1b
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+var { Toolbox } = require("devtools/client/framework/toolbox");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/toolbox.properties"
+);
+
+add_task(async function() {
+ const tab = await addTab("about:blank");
+ const target = await TargetFactory.forTab(tab);
+
+ const toolIDs = gDevTools
+ .getToolDefinitionArray()
+ .filter(def => def.isTargetSupported(target) && def.id !== "options")
+ .map(def => def.id);
+
+ const toolbox = await gDevTools.showToolbox(
+ target,
+ toolIDs[0],
+ Toolbox.HostType.BOTTOM
+ );
+ 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_target.js b/devtools/client/framework/test/browser_toolbox_target.js
new file mode 100644
index 0000000000..2d72fe0810
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_target.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test about:devtools-toolbox?target which allows opening a toolbox in an
+// iframe while defining which document to debug by setting a `target`
+// attribute refering to the document to debug.
+
+const { Toolbox } = require("devtools/client/framework/toolbox");
+
+add_task(async function() {
+ // iframe loads the document to debug
+ const iframe = document.createXULElement("browser");
+ iframe.setAttribute("type", "content");
+ document.documentElement.appendChild(iframe);
+
+ let onLoad = once(iframe, "load", true);
+ iframe.setAttribute("src", "data:text/html,document to debug");
+ await onLoad;
+ is(iframe.contentWindow.document.body.innerHTML, "document to debug");
+
+ // toolbox loads the toolbox document
+ const toolboxIframe = document.createXULElement("iframe");
+ document.documentElement.appendChild(toolboxIframe);
+
+ // Important step to define which target to debug
+ toolboxIframe.target = iframe;
+
+ const onToolboxReady = gDevTools.once("toolbox-ready");
+
+ onLoad = once(toolboxIframe, "load", true);
+ toolboxIframe.setAttribute("src", "about:devtools-toolbox?target");
+ await onLoad;
+
+ // Also wait for toolbox-ready, as toolbox document load isn't enough, there
+ // is plenty of asynchronous steps during toolbox load
+ info("Waiting for toolbox-ready");
+ const toolbox = await onToolboxReady;
+
+ is(
+ toolbox.hostType,
+ Toolbox.HostType.PAGE,
+ "Host type of this toolbox shuld be Toolbox.HostType.PAGE"
+ );
+
+ const onToolboxDestroyed = gDevTools.once("toolbox-destroyed");
+
+ info("Removing the iframes");
+ toolboxIframe.remove();
+
+ // And wait for toolbox-destroyed as toolbox unload is also full of
+ // asynchronous operation that outlast unload event
+ info("Waiting for toolbox-destroyed");
+ await onToolboxDestroyed;
+ info("Toolbox destroyed");
+
+ iframe.remove();
+});
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..6e89794dd1
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_telemetry_activate_splitconsole.js
@@ -0,0 +1,107 @@
+/* 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 target = await TargetFactory.forTab(tab);
+ const toolbox = await gDevTools.showToolbox(target, "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
+ ok(timestamp > 0, "timestamp is greater than 0");
+ is(category, expected.category, "category is correct");
+ is(method, expected.method, "method is correct");
+ is(object, expected.object, "object is correct");
+ 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..65fffa8d8c
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_telemetry_close.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Toolbox } = require("devtools/client/framework/toolbox");
+const { TelemetryTestUtils } = ChromeUtils.import(
+ "resource://testing-common/TelemetryTestUtils.jsm"
+);
+
+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 target = await TargetFactory.forTab(tab);
+ const toolbox = await gDevTools.showToolbox(target, 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..e4e3afc93a
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_telemetry_enter.js
@@ -0,0 +1,153 @@
+/* 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);
+ const target = await TargetFactory.forTab(tab);
+
+ // 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.showToolbox(target, "inspector");
+
+ // Switch between a few tools
+ await gDevTools.showToolbox(target, "jsdebugger");
+ await gDevTools.showToolbox(target, "styleeditor");
+ await gDevTools.showToolbox(target, "netmonitor");
+ await gDevTools.showToolbox(target, "storage");
+ await gDevTools.showToolbox(target, "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
+ ok(timestamp > 0, "timestamp is greater than 0");
+ is(category, expected.category, "category is correct");
+ is(method, expected.method, "method is correct");
+ is(object, expected.object, "object is correct");
+ 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..69bbe635f1
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_telemetry_exit.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,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);
+ const target = await TargetFactory.forTab(tab);
+
+ // Open the toolbox
+ await gDevTools.showToolbox(target, "inspector");
+
+ // Switch between a few tools
+ await gDevTools.showToolbox(target, "jsdebugger");
+ await gDevTools.showToolbox(target, "styleeditor");
+ await gDevTools.showToolbox(target, "netmonitor");
+ await gDevTools.showToolbox(target, "storage");
+ await gDevTools.showToolbox(target, "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
+ ok(timestamp > 0, "timestamp is greater than 0");
+ is(category, expected.category, "category is correct");
+ is(method, expected.method, "method is correct");
+ is(object, expected.object, "object is correct");
+ 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..b1dacbf6cd
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_telemetry_open_event.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the "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);
+ const events = snapshot.parent.filter(
+ event =>
+ event[1] === "devtools.main" && event[2] === "open" && event[4] === null
+ );
+
+ 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..3fae663da6
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_textbox_context_menu.js
@@ -0,0 +1,146 @@
+/* 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() {
+ // We have to disable CSP for this test otherwise the CSP of
+ // about:devtools-toolbox will block the data: url.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.csp.enable", false],
+ ["dom.security.skip_about_page_has_csp_assert", true],
+ ],
+ });
+
+ 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");
+ EventUtils.sendKey("ESCAPE", toolbox.win);
+ await onContextMenuHidden;
+});
+
+add_task(async function automaticallyBindTexbox() {
+ // We have to disable CSP for this test otherwise the CSP of
+ // about:devtools-toolbox will block the data: url.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.csp.enable", false],
+ ["dom.security.skip_about_page_has_csp_assert", true],
+ ],
+ });
+
+ info(
+ "Registering a tool with an input field and making sure the context menu works"
+ );
+ gDevTools.registerTool({
+ id: textboxToolId,
+ isTargetSupported: () => true,
+ url: `data:text/html;charset=utf8,<input /><input type='text' />
+ <input type='search' /><textarea></textarea><input type='radio' />`,
+ label: "Context menu works without tool intervention",
+ build: function(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");
+ 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..eafc2c3df7
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_theme_registration.js
@@ -0,0 +1,169 @@
+/* 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");
+ const target = await TargetFactory.forTab(tab);
+ toolbox = await gDevTools.showToolbox(target, "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(),
+ LIGHT_THEME_NAME,
+ "getTheme returns the expected theme"
+ );
+ is(
+ eventsRecorded.pop(),
+ LIGHT_THEME_NAME,
+ "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 light theme must be selected now.
+ is(
+ themeBox.querySelector(`#devtools-theme-box [value=${LIGHT_THEME_NAME}]`)
+ .checked,
+ true,
+ `${LIGHT_THEME_NAME} 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..c153c5e97d
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_toggle.js
@@ -0,0 +1,117 @@
+/* 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("devtools/client/framework/toolbox");
+
+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 + "'");
+ const target = await TargetFactory.forTab(tab);
+ await gDevTools.showToolbox(target);
+
+ await testToggleDockedToolbox(tab, key, modifiers);
+ await testToggleDetachedToolbox(tab, key, modifiers);
+
+ await cleanup();
+}
+
+async function testToggleDockedToolbox(tab, key, modifiers) {
+ const toolbox = await 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 = await 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");
+}
+
+async function getToolboxForTab(tab) {
+ const target = await TargetFactory.forTab(tab);
+ return gDevTools.getToolbox(target);
+}
+
+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..e3ad4edc7b
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_tool_ready.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(5);
+
+function performChecks(target) {
+ return (async function() {
+ const toolIds = gDevTools
+ .getToolDefinitionArray()
+ .filter(def => def.isTargetSupported(target))
+ .map(def => def.id);
+
+ let toolbox;
+ for (let index = 0; index < toolIds.length; index++) {
+ const toolId = toolIds[index];
+
+ info("About to open " + index + "/" + toolId);
+ toolbox = await gDevTools.showToolbox(target, toolId);
+ ok(toolbox, "toolbox exists for " + toolId);
+ is(toolbox.currentToolId, toolId, "currentToolId should be " + toolId);
+
+ const panel = toolbox.getCurrentPanel();
+ ok(panel.isReady, toolId + " panel should be ready");
+ }
+
+ await toolbox.destroy();
+ })();
+}
+
+function test() {
+ (async function() {
+ toggleAllTools(true);
+ const tab = await addTab("about:blank");
+ const target = await TargetFactory.forTab(tab);
+ await performChecks(target);
+ 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..aaad0e365c
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_tool_remote_reopen.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { DevToolsServer } = require("devtools/server/devtools-server");
+
+// 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(target) {
+ return (async function() {
+ const toolIds = gDevTools
+ .getToolDefinitionArray()
+ .filter(def => def.isTargetSupported(target))
+ .map(def => def.id);
+
+ let toolbox;
+ for (let index = 0; index < toolIds.length; index++) {
+ const toolId = toolIds[index];
+
+ info("About to open " + index + "/" + toolId);
+ toolbox = await gDevTools.showToolbox(target, toolId, "window");
+ ok(toolbox, "toolbox exists for " + toolId);
+ is(toolbox.currentToolId, toolId, "currentToolId should be " + toolId);
+
+ const panel = toolbox.getCurrentPanel();
+ ok(panel.isReady, toolId + " panel should be ready");
+ }
+
+ await toolbox.destroy();
+ })();
+}
+
+function test() {
+ (async function() {
+ toggleAllTools(true);
+ const tab = await addTab("about:blank");
+
+ const target = await TargetFactory.forTab(tab);
+ const { client } = target;
+ await runTools(target);
+
+ 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;
+ }
+ // Bug 1056342: Profiler fails today because of framerate actor, but
+ // this appears more complex to rework, so leave it for that bug to
+ // resolve.
+ if (actor.includes("framerateActor")) {
+ todo(false, "Front for " + actor + " still held in pool!");
+ 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..f14e40a6d4
--- /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("devtools/client/framework/toolbox");
+
+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..c4a34711ce
--- /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("devtools/client/framework/toolbox");
+
+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..62bf64fb0b
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_toolbar_overflow_button_visibility.js
@@ -0,0 +1,73 @@
+/* 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("devtools/client/framework/toolbox");
+
+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);
+ ok(
+ 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..5c824da790
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_toolbar_reorder_by_dnd.js
@@ -0,0 +1,190 @@
+/* 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("devtools/client/framework/toolbox");
+
+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..a0f76b053e
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_toolbar_reorder_by_width.js
@@ -0,0 +1,106 @@
+/* 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("devtools/client/framework/toolbox");
+
+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,
+ isTargetSupported: () => true,
+ build: function() {},
+ });
+ 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..19157f1cb4
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_toolbar_reorder_with_extension.js
@@ -0,0 +1,148 @@
+/* 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("devtools/client/framework/toolbox");
+
+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",
+ applications: {
+ gecko: { id: EXTENSION },
+ },
+ },
+ files: {
+ "extension.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <script src="extension.js"></script>
+ </body>
+ </html>`,
+ "extension.js": async () => {
+ // eslint-disable-next-line
+ await browser.devtools.panels.create("extension", "fake-icon.png", "empty.html");
+ // eslint-disable-next-line
+ 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..e949d1a56e
--- /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("devtools/client/framework/toolbox");
+
+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",
+ applications: {
+ gecko: { id: EXTENSION },
+ },
+ },
+ files: {
+ "extension.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <script src="extension.js"></script>
+ </body>
+ </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_toolbar_reorder_with_secondary_toolbox.js b/devtools/client/framework/test/browser_toolbox_toolbar_reorder_with_secondary_toolbox.js
new file mode 100644
index 0000000000..1583cdfa32
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_toolbar_reorder_with_secondary_toolbox.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for reordering with secondary toolbox such as Browser Content Toolbox.
+// We test whether the ordering preference will not change when the secondary toolbox
+// was closed without reordering.
+
+const {
+ gDevToolsBrowser,
+} = require("devtools/client/framework/devtools-browser");
+
+add_task(async function() {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
+ });
+
+ // Keep initial tabs order preference of devtools so as to compare after re-ordering
+ // tabs on browser content toolbox.
+ const initialTabsOrderOnDevTools = Services.prefs.getCharPref(
+ "devtools.toolbox.tabsOrder"
+ );
+
+ info("Prepare the toolbox on browser content toolbox");
+ await addTab(`${URL_ROOT}doc_empty-tab-01.html`);
+ // Select "memory" tool from first, because the webconsole might connect to the content.
+ Services.prefs.setCharPref("devtools.toolbox.selectedTool", "memory");
+ const toolbox = await gDevToolsBrowser.openContentProcessToolbox(gBrowser);
+
+ // A RDP request is made during toolbox opening and this request isn't awaited for
+ // during the call to gDevTools.showToolbox. This relates to the autohide menu.
+ // Await for this request to be finished and the DOM elements relating to it to be disabled
+ // before trying to close the toolbox.
+ await waitForDOM(toolbox.win.document, "#toolbox-meatball-menu-noautohide");
+
+ info(
+ "Check whether the value of devtools.toolbox.tabsOrder was not affected after closed"
+ );
+ const onToolboxDestroyed = toolbox.once("destroyed");
+ toolbox.topWindow.close();
+ await onToolboxDestroyed;
+ is(
+ Services.prefs.getCharPref("devtools.toolbox.tabsOrder"),
+ initialTabsOrderOnDevTools,
+ "The preference of devtools.toolbox.tabsOrder should not be affected"
+ );
+});
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..d80608f976
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_tools_per_toolbox_registration.js
@@ -0,0 +1,141 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URL = `data:text/html,<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ test for registering and unregistering tools to a specific toolbox
+ </body>
+ </html>`;
+
+const TOOL_ID = "test-toolbox-tool";
+var toolbox;
+var target;
+
+function test() {
+ addTab(TEST_URL).then(async tab => {
+ target = await TargetFactory.forTab(tab);
+
+ gDevTools
+ .showToolbox(target)
+ .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,
+ isTargetSupported: () => true,
+ build: function() {
+ 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
+ .showToolbox(target, 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..85c5f06575
--- /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}doc_viewsource.html`;
+var JS_URL = `${URL_ROOT}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..3645fadf97
--- /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}doc_viewsource.html`;
+var JS_URL = `${URL_ROOT}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.target.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..47f2ea093c
--- /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}doc_viewsource.html`;
+var CSS_URL = `${URL_ROOT}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_window_reload_target.js b/devtools/client/framework/test/browser_toolbox_window_reload_target.js
new file mode 100644
index 0000000000..03e429f0ea
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_reload_target.js
@@ -0,0 +1,110 @@
+/* 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.import(
+ "resource://testing-common/PromiseTestUtils.jsm"
+);
+
+// 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," +
+ "<html><head><title>Test reload</title></head>" +
+ "<body><h1>Testing reload from devtools</h1></body></html>";
+
+const { Toolbox } = require("devtools/client/framework/toolbox");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+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 target = await TargetFactory.forTab(gBrowser.selectedTab);
+
+ info("Getting the entire list of tools supported in this tab");
+ const toolIDs = gDevTools
+ .getToolDefinitionArray()
+ .filter(def => def.isTargetSupported(target))
+ .map(def => def.id);
+
+ info(
+ "Display the toolbox, docked at the bottom, with the first tool selected"
+ );
+ const toolbox = await gDevTools.showToolbox(
+ target,
+ toolIDs[0],
+ 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);
+
+ 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++;
+}
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..85793bd1a8
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_reload_target_force.js
@@ -0,0 +1,46 @@
+/* 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 { LocalizationHelper } = require("devtools/shared/l10n");
+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 target = await TargetFactory.forTab(gBrowser.selectedTab);
+
+ info("Open the toolbox with the inspector selected");
+ const toolbox = await gDevTools.showToolbox(target, "inspector");
+
+ await testReload("toolbox.reload.key", toolbox, "max-age=0");
+ await testReload("toolbox.reload2.key", toolbox, "max-age=0");
+ 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..e542f67498
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_shortcuts.js
@@ -0,0 +1,101 @@
+/* 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("devtools/client/framework/toolbox");
+
+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 target = await TargetFactory.forTab(gBrowser.selectedTab);
+ idIndex = 0;
+ gDevTools
+ .showToolbox(target, toolIDs[0], 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..7bc3a950e4
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_title_changes.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+requestLongerTimeout(5);
+
+var { Toolbox } = require("devtools/client/framework/toolbox");
+
+function test() {
+ const URL_1 = "data:text/plain;charset=UTF-8,abcde";
+ const URL_2 = "data:text/plain;charset=UTF-8,12345";
+ const URL_3 = URL_ROOT + "browser_toolbox_window_title_changes_page.html";
+
+ const TOOL_ID_1 = "webconsole";
+ const TOOL_ID_2 = "jsdebugger";
+
+ const NAME_1 = "";
+ const NAME_2 = "";
+ const NAME_3 = "Toolbox test for title update";
+
+ let toolbox;
+ let panel;
+
+ addTab(URL_1).then(async function() {
+ let target = await TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools
+ .showToolbox(target, null, Toolbox.HostType.BOTTOM)
+ .then(function(aToolbox) {
+ toolbox = aToolbox;
+ })
+ .then(() => toolbox.selectTool(TOOL_ID_1))
+
+ // undock toolbox and check title
+ .then(() => {
+ // 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
+ return toolbox
+ .switchHost(Toolbox.HostType.WINDOW)
+ .then(() => waitForTitleChange(toolbox));
+ })
+ .then(checkTitle.bind(null, NAME_1, URL_1, "toolbox undocked"))
+
+ // switch to different tool and check title
+ .then(async () => {
+ const onTitleChanged = waitForTitleChange(toolbox);
+ panel = await toolbox.selectTool(TOOL_ID_2);
+ return onTitleChanged;
+ })
+ .then(checkTitle.bind(null, NAME_1, URL_1, "tool changed"))
+
+ // navigate to different local url and check title
+ .then(async function() {
+ const onTitleChanged = waitForTitleChange(toolbox);
+ const waitForReloaded = panel.once("reloaded");
+ await navigateTo(URL_2);
+ await waitForReloaded;
+ return onTitleChanged;
+ })
+ .then(checkTitle.bind(null, NAME_2, URL_2, "url changed"))
+
+ // navigate to a real url and check title
+ .then(async () => {
+ const onTitleChanged = waitForTitleChange(toolbox);
+ const waitForReloaded = panel.once("reloaded");
+ await navigateTo(URL_3);
+ await waitForReloaded;
+ return onTitleChanged;
+ })
+ .then(checkTitle.bind(null, NAME_3, URL_3, "url changed"))
+
+ // destroy toolbox, create new one hosted in a window (with a
+ // different tool id), and check title
+ .then(function() {
+ // Give the tools a chance to handle the navigation event before
+ // destroying the toolbox.
+ executeSoon(function() {
+ toolbox
+ .destroy()
+ .then(async function() {
+ // After destroying the toolbox, a fresh target is required.
+ target = await TargetFactory.forTab(gBrowser.selectedTab);
+ return gDevTools.showToolbox(
+ target,
+ null,
+ Toolbox.HostType.WINDOW
+ );
+ })
+ .then(function(aToolbox) {
+ toolbox = aToolbox;
+ })
+ .then(() => {
+ const onTitleChanged = waitForTitleChange(toolbox);
+ toolbox.selectTool(TOOL_ID_1);
+ return onTitleChanged;
+ })
+ .then(
+ checkTitle.bind(
+ null,
+ NAME_3,
+ URL_3,
+ "toolbox destroyed and recreated"
+ )
+ )
+
+ // clean up
+ .then(() => toolbox.destroy())
+ .then(function() {
+ toolbox = null;
+ gBrowser.removeCurrentTab();
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+ Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
+ Services.prefs.clearUserPref("devtools.toolbox.sideEnabled");
+ finish();
+ });
+ });
+ });
+ });
+}
+
+function checkTitle(name, url, context) {
+ const win = Services.wm.getMostRecentWindow("devtools:toolbox");
+ let expectedTitle;
+ if (name) {
+ expectedTitle = `Developer Tools — ${name} — ${url}`;
+ } else {
+ expectedTitle = `Developer Tools — ${url}`;
+ }
+ is(win.document.title, expectedTitle, context);
+}
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 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Toolbox test for title update</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body></body>
+</html>
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..07753a93cd
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js
@@ -0,0 +1,147 @@
+/* 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("devtools/client/framework/toolbox");
+const URL = URL_ROOT + "browser_toolbox_window_title_frame_select_page.html";
+const IFRAME_URL = URL_ROOT + "browser_toolbox_window_title_changes_page.html";
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/toolbox.properties"
+);
+
+add_task(async function() {
+ Services.prefs.setBoolPref("devtools.command-button-frames.enabled", true);
+
+ await addTab(URL);
+ const target = await TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = await gDevTools.showToolbox(
+ target,
+ null,
+ 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 willNavigate = toolbox.target.once("will-navigate");
+
+ const onTitleChanged = waitForTitleChange(toolbox);
+
+ // 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();
+
+ await willNavigate;
+ await onInspectorReloaded;
+ await onTitleChanged;
+
+ 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.toolbox.sideEnabled");
+ 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 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Page title</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <iframe src="browser_toolbox_window_title_changes_page.html"></iframe>
+ </head>
+ <body></body>
+</html>
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..7a5bd31529
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_zoom.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const { Toolbox } = require("devtools/client/framework/toolbox");
+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 target = await TargetFactory.forTab(gBrowser.selectedTab);
+ const toolbox = await gDevTools.showToolbox(
+ target,
+ "styleeditor",
+ 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.windowUtils.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..dab4b4dc87
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_zoom_popup.js
@@ -0,0 +1,187 @@
+/* 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("devtools/client/framework/toolbox");
+
+// 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,<iframe/>";
+
+add_task(async function() {
+ registerCleanupFunction(async function() {
+ Services.prefs.clearUserPref("devtools.toolbox.zoomValue");
+ });
+ const zoom = 1.4;
+ Services.prefs.setCharPref("devtools.toolbox.zoomValue", zoom.toString(10));
+
+ info("Load iframe page for checking the frame menu with x1.4 zoom.");
+ await addTab(TEST_URL);
+ const target = await TargetFactory.forTab(gBrowser.selectedTab);
+ const toolbox = await gDevTools.showToolbox(
+ target,
+ "inspector",
+ Toolbox.HostType.WINDOW
+ );
+ const inspector = toolbox.getCurrentPanel();
+ const hostWindow = toolbox.win.parent;
+ const originWidth = hostWindow.outerWidth;
+ const originHeight = hostWindow.outerHeight;
+ const windowUtils = toolbox.win.windowUtils;
+
+ info("Waiting for the toolbox window will to be rendered with zoom x1.4");
+ await waitUntil(() => {
+ return parseFloat(windowUtils.fullZoom.toFixed(1)) === zoom;
+ });
+
+ info(
+ "Resizing and moving the toolbox window in order to display the chevron menu."
+ );
+ // If the window is displayed bottom of screen, the menu might be displayed
+ // above the button so move it to the top of the screen first.
+ await moveWindowTo(hostWindow, 0, 0);
+
+ // Shrink the width of the window such that the inspector's tab menu button
+ // and chevron button are visible.
+ const prevTabs = toolbox.doc.querySelectorAll(".devtools-tab").length;
+ hostWindow.resizeTo(400, hostWindow.outerHeight);
+ await waitUntil(() => {
+ return (
+ hostWindow.outerWidth === 400 &&
+ toolbox.doc.getElementById("tools-chevron-menu-button") &&
+ inspector.panelDoc.querySelector(".all-tabs-menu") &&
+ prevTabs != toolbox.doc.querySelectorAll(".devtools-tab").length
+ );
+ });
+
+ const menuList = [
+ toolbox.win.document.getElementById("toolbox-meatball-menu-button"),
+ toolbox.win.document.getElementById("command-button-frames"),
+ toolbox.win.document.getElementById("tools-chevron-menu-button"),
+ inspector.panelDoc.querySelector(".all-tabs-menu"),
+ ];
+
+ for (const menu of menuList) {
+ const {
+ buttonBounds,
+ menuType,
+ menuBounds,
+ arrowBounds,
+ } = await getButtonAndMenuInfo(toolbox, menu);
+
+ switch (menuType) {
+ case "native":
+ {
+ // Allow rounded error and platform offset value.
+ // horizontal : IntID::ContextMenuOffsetHorizontal of GTK and Windows
+ // uses 2.
+ // vertical: IntID::ContextMenuOffsetVertical of macOS uses -6.
+ const xDelta = Math.abs(menuBounds.left - buttonBounds.left);
+ const yDelta = Math.abs(menuBounds.top - buttonBounds.bottom);
+ ok(xDelta < 2, "xDelta is lower than 2: " + xDelta + ". #" + menu.id);
+ ok(yDelta < 6, "yDelta is lower than 6: " + yDelta + ". #" + menu.id);
+ }
+ break;
+
+ case "doorhanger":
+ {
+ // Calculate the center of the button and center of the arrow and
+ // check they align.
+ const buttonCenter = buttonBounds.left + buttonBounds.width / 2;
+ const arrowCenter = arrowBounds.left + arrowBounds.width / 2;
+ const delta = Math.abs(arrowCenter - buttonCenter);
+ ok(
+ Math.round(delta) <= 1,
+ "Center of arrow is within 1px of button center" +
+ ` (delta: ${delta})`
+ );
+ }
+ break;
+ }
+ }
+
+ const onResize = once(hostWindow, "resize");
+ hostWindow.resizeTo(originWidth, originHeight);
+ await onResize;
+
+ await toolbox.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+/**
+ * Get the bounds of a menu button and its popup panel. The popup panel is
+ * measured by clicking the menu button and looking for its panel (and then
+ * hiding it again).
+ *
+ * @param {Object} doc
+ * The toolbox document to query.
+ * @param {Object} menuButton
+ * The button whose size and popup size we should measure.
+ * @return {Object}
+ * An object with the following properties:
+ * - buttonBounds {DOMRect} Bounds of the button.
+ * - menuType {string} Type of the menu, "native" or "doorhanger".
+ * - menuBounds {DOMRect} Bounds of the menu panel.
+ * - arrowBounds {DOMRect|null} Bounds of the arrow. Only set when
+ * menuType is "doorhanger", null otherwise.
+ */
+async function getButtonAndMenuInfo(toolbox, menuButton) {
+ const { doc, topDoc } = toolbox;
+ info("Show popup menu with click event.");
+ AccessibilityUtils.setEnv({
+ // Keyboard accessibility is handled on the toolbox toolbar container level.
+ // Users can use arrow keys to navigate between and select tabs.
+ nonNegativeTabIndexRule: false,
+ });
+ await EventUtils.sendMouseEvent(
+ {
+ type: "click",
+ screenX: 1,
+ },
+ menuButton,
+ doc.defaultView
+ );
+ AccessibilityUtils.resetEnv();
+
+ let menuPopup;
+ let menuType;
+ let arrowBounds = null;
+ if (menuButton.hasAttribute("aria-controls")) {
+ menuType = "doorhanger";
+ menuPopup = doc.getElementById(menuButton.getAttribute("aria-controls"));
+ await waitUntil(() => menuPopup.classList.contains("tooltip-visible"));
+ } else {
+ menuType = "native";
+ await waitUntil(() => {
+ const popupset = topDoc.querySelector("popupset");
+ menuPopup = popupset?.querySelector('menupopup[menu-api="true"]');
+ return menuPopup?.state === "open";
+ });
+ }
+ ok(menuPopup, "Menu popup is displayed.");
+
+ const buttonBounds = menuButton
+ .getBoxQuads({ relativeTo: doc })[0]
+ .getBounds();
+ const menuBounds = menuPopup.getBoxQuads({ relativeTo: doc })[0].getBounds();
+
+ if (menuType === "doorhanger") {
+ const arrow = menuPopup.querySelector(".tooltip-arrow");
+ arrowBounds = arrow.getBoxQuads({ relativeTo: doc })[0].getBounds();
+ }
+
+ info("Hide popup menu.");
+ if (menuType === "doorhanger") {
+ EventUtils.sendKey("Escape", doc.defaultView);
+ await waitUntil(() => !menuPopup.classList.contains("tooltip-visible"));
+ } else {
+ const popupHidden = once(menuPopup, "popuphidden");
+ menuPopup.hidePopup();
+ await popupHidden;
+ }
+
+ return { buttonBounds, menuType, menuBounds, arrowBounds };
+}
diff --git a/devtools/client/framework/test/browser_two_tabs.js b/devtools/client/framework/test/browser_two_tabs.js
new file mode 100644
index 0000000000..1ed856cd3d
--- /dev/null
+++ b/devtools/client/framework/test/browser_two_tabs.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check regression when opening two tabs
+ */
+
+var { DevToolsServer } = require("devtools/server/devtools-server");
+var { DevToolsClient } = require("devtools/client/devtools-client");
+
+const TAB_URL_1 = "data:text/html;charset=utf-8,foo";
+const TAB_URL_2 = "data:text/html;charset=utf-8,bar";
+
+add_task(async () => {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ const tab1 = await addTab(TAB_URL_1);
+ const tab2 = await addTab(TAB_URL_2);
+
+ // Connect to devtools server to fetch the two target actors for each tab
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ await client.connect();
+
+ const tabDescriptors = await client.mainRoot.listTabs();
+ const tabs = await Promise.all(tabDescriptors.map(d => d.getTarget()));
+ const targetFront1 = tabs.find(a => a.url === TAB_URL_1);
+ const targetFront2 = tabs.find(a => a.url === TAB_URL_2);
+
+ await checkGetTab(client, tab1, tab2, targetFront1, targetFront2);
+ await checkGetTabFailures(client);
+ await checkSelectedTargetActor(targetFront2);
+
+ await removeTab(tab2);
+ await checkFirstTargetActor(targetFront1);
+
+ await removeTab(tab1);
+ await client.close();
+});
+
+async function checkGetTab(client, tab1, tab2, targetFront1, targetFront2) {
+ let front = await getTabTarget(client, { tab: tab1 });
+ is(targetFront1, front, "getTab returns the same target form for first tab");
+ const filter = {};
+ // Filter either by tabId or outerWindowID,
+ // if we are running tests OOP or not.
+ if (tab1.linkedBrowser.frameLoader.remoteTab) {
+ filter.tabId = tab1.linkedBrowser.frameLoader.remoteTab.tabId;
+ } else {
+ const { docShell } = tab1.linkedBrowser.contentWindow;
+ filter.outerWindowID = docShell.outerWindowID;
+ }
+ front = await getTabTarget(client, filter);
+ is(
+ targetFront1,
+ front,
+ "getTab returns the same target form when filtering by tabId/outerWindowID"
+ );
+ front = await getTabTarget(client, { tab: tab2 });
+ is(targetFront2, front, "getTab returns the same target form for second tab");
+}
+
+async function checkGetTabFailures(client) {
+ try {
+ await getTabTarget(client, { tabId: -999 });
+ ok(false, "getTab unexpectedly succeed with a wrong tabId");
+ } catch (error) {
+ is(
+ error.message,
+ "Protocol error (noTab): Unable to find tab with tabId '-999' from: " +
+ client.mainRoot.actorID
+ );
+ }
+
+ try {
+ await getTabTarget(client, { outerWindowID: -999 });
+ ok(false, "getTab unexpectedly succeed with a wrong outerWindowID");
+ } catch (error) {
+ is(
+ error.message,
+ "Protocol error (noTab): Unable to find tab with outerWindowID '-999' from: " +
+ client.mainRoot.actorID
+ );
+ }
+}
+
+async function checkSelectedTargetActor(targetFront2) {
+ // Send a naive request to the second target actor to check if it works
+ await targetFront2.attach();
+ const consoleFront = await targetFront2.getFront("console");
+ const response = await consoleFront.startListeners([]);
+ ok(
+ "startedListeners" in response,
+ "Actor from the selected tab should respond to the request."
+ );
+}
+
+async function checkFirstTargetActor(targetFront1) {
+ // then send a request to the first target actor to check if it still works
+ await targetFront1.attach();
+ const consoleFront = await targetFront1.getFront("console");
+ const response = await consoleFront.startListeners([]);
+ ok(
+ "startedListeners" in response,
+ "Actor from the first tab should still respond."
+ );
+}
+
+async function getTabTarget(client, filter) {
+ const descriptor = await client.mainRoot.getTab(filter);
+ return descriptor.getTarget();
+}
diff --git a/devtools/client/framework/test/code_binary_search.coffee b/devtools/client/framework/test/code_binary_search.coffee
new file mode 100644
index 0000000000..e3dacdaaab
--- /dev/null
+++ b/devtools/client/framework/test/code_binary_search.coffee
@@ -0,0 +1,18 @@
+# Uses a binary search algorithm to locate a value in the specified array.
+window.binary_search = (items, value) ->
+
+ start = 0
+ stop = items.length - 1
+ pivot = Math.floor (start + stop) / 2
+
+ while items[pivot] isnt value and start < stop
+
+ # Adjust the search area.
+ stop = pivot - 1 if value < items[pivot]
+ start = pivot + 1 if value > items[pivot]
+
+ # Recalculate the pivot.
+ pivot = Math.floor (stop + start) / 2
+
+ # Make sure we've found the correct value.
+ if items[pivot] is value then pivot else -1 \ No newline at end of file
diff --git a/devtools/client/framework/test/code_binary_search.js b/devtools/client/framework/test/code_binary_search.js
new file mode 100644
index 0000000000..c43848a60c
--- /dev/null
+++ b/devtools/client/framework/test/code_binary_search.js
@@ -0,0 +1,29 @@
+// Generated by CoffeeScript 1.6.1
+(function() {
+
+ window.binary_search = function(items, value) {
+ var pivot, start, stop;
+ start = 0;
+ stop = items.length - 1;
+ pivot = Math.floor((start + stop) / 2);
+ while (items[pivot] !== value && start < stop) {
+ if (value < items[pivot]) {
+ stop = pivot - 1;
+ }
+ if (value > items[pivot]) {
+ start = pivot + 1;
+ }
+ pivot = Math.floor((stop + start) / 2);
+ }
+ if (items[pivot] === value) {
+ return pivot;
+ } else {
+ return -1;
+ }
+ };
+
+}).call(this);
+
+/*
+//# sourceMappingURL=code_binary_search.map
+*/
diff --git a/devtools/client/framework/test/code_binary_search.map b/devtools/client/framework/test/code_binary_search.map
new file mode 100644
index 0000000000..8d22511252
--- /dev/null
+++ b/devtools/client/framework/test/code_binary_search.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "file": "code_binary_search.js",
+ "sourceRoot": "",
+ "sources": [
+ "code_binary_search.coffee"
+ ],
+ "names": [],
+ "mappings": ";AACA;CAAA;CAAA,CAAA,CAAuB,EAAA,CAAjB,GAAkB,IAAxB;CAEE,OAAA,UAAA;CAAA,EAAQ,CAAR,CAAA;CAAA,EACQ,CAAR,CAAa,CAAL;CADR,EAEQ,CAAR,CAAA;CAEA,EAA0C,CAAR,CAAtB,MAAN;CAGJ,EAA6B,CAAR,CAAA,CAArB;CAAA,EAAQ,CAAR,CAAQ,GAAR;QAAA;CACA,EAA6B,CAAR,CAAA,CAArB;CAAA,EAAQ,EAAR,GAAA;QADA;CAAA,EAIQ,CAAI,CAAZ,CAAA;CAXF,IAIA;CAUA,GAAA,CAAS;CAAT,YAA8B;MAA9B;AAA0C,CAAD,YAAA;MAhBpB;CAAvB,EAAuB;CAAvB"
+}
diff --git a/devtools/client/framework/test/code_binary_search_absolute.js b/devtools/client/framework/test/code_binary_search_absolute.js
new file mode 100644
index 0000000000..7a529f3e88
--- /dev/null
+++ b/devtools/client/framework/test/code_binary_search_absolute.js
@@ -0,0 +1,29 @@
+// Generated by CoffeeScript 1.6.1
+(function() {
+
+ window.binary_search = function(items, value) {
+ var pivot, start, stop;
+ start = 0;
+ stop = items.length - 1;
+ pivot = Math.floor((start + stop) / 2);
+ while (items[pivot] !== value && start < stop) {
+ if (value < items[pivot]) {
+ stop = pivot - 1;
+ }
+ if (value > items[pivot]) {
+ start = pivot + 1;
+ }
+ pivot = Math.floor((stop + start) / 2);
+ }
+ if (items[pivot] === value) {
+ return pivot;
+ } else {
+ return -1;
+ }
+ };
+
+}).call(this);
+
+/*
+//# sourceMappingURL=code_binary_search_absolute.map
+*/
diff --git a/devtools/client/framework/test/code_binary_search_absolute.map b/devtools/client/framework/test/code_binary_search_absolute.map
new file mode 100644
index 0000000000..ce124f287b
--- /dev/null
+++ b/devtools/client/framework/test/code_binary_search_absolute.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "file": "code_binary_search.js",
+ "sourceRoot": "http://example.com/browser/devtools/client/framework/test/",
+ "sources": [
+ "code_binary_search.coffee"
+ ],
+ "names": [],
+ "mappings": ";AACA;CAAA;CAAA,CAAA,CAAuB,EAAA,CAAjB,GAAkB,IAAxB;CAEE,OAAA,UAAA;CAAA,EAAQ,CAAR,CAAA;CAAA,EACQ,CAAR,CAAa,CAAL;CADR,EAEQ,CAAR,CAAA;CAEA,EAA0C,CAAR,CAAtB,MAAN;CAGJ,EAA6B,CAAR,CAAA,CAArB;CAAA,EAAQ,CAAR,CAAQ,GAAR;QAAA;CACA,EAA6B,CAAR,CAAA,CAArB;CAAA,EAAQ,EAAR,GAAA;QADA;CAAA,EAIQ,CAAI,CAAZ,CAAA;CAXF,IAIA;CAUA,GAAA,CAAS;CAAT,YAA8B;MAA9B;AAA0C,CAAD,YAAA;MAhBpB;CAAvB,EAAuB;CAAvB"
+}
diff --git a/devtools/client/framework/test/code_bundle_cross_domain.js b/devtools/client/framework/test/code_bundle_cross_domain.js
new file mode 100644
index 0000000000..7b50467508
--- /dev/null
+++ b/devtools/client/framework/test/code_bundle_cross_domain.js
@@ -0,0 +1,93 @@
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, {
+/******/ configurable: false,
+/******/ enumerable: true,
+/******/ get: getter
+/******/ });
+/******/ }
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Original source code for the cross-domain source map test.
+// The generated file was made with
+// webpack --devtool source-map code_cross_domain.js code_bundle_cross_domain.js
+// ... and then edited to replace the generated sourceMappingURL.
+
+
+
+function f() {
+ console.log("anything will do");
+}
+
+f();
+
+// Avoid script GC.
+window.f = f;
+
+
+/***/ })
+/******/ ]);
+//# sourceMappingURL=http://test2.mochi.test:8888/browser/devtools/client/framework/test/code_bundle_cross_domain.js.map
diff --git a/devtools/client/framework/test/code_bundle_cross_domain.js.map b/devtools/client/framework/test/code_bundle_cross_domain.js.map
new file mode 100644
index 0000000000..59df6f6b41
--- /dev/null
+++ b/devtools/client/framework/test/code_bundle_cross_domain.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["webpack:///webpack/bootstrap 7b928b82bd207211f478","webpack:///./code_cross_domain.js"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAK;AACL;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;AAEA;AACA;;;;;;;;AC7DA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA","file":"code_bundle_cross_domain.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 7b928b82bd207211f478","/* Any copyright is dedicated to the Public Domain.\n http://creativecommons.org/publicdomain/zero/1.0/ */\n\n// Original source code for the cross-domain source map test.\n// The generated file was made with\n// webpack --devtool source-map code_cross_domain.js code_bundle_cross_domain.js\n// ... and then edited to replace the generated sourceMappingURL.\n\n\"use strict\";\n\nfunction f() {\n console.log(\"anything will do\");\n}\n\nf();\n\n// Avoid script GC.\nwindow.f = f;\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./code_cross_domain.js\n// module id = 0\n// module chunks = 0"],"sourceRoot":""} \ No newline at end of file
diff --git a/devtools/client/framework/test/code_bundle_late_script.js b/devtools/client/framework/test/code_bundle_late_script.js
new file mode 100644
index 0000000000..3055d249bf
--- /dev/null
+++ b/devtools/client/framework/test/code_bundle_late_script.js
@@ -0,0 +1,116 @@
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
+/******/ }
+/******/ };
+/******/
+/******/ // define __esModule on exports
+/******/ __webpack_require__.r = function(exports) {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/
+/******/ // create a fake namespace object
+/******/ // mode & 1: value is a module id, require it
+/******/ // mode & 2: merge all properties of value into the ns
+/******/ // mode & 4: return value when already ns object
+/******/ // mode & 8|1: behave like require
+/******/ __webpack_require__.t = function(value, mode) {
+/******/ if(mode & 1) value = __webpack_require__(value);
+/******/ if(mode & 8) return value;
+/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
+/******/ var ns = Object.create(null);
+/******/ __webpack_require__.r(ns);
+/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
+/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
+/******/ return ns;
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = "./code_late_script.js");
+/******/ })
+/************************************************************************/
+/******/ ({
+
+/***/ "./code_late_script.js":
+/*!*****************************!*\
+ !*** ./code_late_script.js ***!
+ \*****************************/
+/*! no static exports found */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Original source code for the inline source map test.
+// The generated file was made with
+// webpack --devtool source-map code_late_script.js code_bundle_late_script.js
+
+
+
+function f() {
+ console.log("The first version of the script");
+}
+
+f();
+
+
+/***/ })
+
+/******/ });
+//# sourceMappingURL=code_bundle_late_script.js.map \ No newline at end of file
diff --git a/devtools/client/framework/test/code_bundle_late_script.js.map b/devtools/client/framework/test/code_bundle_late_script.js.map
new file mode 100644
index 0000000000..319fdadc51
--- /dev/null
+++ b/devtools/client/framework/test/code_bundle_late_script.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./code_late_script.js"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,kDAA0C,gCAAgC;AAC1E;AACA;;AAEA;AACA;AACA;AACA,gEAAwD,kBAAkB;AAC1E;AACA,yDAAiD,cAAc;AAC/D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iDAAyC,iCAAiC;AAC1E,wHAAgH,mBAAmB,EAAE;AACrI;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;;AAGA;AACA;;;;;;;;;;;;;AClFA;AACA;;AAEA;AACA;AACA;;AAEa;;AAEb;AACA;AACA;;AAEA","file":"code_bundle_late_script.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./code_late_script.js\");\n","/* Any copyright is dedicated to the Public Domain.\n http://creativecommons.org/publicdomain/zero/1.0/ */\n\n// Original source code for the inline source map test.\n// The generated file was made with\n// webpack --devtool source-map code_late_script.js code_bundle_late_script.js\n\n\"use strict\";\n\nfunction f() {\n console.log(\"The first version of the script\");\n}\n\nf();\n"],"sourceRoot":""} \ No newline at end of file
diff --git a/devtools/client/framework/test/code_bundle_no_race.js b/devtools/client/framework/test/code_bundle_no_race.js
new file mode 100644
index 0000000000..43ebc6e89e
--- /dev/null
+++ b/devtools/client/framework/test/code_bundle_no_race.js
@@ -0,0 +1,95 @@
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // identity function for calling harmony imports with the correct context
+/******/ __webpack_require__.i = function(value) { return value; };
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, {
+/******/ configurable: false,
+/******/ enumerable: true,
+/******/ get: getter
+/******/ });
+/******/ }
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Original source code for the inline source map test.
+// The generated file was made with
+// webpack --devtool source-map code_no_race.js code_bundle_no_race.js
+
+
+
+function f() {
+ console.log("anything will do");
+}
+
+f();
+
+// Avoid script GC.
+window.f = f;
+
+
+/***/ })
+/******/ ]);
+//# sourceMappingURL=code_bundle_no_race.js.map \ No newline at end of file
diff --git a/devtools/client/framework/test/code_bundle_no_race.js.map b/devtools/client/framework/test/code_bundle_no_race.js.map
new file mode 100644
index 0000000000..df3f096283
--- /dev/null
+++ b/devtools/client/framework/test/code_bundle_no_race.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["webpack:///webpack/bootstrap bac8dffc0cc5eb13fa9d","webpack:///./code_no_race.js"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA,mDAA2C,cAAc;;AAEzD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAK;AACL;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;AAEA;AACA;;;;;;;;AChEA;AACA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA","file":"code_bundle_no_race.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// identity function for calling harmony imports with the correct context\n \t__webpack_require__.i = function(value) { return value; };\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap bac8dffc0cc5eb13fa9d","/* Any copyright is dedicated to the Public Domain.\n http://creativecommons.org/publicdomain/zero/1.0/ */\n\n// Original source code for the inline source map test.\n// The generated file was made with\n// webpack --devtool source-map code_no_race.js code_bundle_no_race.js\n\n\"use strict\";\n\nfunction f() {\n console.log(\"anything will do\");\n}\n\nf();\n\n// Avoid script GC.\nwindow.f = f;\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./code_no_race.js\n// module id = 0\n// module chunks = 0"],"sourceRoot":""} \ No newline at end of file
diff --git a/devtools/client/framework/test/code_bundle_reload_1.js b/devtools/client/framework/test/code_bundle_reload_1.js
new file mode 100644
index 0000000000..fdf40740fe
--- /dev/null
+++ b/devtools/client/framework/test/code_bundle_reload_1.js
@@ -0,0 +1,94 @@
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // identity function for calling harmony imports with the correct context
+/******/ __webpack_require__.i = function(value) { return value; };
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, {
+/******/ configurable: false,
+/******/ enumerable: true,
+/******/ get: getter
+/******/ });
+/******/ }
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Original source code for the inline source map test.
+// The generated file was made with
+// webpack --devtool source-map code_reload_1.js code_bundle_reload_1.js
+// perl -pi -e 's/sjs_code_bundle_reload_map.sjs/sjs_code_bundle_reload_map.sjs/' \
+// code_bundle_reload_1.js
+
+
+
+function f() {
+ console.log("The first version of the script");
+}
+
+f();
+
+
+/***/ })
+/******/ ]);
+//# sourceMappingURL=sjs_code_bundle_reload_map.sjs \ No newline at end of file
diff --git a/devtools/client/framework/test/code_bundle_reload_1.js.map b/devtools/client/framework/test/code_bundle_reload_1.js.map
new file mode 100644
index 0000000000..2e77d393bb
--- /dev/null
+++ b/devtools/client/framework/test/code_bundle_reload_1.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["webpack:///webpack/bootstrap 59857d9393d4518a63ff","webpack:///./code_reload_1.js"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA,mDAA2C,cAAc;;AAEzD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAK;AACL;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;AAEA;AACA;;;;;;;;AChEA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA","file":"code_bundle_reload_1.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// identity function for calling harmony imports with the correct context\n \t__webpack_require__.i = function(value) { return value; };\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 59857d9393d4518a63ff","/* Any copyright is dedicated to the Public Domain.\n http://creativecommons.org/publicdomain/zero/1.0/ */\n\n// Original source code for the inline source map test.\n// The generated file was made with\n// webpack --devtool source-map code_reload_1.js code_bundle_reload_1.js\n// perl -pi -e 's/code_bundle_reload_1.js.map/sjs_code_bundle_reload_map.sjs/' \\\n// code_bundle_reload_1.js\n\n\"use strict\";\n\nfunction f() {\n console.log(\"The first version of the script\");\n}\n\nf();\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./code_reload_1.js\n// module id = 0\n// module chunks = 0"],"sourceRoot":""} \ No newline at end of file
diff --git a/devtools/client/framework/test/code_bundle_reload_2.js b/devtools/client/framework/test/code_bundle_reload_2.js
new file mode 100644
index 0000000000..2704a28e8c
--- /dev/null
+++ b/devtools/client/framework/test/code_bundle_reload_2.js
@@ -0,0 +1,94 @@
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // identity function for calling harmony imports with the correct context
+/******/ __webpack_require__.i = function(value) { return value; };
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, {
+/******/ configurable: false,
+/******/ enumerable: true,
+/******/ get: getter
+/******/ });
+/******/ }
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Original source code for the inline source map test.
+// The generated file was made with
+// webpack --devtool source-map code_reload_2.js code_bundle_reload_2.js
+// perl -pi -e 's/sjs_code_bundle_reload_map.sjs/sjs_code_bundle_reload_map.sjs/' \
+// code_bundle_reload_2.js
+
+
+
+function f() {
+ console.log("The second version of the script");
+}
+
+f();
+
+
+/***/ })
+/******/ ]);
+//# sourceMappingURL=sjs_code_bundle_reload_map.sjs \ No newline at end of file
diff --git a/devtools/client/framework/test/code_bundle_reload_2.js.map b/devtools/client/framework/test/code_bundle_reload_2.js.map
new file mode 100644
index 0000000000..a306f3b488
--- /dev/null
+++ b/devtools/client/framework/test/code_bundle_reload_2.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["webpack:///webpack/bootstrap 9497621dfe5d6f67322e","webpack:///./code_reload_2.js"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA,mDAA2C,cAAc;;AAEzD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAK;AACL;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;AAEA;AACA;;;;;;;;AChEA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA","file":"code_bundle_reload_2.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// identity function for calling harmony imports with the correct context\n \t__webpack_require__.i = function(value) { return value; };\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 9497621dfe5d6f67322e","/* Any copyright is dedicated to the Public Domain.\n http://creativecommons.org/publicdomain/zero/1.0/ */\n\n// Original source code for the inline source map test.\n// The generated file was made with\n// webpack --devtool source-map code_reload_2.js code_bundle_reload_2.js\n// perl -pi -e 's/code_bundle_reload_2.js.map/sjs_code_bundle_reload_map.sjs/' \\\n// code_bundle_reload_2.js\n\n\"use strict\";\n\nfunction f() {\n console.log(\"The second version of the script\");\n}\n\nf();\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./code_reload_2.js\n// module id = 0\n// module chunks = 0"],"sourceRoot":""} \ No newline at end of file
diff --git a/devtools/client/framework/test/code_cross_domain.js b/devtools/client/framework/test/code_cross_domain.js
new file mode 100644
index 0000000000..0e845c1466
--- /dev/null
+++ b/devtools/client/framework/test/code_cross_domain.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Original source code for the cross-domain source map test.
+// The generated file was made with
+// webpack --devtool source-map code_cross_domain.js code_bundle_cross_domain.js
+// ... and then the bundle was edited to replace the generated
+// sourceMappingURL.
+
+"use strict";
+
+function f() {
+ console.log("anything will do");
+}
+
+f();
+
+// Avoid script GC.
+window.f = f;
diff --git a/devtools/client/framework/test/code_inline_bundle.js b/devtools/client/framework/test/code_inline_bundle.js
new file mode 100644
index 0000000000..ff133a5376
--- /dev/null
+++ b/devtools/client/framework/test/code_inline_bundle.js
@@ -0,0 +1,92 @@
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // identity function for calling harmony imports with the correct context
+/******/ __webpack_require__.i = function(value) { return value; };
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, {
+/******/ configurable: false,
+/******/ enumerable: true,
+/******/ get: getter
+/******/ });
+/******/ }
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Original source code for the inline source map test.
+// The generated file was made with
+// webpack --devtool inline-source-map code_inline_original.js code_inline_bundle.js
+
+
+
+function f() {
+ console.log("I'm a goldfish with a merry face");
+}
+
+f();
+
+
+/***/ })
+/******/ ]);
+//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vd2VicGFjay9ib290c3RyYXAgNDJlMDQyN2ExYTZlMzk3NTdjOGMiLCJ3ZWJwYWNrOi8vLy4vY29kZV9pbmxpbmVfb3JpZ2luYWwuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOzs7QUFHQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQSxtREFBMkMsY0FBYzs7QUFFekQ7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxhQUFLO0FBQ0w7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQSxtQ0FBMkIsMEJBQTBCLEVBQUU7QUFDdkQseUNBQWlDLGVBQWU7QUFDaEQ7QUFDQTtBQUNBOztBQUVBO0FBQ0EsOERBQXNELCtEQUErRDs7QUFFckg7QUFDQTs7QUFFQTtBQUNBOzs7Ozs7OztBQ2hFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTs7QUFFQTs7QUFFQTtBQUNBO0FBQ0E7O0FBRUEiLCJmaWxlIjoiY29kZV9pbmxpbmVfYnVuZGxlLmpzIiwic291cmNlc0NvbnRlbnQiOlsiIFx0Ly8gVGhlIG1vZHVsZSBjYWNoZVxuIFx0dmFyIGluc3RhbGxlZE1vZHVsZXMgPSB7fTtcblxuIFx0Ly8gVGhlIHJlcXVpcmUgZnVuY3Rpb25cbiBcdGZ1bmN0aW9uIF9fd2VicGFja19yZXF1aXJlX18obW9kdWxlSWQpIHtcblxuIFx0XHQvLyBDaGVjayBpZiBtb2R1bGUgaXMgaW4gY2FjaGVcbiBcdFx0aWYoaW5zdGFsbGVkTW9kdWxlc1ttb2R1bGVJZF0pIHtcbiBcdFx0XHRyZXR1cm4gaW5zdGFsbGVkTW9kdWxlc1ttb2R1bGVJZF0uZXhwb3J0cztcbiBcdFx0fVxuIFx0XHQvLyBDcmVhdGUgYSBuZXcgbW9kdWxlIChhbmQgcHV0IGl0IGludG8gdGhlIGNhY2hlKVxuIFx0XHR2YXIgbW9kdWxlID0gaW5zdGFsbGVkTW9kdWxlc1ttb2R1bGVJZF0gPSB7XG4gXHRcdFx0aTogbW9kdWxlSWQsXG4gXHRcdFx0bDogZmFsc2UsXG4gXHRcdFx0ZXhwb3J0czoge31cbiBcdFx0fTtcblxuIFx0XHQvLyBFeGVjdXRlIHRoZSBtb2R1bGUgZnVuY3Rpb25cbiBcdFx0bW9kdWxlc1ttb2R1bGVJZF0uY2FsbChtb2R1bGUuZXhwb3J0cywgbW9kdWxlLCBtb2R1bGUuZXhwb3J0cywgX193ZWJwYWNrX3JlcXVpcmVfXyk7XG5cbiBcdFx0Ly8gRmxhZyB0aGUgbW9kdWxlIGFzIGxvYWRlZFxuIFx0XHRtb2R1bGUubCA9IHRydWU7XG5cbiBcdFx0Ly8gUmV0dXJuIHRoZSBleHBvcnRzIG9mIHRoZSBtb2R1bGVcbiBcdFx0cmV0dXJuIG1vZHVsZS5leHBvcnRzO1xuIFx0fVxuXG5cbiBcdC8vIGV4cG9zZSB0aGUgbW9kdWxlcyBvYmplY3QgKF9fd2VicGFja19tb2R1bGVzX18pXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLm0gPSBtb2R1bGVzO1xuXG4gXHQvLyBleHBvc2UgdGhlIG1vZHVsZSBjYWNoZVxuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5jID0gaW5zdGFsbGVkTW9kdWxlcztcblxuIFx0Ly8gaWRlbnRpdHkgZnVuY3Rpb24gZm9yIGNhbGxpbmcgaGFybW9ueSBpbXBvcnRzIHdpdGggdGhlIGNvcnJlY3QgY29udGV4dFxuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5pID0gZnVuY3Rpb24odmFsdWUpIHsgcmV0dXJuIHZhbHVlOyB9O1xuXG4gXHQvLyBkZWZpbmUgZ2V0dGVyIGZ1bmN0aW9uIGZvciBoYXJtb255IGV4cG9ydHNcbiBcdF9fd2VicGFja19yZXF1aXJlX18uZCA9IGZ1bmN0aW9uKGV4cG9ydHMsIG5hbWUsIGdldHRlcikge1xuIFx0XHRpZighX193ZWJwYWNrX3JlcXVpcmVfXy5vKGV4cG9ydHMsIG5hbWUpKSB7XG4gXHRcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsIG5hbWUsIHtcbiBcdFx0XHRcdGNvbmZpZ3VyYWJsZTogZmFsc2UsXG4gXHRcdFx0XHRlbnVtZXJhYmxlOiB0cnVlLFxuIFx0XHRcdFx0Z2V0OiBnZXR0ZXJcbiBcdFx0XHR9KTtcbiBcdFx0fVxuIFx0fTtcblxuIFx0Ly8gZ2V0RGVmYXVsdEV4cG9ydCBmdW5jdGlvbiBmb3IgY29tcGF0aWJpbGl0eSB3aXRoIG5vbi1oYXJtb255IG1vZHVsZXNcbiBcdF9fd2VicGFja19yZXF1aXJlX18ubiA9IGZ1bmN0aW9uKG1vZHVsZSkge1xuIFx0XHR2YXIgZ2V0dGVyID0gbW9kdWxlICYmIG1vZHVsZS5fX2VzTW9kdWxlID9cbiBcdFx0XHRmdW5jdGlvbiBnZXREZWZhdWx0KCkgeyByZXR1cm4gbW9kdWxlWydkZWZhdWx0J107IH0gOlxuIFx0XHRcdGZ1bmN0aW9uIGdldE1vZHVsZUV4cG9ydHMoKSB7IHJldHVybiBtb2R1bGU7IH07XG4gXHRcdF9fd2VicGFja19yZXF1aXJlX18uZChnZXR0ZXIsICdhJywgZ2V0dGVyKTtcbiBcdFx0cmV0dXJuIGdldHRlcjtcbiBcdH07XG5cbiBcdC8vIE9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbFxuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5vID0gZnVuY3Rpb24ob2JqZWN0LCBwcm9wZXJ0eSkgeyByZXR1cm4gT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKG9iamVjdCwgcHJvcGVydHkpOyB9O1xuXG4gXHQvLyBfX3dlYnBhY2tfcHVibGljX3BhdGhfX1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5wID0gXCJcIjtcblxuIFx0Ly8gTG9hZCBlbnRyeSBtb2R1bGUgYW5kIHJldHVybiBleHBvcnRzXG4gXHRyZXR1cm4gX193ZWJwYWNrX3JlcXVpcmVfXyhfX3dlYnBhY2tfcmVxdWlyZV9fLnMgPSAwKTtcblxuXG5cbi8vIFdFQlBBQ0sgRk9PVEVSIC8vXG4vLyB3ZWJwYWNrL2Jvb3RzdHJhcCA0MmUwNDI3YTFhNmUzOTc1N2M4YyIsIi8qIEFueSBjb3B5cmlnaHQgaXMgZGVkaWNhdGVkIHRvIHRoZSBQdWJsaWMgRG9tYWluLlxuIGh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL3B1YmxpY2RvbWFpbi96ZXJvLzEuMC8gKi9cblxuLy8gT3JpZ2luYWwgc291cmNlIGNvZGUgZm9yIHRoZSBpbmxpbmUgc291cmNlIG1hcCB0ZXN0LlxuLy8gVGhlIGdlbmVyYXRlZCBmaWxlIHdhcyBtYWRlIHdpdGhcbi8vICAgIHdlYnBhY2sgLS1kZXZ0b29sIGlubGluZS1zb3VyY2UtbWFwIGNvZGVfaW5saW5lX29yaWdpbmFsLmpzIGNvZGVfaW5saW5lX2J1bmRsZS5qc1xuXG5cInVzZSBzdHJpY3RcIjtcblxuZnVuY3Rpb24gZigpIHtcbiAgY29uc29sZS5sb2coXCJJJ20gYSBnb2xkZmlzaCB3aXRoIGEgbWVycnkgZmFjZVwiKTtcbn1cblxuZigpO1xuXG5cblxuLy8vLy8vLy8vLy8vLy8vLy8vXG4vLyBXRUJQQUNLIEZPT1RFUlxuLy8gLi9jb2RlX2lubGluZV9vcmlnaW5hbC5qc1xuLy8gbW9kdWxlIGlkID0gMFxuLy8gbW9kdWxlIGNodW5rcyA9IDAiXSwic291cmNlUm9vdCI6IiJ9 \ No newline at end of file
diff --git a/devtools/client/framework/test/code_inline_original.js b/devtools/client/framework/test/code_inline_original.js
new file mode 100644
index 0000000000..c1b0b033cd
--- /dev/null
+++ b/devtools/client/framework/test/code_inline_original.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Original source code for the inline source map test.
+// The generated file was made with
+// webpack --devtool inline-source-map code_inline_original.js code_inline_bundle.js
+
+"use strict";
+
+function f() {
+ console.log("I'm a goldfish with a merry face");
+}
+
+f();
diff --git a/devtools/client/framework/test/code_late_script.js b/devtools/client/framework/test/code_late_script.js
new file mode 100644
index 0000000000..a9ed62dba9
--- /dev/null
+++ b/devtools/client/framework/test/code_late_script.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Original source code for the inline source map test.
+// The generated file was made with
+// webpack --devtool source-map code_late_script.js --output code_bundle_late_script.js --mode development
+
+"use strict";
+
+function f() {
+ console.log("The first version of the script");
+}
+
+f();
diff --git a/devtools/client/framework/test/code_math.js b/devtools/client/framework/test/code_math.js
new file mode 100644
index 0000000000..0aace9b59f
--- /dev/null
+++ b/devtools/client/framework/test/code_math.js
@@ -0,0 +1,7 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function add(a, b, k) {
+ var result = a + b;
+ return k(result);
+}
diff --git a/devtools/client/framework/test/code_no_race.js b/devtools/client/framework/test/code_no_race.js
new file mode 100644
index 0000000000..3c7fd72efd
--- /dev/null
+++ b/devtools/client/framework/test/code_no_race.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Original source code for the inline source map test.
+// The generated file was made with
+// webpack --devtool source-map code_no_race.js code_bundle_no_race.js
+
+"use strict";
+
+function f() {
+ console.log("anything will do");
+}
+
+f();
+
+// Avoid script GC.
+window.f = f;
diff --git a/devtools/client/framework/test/code_reload_1.js b/devtools/client/framework/test/code_reload_1.js
new file mode 100644
index 0000000000..e6eecb09f3
--- /dev/null
+++ b/devtools/client/framework/test/code_reload_1.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Original source code for the inline source map test.
+// The generated file was made with
+// webpack --devtool source-map code_reload_1.js code_bundle_reload_1.js
+// perl -pi -e 's/code_bundle_reload_1.js.map/sjs_code_bundle_reload_map.sjs/' \
+// code_bundle_reload_1.js
+
+"use strict";
+
+function f() {
+ console.log("The first version of the script");
+}
+
+f();
diff --git a/devtools/client/framework/test/code_reload_2.js b/devtools/client/framework/test/code_reload_2.js
new file mode 100644
index 0000000000..0e1be97a6a
--- /dev/null
+++ b/devtools/client/framework/test/code_reload_2.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Original source code for the inline source map test.
+// The generated file was made with
+// webpack --devtool source-map code_reload_2.js code_bundle_reload_2.js
+// perl -pi -e 's/code_bundle_reload_2.js.map/sjs_code_bundle_reload_map.sjs/' \
+// code_bundle_reload_2.js
+
+"use strict";
+
+function f() {
+ console.log("The second version of the script");
+}
+
+f();
diff --git a/devtools/client/framework/test/doc_cached-resource.html b/devtools/client/framework/test/doc_cached-resource.html
new file mode 100644
index 0000000000..31fd315264
--- /dev/null
+++ b/devtools/client/framework/test/doc_cached-resource.html
@@ -0,0 +1,15 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <iframe src="http://example.org/browser/devtools/client/framework/test/doc_cached-resource_iframe.html"></iframe>
+ <script>
+ console.log("Hello from parent");
+ </script>
+ </body>
+</html>
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 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <script>
+ console.log("Hello from child");
+ </script>
+ </body>
+</html>
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 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Empty test page 1</title>
+ </head>
+
+ <body>
+ </body>
+
+</html>
diff --git a/devtools/client/framework/test/doc_reload.html b/devtools/client/framework/test/doc_reload.html
new file mode 100644
index 0000000000..6894782cd3
--- /dev/null
+++ b/devtools/client/framework/test/doc_reload.html
@@ -0,0 +1,15 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <script src="sjs_code_reload.sjs"></script>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Empty test page 1</title>
+ </head>
+
+ <body>
+ </body>
+
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Toolbox test for View Source methods</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <link charset="UTF-8" rel="stylesheet" href="doc_theme.css" />
+ <script src="code_math.js"></script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/devtools/client/framework/test/head.js b/devtools/client/framework/test/head.js
new file mode 100644
index 0000000000..20f3fe63a7
--- /dev/null
+++ b/devtools/client/framework/test/head.js
@@ -0,0 +1,475 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from ../../shared/test/shared-head.js */
+/* import-globals-from ../../shared/test/telemetry-test-helpers.js */
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+const EventEmitter = require("devtools/shared/event-emitter");
+
+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 { DevToolsServer } = require("devtools/server/devtools-server");
+ const { DevToolsClient } = require("devtools/client/devtools-client");
+
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+ DevToolsServer.allowChromeProcess = true;
+
+ SimpleTest.registerCleanupFunction(() => {
+ DevToolsServer.destroy();
+ });
+
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ await client.connect();
+ const mainProcessDescriptor = await client.mainRoot.getMainProcess();
+ const mainProcessTargetFront = await mainProcessDescriptor.getTarget();
+
+ callback(client, mainProcessTargetFront);
+}
+
+function getSourceActor(aSources, aURL) {
+ const item = aSources.getItemForAttachment(a => a.source.url === aURL);
+ return item && item.value;
+}
+
+/**
+ * Synthesize a keypress from a <key> element, taking into account
+ * any modifiers.
+ * @param {Element} el the <key> 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 <script> referencing URL. Return a promise that
+ * resolves when this has happened
+ * @param {String} url
+ * the url
+ * @return {Promise} a promise that resolves when the element has been created
+ */
+function createScript(url) {
+ info(`Creating script: ${url}`);
+ // This is not ideal if called multiple times, as it loads the frame script
+ // separately each time. See bug 1443680.
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [url], urlChild => {
+ const script = content.document.createElement("script");
+ script.setAttribute("src", urlChild);
+ content.document.body.appendChild(script);
+ });
+}
+
+/**
+ * Wait for the toolbox to notice that a given source is loaded
+ * @param {Toolbox} toolbox
+ * @param {String} url
+ * the url to wait for
+ * @return {Promise} a promise that is resolved when the source is loaded
+ */
+function waitForSourceLoad(toolbox, url) {
+ info(`Waiting for source ${url} to be available...`);
+ return new Promise(resolve => {
+ const { resourceWatcher } = toolbox;
+
+ function onAvailable(sources) {
+ for (const source of sources) {
+ if (source.url === url) {
+ resourceWatcher.unwatchResources([resourceWatcher.TYPES.SOURCE], {
+ onAvailable,
+ });
+ resolve();
+ }
+ }
+ }
+ resourceWatcher.watchResources([resourceWatcher.TYPES.SOURCE], {
+ onAvailable,
+ });
+ });
+}
+
+/**
+ * When a Toolbox is started it creates a DevToolPanel for each of the tools
+ * by calling toolDefinition.build(). The returned object should
+ * at least implement these functions. They will be used by the ToolBox.
+ *
+ * There may be no benefit in doing this as an abstract type, but if nothing
+ * else gives us a place to write documentation.
+ */
+function DevToolPanel(iframeWindow, toolbox) {
+ EventEmitter.decorate(this);
+
+ this._toolbox = toolbox;
+ this._window = iframeWindow;
+}
+
+DevToolPanel.prototype = {
+ open: function() {
+ return new Promise(resolve => {
+ executeSoon(() => {
+ this._isReady = true;
+ this.emit("ready");
+ resolve(this);
+ });
+ });
+ },
+
+ get document() {
+ return this._window.document;
+ },
+
+ get target() {
+ return this._toolbox.target;
+ },
+
+ get toolbox() {
+ return this._toolbox;
+ },
+
+ get isReady() {
+ return this._isReady;
+ },
+
+ _isReady: false,
+
+ destroy: function() {
+ return Promise.resolve(null);
+ },
+};
+
+/**
+ * Create a simple devtools test panel that implements the minimum API needed to be
+ * registered and opened in the toolbox.
+ */
+function createTestPanel(iframeWindow, toolbox) {
+ return new DevToolPanel(iframeWindow, toolbox);
+}
+
+async function openChevronMenu(toolbox) {
+ const chevronMenuButton = toolbox.doc.querySelector(".tools-chevron-menu");
+ EventUtils.synthesizeMouseAtCenter(chevronMenuButton, {}, toolbox.win);
+
+ const menuPopup = toolbox.doc.getElementById(
+ "tools-chevron-menu-button-panel"
+ );
+ ok(menuPopup, "tools-chevron-menupopup is available");
+
+ info("Waiting for the menu popup to be displayed");
+ await waitUntil(() => menuPopup.classList.contains("tooltip-visible"));
+}
+
+async function closeChevronMenu(toolbox) {
+ // In order to close the popup menu with escape key, set the focus to the chevron
+ // button at first.
+ const chevronMenuButton = toolbox.doc.querySelector(".tools-chevron-menu");
+ chevronMenuButton.focus();
+
+ EventUtils.sendKey("ESCAPE", toolbox.doc.defaultView);
+ const menuPopup = toolbox.doc.getElementById(
+ "tools-chevron-menu-button-panel"
+ );
+
+ info("Closing the chevron popup menu");
+ await waitUntil(() => !menuPopup.classList.contains("tooltip-visible"));
+}
+
+function prepareToolTabReorderTest(toolbox, startingOrder) {
+ Services.prefs.setCharPref(
+ "devtools.toolbox.tabsOrder",
+ startingOrder.join(",")
+ );
+ ok(
+ !toolbox.doc.getElementById("tools-chevron-menu-button"),
+ "The size of the screen being too small"
+ );
+
+ for (const id of startingOrder) {
+ ok(getElementByToolId(toolbox, id), `Tab element should exist for ${id}`);
+ }
+}
+
+async function dndToolTab(toolbox, dragTarget, dropTarget, passedTargets = []) {
+ info(`Drag ${dragTarget} to ${dropTarget}`);
+ const dragTargetEl = getElementByToolIdOrExtensionIdOrSelector(
+ toolbox,
+ dragTarget
+ );
+
+ const onReady = dragTargetEl.classList.contains("selected")
+ ? Promise.resolve()
+ : toolbox.once("select");
+ EventUtils.synthesizeMouseAtCenter(
+ dragTargetEl,
+ { type: "mousedown" },
+ dragTargetEl.ownerGlobal
+ );
+ await onReady;
+
+ for (const passedTarget of passedTargets) {
+ info(`Via ${passedTarget}`);
+ const passedTargetEl = getElementByToolIdOrExtensionIdOrSelector(
+ toolbox,
+ passedTarget
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ passedTargetEl,
+ { type: "mousemove" },
+ passedTargetEl.ownerGlobal
+ );
+ }
+
+ if (dropTarget) {
+ const dropTargetEl = getElementByToolIdOrExtensionIdOrSelector(
+ toolbox,
+ dropTarget
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ dropTargetEl,
+ { type: "mousemove" },
+ dropTargetEl.ownerGlobal
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ dropTargetEl,
+ { type: "mouseup" },
+ dropTargetEl.ownerGlobal
+ );
+ } else {
+ const containerEl = toolbox.doc.getElementById("toolbox-container");
+ EventUtils.synthesizeMouse(
+ containerEl,
+ 0,
+ 0,
+ { type: "mouseout" },
+ containerEl.ownerGlobal
+ );
+ }
+
+ // Wait for updating the preference.
+ await new Promise(resolve => {
+ const onUpdated = () => {
+ Services.prefs.removeObserver("devtools.toolbox.tabsOrder", onUpdated);
+ resolve();
+ };
+
+ Services.prefs.addObserver("devtools.toolbox.tabsOrder", onUpdated);
+ });
+}
+
+function assertToolTabOrder(toolbox, expectedOrder) {
+ info("Check the order of the tabs on the toolbar");
+
+ const tabEls = toolbox.doc.querySelectorAll(".devtools-tab");
+
+ for (let i = 0; i < expectedOrder.length; i++) {
+ const isOrdered =
+ tabEls[i].dataset.id === expectedOrder[i] ||
+ tabEls[i].dataset.extensionId === expectedOrder[i];
+ ok(isOrdered, `The tab[${expectedOrder[i]}] should exist at [${i}]`);
+ }
+}
+
+function assertToolTabSelected(toolbox, dragTarget) {
+ info("Check whether the drag target was selected");
+ const dragTargetEl = getElementByToolIdOrExtensionIdOrSelector(
+ toolbox,
+ dragTarget
+ );
+ ok(
+ dragTargetEl.classList.contains("selected"),
+ "The dragged tool should be selected"
+ );
+}
+
+function assertToolTabPreferenceOrder(expectedOrder) {
+ info("Check the order in DevTools preference for tabs order");
+ is(
+ Services.prefs.getCharPref("devtools.toolbox.tabsOrder"),
+ expectedOrder.join(","),
+ "The preference should be correct"
+ );
+}
+
+function getElementByToolId(toolbox, id) {
+ for (const tabEl of toolbox.doc.querySelectorAll(".devtools-tab")) {
+ if (tabEl.dataset.id === id || tabEl.dataset.extensionId === id) {
+ return tabEl;
+ }
+ }
+
+ return null;
+}
+
+function getElementByToolIdOrExtensionIdOrSelector(toolbox, idOrSelector) {
+ const tabEl = getElementByToolId(toolbox, idOrSelector);
+ return tabEl ? tabEl : toolbox.doc.querySelector(idOrSelector);
+}
+
+/**
+ * Returns a toolbox tab element, even if it's overflowed
+ **/
+function getToolboxTab(doc, toolId) {
+ return (
+ doc.getElementById(`toolbox-tab-${toolId}`) ||
+ doc.getElementById(`tools-chevron-menupopup-${toolId}`)
+ );
+}
+
+function getWindow(toolbox) {
+ return toolbox.topWindow;
+}
+
+async function resizeWindow(toolbox, width, height) {
+ const hostWindow = toolbox.win.parent;
+ const originalWidth = hostWindow.outerWidth;
+ const originalHeight = hostWindow.outerHeight;
+ const toWidth = width || originalWidth;
+ const toHeight = height || originalHeight;
+
+ const onResize = once(hostWindow, "resize");
+ hostWindow.resizeTo(toWidth, toHeight);
+ await onResize;
+}
+
+function assertSelectedLocationInDebugger(debuggerPanel, line, column) {
+ const location = debuggerPanel._selectors.getSelectedLocation(
+ debuggerPanel._getState()
+ );
+ is(location.line, line);
+ is(location.column, column);
+}
+
+/**
+ * Open a new tab on about:devtools-toolbox with the provided params object used as
+ * queryString.
+ */
+async function openAboutToolbox(params) {
+ info("Open about:devtools-toolbox");
+ const querystring = new URLSearchParams();
+ Object.keys(params).forEach(x => querystring.append(x, params[x]));
+
+ const tab = await addTab(`about:devtools-toolbox?${querystring}`);
+ const browser = tab.linkedBrowser;
+
+ return {
+ tab,
+ document: browser.contentDocument,
+ };
+}
+
+/**
+ * Load FTL.
+ *
+ * @param {Toolbox} toolbox
+ * Toolbox instance.
+ * @param {String} path
+ * Path to the FTL file.
+ */
+function loadFTL(toolbox, path) {
+ const win = toolbox.doc.ownerGlobal;
+
+ if (win.MozXULElement) {
+ win.MozXULElement.insertFTLIfNeeded(path);
+ }
+}
+
+/**
+ * Emit a reload key shortcut from a given toolbox, and wait for the reload to
+ * be completed.
+ *
+ * @param {String} shortcut
+ * The key shortcut to send, as expected by the devtools shortcuts
+ * helpers (eg. "CmdOrCtrl+F5").
+ * @param {Toolbox} toolbox
+ * The toolbox through which the event should be emitted.
+ */
+async function sendToolboxReloadShortcut(shortcut, toolbox) {
+ const promises = [];
+
+ // If we have a jsdebugger panel, wait for it to complete its reload.
+ const jsdebugger = toolbox.getPanel("jsdebugger");
+ if (jsdebugger) {
+ promises.push(jsdebugger.once("reloaded"));
+ }
+
+ // If we have an inspector panel, wait for it to complete its reload.
+ const inspector = toolbox.getPanel("inspector");
+ if (inspector) {
+ promises.push(
+ inspector.once("reloaded"),
+ inspector.once("inspector-updated")
+ );
+ }
+
+ const loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ promises.push(loadPromise);
+
+ info("Focus the toolbox window and emit the reload shortcut: " + shortcut);
+ toolbox.win.focus();
+ synthesizeKeyShortcut(shortcut, toolbox.win);
+
+ info("Wait for page and toolbox reload promises");
+ await Promise.all(promises);
+}
+
+function getErrorIcon(toolbox) {
+ return toolbox.doc.querySelector(".toolbox-error");
+}
+
+function getErrorIconCount(toolbox) {
+ const textContent = getErrorIcon(toolbox)?.textContent;
+ try {
+ const int = parseInt(textContent, 10);
+ // 99+ parses to 99, so we check if the parsedInt does not match the textContent.
+ return int.toString() === textContent ? int : textContent;
+ } catch (e) {
+ // In case the parseInt threw, return the actual textContent so the test can display
+ // an easy to debug failure.
+ return textContent;
+ }
+}
diff --git a/devtools/client/framework/test/helper_disable_cache.js b/devtools/client/framework/test/helper_disable_cache.js
new file mode 100644
index 0000000000..ddfdde17e2
--- /dev/null
+++ b/devtools/client/framework/test/helper_disable_cache.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This file assumes we have head.js globals for the scope where this is loaded.
+/* import-globals-from head.js */
+
+/* exported initTab, checkCacheStateForAllTabs, setDisableCacheCheckboxChecked,
+ finishUp */
+
+// Common code shared by browser_toolbox_options_disable_cache-*.js
+const TEST_URI = URL_ROOT + "browser_toolbox_options_disable_cache.sjs";
+var tabs = [
+ {
+ title: "Tab 0",
+ desc: "Toggles cache on.",
+ startToolbox: true,
+ },
+ {
+ title: "Tab 1",
+ desc: "Toolbox open before Tab 1 toggles cache.",
+ startToolbox: true,
+ },
+ {
+ title: "Tab 2",
+ desc: "Opens toolbox after Tab 1 has toggled cache. Also closes and opens.",
+ startToolbox: false,
+ },
+ {
+ title: "Tab 3",
+ desc: "No toolbox",
+ startToolbox: false,
+ },
+];
+
+async function initTab(tabX, startToolbox) {
+ tabX.tab = await addTab(TEST_URI);
+ tabX.target = await TargetFactory.forTab(tabX.tab);
+
+ if (startToolbox) {
+ tabX.toolbox = await gDevTools.showToolbox(tabX.target, "options");
+ }
+}
+
+async function checkCacheStateForAllTabs(states) {
+ for (let i = 0; i < tabs.length; i++) {
+ const tab = tabs[i];
+ await checkCacheEnabled(tab, states[i]);
+ }
+}
+
+async function checkCacheEnabled(tabX, expected) {
+ gBrowser.selectedTab = tabX.tab;
+
+ await reloadTab(tabX);
+
+ const oldGuid = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function() {
+ const doc = content.document;
+ const h1 = doc.querySelector("h1");
+ return h1.textContent;
+ }
+ );
+
+ await reloadTab(tabX);
+
+ const guid = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function() {
+ const doc = content.document;
+ const h1 = doc.querySelector("h1");
+ return h1.textContent;
+ }
+ );
+
+ if (expected) {
+ is(guid, oldGuid, tabX.title + " cache is enabled");
+ } else {
+ isnot(guid, oldGuid, tabX.title + " cache is not enabled");
+ }
+}
+
+async function setDisableCacheCheckboxChecked(tabX, state) {
+ gBrowser.selectedTab = tabX.tab;
+
+ const panel = tabX.toolbox.getCurrentPanel();
+ const cbx = panel.panelDoc.getElementById("devtools-disable-cache");
+
+ if (cbx.checked !== state) {
+ info("Setting disable cache checkbox to " + state + " for " + tabX.title);
+ const onReconfigured = tabX.toolbox.once("cache-reconfigured");
+ cbx.click();
+
+ // We have to wait for the reconfigure request to be finished before reloading
+ // the page.
+ await onReconfigured;
+ }
+}
+
+function reloadTab(tabX) {
+ const browser = gBrowser.selectedBrowser;
+
+ const reloadTabPromise = BrowserTestUtils.browserLoaded(browser).then(
+ function() {
+ info("Reloaded tab " + tabX.title);
+ }
+ );
+
+ info("Reloading tab " + tabX.title);
+ SpecialPowers.spawn(browser, [], () => {
+ content.location.reload(false);
+ });
+
+ return reloadTabPromise;
+}
+
+async function destroyTab(tabX) {
+ const toolbox = gDevTools.getToolbox(tabX.target);
+
+ let onceDestroyed = promise.resolve();
+ if (toolbox) {
+ onceDestroyed = gDevTools.once("toolbox-destroyed");
+ }
+
+ info("Removing tab " + tabX.title);
+ gBrowser.removeTab(tabX.tab);
+ info("Removed tab " + tabX.title);
+
+ info("Waiting for toolbox-destroyed");
+ await onceDestroyed;
+}
+
+async function finishUp() {
+ for (const tab of tabs) {
+ await destroyTab(tab);
+ }
+
+ tabs = null;
+}
diff --git a/devtools/client/framework/test/helper_enable_devtools_popup.js b/devtools/client/framework/test/helper_enable_devtools_popup.js
new file mode 100644
index 0000000000..84348c0dc1
--- /dev/null
+++ b/devtools/client/framework/test/helper_enable_devtools_popup.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { listenOnce } = require("devtools/shared/async-utils");
+
+/**
+ * Helpers dedicated to the browser_enable_devtools_popup* tests.
+ * Those tests usually test the same exact things, but using different
+ * configurations.
+ */
+
+function openDevToolsWithKey(key, modifiers) {
+ const onToolboxReady = gDevTools.once("toolbox-ready");
+ EventUtils.synthesizeKey(key, modifiers);
+ return onToolboxReady;
+}
+/* exported openDevToolsWithKey */
+
+function closeDevToolsWithKey(toolbox, key, modifiers) {
+ const onToolboxDestroyed = toolbox.once("destroyed");
+ EventUtils.synthesizeKey(key, modifiers);
+ return onToolboxDestroyed;
+}
+/* exported closeDevToolsWithKey */
+
+/**
+ * The popup element might still be in its template wrapper.
+ */
+function unwrapEnableDevToolsPopup(tab) {
+ const panelWrapper = tab.ownerDocument.getElementById(
+ "wrapper-enable-devtools-popup"
+ );
+ if (panelWrapper) {
+ info("Unwrapping enable devtools popup");
+ panelWrapper.replaceWith(panelWrapper.content);
+ }
+}
+
+/**
+ * Test if F12 is currently disabled:
+ * - press F12 -> popup is displayed
+ * - press F12 again -> popup is hidden
+ * - no toolbox was opened during the process
+ */
+async function checkF12IsDisabled(tab) {
+ unwrapEnableDevToolsPopup(tab);
+
+ const popup = tab.ownerDocument.getElementById("enable-devtools-popup");
+ is(popup.state, "closed", "The enable devtools popup is initially hidden");
+
+ const failOnToolboxReady = () => {
+ ok(false, "The devtools toolbox should not open");
+ };
+ gDevTools.on("toolbox-ready", failOnToolboxReady);
+
+ info("Press F12 and wait for the enable devtools popup to be displayed");
+ const onPopupShown = listenOnce(popup, "popupshown");
+ EventUtils.synthesizeKey("VK_F12");
+ await onPopupShown;
+ is(popup.state, "open", "The enable devtools popup is now visible");
+
+ info("Press F12 again and wait for the enable devtools popup to hide");
+ const onPopupHidden = listenOnce(popup, "popuphidden");
+ EventUtils.synthesizeKey("VK_F12");
+ await onPopupHidden;
+ is(popup.state, "closed", "The enable devtools popup is hidden again");
+
+ gDevTools.off("toolbox-ready", failOnToolboxReady);
+}
+/* exported checkF12IsDisabled */
+
+/**
+ * Test that DevTools can be open with another keyboard shortcut than F12.
+ * The enable-devtools popup should not be displayed.
+ */
+async function openDevToolsWithInspectorKey(tab) {
+ unwrapEnableDevToolsPopup(tab);
+
+ info("Open DevTools via another shortcut (only F12 should be disabled)");
+ const popup = tab.ownerDocument.getElementById("enable-devtools-popup");
+
+ // We are going to use F12 but the popup should never show up.
+ const failOnPopupShown = () => {
+ ok(false, "The enable devtools popup should not be displayed");
+ };
+ popup.addEventListener("popupshown", failOnPopupShown);
+
+ const toolbox = await openDevToolsWithKey("I", {
+ accelKey: true,
+ shiftKey: !navigator.userAgent.match(/Mac/),
+ altKey: navigator.userAgent.match(/Mac/),
+ });
+
+ is(popup.state, "closed", "The enable devtools popup is still hidden");
+ popup.removeEventListener("popupshown", failOnPopupShown);
+
+ return toolbox;
+}
+/* exported openDevToolsWithInspectorKey */
+
+/**
+ * Test that the toolbox can be closed with F12, without triggering the popup.
+ */
+async function closeDevToolsWithF12(tab, toolbox) {
+ unwrapEnableDevToolsPopup(tab);
+
+ const popup = tab.ownerDocument.getElementById("enable-devtools-popup");
+
+ // We are going to use F12 but the popup should never show up.
+ const failOnPopupShown = () => {
+ ok(false, "The enable devtools popup should not be displayed");
+ };
+ popup.addEventListener("popupshown", failOnPopupShown);
+
+ info("Press F12 and wait for the toolbox to be destroyed");
+ await closeDevToolsWithKey(toolbox, "VK_F12");
+ is(popup.state, "closed", "The enable devtools popup is still hidden");
+
+ popup.removeEventListener("popupshown", failOnPopupShown);
+}
+/* exported closeDevToolsWithF12 */
+
+/**
+ * Test if F12 is enabled:
+ * - press F12 -> toolbox opens
+ * - press F12 -> toolbox closes
+ * - no enable devtools popup was opened during the process
+ */
+async function checkF12IsEnabled(tab) {
+ unwrapEnableDevToolsPopup(tab);
+
+ const popup = tab.ownerDocument.getElementById("enable-devtools-popup");
+
+ // We are going to use F12 several times, but the popup should never show up.
+ // Add a listener on popupshown to make sure this doesn't happen
+ const failOnPopupShown = () => {
+ ok(false, "The enable devtools popup should not be displayed");
+ };
+ popup.addEventListener("popupshown", failOnPopupShown);
+
+ info("Check that F12 can now open the toolbox.");
+ const toolbox = await openDevToolsWithKey("VK_F12");
+ is(popup.state, "closed", "The enable devtools popup is still hidden");
+
+ info("Press F12 and wait for the toolbox to be destroyed");
+ await closeDevToolsWithKey(toolbox, "VK_F12");
+ is(popup.state, "closed", "The enable devtools popup is still hidden");
+
+ // cleanup
+ popup.removeEventListener("popupshown", failOnPopupShown);
+}
+/* exported checkF12IsEnabled */
diff --git a/devtools/client/framework/test/metrics/browser_metrics.ini b/devtools/client/framework/test/metrics/browser_metrics.ini
new file mode 100644
index 0000000000..18daaaaa47
--- /dev/null
+++ b/devtools/client/framework/test/metrics/browser_metrics.ini
@@ -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
+
+# Tests counting the numbers of loaded modules have distinct .ini file to execute the test
+# individually, without any other test being executed before or after, as it could impact
+# the number of loaded modules.
+# This ini file is for all the _other_ tests, where such setup isn't relevant.
+[browser_metrics_pool.js]
+skip-if = debug || asan
diff --git a/devtools/client/framework/test/metrics/browser_metrics_debugger.ini b/devtools/client/framework/test/metrics/browser_metrics_debugger.ini
new file mode 100644
index 0000000000..d1ec7232f6
--- /dev/null
+++ b/devtools/client/framework/test/metrics/browser_metrics_debugger.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.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_metrics_debugger.js]
+skip-if = os != 'linux' || debug || asan # Results should be platform agnostic - only run on linux64-opt
diff --git a/devtools/client/framework/test/metrics/browser_metrics_debugger.js b/devtools/client/framework/test/metrics/browser_metrics_debugger.js
new file mode 100644
index 0000000000..84075f895e
--- /dev/null
+++ b/devtools/client/framework/test/metrics/browser_metrics_debugger.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test records the number of modules loaded by DevTools, as well as the total count
+ * of characters in those modules, when opening the debugger. These metrics are
+ * retrieved by perfherder via logs.
+ */
+
+const TEST_URL =
+ "data:text/html;charset=UTF-8,<div>Debugger modules load test</div>";
+
+add_task(async function() {
+ // Disable randomly spawning processes during tests
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ const toolbox = await openNewTabAndToolbox(TEST_URL, "jsdebugger");
+ const toolboxBrowserLoader = toolbox.win.getBrowserLoaderForWindow();
+
+ // Retrieve the browser loader dedicated to the Debugger.
+ const panel = toolbox.getCurrentPanel();
+ const debuggerLoader = panel.panelWin.getBrowserLoaderForWindow();
+
+ const loaders = [
+ loader.loader,
+ toolboxBrowserLoader.loader,
+ debuggerLoader.loader,
+ ];
+
+ const allowedDupes = [
+ "@loader/unload.js",
+ "@loader/options.js",
+ "chrome.js",
+ "resource://devtools/client/shared/vendor/react-dom.js",
+ "resource://devtools/client/shared/vendor/react.js",
+ "resource://devtools/client/debugger/dist/vendors.js",
+ "resource://devtools/client/shared/vendor/react-prop-types.js",
+ "resource://devtools/client/shared/vendor/react-dom-factories.js",
+ "resource://devtools/client/shared/vendor/react-redux.js",
+ "resource://devtools/client/shared/vendor/redux.js",
+ "resource://devtools/client/debugger/src/workers/parser/index.js",
+ "resource://devtools/client/shared/source-map/index.js",
+ "resource://devtools/client/shared/components/menu/MenuButton.js",
+ "resource://devtools/client/shared/components/menu/MenuItem.js",
+ "resource://devtools/client/shared/components/menu/MenuList.js",
+ ];
+ runDuplicatedModulesTest(loaders, allowedDupes);
+
+ runMetricsTest({
+ filterString: "devtools/client/debugger",
+ loaders,
+ panelName: "debugger",
+ });
+
+ // 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.target.client.waitForRequestsToSettle();
+});
diff --git a/devtools/client/framework/test/metrics/browser_metrics_inspector.ini b/devtools/client/framework/test/metrics/browser_metrics_inspector.ini
new file mode 100644
index 0000000000..2ffc31ed80
--- /dev/null
+++ b/devtools/client/framework/test/metrics/browser_metrics_inspector.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.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_metrics_inspector.js]
+skip-if = os != 'linux' || debug || asan # Results should be platform agnostic - only run on linux64-opt
diff --git a/devtools/client/framework/test/metrics/browser_metrics_inspector.js b/devtools/client/framework/test/metrics/browser_metrics_inspector.js
new file mode 100644
index 0000000000..41db12cdbf
--- /dev/null
+++ b/devtools/client/framework/test/metrics/browser_metrics_inspector.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test records the number of modules loaded by DevTools, as well as the total count
+ * of characters in those modules, when opening the inspector. These metrics are retrieved
+ * by perfherder via logs.
+ */
+
+const TEST_URL =
+ "data:text/html;charset=UTF-8,<div>Inspector modules load test</div>";
+
+add_task(async function() {
+ const toolbox = await openNewTabAndToolbox(TEST_URL, "inspector");
+ const toolboxBrowserLoader = toolbox.win.getBrowserLoaderForWindow();
+
+ // Most panels involve three loaders:
+ // - the global devtools loader
+ // - the browser loader used by the toolbox
+ // - a specific browser loader created for the panel
+ // But the inspector is a specific case, because it reuses the BrowserLoader
+ // of the toolbox to load its react components. This is why we only list
+ // two loaders here.
+ const loaders = [loader.loader, toolboxBrowserLoader.loader];
+
+ runDuplicatedModulesTest(loaders, [
+ "@loader/unload.js",
+ "@loader/options.js",
+ "chrome.js",
+ "resource://devtools/client/shared/vendor/react.js",
+ "resource://devtools/client/shared/vendor/react-dom-factories.js",
+ "resource://devtools/client/shared/vendor/react-prop-types.js",
+ "resource://devtools/client/shared/vendor/redux.js",
+ ]);
+
+ runMetricsTest({
+ filterString: "devtools/client/inspector",
+ loaders,
+ panelName: "inspector",
+ });
+});
diff --git a/devtools/client/framework/test/metrics/browser_metrics_netmonitor.ini b/devtools/client/framework/test/metrics/browser_metrics_netmonitor.ini
new file mode 100644
index 0000000000..8cb733e546
--- /dev/null
+++ b/devtools/client/framework/test/metrics/browser_metrics_netmonitor.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.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_metrics_netmonitor.js]
+skip-if = os != 'linux' || debug || asan # Results should be platform agnostic - only run on linux64-opt
diff --git a/devtools/client/framework/test/metrics/browser_metrics_netmonitor.js b/devtools/client/framework/test/metrics/browser_metrics_netmonitor.js
new file mode 100644
index 0000000000..e9648b7807
--- /dev/null
+++ b/devtools/client/framework/test/metrics/browser_metrics_netmonitor.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test records the number of modules loaded by DevTools, as well as the total count
+ * of characters in those modules, when opening the netmonitor. These metrics are
+ * retrieved by perfherder via logs.
+ */
+
+const TEST_URL =
+ "data:text/html;charset=UTF-8,<div>Netmonitor modules load test</div>";
+
+add_task(async function() {
+ const toolbox = await openNewTabAndToolbox(TEST_URL, "netmonitor");
+ const toolboxBrowserLoader = toolbox.win.getBrowserLoaderForWindow();
+
+ // Retrieve the browser loader dedicated to the Netmonitor.
+ const panel = toolbox.getCurrentPanel();
+ const netmonitorLoader = panel.panelWin.getBrowserLoaderForWindow();
+
+ const loaders = [
+ loader.loader,
+ toolboxBrowserLoader.loader,
+ netmonitorLoader.loader,
+ ];
+
+ // Uncomment after Bug 1581068 is fixed, otherwise the test might fail too
+ // frequently.
+
+ // const allowedDupes = [
+ // "@loader/unload.js",
+ // "@loader/options.js",
+ // "chrome.js",
+ // "resource://devtools/client/netmonitor/src/api.js",
+ // "resource://devtools/client/shared/vendor/redux.js",
+ // "resource://devtools/client/netmonitor/src/connector/index.js",
+ // "resource://devtools/client/netmonitor/src/create-store.js",
+ // "resource://devtools/client/netmonitor/src/constants.js",
+ // "resource://devtools/client/netmonitor/src/middleware/batching.js",
+ // "resource://devtools/client/netmonitor/src/middleware/prefs.js",
+ // "resource://devtools/client/netmonitor/src/middleware/recording.js",
+ // "resource://devtools/client/netmonitor/src/selectors/index.js",
+ // "resource://devtools/client/netmonitor/src/selectors/requests.js",
+ // "resource://devtools/client/shared/vendor/reselect.js",
+ // "resource://devtools/client/netmonitor/src/utils/filter-predicates.js",
+ // "resource://devtools/client/netmonitor/src/utils/filter-text-utils.js",
+ // "resource://devtools/client/netmonitor/src/utils/format-utils.js",
+ // "resource://devtools/client/netmonitor/src/utils/l10n.js",
+ // "resource://devtools/client/netmonitor/src/utils/sort-predicates.js",
+ // "resource://devtools/client/netmonitor/src/utils/request-utils.js",
+ // "resource://devtools/client/netmonitor/src/selectors/search.js",
+ // "resource://devtools/client/netmonitor/src/selectors/timing-markers.js",
+ // "resource://devtools/client/netmonitor/src/selectors/ui.js",
+ // "resource://devtools/client/netmonitor/src/selectors/messages.js",
+ // "resource://devtools/client/netmonitor/src/middleware/throttling.js",
+ // "resource://devtools/client/shared/components/throttling/actions.js",
+ // "resource://devtools/client/netmonitor/src/middleware/event-telemetry.js",
+ // "resource://devtools/client/netmonitor/src/reducers/index.js",
+ // "resource://devtools/client/netmonitor/src/reducers/batching.js",
+ // "resource://devtools/client/netmonitor/src/reducers/requests.js",
+ // "resource://devtools/client/netmonitor/src/reducers/search.js",
+ // "resource://devtools/client/netmonitor/src/reducers/sort.js",
+ // "resource://devtools/client/netmonitor/src/reducers/filters.js",
+ // "resource://devtools/client/netmonitor/src/reducers/timing-markers.js",
+ // "resource://devtools/client/netmonitor/src/reducers/ui.js",
+ // "resource://devtools/client/netmonitor/src/reducers/messages.js",
+ // "resource://devtools/client/shared/components/throttling/reducer.js",
+ // "resource://devtools/client/netmonitor/src/actions/index.js",
+ // "resource://devtools/client/netmonitor/src/actions/batching.js",
+ // "resource://devtools/client/netmonitor/src/actions/filters.js",
+ // "resource://devtools/client/netmonitor/src/actions/requests.js",
+ // "resource://devtools/client/netmonitor/src/actions/selection.js",
+ // "resource://devtools/client/netmonitor/src/actions/sort.js",
+ // "resource://devtools/client/netmonitor/src/actions/timing-markers.js",
+ // "resource://devtools/client/netmonitor/src/actions/ui.js",
+ // "resource://devtools/client/netmonitor/src/actions/messages.js",
+ // "resource://devtools/client/netmonitor/src/actions/search.js",
+ // "resource://devtools/client/netmonitor/src/workers/search/index.js",
+ // "resource://devtools/client/netmonitor/src/workers/worker-utils.js",
+ // ];
+ // runDuplicatedModulesTest(loaders, allowedDupes);
+
+ runMetricsTest({
+ filterString: "devtools/client/netmonitor",
+ loaders,
+ panelName: "netmonitor",
+ });
+});
diff --git a/devtools/client/framework/test/metrics/browser_metrics_pool.js b/devtools/client/framework/test/metrics/browser_metrics_pool.js
new file mode 100644
index 0000000000..99e59a77f3
--- /dev/null
+++ b/devtools/client/framework/test/metrics/browser_metrics_pool.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { DevToolsServer } = require("devtools/server/devtools-server");
+const { Pool } = require("devtools/shared/protocol");
+
+// Test parameters
+const ROOT_POOLS = 100;
+const POOL_DEPTH = 10;
+const POOLS_BY_LEVEL = 100;
+// Number of Pools that will be added once the environment is set up.
+const ADDITIONAL_POOLS = 5000;
+
+add_task(async function() {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+ const conn = DevToolsServer.connectPipe()._serverConnection;
+
+ info("Add multiple Pools to the connection");
+ const pools = setupTestEnvironment(conn);
+
+ let sumResult = 0;
+
+ info("Test how long it takes to manage new Pools");
+ let start = performance.now();
+ let parentPool = pools[pools.length - 1];
+ const newPools = [];
+ for (let i = 0; i < ADDITIONAL_POOLS; i++) {
+ const pool = new Pool(conn, `${parentPool.label}-${i}`);
+ newPools.push(pool);
+ parentPool.manage(pool);
+ }
+ const manageResult = performance.now() - start;
+ sumResult += manageResult;
+
+ info("Test how long it takes to manage Pools that were already managed");
+ start = performance.now();
+ parentPool = pools[pools.length - 2];
+ for (const pool of newPools) {
+ parentPool.manage(pool);
+ }
+ const manageAlreadyManagedResult = performance.now() - start;
+ sumResult += manageAlreadyManagedResult;
+
+ info("Test how long it takes to unmanage Pools");
+ start = performance.now();
+ for (const pool of newPools) {
+ parentPool.unmanage(pool);
+ }
+ const unmanageResult = performance.now() - start;
+ sumResult += unmanageResult;
+
+ info("Test how long it takes to destroy all the Pools");
+ start = performance.now();
+ conn.onClosed();
+ const destroyResult = performance.now() - start;
+ sumResult += destroyResult;
+
+ const PERFHERDER_DATA = {
+ framework: {
+ name: "devtools",
+ },
+ suites: [
+ {
+ name: "server.pool",
+ value: sumResult,
+ subtests: [
+ {
+ name: "server.pool.manage",
+ value: manageResult,
+ },
+ {
+ name: "server.pool.manage-already-managed",
+ value: manageAlreadyManagedResult,
+ },
+ {
+ name: "server.pool.unmanage",
+ value: unmanageResult,
+ },
+ {
+ name: "server.pool.destroy",
+ value: destroyResult,
+ },
+ ],
+ },
+ ],
+ };
+ info("PERFHERDER_DATA: " + JSON.stringify(PERFHERDER_DATA));
+});
+
+// Some Pool operations might be impacted by the number of existing pools in a connection,
+// so it's important to have a sizeable number of Pools in order to assert Pool performances.
+function setupTestEnvironment(conn) {
+ const pools = [];
+ for (let i = 0; i < ROOT_POOLS; i++) {
+ const rootPool = new Pool(conn, "root-pool-" + i);
+ pools.push(rootPool);
+ let parent = rootPool;
+ for (let j = 0; j < POOL_DEPTH; j++) {
+ const intermediatePool = new Pool(conn, `pool-${i}-${j}`);
+ pools.push(intermediatePool);
+ parent.manage(intermediatePool);
+
+ for (let k = 0; k < POOLS_BY_LEVEL; k++) {
+ const pool = new Pool(conn, `pool-${i}-${j}-${k}`);
+ pools.push(pool);
+ intermediatePool.manage(pool);
+ }
+
+ parent = intermediatePool;
+ }
+ }
+ return pools;
+}
diff --git a/devtools/client/framework/test/metrics/browser_metrics_webconsole.ini b/devtools/client/framework/test/metrics/browser_metrics_webconsole.ini
new file mode 100644
index 0000000000..87d7c2c7b0
--- /dev/null
+++ b/devtools/client/framework/test/metrics/browser_metrics_webconsole.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.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_metrics_webconsole.js]
+skip-if = os != 'linux' || debug || asan # Results should be platform agnostic - only run on linux64-opt
diff --git a/devtools/client/framework/test/metrics/browser_metrics_webconsole.js b/devtools/client/framework/test/metrics/browser_metrics_webconsole.js
new file mode 100644
index 0000000000..cc24bfebbb
--- /dev/null
+++ b/devtools/client/framework/test/metrics/browser_metrics_webconsole.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test records the number of modules loaded by DevTools, as well as the total count
+ * of characters in those modules, when opening the webconsole. These metrics are
+ * retrieved by perfherder via logs.
+ */
+
+const TEST_URL =
+ "data:text/html;charset=UTF-8,<div>Webconsole modules load test</div>";
+
+add_task(async function() {
+ const toolbox = await openNewTabAndToolbox(TEST_URL, "webconsole");
+ const toolboxBrowserLoader = toolbox.win.getBrowserLoaderForWindow();
+
+ // Retrieve the browser loader dedicated to the WebConsole.
+ const panel = toolbox.getCurrentPanel();
+ const webconsoleLoader = panel._frameWindow.getBrowserLoaderForWindow();
+
+ const loaders = [
+ loader.loader,
+ toolboxBrowserLoader.loader,
+ webconsoleLoader.loader,
+ ];
+
+ const allowedDupes = [
+ "@loader/unload.js",
+ "@loader/options.js",
+ "chrome.js",
+ "resource://devtools/client/webconsole/constants.js",
+ "resource://devtools/client/webconsole/utils.js",
+ "resource://devtools/client/webconsole/utils/messages.js",
+ "resource://devtools/client/webconsole/utils/l10n.js",
+ "resource://devtools/client/netmonitor/src/utils/request-utils.js",
+ "resource://devtools/client/webconsole/types.js",
+ "resource://devtools/client/shared/components/menu/MenuButton.js",
+ "resource://devtools/client/shared/components/menu/MenuItem.js",
+ "resource://devtools/client/shared/components/menu/MenuList.js",
+ "resource://devtools/client/shared/vendor/react.js",
+ "resource://devtools/client/shared/vendor/react-dom.js",
+ "resource://devtools/client/shared/vendor/react-prop-types.js",
+ "resource://devtools/client/shared/vendor/react-dom-factories.js",
+ "resource://devtools/client/shared/vendor/react-redux.js",
+ "resource://devtools/client/shared/vendor/redux.js",
+ "resource://devtools/client/shared/redux/middleware/thunk.js",
+ ];
+ runDuplicatedModulesTest(loaders, allowedDupes);
+
+ runMetricsTest({
+ filterString: "devtools/client/webconsole",
+ loaders,
+ panelName: "webconsole",
+ });
+});
diff --git a/devtools/client/framework/test/metrics/head.js b/devtools/client/framework/test/metrics/head.js
new file mode 100644
index 0000000000..d447a4012f
--- /dev/null
+++ b/devtools/client/framework/test/metrics/head.js
@@ -0,0 +1,189 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from ../../../shared/test/shared-head.js */
+/* import-globals-from ../../../shared/test/telemetry-test-helpers.js */
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+// So that PERFHERDER data can be extracted from the logs.
+SimpleTest.requestCompleteLog();
+
+function getFilteredModules(filters, loaders) {
+ let modules = [];
+ for (const l of loaders) {
+ const loaderModulesMap = l.modules;
+ const loaderModulesPaths = Object.keys(loaderModulesMap);
+ modules = modules.concat(loaderModulesPaths);
+ }
+ return modules.filter(url => filters.some(filter => url.includes(filter)));
+}
+
+function countCharsInModules(modules) {
+ return modules.reduce((sum, uri) => {
+ try {
+ return sum + require("raw!" + uri).length;
+ } catch (e) {
+ // Ignore failures
+ return sum;
+ }
+ }, 0);
+}
+
+/**
+ * Record module loading data.
+ *
+ * @param {Object}
+ * - filterString {String} path to use to filter modules specific to the current panel
+ * - loaders {Array} Array of Loaders to check for modules
+ * - panelName {String} reused in identifiers for perfherder data
+ */
+function runMetricsTest({ filterString, loaders, panelName }) {
+ const allModules = getFilteredModules([""], loaders);
+ const panelModules = getFilteredModules([filterString], loaders);
+ const vendoredModules = getFilteredModules(
+ ["devtools/client/debugger/dist/vendors", "devtools/client/shared/vendor/"],
+ loaders
+ );
+
+ const allModulesCount = allModules.length;
+ const panelModulesCount = panelModules.length;
+ const vendoredModulesCount = vendoredModules.length;
+
+ const allModulesChars = countCharsInModules(allModules);
+ const panelModulesChars = countCharsInModules(panelModules);
+ const vendoredModulesChars = countCharsInModules(vendoredModules);
+
+ const PERFHERDER_DATA = {
+ framework: {
+ name: "devtools",
+ },
+ suites: [
+ {
+ name: panelName + "-metrics",
+ value: allModulesChars,
+ subtests: [
+ {
+ name: panelName + "-modules",
+ value: panelModulesCount,
+ },
+ {
+ name: panelName + "-chars",
+ value: panelModulesChars,
+ },
+ {
+ name: "all-modules",
+ value: allModulesCount,
+ },
+ {
+ name: "all-chars",
+ value: allModulesChars,
+ },
+ {
+ name: "vendored-modules",
+ value: vendoredModulesCount,
+ },
+ {
+ name: "vendored-chars",
+ value: vendoredModulesChars,
+ },
+ ],
+ },
+ ],
+ };
+ info("PERFHERDER_DATA: " + JSON.stringify(PERFHERDER_DATA));
+
+ // Simply check that we found valid values.
+ ok(
+ allModulesCount > panelModulesCount && panelModulesCount > 0,
+ "Successfully recorded module count for " + panelName
+ );
+ ok(
+ allModulesChars > panelModulesChars && panelModulesChars > 0,
+ "Successfully recorded char count for " + panelName
+ );
+
+ // Easy way to check how many vendored chars we have for a given panel.
+ const percentage = ((100 * vendoredModulesChars) / allModulesChars).toFixed(
+ 1
+ );
+ info(`Percentage of vendored chars for ${panelName}: ${percentage}%`);
+}
+
+function getDuplicatedModules(loaders) {
+ const allModules = getFilteredModules([""], loaders);
+
+ const uniqueModules = new Set();
+ const duplicatedModules = new Set();
+ for (const mod of allModules) {
+ if (uniqueModules.has(mod)) {
+ duplicatedModules.add(mod);
+ }
+
+ uniqueModules.add(mod);
+ }
+
+ return duplicatedModules;
+}
+
+/**
+ * Check that modules are only loaded once in a given set of loaders.
+ * Panels might load the same module twice by mistake if they are both using
+ * a BrowserLoader and the regular DevTools Loader.
+ *
+ * @param {Array} loaders
+ * Array of Loader instances.
+ * @param {Array} allowedDupes
+ * Array of Strings which are paths to known duplicated modules.
+ * The test will also fail if a allowedDupesed module is not found in the
+ * duplicated modules.
+ */
+function runDuplicatedModulesTest(loaders, allowedDupes) {
+ const { AppConstants } = require("resource://gre/modules/AppConstants.jsm");
+ if (AppConstants.DEBUG_JS_MODULES) {
+ // DevTools load different modules when DEBUG_JS_MODULES is true, which
+ // makes the hardcoded allowedDupes incorrect. Fail the test early and return.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1590630.
+ ok(
+ false,
+ "The DevTools metrics tests should not run with " +
+ "`--enable-debug-js-modules`. Please disable this option " +
+ "and run the test again."
+ );
+ // early return to avoid polluting the logs with irrelevant errors.
+ return;
+ }
+
+ const duplicatedModules = getDuplicatedModules(loaders);
+
+ // Remove allowedDupes entries, and fail if an allowed entry is not found.
+ for (const mod of allowedDupes) {
+ const deleted = duplicatedModules.delete(mod);
+ if (!deleted) {
+ ok(
+ false,
+ "module not found in the duplicated modules: [" +
+ mod +
+ "]. The allowedDupes array should be updated to remove it."
+ );
+ }
+ }
+
+ // Prepare a log string with the paths of all duplicated modules.
+ let duplicatedModulesLog = "";
+ for (const mod of duplicatedModules) {
+ duplicatedModulesLog += ` [duplicated module] ${mod}\n`;
+ }
+
+ // Check that duplicatedModules Set is empty.
+ is(
+ duplicatedModules.size,
+ 0,
+ "Duplicated module load detected. List of duplicated modules:\n" +
+ duplicatedModulesLog
+ );
+}
diff --git a/devtools/client/framework/test/node/.eslintrc.js b/devtools/client/framework/test/node/.eslintrc.js
new file mode 100644
index 0000000000..ffb3e70473
--- /dev/null
+++ b/devtools/client/framework/test/node/.eslintrc.js
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+module.exports = {
+ env: {
+ jest: true,
+ },
+};
diff --git a/devtools/client/framework/test/node/README.md b/devtools/client/framework/test/node/README.md
new file mode 100644
index 0000000000..9fb86edfc5
--- /dev/null
+++ b/devtools/client/framework/test/node/README.md
@@ -0,0 +1,22 @@
+# Jest Tests for devtools/client/framework
+
+## About
+
+DevTools React components can be tested using [jest](https://jestjs.io/). Jest allows to test our UI components in isolation and complement our end to end mochitests.
+
+## Run locally
+
+We use yarn for dependency management. To run the tests locally:
+```
+ cd devtools/client/shared/framework/test/node
+ yarn && yarn test
+```
+
+## Run on try
+
+The tests run on try on linux64 platforms. The complete name of the try job is `devtools-tests`. In treeherder, they will show up as `node(devtools)`.
+
+Adding the tests to a try push depends on the try selector you are using.
+- try fuzzy: look for the job named `source-test-node-devtools-tests`
+
+The configuration file for try can be found at `taskcluster/ci/source-test/node.yml`
diff --git a/devtools/client/framework/test/node/babel.config.js b/devtools/client/framework/test/node/babel.config.js
new file mode 100644
index 0000000000..90cffba9c3
--- /dev/null
+++ b/devtools/client/framework/test/node/babel.config.js
@@ -0,0 +1,13 @@
+/* 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";
+
+module.exports = {
+ plugins: [
+ "@babel/plugin-proposal-async-generator-functions",
+ "@babel/plugin-proposal-optional-chaining",
+ "@babel/plugin-proposal-nullish-coalescing-operator",
+ ],
+};
diff --git a/devtools/client/framework/test/node/components/__snapshots__/debug-target-info.test.js.snap b/devtools/client/framework/test/node/components/__snapshots__/debug-target-info.test.js.snap
new file mode 100644
index 0000000000..db3d103e94
--- /dev/null
+++ b/devtools/client/framework/test/node/components/__snapshots__/debug-target-info.test.js.snap
@@ -0,0 +1,526 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DebugTargetInfo component Connection info renders the expected snapshot for USB Release target 1`] = `
+<header
+ className="debug-target-info qa-debug-target-info"
+>
+ <span
+ className="iconized-label qa-connection-info"
+ >
+ <img
+ alt="usb icon"
+ src="chrome://devtools/skin/images/aboutdebugging-usb-icon.svg"
+ />
+ toolbox.debugTargetInfo.connection.usb
+ </span>
+ <span
+ className="iconized-label qa-runtime-info"
+ >
+ <img
+ className="channel-icon qa-runtime-icon"
+ src="chrome://devtools/skin/images/aboutdebugging-firefox-release.svg"
+ />
+ <b
+ className="devtools-ellipsis-text"
+ >
+ toolbox.debugTargetInfo.runtimeLabel-usbRuntimeBrandName-1.0.0
+ </b>
+ <span
+ className="devtools-ellipsis-text"
+ >
+ usbDeviceName
+ </span>
+ </span>
+ <span
+ className="iconized-label debug-target-title"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.targetType.tab"
+ src="chrome://devtools/skin/images/globe.svg"
+ />
+ <b
+ className="devtools-ellipsis-text qa-target-title"
+ >
+ Test Tab Name
+ </b>
+ </span>
+ <div
+ className="debug-target-navigation"
+ >
+ <button
+ className="iconized-label navigation-button qa-back-button"
+ onClick={[Function]}
+ title="toolbox.debugTargetInfo.back"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.back"
+ src="chrome://browser/skin/back.svg"
+ />
+ </button>
+ <button
+ className="iconized-label navigation-button qa-forward-button"
+ onClick={[Function]}
+ title="toolbox.debugTargetInfo.forward"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.forward"
+ src="chrome://browser/skin/forward.svg"
+ />
+ </button>
+ <button
+ className="iconized-label navigation-button qa-reload-button"
+ onClick={[Function]}
+ title="toolbox.debugTargetInfo.reload"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.reload"
+ src="chrome://browser/skin/reload.svg"
+ />
+ </button>
+ </div>
+ <span
+ className="debug-target-url"
+ >
+ <form
+ className="debug-target-url-form"
+ onSubmit={[Function]}
+ >
+ <input
+ className="devtools-textinput debug-target-url-input"
+ defaultValue="http://some.target/url"
+ onChange={[Function]}
+ onFocus={[Function]}
+ />
+ </form>
+ </span>
+</header>
+`;
+
+exports[`DebugTargetInfo component Target icon renders the expected snapshot for a process target 1`] = `
+<header
+ className="debug-target-info qa-debug-target-info"
+>
+ <span
+ className="iconized-label qa-connection-info"
+ >
+ <img
+ alt="usb icon"
+ src="chrome://devtools/skin/images/aboutdebugging-usb-icon.svg"
+ />
+ toolbox.debugTargetInfo.connection.usb
+ </span>
+ <span
+ className="iconized-label qa-runtime-info"
+ >
+ <img
+ className="channel-icon qa-runtime-icon"
+ src="chrome://devtools/skin/images/aboutdebugging-firefox-release.svg"
+ />
+ <b
+ className="devtools-ellipsis-text"
+ >
+ toolbox.debugTargetInfo.runtimeLabel-usbRuntimeBrandName-1.0.0
+ </b>
+ <span
+ className="devtools-ellipsis-text"
+ >
+ usbDeviceName
+ </span>
+ </span>
+ <span
+ className="iconized-label debug-target-title"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.targetType.process"
+ src="chrome://devtools/skin/images/settings.svg"
+ />
+ <b
+ className="devtools-ellipsis-text qa-target-title"
+ >
+ Test Tab Name
+ </b>
+ </span>
+ <span
+ className="debug-target-url"
+ >
+ <span
+ className="debug-target-url-readonly devtools-ellipsis-text"
+ >
+ http://some.target/url
+ </span>
+ </span>
+</header>
+`;
+
+exports[`DebugTargetInfo component Target icon renders the expected snapshot for a tab target 1`] = `
+<header
+ className="debug-target-info qa-debug-target-info"
+>
+ <span
+ className="iconized-label qa-connection-info"
+ >
+ <img
+ alt="usb icon"
+ src="chrome://devtools/skin/images/aboutdebugging-usb-icon.svg"
+ />
+ toolbox.debugTargetInfo.connection.usb
+ </span>
+ <span
+ className="iconized-label qa-runtime-info"
+ >
+ <img
+ className="channel-icon qa-runtime-icon"
+ src="chrome://devtools/skin/images/aboutdebugging-firefox-release.svg"
+ />
+ <b
+ className="devtools-ellipsis-text"
+ >
+ toolbox.debugTargetInfo.runtimeLabel-usbRuntimeBrandName-1.0.0
+ </b>
+ <span
+ className="devtools-ellipsis-text"
+ >
+ usbDeviceName
+ </span>
+ </span>
+ <span
+ className="iconized-label debug-target-title"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.targetType.tab"
+ src="chrome://devtools/skin/images/globe.svg"
+ />
+ <b
+ className="devtools-ellipsis-text qa-target-title"
+ >
+ Test Tab Name
+ </b>
+ </span>
+ <div
+ className="debug-target-navigation"
+ >
+ <button
+ className="iconized-label navigation-button qa-back-button"
+ onClick={[Function]}
+ title="toolbox.debugTargetInfo.back"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.back"
+ src="chrome://browser/skin/back.svg"
+ />
+ </button>
+ <button
+ className="iconized-label navigation-button qa-forward-button"
+ onClick={[Function]}
+ title="toolbox.debugTargetInfo.forward"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.forward"
+ src="chrome://browser/skin/forward.svg"
+ />
+ </button>
+ <button
+ className="iconized-label navigation-button qa-reload-button"
+ onClick={[Function]}
+ title="toolbox.debugTargetInfo.reload"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.reload"
+ src="chrome://browser/skin/reload.svg"
+ />
+ </button>
+ </div>
+ <span
+ className="debug-target-url"
+ >
+ <form
+ className="debug-target-url-form"
+ onSubmit={[Function]}
+ >
+ <input
+ className="devtools-textinput debug-target-url-input"
+ defaultValue="http://some.target/url"
+ onChange={[Function]}
+ onFocus={[Function]}
+ />
+ </form>
+ </span>
+</header>
+`;
+
+exports[`DebugTargetInfo component Target icon renders the expected snapshot for a worker target 1`] = `
+<header
+ className="debug-target-info qa-debug-target-info"
+>
+ <span
+ className="iconized-label qa-connection-info"
+ >
+ <img
+ alt="usb icon"
+ src="chrome://devtools/skin/images/aboutdebugging-usb-icon.svg"
+ />
+ toolbox.debugTargetInfo.connection.usb
+ </span>
+ <span
+ className="iconized-label qa-runtime-info"
+ >
+ <img
+ className="channel-icon qa-runtime-icon"
+ src="chrome://devtools/skin/images/aboutdebugging-firefox-release.svg"
+ />
+ <b
+ className="devtools-ellipsis-text"
+ >
+ toolbox.debugTargetInfo.runtimeLabel-usbRuntimeBrandName-1.0.0
+ </b>
+ <span
+ className="devtools-ellipsis-text"
+ >
+ usbDeviceName
+ </span>
+ </span>
+ <span
+ className="iconized-label debug-target-title"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.targetType.worker"
+ src="chrome://devtools/skin/images/debugging-workers.svg"
+ />
+ <b
+ className="devtools-ellipsis-text qa-target-title"
+ >
+ Test Tab Name
+ </b>
+ </span>
+ <span
+ className="debug-target-url"
+ >
+ <span
+ className="debug-target-url-readonly devtools-ellipsis-text"
+ >
+ http://some.target/url
+ </span>
+ </span>
+</header>
+`;
+
+exports[`DebugTargetInfo component Target icon renders the expected snapshot for an extension target 1`] = `
+<header
+ className="debug-target-info qa-debug-target-info"
+>
+ <span
+ className="iconized-label qa-connection-info"
+ >
+ <img
+ alt="usb icon"
+ src="chrome://devtools/skin/images/aboutdebugging-usb-icon.svg"
+ />
+ toolbox.debugTargetInfo.connection.usb
+ </span>
+ <span
+ className="iconized-label qa-runtime-info"
+ >
+ <img
+ className="channel-icon qa-runtime-icon"
+ src="chrome://devtools/skin/images/aboutdebugging-firefox-release.svg"
+ />
+ <b
+ className="devtools-ellipsis-text"
+ >
+ toolbox.debugTargetInfo.runtimeLabel-usbRuntimeBrandName-1.0.0
+ </b>
+ <span
+ className="devtools-ellipsis-text"
+ >
+ usbDeviceName
+ </span>
+ </span>
+ <span
+ className="iconized-label debug-target-title"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.targetType.extension"
+ src="chrome://devtools/skin/images/debugging-addons.svg"
+ />
+ <b
+ className="devtools-ellipsis-text qa-target-title"
+ >
+ Test Tab Name
+ </b>
+ </span>
+ <span
+ className="debug-target-url"
+ >
+ <span
+ className="debug-target-url-readonly devtools-ellipsis-text"
+ >
+ http://some.target/url
+ </span>
+ </span>
+</header>
+`;
+
+exports[`DebugTargetInfo component Target title renders the expected snapshot for This Firefox target 1`] = `
+<header
+ className="debug-target-info qa-debug-target-info"
+>
+ <span
+ className="iconized-label qa-runtime-info"
+ >
+ <img
+ className="channel-icon qa-runtime-icon"
+ src="chrome://devtools/skin/images/aboutdebugging-firefox-release.svg"
+ />
+ <b
+ className="devtools-ellipsis-text"
+ >
+ toolbox.debugTargetInfo.runtimeLabel.thisFirefox-1.0.0
+ </b>
+ <span
+ className="devtools-ellipsis-text"
+ />
+ </span>
+ <span
+ className="iconized-label debug-target-title"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.targetType.tab"
+ src="chrome://devtools/skin/images/globe.svg"
+ />
+ <b
+ className="devtools-ellipsis-text qa-target-title"
+ >
+ Test Tab Name
+ </b>
+ </span>
+ <div
+ className="debug-target-navigation"
+ >
+ <button
+ className="iconized-label navigation-button qa-back-button"
+ onClick={[Function]}
+ title="toolbox.debugTargetInfo.back"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.back"
+ src="chrome://browser/skin/back.svg"
+ />
+ </button>
+ <button
+ className="iconized-label navigation-button qa-forward-button"
+ onClick={[Function]}
+ title="toolbox.debugTargetInfo.forward"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.forward"
+ src="chrome://browser/skin/forward.svg"
+ />
+ </button>
+ <button
+ className="iconized-label navigation-button qa-reload-button"
+ onClick={[Function]}
+ title="toolbox.debugTargetInfo.reload"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.reload"
+ src="chrome://browser/skin/reload.svg"
+ />
+ </button>
+ </div>
+ <span
+ className="debug-target-url"
+ >
+ <form
+ className="debug-target-url-form"
+ onSubmit={[Function]}
+ >
+ <input
+ className="devtools-textinput debug-target-url-input"
+ defaultValue="http://some.target/url"
+ onChange={[Function]}
+ onFocus={[Function]}
+ />
+ </form>
+ </span>
+</header>
+`;
+
+exports[`DebugTargetInfo component Target title renders the expected snapshot for a Toolbox with an unnamed target 1`] = `
+<header
+ className="debug-target-info qa-debug-target-info"
+>
+ <span
+ className="iconized-label qa-runtime-info"
+ >
+ <img
+ className="channel-icon qa-runtime-icon"
+ src="chrome://devtools/skin/images/aboutdebugging-firefox-release.svg"
+ />
+ <b
+ className="devtools-ellipsis-text"
+ >
+ toolbox.debugTargetInfo.runtimeLabel.thisFirefox-1.0.0
+ </b>
+ <span
+ className="devtools-ellipsis-text"
+ />
+ </span>
+ <span
+ className="iconized-label debug-target-title"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.targetType.tab"
+ src="chrome://devtools/skin/images/globe.svg"
+ />
+ </span>
+ <div
+ className="debug-target-navigation"
+ >
+ <button
+ className="iconized-label navigation-button qa-back-button"
+ onClick={[Function]}
+ title="toolbox.debugTargetInfo.back"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.back"
+ src="chrome://browser/skin/back.svg"
+ />
+ </button>
+ <button
+ className="iconized-label navigation-button qa-forward-button"
+ onClick={[Function]}
+ title="toolbox.debugTargetInfo.forward"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.forward"
+ src="chrome://browser/skin/forward.svg"
+ />
+ </button>
+ <button
+ className="iconized-label navigation-button qa-reload-button"
+ onClick={[Function]}
+ title="toolbox.debugTargetInfo.reload"
+ >
+ <img
+ alt="toolbox.debugTargetInfo.reload"
+ src="chrome://browser/skin/reload.svg"
+ />
+ </button>
+ </div>
+ <span
+ className="debug-target-url"
+ >
+ <form
+ className="debug-target-url-form"
+ onSubmit={[Function]}
+ >
+ <input
+ className="devtools-textinput debug-target-url-input"
+ defaultValue="http://some.target/without/a/name"
+ onChange={[Function]}
+ onFocus={[Function]}
+ />
+ </form>
+ </span>
+</header>
+`;
diff --git a/devtools/client/framework/test/node/components/debug-target-info.test.js b/devtools/client/framework/test/node/components/debug-target-info.test.js
new file mode 100644
index 0000000000..d04941ad16
--- /dev/null
+++ b/devtools/client/framework/test/node/components/debug-target-info.test.js
@@ -0,0 +1,195 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Unit tests for the DebugTargetInfo component.
+ */
+
+const renderer = require("react-test-renderer");
+const React = require("devtools/client/shared/vendor/react");
+const DebugTargetInfo = React.createFactory(
+ require("devtools/client/framework/components/DebugTargetInfo")
+);
+const {
+ CONNECTION_TYPES,
+ DEBUG_TARGET_TYPES,
+} = require("devtools/client/shared/remote-debugging/constants");
+
+/**
+ * Stub for the L10N property expected by the DebugTargetInfo component.
+ */
+const stubL10N = {
+ getStr: id => id,
+ getFormatStr: (id, ...args) => [id, ...args].join("-"),
+};
+
+const findByClassName = (testInstance, className) => {
+ return testInstance.findAll(node => {
+ return node.props.className && node.props.className.includes(className);
+ });
+};
+
+const TEST_TOOLBOX = {
+ target: {
+ name: "Test Tab Name",
+ url: "http://some.target/url",
+ traits: {
+ navigation: true,
+ },
+ },
+ doc: {},
+};
+
+const TEST_TOOLBOX_NO_NAME = {
+ target: {
+ url: "http://some.target/without/a/name",
+ traits: {
+ navigation: true,
+ },
+ },
+ doc: {},
+};
+
+const USB_DEVICE_DESCRIPTION = {
+ deviceName: "usbDeviceName",
+ icon: "chrome://devtools/skin/images/aboutdebugging-firefox-release.svg",
+ name: "usbRuntimeBrandName",
+ version: "1.0.0",
+};
+
+const THIS_FIREFOX_DEVICE_DESCRIPTION = {
+ icon: "chrome://devtools/skin/images/aboutdebugging-firefox-release.svg",
+ version: "1.0.0",
+ name: "thisFirefoxRuntimeBrandName",
+};
+
+const USB_TARGET_INFO = {
+ debugTargetData: {
+ connectionType: CONNECTION_TYPES.USB,
+ runtimeInfo: USB_DEVICE_DESCRIPTION,
+ targetType: DEBUG_TARGET_TYPES.TAB,
+ },
+ toolbox: TEST_TOOLBOX,
+ L10N: stubL10N,
+};
+
+const THIS_FIREFOX_TARGET_INFO = {
+ debugTargetData: {
+ connectionType: CONNECTION_TYPES.THIS_FIREFOX,
+ runtimeInfo: THIS_FIREFOX_DEVICE_DESCRIPTION,
+ targetType: DEBUG_TARGET_TYPES.TAB,
+ },
+ toolbox: TEST_TOOLBOX,
+ L10N: stubL10N,
+};
+
+const THIS_FIREFOX_NO_NAME_TARGET_INFO = {
+ debugTargetData: {
+ connectionType: CONNECTION_TYPES.THIS_FIREFOX,
+ runtimeInfo: THIS_FIREFOX_DEVICE_DESCRIPTION,
+ targetType: DEBUG_TARGET_TYPES.TAB,
+ },
+ toolbox: TEST_TOOLBOX_NO_NAME,
+ L10N: stubL10N,
+};
+
+describe("DebugTargetInfo component", () => {
+ describe("Connection info", () => {
+ it("displays connection info for USB Release target", () => {
+ const component = renderer.create(DebugTargetInfo(USB_TARGET_INFO));
+ expect(
+ findByClassName(component.root, "qa-connection-info").length
+ ).toEqual(1);
+ });
+
+ it("renders the expected snapshot for USB Release target", () => {
+ const component = renderer.create(DebugTargetInfo(USB_TARGET_INFO));
+ expect(component.toJSON()).toMatchSnapshot();
+ });
+
+ it("hides the connection info for This Firefox target", () => {
+ const component = renderer.create(
+ DebugTargetInfo(THIS_FIREFOX_TARGET_INFO)
+ );
+ expect(
+ findByClassName(component.root, "qa-connection-info").length
+ ).toEqual(0);
+ });
+ });
+
+ describe("Target title", () => {
+ it("displays the target title if the target of the Toolbox has a name", () => {
+ const component = renderer.create(
+ DebugTargetInfo(THIS_FIREFOX_TARGET_INFO)
+ );
+ expect(findByClassName(component.root, "qa-target-title").length).toEqual(
+ 1
+ );
+ });
+
+ it("renders the expected snapshot for This Firefox target", () => {
+ const component = renderer.create(
+ DebugTargetInfo(THIS_FIREFOX_TARGET_INFO)
+ );
+ expect(component.toJSON()).toMatchSnapshot();
+ });
+
+ it("doesn't display the target title if the target of the Toolbox has no name", () => {
+ const component = renderer.create(
+ DebugTargetInfo(THIS_FIREFOX_NO_NAME_TARGET_INFO)
+ );
+ expect(findByClassName(component.root, "qa-target-title").length).toEqual(
+ 0
+ );
+ });
+
+ it("renders the expected snapshot for a Toolbox with an unnamed target", () => {
+ const component = renderer.create(
+ DebugTargetInfo(THIS_FIREFOX_NO_NAME_TARGET_INFO)
+ );
+ expect(component.toJSON()).toMatchSnapshot();
+ });
+ });
+
+ describe("Target icon", () => {
+ const buildProps = (base, extraDebugTargetData) => {
+ const props = Object.assign({}, base);
+ Object.assign(props.debugTargetData, extraDebugTargetData);
+ return props;
+ };
+
+ it("renders the expected snapshot for a tab target", () => {
+ const props = buildProps(USB_TARGET_INFO, {
+ targetType: DEBUG_TARGET_TYPES.TAB,
+ });
+ const component = renderer.create(DebugTargetInfo(props));
+ expect(component.toJSON()).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot for a worker target", () => {
+ const props = buildProps(USB_TARGET_INFO, {
+ targetType: DEBUG_TARGET_TYPES.WORKER,
+ });
+ const component = renderer.create(DebugTargetInfo(props));
+ expect(component.toJSON()).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot for an extension target", () => {
+ const props = buildProps(USB_TARGET_INFO, {
+ targetType: DEBUG_TARGET_TYPES.EXTENSION,
+ });
+ const component = renderer.create(DebugTargetInfo(props));
+ expect(component.toJSON()).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot for a process target", () => {
+ const props = buildProps(USB_TARGET_INFO, {
+ targetType: DEBUG_TARGET_TYPES.PROCESS,
+ });
+ const component = renderer.create(DebugTargetInfo(props));
+ expect(component.toJSON()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/devtools/client/framework/test/node/jest.config.js b/devtools/client/framework/test/node/jest.config.js
new file mode 100644
index 0000000000..0d2124593d
--- /dev/null
+++ b/devtools/client/framework/test/node/jest.config.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* global __dirname */
+
+const sharedJestConfig = require(`${__dirname}/../../../shared/test-helpers/shared-jest.config`);
+
+module.exports = {
+ ...sharedJestConfig,
+ setupFiles: ["<rootDir>setup.js"],
+};
diff --git a/devtools/client/framework/test/node/package.json b/devtools/client/framework/test/node/package.json
new file mode 100644
index 0000000000..1ff1abef5b
--- /dev/null
+++ b/devtools/client/framework/test/node/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "devtools-client-framework-tests",
+ "license": "MPL-2.0",
+ "version": "0.0.1",
+ "engines": {
+ "node": ">=8.9.4"
+ },
+ "scripts": {
+ "test": "jest",
+ "test-ci": "jest --json"
+ },
+ "dependencies": {
+ "@babel/plugin-proposal-async-generator-functions": "^7.2.0",
+ "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-proposal-optional-chaining": "^7.8.3",
+ "babel-jest": "^25.1.0",
+ "jest": "^25.1.0",
+ "react-test-renderer": "16.4.1",
+ "react": "16.4.1",
+ "react-dom": "16.4.1"
+ }
+} \ No newline at end of file
diff --git a/devtools/client/framework/test/node/setup.js b/devtools/client/framework/test/node/setup.js
new file mode 100644
index 0000000000..77ef114f4f
--- /dev/null
+++ b/devtools/client/framework/test/node/setup.js
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const {
+ setMocksInGlobal,
+} = require("devtools/client/shared/test-helpers/shared-node-helpers");
+setMocksInGlobal();
diff --git a/devtools/client/framework/test/node/store/targets.test.js b/devtools/client/framework/test/node/store/targets.test.js
new file mode 100644
index 0000000000..dbf0136da1
--- /dev/null
+++ b/devtools/client/framework/test/node/store/targets.test.js
@@ -0,0 +1,141 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Unit tests for targets management on the toolbox store.
+ */
+
+const { createToolboxStore } = require("devtools/client/framework/store");
+const actions = require("devtools/client/framework/actions/targets");
+const {
+ getSelectedTarget,
+ getToolboxTargets,
+} = require("devtools/client/framework/reducers/targets");
+
+describe("Toolbox store - targets", () => {
+ describe("registerTarget", () => {
+ it("adds the target to the list", () => {
+ const store = createToolboxStore();
+
+ const targetFront1 = {
+ actorID: "target/1",
+ };
+
+ store.dispatch(actions.registerTarget(targetFront1));
+
+ let targets = getToolboxTargets(store.getState());
+ expect(targets.length).toEqual(1);
+ expect(targets[0].actorID).toEqual("target/1");
+
+ const targetFront2 = {
+ actorID: "target/2",
+ };
+
+ store.dispatch(actions.registerTarget(targetFront2));
+
+ targets = getToolboxTargets(store.getState());
+ expect(targets.length).toEqual(2);
+ expect(targets[0].actorID).toEqual("target/1");
+ expect(targets[1].actorID).toEqual("target/2");
+ });
+ });
+
+ describe("selectTarget", () => {
+ it("updates the selected property when the target is known", () => {
+ const store = createToolboxStore();
+ const targetFront1 = {
+ actorID: "target/1",
+ };
+ store.dispatch(actions.registerTarget(targetFront1));
+ store.dispatch(actions.selectTarget("target/1"));
+ expect(getSelectedTarget(store.getState()).actorID).toBe("target/1");
+ });
+
+ it("does not update the selected property when the target is unknown", () => {
+ const store = createToolboxStore();
+ const targetFront1 = {
+ actorID: "target/1",
+ };
+ store.dispatch(actions.registerTarget(targetFront1));
+ store.dispatch(actions.selectTarget("target/1"));
+ expect(getSelectedTarget(store.getState()).actorID).toBe("target/1");
+
+ store.dispatch(actions.selectTarget("target/unknown"));
+ expect(getSelectedTarget(store.getState()).actorID).toBe("target/1");
+ });
+
+ it("does not update the state when the target is already selected", () => {
+ const store = createToolboxStore();
+ const targetFront1 = {
+ actorID: "target/1",
+ };
+ store.dispatch(actions.registerTarget(targetFront1));
+ store.dispatch(actions.selectTarget("target/1"));
+
+ const state = store.getState();
+ store.dispatch(actions.selectTarget("target/1"));
+ expect(store.getState()).toStrictEqual(state);
+ });
+ });
+
+ describe("unregisterTarget", () => {
+ it("removes the target from the list", () => {
+ const store = createToolboxStore();
+
+ const targetFront1 = {
+ actorID: "target/1",
+ };
+ const targetFront2 = {
+ actorID: "target/2",
+ };
+
+ store.dispatch(actions.registerTarget(targetFront1));
+ store.dispatch(actions.registerTarget(targetFront2));
+
+ let targets = getToolboxTargets(store.getState());
+ expect(targets.length).toEqual(2);
+
+ store.dispatch(actions.unregisterTarget(targetFront1));
+ targets = getToolboxTargets(store.getState());
+ expect(targets.length).toEqual(1);
+ expect(targets[0].actorID).toEqual("target/2");
+
+ store.dispatch(actions.unregisterTarget(targetFront2));
+ expect(getToolboxTargets(store.getState()).length).toEqual(0);
+ });
+
+ it("does not update the state when the target is unknown", () => {
+ const store = createToolboxStore();
+
+ const targetFront1 = {
+ actorID: "target/1",
+ };
+ const targetFront2 = {
+ actorID: "target/unknown",
+ };
+
+ store.dispatch(actions.registerTarget(targetFront1));
+
+ const state = store.getState();
+ store.dispatch(actions.unregisterTarget(targetFront2));
+ expect(store.getState()).toStrictEqual(state);
+ });
+
+ it("resets the selected property when it was the selected target", () => {
+ const store = createToolboxStore();
+
+ const targetFront1 = {
+ actorID: "target/1",
+ };
+
+ store.dispatch(actions.registerTarget(targetFront1));
+ store.dispatch(actions.selectTarget("target/1"));
+ expect(getSelectedTarget(store.getState()).actorID).toBe("target/1");
+
+ store.dispatch(actions.unregisterTarget(targetFront1));
+ expect(getSelectedTarget(store.getState())).toBe(null);
+ });
+ });
+});
diff --git a/devtools/client/framework/test/node/yarn.lock b/devtools/client/framework/test/node/yarn.lock
new file mode 100644
index 0000000000..2a71218b83
--- /dev/null
+++ b/devtools/client/framework/test/node/yarn.lock
@@ -0,0 +1,3144 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
+ dependencies:
+ "@babel/highlight" "^7.8.3"
+
+"@babel/core@^7.1.0", "@babel/core@^7.7.5":
+ version "7.9.0"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e"
+ dependencies:
+ "@babel/code-frame" "^7.8.3"
+ "@babel/generator" "^7.9.0"
+ "@babel/helper-module-transforms" "^7.9.0"
+ "@babel/helpers" "^7.9.0"
+ "@babel/parser" "^7.9.0"
+ "@babel/template" "^7.8.6"
+ "@babel/traverse" "^7.9.0"
+ "@babel/types" "^7.9.0"
+ convert-source-map "^1.7.0"
+ debug "^4.1.0"
+ gensync "^1.0.0-beta.1"
+ json5 "^2.1.2"
+ lodash "^4.17.13"
+ resolve "^1.3.2"
+ semver "^5.4.1"
+ source-map "^0.5.0"
+
+"@babel/generator@^7.9.0":
+ version "7.9.4"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.4.tgz#12441e90c3b3c4159cdecf312075bf1a8ce2dbce"
+ dependencies:
+ "@babel/types" "^7.9.0"
+ jsesc "^2.5.1"
+ lodash "^4.17.13"
+ source-map "^0.5.0"
+
+"@babel/helper-annotate-as-pure@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee"
+ dependencies:
+ "@babel/types" "^7.8.3"
+
+"@babel/helper-function-name@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz#eeeb665a01b1f11068e9fb86ad56a1cb1a824cca"
+ dependencies:
+ "@babel/helper-get-function-arity" "^7.8.3"
+ "@babel/template" "^7.8.3"
+ "@babel/types" "^7.8.3"
+
+"@babel/helper-get-function-arity@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5"
+ dependencies:
+ "@babel/types" "^7.8.3"
+
+"@babel/helper-member-expression-to-functions@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz#659b710498ea6c1d9907e0c73f206eee7dadc24c"
+ dependencies:
+ "@babel/types" "^7.8.3"
+
+"@babel/helper-module-imports@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz#7fe39589b39c016331b6b8c3f441e8f0b1419498"
+ dependencies:
+ "@babel/types" "^7.8.3"
+
+"@babel/helper-module-transforms@^7.9.0":
+ version "7.9.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz#43b34dfe15961918707d247327431388e9fe96e5"
+ dependencies:
+ "@babel/helper-module-imports" "^7.8.3"
+ "@babel/helper-replace-supers" "^7.8.6"
+ "@babel/helper-simple-access" "^7.8.3"
+ "@babel/helper-split-export-declaration" "^7.8.3"
+ "@babel/template" "^7.8.6"
+ "@babel/types" "^7.9.0"
+ lodash "^4.17.13"
+
+"@babel/helper-optimise-call-expression@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz#7ed071813d09c75298ef4f208956006b6111ecb9"
+ dependencies:
+ "@babel/types" "^7.8.3"
+
+"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670"
+
+"@babel/helper-remap-async-to-generator@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.8.3.tgz#273c600d8b9bf5006142c1e35887d555c12edd86"
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.8.3"
+ "@babel/helper-wrap-function" "^7.8.3"
+ "@babel/template" "^7.8.3"
+ "@babel/traverse" "^7.8.3"
+ "@babel/types" "^7.8.3"
+
+"@babel/helper-replace-supers@^7.8.6":
+ version "7.8.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8"
+ dependencies:
+ "@babel/helper-member-expression-to-functions" "^7.8.3"
+ "@babel/helper-optimise-call-expression" "^7.8.3"
+ "@babel/traverse" "^7.8.6"
+ "@babel/types" "^7.8.6"
+
+"@babel/helper-simple-access@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz#7f8109928b4dab4654076986af575231deb639ae"
+ dependencies:
+ "@babel/template" "^7.8.3"
+ "@babel/types" "^7.8.3"
+
+"@babel/helper-split-export-declaration@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9"
+ dependencies:
+ "@babel/types" "^7.8.3"
+
+"@babel/helper-validator-identifier@^7.9.0":
+ version "7.9.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed"
+
+"@babel/helper-wrap-function@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz#9dbdb2bb55ef14aaa01fe8c99b629bd5352d8610"
+ dependencies:
+ "@babel/helper-function-name" "^7.8.3"
+ "@babel/template" "^7.8.3"
+ "@babel/traverse" "^7.8.3"
+ "@babel/types" "^7.8.3"
+
+"@babel/helpers@^7.9.0":
+ version "7.9.2"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.2.tgz#b42a81a811f1e7313b88cba8adc66b3d9ae6c09f"
+ dependencies:
+ "@babel/template" "^7.8.3"
+ "@babel/traverse" "^7.9.0"
+ "@babel/types" "^7.9.0"
+
+"@babel/highlight@^7.8.3":
+ version "7.9.0"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079"
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.9.0"
+ chalk "^2.0.0"
+ js-tokens "^4.0.0"
+
+"@babel/parser@^7.1.0", "@babel/parser@^7.7.5", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0":
+ version "7.9.4"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8"
+
+"@babel/plugin-proposal-async-generator-functions@^7.2.0":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz#bad329c670b382589721b27540c7d288601c6e6f"
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.3"
+ "@babel/helper-remap-async-to-generator" "^7.8.3"
+ "@babel/plugin-syntax-async-generators" "^7.8.0"
+
+"@babel/plugin-proposal-nullish-coalescing-operator@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.8.3.tgz#e4572253fdeed65cddeecfdab3f928afeb2fd5d2"
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.3"
+ "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
+
+"@babel/plugin-proposal-optional-chaining@^7.8.3":
+ version "7.9.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz#31db16b154c39d6b8a645292472b98394c292a58"
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.3"
+ "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+
+"@babel/plugin-syntax-async-generators@^7.8.0":
+ version "7.8.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-bigint@^7.0.0":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea"
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-object-rest-spread@^7.0.0":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-chaining@^7.8.0":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
+ version "7.8.6"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
+ dependencies:
+ "@babel/code-frame" "^7.8.3"
+ "@babel/parser" "^7.8.6"
+ "@babel/types" "^7.8.6"
+
+"@babel/traverse@^7.1.0", "@babel/traverse@^7.7.4", "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0":
+ version "7.9.0"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.0.tgz#d3882c2830e513f4fe4cec9fe76ea1cc78747892"
+ dependencies:
+ "@babel/code-frame" "^7.8.3"
+ "@babel/generator" "^7.9.0"
+ "@babel/helper-function-name" "^7.8.3"
+ "@babel/helper-split-export-declaration" "^7.8.3"
+ "@babel/parser" "^7.9.0"
+ "@babel/types" "^7.9.0"
+ debug "^4.1.0"
+ globals "^11.1.0"
+ lodash "^4.17.13"
+
+"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0":
+ version "7.9.0"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.0.tgz#00b064c3df83ad32b2dbf5ff07312b15c7f1efb5"
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.9.0"
+ lodash "^4.17.13"
+ to-fast-properties "^2.0.0"
+
+"@bcoe/v8-coverage@^0.2.3":
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
+
+"@cnakazawa/watch@^1.0.3":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
+ dependencies:
+ exec-sh "^0.3.2"
+ minimist "^1.2.0"
+
+"@istanbuljs/load-nyc-config@^1.0.0":
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b"
+ dependencies:
+ camelcase "^5.3.1"
+ find-up "^4.1.0"
+ js-yaml "^3.13.1"
+ resolve-from "^5.0.0"
+
+"@istanbuljs/schema@^0.1.2":
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd"
+
+"@jest/console@^25.1.0":
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/@jest/console/-/console-25.1.0.tgz#1fc765d44a1e11aec5029c08e798246bd37075ab"
+ dependencies:
+ "@jest/source-map" "^25.1.0"
+ chalk "^3.0.0"
+ jest-util "^25.1.0"
+ slash "^3.0.0"
+
+"@jest/core@^25.1.0":
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/@jest/core/-/core-25.1.0.tgz#3d4634fc3348bb2d7532915d67781cdac0869e47"
+ dependencies:
+ "@jest/console" "^25.1.0"
+ "@jest/reporters" "^25.1.0"
+ "@jest/test-result" "^25.1.0"
+ "@jest/transform" "^25.1.0"
+ "@jest/types" "^25.1.0"
+ ansi-escapes "^4.2.1"
+ chalk "^3.0.0"
+ exit "^0.1.2"
+ graceful-fs "^4.2.3"
+ jest-changed-files "^25.1.0"
+ jest-config "^25.1.0"
+ jest-haste-map "^25.1.0"
+ jest-message-util "^25.1.0"
+ jest-regex-util "^25.1.0"
+ jest-resolve "^25.1.0"
+ jest-resolve-dependencies "^25.1.0"
+ jest-runner "^25.1.0"
+ jest-runtime "^25.1.0"
+ jest-snapshot "^25.1.0"
+ jest-util "^25.1.0"
+ jest-validate "^25.1.0"
+ jest-watcher "^25.1.0"
+ micromatch "^4.0.2"
+ p-each-series "^2.1.0"
+ realpath-native "^1.1.0"
+ rimraf "^3.0.0"
+ slash "^3.0.0"
+ strip-ansi "^6.0.0"
+
+"@jest/environment@^25.1.0":
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-25.1.0.tgz#4a97f64770c9d075f5d2b662b5169207f0a3f787"
+ dependencies:
+ "@jest/fake-timers" "^25.1.0"
+ "@jest/types" "^25.1.0"
+ jest-mock "^25.1.0"
+
+"@jest/fake-timers@^25.1.0":
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-25.1.0.tgz#a1e0eff51ffdbb13ee81f35b52e0c1c11a350ce8"
+ dependencies:
+ "@jest/types" "^25.1.0"
+ jest-message-util "^25.1.0"
+ jest-mock "^25.1.0"
+ jest-util "^25.1.0"
+ lolex "^5.0.0"
+
+"@jest/reporters@^25.1.0":
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-25.1.0.tgz#9178ecf136c48f125674ac328f82ddea46e482b0"
+ dependencies:
+ "@bcoe/v8-coverage" "^0.2.3"
+ "@jest/console" "^25.1.0"
+ "@jest/environment" "^25.1.0"
+ "@jest/test-result" "^25.1.0"
+ "@jest/transform" "^25.1.0"
+ "@jest/types" "^25.1.0"
+ chalk "^3.0.0"
+ collect-v8-coverage "^1.0.0"
+ exit "^0.1.2"
+ glob "^7.1.2"
+ istanbul-lib-coverage "^3.0.0"
+ istanbul-lib-instrument "^4.0.0"
+ istanbul-lib-report "^3.0.0"
+ istanbul-lib-source-maps "^4.0.0"
+ istanbul-reports "^3.0.0"
+ jest-haste-map "^25.1.0"
+ jest-resolve "^25.1.0"
+ jest-runtime "^25.1.0"
+ jest-util "^25.1.0"
+ jest-worker "^25.1.0"
+ slash "^3.0.0"
+ source-map "^0.6.0"
+ string-length "^3.1.0"
+ terminal-link "^2.0.0"
+ v8-to-istanbul "^4.0.1"
+ optionalDependencies:
+ node-notifier "^6.0.0"
+
+"@jest/source-map@^25.1.0":
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-25.1.0.tgz#b012e6c469ccdbc379413f5c1b1ffb7ba7034fb0"
+ dependencies:
+ callsites "^3.0.0"
+ graceful-fs "^4.2.3"
+ source-map "^0.6.0"
+
+"@jest/test-result@^25.1.0":
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-25.1.0.tgz#847af2972c1df9822a8200457e64be4ff62821f7"
+ dependencies:
+ "@jest/console" "^25.1.0"
+ "@jest/transform" "^25.1.0"
+ "@jest/types" "^25.1.0"
+ "@types/istanbul-lib-coverage" "^2.0.0"
+ collect-v8-coverage "^1.0.0"
+
+"@jest/test-sequencer@^25.1.0":
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-25.1.0.tgz#4df47208542f0065f356fcdb80026e3c042851ab"
+ dependencies:
+ "@jest/test-result" "^25.1.0"
+ jest-haste-map "^25.1.0"
+ jest-runner "^25.1.0"
+ jest-runtime "^25.1.0"
+
+"@jest/transform@^25.1.0":
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-25.1.0.tgz#221f354f512b4628d88ce776d5b9e601028ea9da"
+ dependencies:
+ "@babel/core" "^7.1.0"
+ "@jest/types" "^25.1.0"
+ babel-plugin-istanbul "^6.0.0"
+ chalk "^3.0.0"
+ convert-source-map "^1.4.0"
+ fast-json-stable-stringify "^2.0.0"
+ graceful-fs "^4.2.3"
+ jest-haste-map "^25.1.0"
+ jest-regex-util "^25.1.0"
+ jest-util "^25.1.0"
+ micromatch "^4.0.2"
+ pirates "^4.0.1"
+ realpath-native "^1.1.0"
+ slash "^3.0.0"
+ source-map "^0.6.1"
+ write-file-atomic "^3.0.0"
+
+"@jest/types@^25.1.0":
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.1.0.tgz#b26831916f0d7c381e11dbb5e103a72aed1b4395"
+ dependencies:
+ "@types/istanbul-lib-coverage" "^2.0.0"
+ "@types/istanbul-reports" "^1.1.1"
+ "@types/yargs" "^15.0.0"
+ chalk "^3.0.0"
+
+"@sinonjs/commons@^1.7.0":
+ version "1.7.1"
+ resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.1.tgz#da5fd19a5f71177a53778073978873964f49acf1"
+ dependencies:
+ type-detect "4.0.8"
+
+"@types/babel__core@^7.1.0":
+ version "7.1.6"
+ resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.6.tgz#16ff42a5ae203c9af1c6e190ed1f30f83207b610"
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+ "@types/babel__generator" "*"
+ "@types/babel__template" "*"
+ "@types/babel__traverse" "*"
+
+"@types/babel__generator@*":
+ version "7.6.1"
+ resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.1.tgz#4901767b397e8711aeb99df8d396d7ba7b7f0e04"
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@types/babel__template@*":
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307"
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6":
+ version "7.0.9"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.9.tgz#be82fab304b141c3eee81a4ce3b034d0eba1590a"
+ dependencies:
+ "@babel/types" "^7.3.0"
+
+"@types/color-name@^1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
+
+"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
+
+"@types/istanbul-lib-report@*":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686"
+ dependencies:
+ "@types/istanbul-lib-coverage" "*"
+
+"@types/istanbul-reports@^1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a"
+ dependencies:
+ "@types/istanbul-lib-coverage" "*"
+ "@types/istanbul-lib-report" "*"
+
+"@types/stack-utils@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
+
+"@types/yargs-parser@*":
+ version "15.0.0"
+ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
+
+"@types/yargs@^15.0.0":
+ version "15.0.4"
+ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.4.tgz#7e5d0f8ca25e9d5849f2ea443cf7c402decd8299"
+ dependencies:
+ "@types/yargs-parser" "*"
+
+abab@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f"
+
+acorn-globals@^4.3.2:
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7"
+ dependencies:
+ acorn "^6.0.1"
+ acorn-walk "^6.0.1"
+
+acorn-walk@^6.0.1:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913"
+
+acorn@^6.0.1:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f"
+
+acorn@^7.1.0:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf"
+
+ajv@^6.5.5:
+ version "6.10.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
+ dependencies:
+ fast-deep-equal "^2.0.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+ansi-escapes@^4.2.1:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61"
+ dependencies:
+ type-fest "^0.11.0"
+
+ansi-regex@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
+
+ansi-regex@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
+
+ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ dependencies:
+ color-convert "^1.9.0"
+
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
+ dependencies:
+ "@types/color-name" "^1.1.1"
+ color-convert "^2.0.1"
+
+anymatch@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+ dependencies:
+ micromatch "^3.1.4"
+ normalize-path "^2.1.1"
+
+anymatch@^3.0.3:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142"
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
+argparse@^1.0.7:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+ dependencies:
+ sprintf-js "~1.0.2"
+
+arr-diff@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+
+arr-flatten@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+
+arr-union@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+
+array-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
+
+array-unique@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+
+asap@~2.0.3:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+
+asn1@~0.2.3:
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
+ dependencies:
+ safer-buffer "~2.1.0"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+
+assign-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+
+astral-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+
+atob@^2.1.1:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
+
+aws-sign2@~0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+
+aws4@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
+
+babel-jest@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-25.1.0.tgz#206093ac380a4b78c4404a05b3277391278f80fb"
+ dependencies:
+ "@jest/transform" "^25.1.0"
+ "@jest/types" "^25.1.0"
+ "@types/babel__core" "^7.1.0"
+ babel-plugin-istanbul "^6.0.0"
+ babel-preset-jest "^25.1.0"
+ chalk "^3.0.0"
+ slash "^3.0.0"
+
+babel-plugin-istanbul@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz#e159ccdc9af95e0b570c75b4573b7c34d671d765"
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@istanbuljs/load-nyc-config" "^1.0.0"
+ "@istanbuljs/schema" "^0.1.2"
+ istanbul-lib-instrument "^4.0.0"
+ test-exclude "^6.0.0"
+
+babel-plugin-jest-hoist@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.1.0.tgz#fb62d7b3b53eb36c97d1bc7fec2072f9bd115981"
+ dependencies:
+ "@types/babel__traverse" "^7.0.6"
+
+babel-preset-jest@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-25.1.0.tgz#d0aebfebb2177a21cde710996fce8486d34f1d33"
+ dependencies:
+ "@babel/plugin-syntax-bigint" "^7.0.0"
+ "@babel/plugin-syntax-object-rest-spread" "^7.0.0"
+ babel-plugin-jest-hoist "^25.1.0"
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+
+base@^0.11.1:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+ dependencies:
+ cache-base "^1.0.1"
+ class-utils "^0.3.5"
+ component-emitter "^1.2.1"
+ define-property "^1.0.0"
+ isobject "^3.0.1"
+ mixin-deep "^1.2.0"
+ pascalcase "^0.1.1"
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
+ dependencies:
+ tweetnacl "^0.14.3"
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^2.3.1:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+ dependencies:
+ arr-flatten "^1.1.0"
+ array-unique "^0.3.2"
+ extend-shallow "^2.0.1"
+ fill-range "^4.0.0"
+ isobject "^3.0.1"
+ repeat-element "^1.1.2"
+ snapdragon "^0.8.1"
+ snapdragon-node "^2.0.1"
+ split-string "^3.0.2"
+ to-regex "^3.0.1"
+
+braces@^3.0.1:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+ dependencies:
+ fill-range "^7.0.1"
+
+browser-process-hrtime@^0.1.2:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4"
+
+browser-resolve@^1.11.3:
+ version "1.11.3"
+ resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6"
+ dependencies:
+ resolve "1.1.7"
+
+bser@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719"
+ dependencies:
+ node-int64 "^0.4.0"
+
+buffer-from@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+
+cache-base@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+ dependencies:
+ collection-visit "^1.0.0"
+ component-emitter "^1.2.1"
+ get-value "^2.0.6"
+ has-value "^1.0.0"
+ isobject "^3.0.1"
+ set-value "^2.0.0"
+ to-object-path "^0.3.0"
+ union-value "^1.0.0"
+ unset-value "^1.0.0"
+
+callsites@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+
+camelcase@^5.0.0, camelcase@^5.3.1:
+ version "5.3.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+
+capture-exit@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4"
+ dependencies:
+ rsvp "^4.8.4"
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+
+chalk@^2.0.0:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+chalk@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
+ dependencies:
+ ansi-styles "^4.1.0"
+ supports-color "^7.1.0"
+
+ci-info@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
+
+class-utils@^0.3.5:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+ dependencies:
+ arr-union "^3.1.0"
+ define-property "^0.2.5"
+ isobject "^3.0.0"
+ static-extend "^0.1.1"
+
+cliui@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
+ dependencies:
+ string-width "^4.2.0"
+ strip-ansi "^6.0.0"
+ wrap-ansi "^6.2.0"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+
+collect-v8-coverage@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.0.tgz#150ee634ac3650b71d9c985eb7f608942334feb1"
+
+collection-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+ dependencies:
+ map-visit "^1.0.0"
+ object-visit "^1.0.0"
+
+color-convert@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+ dependencies:
+ color-name "1.1.3"
+
+color-convert@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+ dependencies:
+ color-name "~1.1.4"
+
+color-name@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+
+color-name@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+
+combined-stream@^1.0.6, combined-stream@~1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828"
+ dependencies:
+ delayed-stream "~1.0.0"
+
+component-emitter@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+
+convert-source-map@^1.4.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20"
+ dependencies:
+ safe-buffer "~5.1.1"
+
+convert-source-map@^1.6.0, convert-source-map@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
+ dependencies:
+ safe-buffer "~5.1.1"
+
+copy-descriptor@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+
+core-js@^1.0.0:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
+
+core-util-is@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+
+cross-spawn@^6.0.0:
+ version "6.0.5"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+ dependencies:
+ nice-try "^1.0.4"
+ path-key "^2.0.1"
+ semver "^5.5.0"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+cross-spawn@^7.0.0:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14"
+ dependencies:
+ path-key "^3.1.0"
+ shebang-command "^2.0.0"
+ which "^2.0.1"
+
+cssom@^0.4.1:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
+
+cssom@~0.3.6:
+ version "0.3.8"
+ resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
+
+cssstyle@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.2.0.tgz#e4c44debccd6b7911ed617a4395e5754bba59992"
+ dependencies:
+ cssom "~0.3.6"
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ dependencies:
+ assert-plus "^1.0.0"
+
+data-urls@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
+ dependencies:
+ abab "^2.0.0"
+ whatwg-mimetype "^2.2.0"
+ whatwg-url "^7.0.0"
+
+debug@^2.2.0, debug@^2.3.3:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ dependencies:
+ ms "2.0.0"
+
+debug@^4.1.0, debug@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
+ dependencies:
+ ms "^2.1.1"
+
+decamelize@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+
+decode-uri-component@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+
+deep-is@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+
+define-properties@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+ dependencies:
+ object-keys "^1.0.12"
+
+define-property@^0.2.5:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+ dependencies:
+ is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+ dependencies:
+ is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+ dependencies:
+ is-descriptor "^1.0.2"
+ isobject "^3.0.1"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+
+detect-newline@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
+
+diff-sequences@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.1.0.tgz#fd29a46f1c913fd66c22645dc75bffbe43051f32"
+
+domexception@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
+ dependencies:
+ webidl-conversions "^4.0.2"
+
+ecc-jsbn@~0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
+ dependencies:
+ jsbn "~0.1.0"
+ safer-buffer "^2.1.0"
+
+emoji-regex@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+
+encoding@^0.1.11:
+ version "0.1.12"
+ resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
+ dependencies:
+ iconv-lite "~0.4.13"
+
+end-of-stream@^1.1.0:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
+ dependencies:
+ once "^1.4.0"
+
+es-abstract@^1.5.1:
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
+ dependencies:
+ es-to-primitive "^1.2.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ is-callable "^1.1.4"
+ is-regex "^1.0.4"
+ object-keys "^1.0.12"
+
+es-to-primitive@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377"
+ dependencies:
+ is-callable "^1.1.4"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.2"
+
+escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+
+escodegen@^1.11.1:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457"
+ dependencies:
+ esprima "^4.0.1"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ optionator "^0.8.1"
+ optionalDependencies:
+ source-map "~0.6.1"
+
+esprima@^4.0.0, esprima@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+
+estraverse@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+
+exec-sh@^0.3.2:
+ version "0.3.4"
+ resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5"
+
+execa@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
+ dependencies:
+ cross-spawn "^6.0.0"
+ get-stream "^4.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
+execa@^3.2.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89"
+ dependencies:
+ cross-spawn "^7.0.0"
+ get-stream "^5.0.0"
+ human-signals "^1.1.1"
+ is-stream "^2.0.0"
+ merge-stream "^2.0.0"
+ npm-run-path "^4.0.0"
+ onetime "^5.1.0"
+ p-finally "^2.0.0"
+ signal-exit "^3.0.2"
+ strip-final-newline "^2.0.0"
+
+exit@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
+
+expand-brackets@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+ dependencies:
+ debug "^2.3.3"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ posix-character-classes "^0.1.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+expect@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/expect/-/expect-25.1.0.tgz#7e8d7b06a53f7d66ec927278db3304254ee683ee"
+ dependencies:
+ "@jest/types" "^25.1.0"
+ ansi-styles "^4.0.0"
+ jest-get-type "^25.1.0"
+ jest-matcher-utils "^25.1.0"
+ jest-message-util "^25.1.0"
+ jest-regex-util "^25.1.0"
+
+extend-shallow@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+ dependencies:
+ is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+ dependencies:
+ assign-symbols "^1.0.0"
+ is-extendable "^1.0.1"
+
+extend@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+
+extglob@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+ dependencies:
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ expand-brackets "^2.1.4"
+ extend-shallow "^2.0.1"
+ fragment-cache "^0.2.1"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+extsprintf@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+
+extsprintf@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+
+fast-deep-equal@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
+
+fast-levenshtein@~2.0.4:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+
+fb-watchman@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
+ dependencies:
+ bser "^2.0.0"
+
+fbjs@^0.8.16:
+ version "0.8.17"
+ resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
+ dependencies:
+ core-js "^1.0.0"
+ isomorphic-fetch "^2.1.1"
+ loose-envify "^1.0.0"
+ object-assign "^4.1.0"
+ promise "^7.1.1"
+ setimmediate "^1.0.5"
+ ua-parser-js "^0.7.18"
+
+fill-range@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+ to-regex-range "^2.1.0"
+
+fill-range@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+ dependencies:
+ to-regex-range "^5.0.1"
+
+find-up@^4.0.0, find-up@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+ dependencies:
+ locate-path "^5.0.0"
+ path-exists "^4.0.0"
+
+for-in@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+
+form-data@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.6"
+ mime-types "^2.1.12"
+
+fragment-cache@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+ dependencies:
+ map-cache "^0.2.2"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+
+fsevents@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805"
+
+function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+
+gensync@^1.0.0-beta.1:
+ version "1.0.0-beta.1"
+ resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
+
+get-caller-file@^2.0.1:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+
+get-stream@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
+ dependencies:
+ pump "^3.0.0"
+
+get-stream@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9"
+ dependencies:
+ pump "^3.0.0"
+
+get-value@^2.0.3, get-value@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ dependencies:
+ assert-plus "^1.0.0"
+
+glob@^7.1.1, glob@^7.1.2, glob@^7.1.3:
+ version "7.1.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+glob@^7.1.4:
+ version "7.1.6"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+globals@^11.1.0:
+ version "11.12.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
+
+graceful-fs@^4.2.3:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
+
+growly@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
+
+har-schema@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+
+har-validator@~5.1.3:
+ version "5.1.3"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+ dependencies:
+ ajv "^6.5.5"
+ har-schema "^2.0.0"
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+
+has-flag@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+
+has-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
+
+has-value@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+ dependencies:
+ get-value "^2.0.3"
+ has-values "^0.1.4"
+ isobject "^2.0.0"
+
+has-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+ dependencies:
+ get-value "^2.0.6"
+ has-values "^1.0.0"
+ isobject "^3.0.0"
+
+has-values@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+
+has-values@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
+has@^1.0.1, has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ dependencies:
+ function-bind "^1.1.1"
+
+html-encoding-sniffer@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
+ dependencies:
+ whatwg-encoding "^1.0.1"
+
+html-escaper@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.1.tgz#beed86b5d2b921e92533aa11bce6d8e3b583dee7"
+
+http-signature@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+ dependencies:
+ assert-plus "^1.0.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+human-signals@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
+
+iconv-lite@0.4.24, iconv-lite@~0.4.13:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+import-local@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6"
+ dependencies:
+ pkg-dir "^4.2.0"
+ resolve-cwd "^3.0.0"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+
+ip-regex@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
+
+is-accessor-descriptor@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+ dependencies:
+ kind-of "^6.0.0"
+
+is-buffer@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+
+is-callable@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
+
+is-ci@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
+ dependencies:
+ ci-info "^2.0.0"
+
+is-data-descriptor@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+ dependencies:
+ kind-of "^6.0.0"
+
+is-date-object@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
+
+is-descriptor@^0.1.0:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+ dependencies:
+ is-accessor-descriptor "^0.1.6"
+ is-data-descriptor "^0.1.4"
+ kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+ dependencies:
+ is-accessor-descriptor "^1.0.0"
+ is-data-descriptor "^1.0.0"
+ kind-of "^6.0.2"
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+
+is-extendable@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+ dependencies:
+ is-plain-object "^2.0.4"
+
+is-fullwidth-code-point@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+
+is-generator-fn@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118"
+
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+
+is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+ dependencies:
+ isobject "^3.0.1"
+
+is-regex@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
+ dependencies:
+ has "^1.0.1"
+
+is-stream@^1.0.1, is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+
+is-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
+
+is-symbol@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38"
+ dependencies:
+ has-symbols "^1.0.0"
+
+is-typedarray@^1.0.0, is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+
+is-windows@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+
+is-wsl@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.1.1.tgz#4a1c152d429df3d441669498e2486d3596ebaf1d"
+
+isarray@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ dependencies:
+ isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+
+isomorphic-fetch@^2.1.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
+ dependencies:
+ node-fetch "^1.0.1"
+ whatwg-fetch ">=0.10.0"
+
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+
+istanbul-lib-coverage@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec"
+
+istanbul-lib-instrument@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz#61f13ac2c96cfefb076fe7131156cc05907874e6"
+ dependencies:
+ "@babel/core" "^7.7.5"
+ "@babel/parser" "^7.7.5"
+ "@babel/template" "^7.7.4"
+ "@babel/traverse" "^7.7.4"
+ "@istanbuljs/schema" "^0.1.2"
+ istanbul-lib-coverage "^3.0.0"
+ semver "^6.3.0"
+
+istanbul-lib-report@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6"
+ dependencies:
+ istanbul-lib-coverage "^3.0.0"
+ make-dir "^3.0.0"
+ supports-color "^7.1.0"
+
+istanbul-lib-source-maps@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz#75743ce6d96bb86dc7ee4352cf6366a23f0b1ad9"
+ dependencies:
+ debug "^4.1.1"
+ istanbul-lib-coverage "^3.0.0"
+ source-map "^0.6.1"
+
+istanbul-reports@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.0.tgz#d4d16d035db99581b6194e119bbf36c963c5eb70"
+ dependencies:
+ html-escaper "^2.0.0"
+ istanbul-lib-report "^3.0.0"
+
+jest-changed-files@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-25.1.0.tgz#73dae9a7d9949fdfa5c278438ce8f2ff3ec78131"
+ dependencies:
+ "@jest/types" "^25.1.0"
+ execa "^3.2.0"
+ throat "^5.0.0"
+
+jest-cli@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-25.1.0.tgz#75f0b09cf6c4f39360906bf78d580be1048e4372"
+ dependencies:
+ "@jest/core" "^25.1.0"
+ "@jest/test-result" "^25.1.0"
+ "@jest/types" "^25.1.0"
+ chalk "^3.0.0"
+ exit "^0.1.2"
+ import-local "^3.0.2"
+ is-ci "^2.0.0"
+ jest-config "^25.1.0"
+ jest-util "^25.1.0"
+ jest-validate "^25.1.0"
+ prompts "^2.0.1"
+ realpath-native "^1.1.0"
+ yargs "^15.0.0"
+
+jest-config@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-25.1.0.tgz#d114e4778c045d3ef239452213b7ad3ec1cbea90"
+ dependencies:
+ "@babel/core" "^7.1.0"
+ "@jest/test-sequencer" "^25.1.0"
+ "@jest/types" "^25.1.0"
+ babel-jest "^25.1.0"
+ chalk "^3.0.0"
+ glob "^7.1.1"
+ jest-environment-jsdom "^25.1.0"
+ jest-environment-node "^25.1.0"
+ jest-get-type "^25.1.0"
+ jest-jasmine2 "^25.1.0"
+ jest-regex-util "^25.1.0"
+ jest-resolve "^25.1.0"
+ jest-util "^25.1.0"
+ jest-validate "^25.1.0"
+ micromatch "^4.0.2"
+ pretty-format "^25.1.0"
+ realpath-native "^1.1.0"
+
+jest-diff@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.1.0.tgz#58b827e63edea1bc80c1de952b80cec9ac50e1ad"
+ dependencies:
+ chalk "^3.0.0"
+ diff-sequences "^25.1.0"
+ jest-get-type "^25.1.0"
+ pretty-format "^25.1.0"
+
+jest-docblock@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-25.1.0.tgz#0f44bea3d6ca6dfc38373d465b347c8818eccb64"
+ dependencies:
+ detect-newline "^3.0.0"
+
+jest-each@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-25.1.0.tgz#a6b260992bdf451c2d64a0ccbb3ac25e9b44c26a"
+ dependencies:
+ "@jest/types" "^25.1.0"
+ chalk "^3.0.0"
+ jest-get-type "^25.1.0"
+ jest-util "^25.1.0"
+ pretty-format "^25.1.0"
+
+jest-environment-jsdom@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-25.1.0.tgz#6777ab8b3e90fd076801efd3bff8e98694ab43c3"
+ dependencies:
+ "@jest/environment" "^25.1.0"
+ "@jest/fake-timers" "^25.1.0"
+ "@jest/types" "^25.1.0"
+ jest-mock "^25.1.0"
+ jest-util "^25.1.0"
+ jsdom "^15.1.1"
+
+jest-environment-node@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-25.1.0.tgz#797bd89b378cf0bd794dc8e3dca6ef21126776db"
+ dependencies:
+ "@jest/environment" "^25.1.0"
+ "@jest/fake-timers" "^25.1.0"
+ "@jest/types" "^25.1.0"
+ jest-mock "^25.1.0"
+ jest-util "^25.1.0"
+
+jest-get-type@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.1.0.tgz#1cfe5fc34f148dc3a8a3b7275f6b9ce9e2e8a876"
+
+jest-haste-map@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-25.1.0.tgz#ae12163d284f19906260aa51fd405b5b2e5a4ad3"
+ dependencies:
+ "@jest/types" "^25.1.0"
+ anymatch "^3.0.3"
+ fb-watchman "^2.0.0"
+ graceful-fs "^4.2.3"
+ jest-serializer "^25.1.0"
+ jest-util "^25.1.0"
+ jest-worker "^25.1.0"
+ micromatch "^4.0.2"
+ sane "^4.0.3"
+ walker "^1.0.7"
+ optionalDependencies:
+ fsevents "^2.1.2"
+
+jest-jasmine2@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-25.1.0.tgz#681b59158a430f08d5d0c1cce4f01353e4b48137"
+ dependencies:
+ "@babel/traverse" "^7.1.0"
+ "@jest/environment" "^25.1.0"
+ "@jest/source-map" "^25.1.0"
+ "@jest/test-result" "^25.1.0"
+ "@jest/types" "^25.1.0"
+ chalk "^3.0.0"
+ co "^4.6.0"
+ expect "^25.1.0"
+ is-generator-fn "^2.0.0"
+ jest-each "^25.1.0"
+ jest-matcher-utils "^25.1.0"
+ jest-message-util "^25.1.0"
+ jest-runtime "^25.1.0"
+ jest-snapshot "^25.1.0"
+ jest-util "^25.1.0"
+ pretty-format "^25.1.0"
+ throat "^5.0.0"
+
+jest-leak-detector@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-25.1.0.tgz#ed6872d15aa1c72c0732d01bd073dacc7c38b5c6"
+ dependencies:
+ jest-get-type "^25.1.0"
+ pretty-format "^25.1.0"
+
+jest-matcher-utils@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-25.1.0.tgz#fa5996c45c7193a3c24e73066fc14acdee020220"
+ dependencies:
+ chalk "^3.0.0"
+ jest-diff "^25.1.0"
+ jest-get-type "^25.1.0"
+ pretty-format "^25.1.0"
+
+jest-message-util@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-25.1.0.tgz#702a9a5cb05c144b9aa73f06e17faa219389845e"
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@jest/test-result" "^25.1.0"
+ "@jest/types" "^25.1.0"
+ "@types/stack-utils" "^1.0.1"
+ chalk "^3.0.0"
+ micromatch "^4.0.2"
+ slash "^3.0.0"
+ stack-utils "^1.0.1"
+
+jest-mock@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-25.1.0.tgz#411d549e1b326b7350b2e97303a64715c28615fd"
+ dependencies:
+ "@jest/types" "^25.1.0"
+
+jest-pnp-resolver@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a"
+
+jest-regex-util@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-25.1.0.tgz#efaf75914267741838e01de24da07b2192d16d87"
+
+jest-resolve-dependencies@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-25.1.0.tgz#8a1789ec64eb6aaa77fd579a1066a783437e70d2"
+ dependencies:
+ "@jest/types" "^25.1.0"
+ jest-regex-util "^25.1.0"
+ jest-snapshot "^25.1.0"
+
+jest-resolve@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-25.1.0.tgz#23d8b6a4892362baf2662877c66aa241fa2eaea3"
+ dependencies:
+ "@jest/types" "^25.1.0"
+ browser-resolve "^1.11.3"
+ chalk "^3.0.0"
+ jest-pnp-resolver "^1.2.1"
+ realpath-native "^1.1.0"
+
+jest-runner@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-25.1.0.tgz#fef433a4d42c89ab0a6b6b268e4a4fbe6b26e812"
+ dependencies:
+ "@jest/console" "^25.1.0"
+ "@jest/environment" "^25.1.0"
+ "@jest/test-result" "^25.1.0"
+ "@jest/types" "^25.1.0"
+ chalk "^3.0.0"
+ exit "^0.1.2"
+ graceful-fs "^4.2.3"
+ jest-config "^25.1.0"
+ jest-docblock "^25.1.0"
+ jest-haste-map "^25.1.0"
+ jest-jasmine2 "^25.1.0"
+ jest-leak-detector "^25.1.0"
+ jest-message-util "^25.1.0"
+ jest-resolve "^25.1.0"
+ jest-runtime "^25.1.0"
+ jest-util "^25.1.0"
+ jest-worker "^25.1.0"
+ source-map-support "^0.5.6"
+ throat "^5.0.0"
+
+jest-runtime@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-25.1.0.tgz#02683218f2f95aad0f2ec1c9cdb28c1dc0ec0314"
+ dependencies:
+ "@jest/console" "^25.1.0"
+ "@jest/environment" "^25.1.0"
+ "@jest/source-map" "^25.1.0"
+ "@jest/test-result" "^25.1.0"
+ "@jest/transform" "^25.1.0"
+ "@jest/types" "^25.1.0"
+ "@types/yargs" "^15.0.0"
+ chalk "^3.0.0"
+ collect-v8-coverage "^1.0.0"
+ exit "^0.1.2"
+ glob "^7.1.3"
+ graceful-fs "^4.2.3"
+ jest-config "^25.1.0"
+ jest-haste-map "^25.1.0"
+ jest-message-util "^25.1.0"
+ jest-mock "^25.1.0"
+ jest-regex-util "^25.1.0"
+ jest-resolve "^25.1.0"
+ jest-snapshot "^25.1.0"
+ jest-util "^25.1.0"
+ jest-validate "^25.1.0"
+ realpath-native "^1.1.0"
+ slash "^3.0.0"
+ strip-bom "^4.0.0"
+ yargs "^15.0.0"
+
+jest-serializer@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-25.1.0.tgz#73096ba90e07d19dec4a0c1dd89c355e2f129e5d"
+
+jest-snapshot@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-25.1.0.tgz#d5880bd4b31faea100454608e15f8d77b9d221d9"
+ dependencies:
+ "@babel/types" "^7.0.0"
+ "@jest/types" "^25.1.0"
+ chalk "^3.0.0"
+ expect "^25.1.0"
+ jest-diff "^25.1.0"
+ jest-get-type "^25.1.0"
+ jest-matcher-utils "^25.1.0"
+ jest-message-util "^25.1.0"
+ jest-resolve "^25.1.0"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ pretty-format "^25.1.0"
+ semver "^7.1.1"
+
+jest-util@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-25.1.0.tgz#7bc56f7b2abd534910e9fa252692f50624c897d9"
+ dependencies:
+ "@jest/types" "^25.1.0"
+ chalk "^3.0.0"
+ is-ci "^2.0.0"
+ mkdirp "^0.5.1"
+
+jest-validate@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-25.1.0.tgz#1469fa19f627bb0a9a98e289f3e9ab6a668c732a"
+ dependencies:
+ "@jest/types" "^25.1.0"
+ camelcase "^5.3.1"
+ chalk "^3.0.0"
+ jest-get-type "^25.1.0"
+ leven "^3.1.0"
+ pretty-format "^25.1.0"
+
+jest-watcher@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-25.1.0.tgz#97cb4a937f676f64c9fad2d07b824c56808e9806"
+ dependencies:
+ "@jest/test-result" "^25.1.0"
+ "@jest/types" "^25.1.0"
+ ansi-escapes "^4.2.1"
+ chalk "^3.0.0"
+ jest-util "^25.1.0"
+ string-length "^3.1.0"
+
+jest-worker@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-25.1.0.tgz#75d038bad6fdf58eba0d2ec1835856c497e3907a"
+ dependencies:
+ merge-stream "^2.0.0"
+ supports-color "^7.0.0"
+
+jest@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/jest/-/jest-25.1.0.tgz#b85ef1ddba2fdb00d295deebbd13567106d35be9"
+ dependencies:
+ "@jest/core" "^25.1.0"
+ import-local "^3.0.2"
+ jest-cli "^25.1.0"
+
+"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+
+js-yaml@^3.13.1:
+ version "3.13.1"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+
+jsdom@^15.1.1:
+ version "15.2.1"
+ resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-15.2.1.tgz#d2feb1aef7183f86be521b8c6833ff5296d07ec5"
+ dependencies:
+ abab "^2.0.0"
+ acorn "^7.1.0"
+ acorn-globals "^4.3.2"
+ array-equal "^1.0.0"
+ cssom "^0.4.1"
+ cssstyle "^2.0.0"
+ data-urls "^1.1.0"
+ domexception "^1.0.1"
+ escodegen "^1.11.1"
+ html-encoding-sniffer "^1.0.2"
+ nwsapi "^2.2.0"
+ parse5 "5.1.0"
+ pn "^1.1.0"
+ request "^2.88.0"
+ request-promise-native "^1.0.7"
+ saxes "^3.1.9"
+ symbol-tree "^3.2.2"
+ tough-cookie "^3.0.1"
+ w3c-hr-time "^1.0.1"
+ w3c-xmlserializer "^1.1.2"
+ webidl-conversions "^4.0.2"
+ whatwg-encoding "^1.0.5"
+ whatwg-mimetype "^2.3.0"
+ whatwg-url "^7.0.0"
+ ws "^7.0.0"
+ xml-name-validator "^3.0.0"
+
+jsesc@^2.5.1:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+
+json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+
+json5@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.2.tgz#43ef1f0af9835dd624751a6b7fa48874fb2d608e"
+ dependencies:
+ minimist "^1.2.5"
+
+jsprim@^1.2.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.3.0"
+ json-schema "0.2.3"
+ verror "1.10.0"
+
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
+
+kleur@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
+
+leven@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
+
+levn@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+ dependencies:
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+
+locate-path@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+ dependencies:
+ p-locate "^4.1.0"
+
+lodash.sortby@^4.7.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
+
+lodash@^4.17.13, lodash@^4.17.15:
+ version "4.17.15"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
+
+lolex@^5.0.0:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/lolex/-/lolex-5.1.2.tgz#953694d098ce7c07bc5ed6d0e42bc6c0c6d5a367"
+ dependencies:
+ "@sinonjs/commons" "^1.7.0"
+
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+ dependencies:
+ js-tokens "^3.0.0 || ^4.0.0"
+
+make-dir@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.2.tgz#04a1acbf22221e1d6ef43559f43e05a90dbb4392"
+ dependencies:
+ semver "^6.0.0"
+
+makeerror@1.0.x:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
+ dependencies:
+ tmpl "1.0.x"
+
+map-cache@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+
+map-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+ dependencies:
+ object-visit "^1.0.0"
+
+merge-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+
+micromatch@^3.1.4:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.1"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ extglob "^2.0.4"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.2"
+ nanomatch "^1.2.9"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.2"
+
+micromatch@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
+ dependencies:
+ braces "^3.0.1"
+ picomatch "^2.0.5"
+
+mime-db@~1.38.0:
+ version "1.38.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad"
+
+mime-types@^2.1.12, mime-types@~2.1.19:
+ version "2.1.22"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd"
+ dependencies:
+ mime-db "~1.38.0"
+
+mimic-fn@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+
+minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
+minimist@^1.1.1, minimist@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+
+minimist@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+
+mixin-deep@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
+ dependencies:
+ for-in "^1.0.2"
+ is-extendable "^1.0.1"
+
+mkdirp@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ dependencies:
+ minimist "0.0.8"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+
+ms@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
+
+nanomatch@^1.2.9:
+ version "1.2.13"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ fragment-cache "^0.2.1"
+ is-windows "^1.0.2"
+ kind-of "^6.0.2"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+
+nice-try@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+
+node-fetch@^1.0.1:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
+ dependencies:
+ encoding "^0.1.11"
+ is-stream "^1.0.1"
+
+node-int64@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
+
+node-modules-regexp@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40"
+
+node-notifier@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-6.0.0.tgz#cea319e06baa16deec8ce5cd7f133c4a46b68e12"
+ dependencies:
+ growly "^1.3.0"
+ is-wsl "^2.1.1"
+ semver "^6.3.0"
+ shellwords "^0.1.1"
+ which "^1.3.1"
+
+normalize-path@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+normalize-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ dependencies:
+ path-key "^2.0.0"
+
+npm-run-path@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
+ dependencies:
+ path-key "^3.0.0"
+
+nwsapi@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
+
+oauth-sign@~0.9.0:
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+
+object-assign@^4.1.0, object-assign@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+
+object-copy@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+ dependencies:
+ copy-descriptor "^0.1.0"
+ define-property "^0.2.5"
+ kind-of "^3.0.3"
+
+object-keys@^1.0.12:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.0.tgz#11bd22348dd2e096a045ab06f6c85bcc340fa032"
+
+object-visit@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+ dependencies:
+ isobject "^3.0.0"
+
+object.getownpropertydescriptors@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.5.1"
+
+object.pick@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+ dependencies:
+ isobject "^3.0.1"
+
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ dependencies:
+ wrappy "1"
+
+onetime@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5"
+ dependencies:
+ mimic-fn "^2.1.0"
+
+optionator@^0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
+ dependencies:
+ deep-is "~0.1.3"
+ fast-levenshtein "~2.0.4"
+ levn "~0.3.0"
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+ wordwrap "~1.0.0"
+
+p-each-series@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+
+p-finally@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561"
+
+p-limit@^2.2.0:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e"
+ dependencies:
+ p-try "^2.0.0"
+
+p-locate@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+ dependencies:
+ p-limit "^2.2.0"
+
+p-try@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+
+parse5@5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
+
+pascalcase@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+
+path-key@^2.0.0, path-key@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+
+path-key@^3.0.0, path-key@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+
+path-parse@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+
+picomatch@^2.0.4, picomatch@^2.0.5:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
+
+pirates@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87"
+ dependencies:
+ node-modules-regexp "^1.0.0"
+
+pkg-dir@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+ dependencies:
+ find-up "^4.0.0"
+
+pn@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
+
+posix-character-classes@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+
+pretty-format@^25.1.0:
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.1.0.tgz#ed869bdaec1356fc5ae45de045e2c8ec7b07b0c8"
+ dependencies:
+ "@jest/types" "^25.1.0"
+ ansi-regex "^5.0.0"
+ ansi-styles "^4.0.0"
+ react-is "^16.12.0"
+
+promise@^7.1.1:
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+ dependencies:
+ asap "~2.0.3"
+
+prompts@^2.0.1:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.2.tgz#480572d89ecf39566d2bd3fe2c9fccb7c4c0b068"
+ dependencies:
+ kleur "^3.0.3"
+ sisteransi "^1.0.4"
+
+prop-types@^15.6.0:
+ version "15.7.2"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
+ dependencies:
+ loose-envify "^1.4.0"
+ object-assign "^4.1.1"
+ react-is "^16.8.1"
+
+psl@^1.1.28:
+ version "1.1.31"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
+
+pump@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+punycode@^2.1.0, punycode@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+
+qs@~6.5.2:
+ version "6.5.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+
+react-dom@16.4.1:
+ version "16.4.1"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.1.tgz#7f8b0223b3a5fbe205116c56deb85de32685dad6"
+ dependencies:
+ fbjs "^0.8.16"
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.0"
+
+react-is@^16.12.0:
+ version "16.13.1"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
+
+react-is@^16.4.1, react-is@^16.8.1:
+ version "16.8.4"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2"
+
+react-test-renderer@16.4.1:
+ version "16.4.1"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.1.tgz#f2fb30c2c7b517db6e5b10ed20bb6b0a7ccd8d70"
+ dependencies:
+ fbjs "^0.8.16"
+ object-assign "^4.1.1"
+ prop-types "^15.6.0"
+ react-is "^16.4.1"
+
+react@16.4.1:
+ version "16.4.1"
+ resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32"
+ dependencies:
+ fbjs "^0.8.16"
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.0"
+
+realpath-native@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c"
+ dependencies:
+ util.promisify "^1.0.0"
+
+regex-not@^1.0.0, regex-not@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+ dependencies:
+ extend-shallow "^3.0.2"
+ safe-regex "^1.1.0"
+
+remove-trailing-separator@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+
+repeat-element@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
+
+repeat-string@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+
+request-promise-core@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9"
+ dependencies:
+ lodash "^4.17.15"
+
+request-promise-native@^1.0.7:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36"
+ dependencies:
+ request-promise-core "1.1.3"
+ stealthy-require "^1.1.1"
+ tough-cookie "^2.3.3"
+
+request@^2.88.0:
+ version "2.88.2"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.8.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.6"
+ extend "~3.0.2"
+ forever-agent "~0.6.1"
+ form-data "~2.3.2"
+ har-validator "~5.1.3"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.19"
+ oauth-sign "~0.9.0"
+ performance-now "^2.1.0"
+ qs "~6.5.2"
+ safe-buffer "^5.1.2"
+ tough-cookie "~2.5.0"
+ tunnel-agent "^0.6.0"
+ uuid "^3.3.2"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+
+require-main-filename@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+
+resolve-cwd@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
+ dependencies:
+ resolve-from "^5.0.0"
+
+resolve-from@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+
+resolve-url@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+
+resolve@1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
+
+resolve@^1.3.2:
+ version "1.15.1"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8"
+ dependencies:
+ path-parse "^1.0.6"
+
+ret@~0.1.10:
+ version "0.1.15"
+ resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+
+rimraf@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+ dependencies:
+ glob "^7.1.3"
+
+rsvp@^4.8.4:
+ version "4.8.5"
+ resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+
+safe-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+ dependencies:
+ ret "~0.1.10"
+
+"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+
+sane@^4.0.3:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded"
+ dependencies:
+ "@cnakazawa/watch" "^1.0.3"
+ anymatch "^2.0.0"
+ capture-exit "^2.0.0"
+ exec-sh "^0.3.2"
+ execa "^1.0.0"
+ fb-watchman "^2.0.0"
+ micromatch "^3.1.4"
+ minimist "^1.1.1"
+ walker "~1.0.5"
+
+saxes@^3.1.9:
+ version "3.1.11"
+ resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b"
+ dependencies:
+ xmlchars "^2.1.1"
+
+semver@^5.4.1:
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+
+semver@^5.5.0:
+ version "5.6.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
+
+semver@^6.0.0, semver@^6.3.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+
+semver@^7.1.1:
+ version "7.1.3"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.3.tgz#e4345ce73071c53f336445cfc19efb1c311df2a6"
+
+set-blocking@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+
+set-value@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1"
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.1"
+ to-object-path "^0.3.0"
+
+set-value@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274"
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.3"
+ split-string "^3.0.1"
+
+setimmediate@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ dependencies:
+ shebang-regex "^1.0.0"
+
+shebang-command@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+ dependencies:
+ shebang-regex "^3.0.0"
+
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+
+shebang-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+
+shellwords@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
+
+signal-exit@^3.0.0, signal-exit@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+
+sisteransi@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
+
+slash@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+
+snapdragon-node@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+ dependencies:
+ define-property "^1.0.0"
+ isobject "^3.0.0"
+ snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+ dependencies:
+ kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
+ dependencies:
+ base "^0.11.1"
+ debug "^2.2.0"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ map-cache "^0.2.2"
+ source-map "^0.5.6"
+ source-map-resolve "^0.5.0"
+ use "^3.1.0"
+
+source-map-resolve@^0.5.0:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259"
+ dependencies:
+ atob "^2.1.1"
+ decode-uri-component "^0.2.0"
+ resolve-url "^0.2.1"
+ source-map-url "^0.4.0"
+ urix "^0.1.0"
+
+source-map-support@^0.5.6:
+ version "0.5.11"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.11.tgz#efac2ce0800355d026326a0ca23e162aeac9a4e2"
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map-url@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
+
+source-map@^0.5.0, source-map@^0.5.6:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+
+source-map@^0.7.3:
+ version "0.7.3"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
+
+split-string@^3.0.1, split-string@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+ dependencies:
+ extend-shallow "^3.0.0"
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+
+sshpk@^1.7.0:
+ version "1.16.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ bcrypt-pbkdf "^1.0.0"
+ dashdash "^1.12.0"
+ ecc-jsbn "~0.1.1"
+ getpass "^0.1.1"
+ jsbn "~0.1.0"
+ safer-buffer "^2.0.2"
+ tweetnacl "~0.14.0"
+
+stack-utils@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8"
+
+static-extend@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+ dependencies:
+ define-property "^0.2.5"
+ object-copy "^0.1.0"
+
+stealthy-require@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
+
+string-length@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/string-length/-/string-length-3.1.0.tgz#107ef8c23456e187a8abd4a61162ff4ac6e25837"
+ dependencies:
+ astral-regex "^1.0.0"
+ strip-ansi "^5.2.0"
+
+string-width@^4.1.0, string-width@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.0"
+
+strip-ansi@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
+ dependencies:
+ ansi-regex "^4.1.0"
+
+strip-ansi@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
+ dependencies:
+ ansi-regex "^5.0.0"
+
+strip-bom@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
+
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+
+strip-final-newline@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
+
+supports-color@^5.3.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+ dependencies:
+ has-flag "^3.0.0"
+
+supports-color@^7.0.0, supports-color@^7.1.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
+ dependencies:
+ has-flag "^4.0.0"
+
+supports-hyperlinks@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz#f663df252af5f37c5d49bbd7eeefa9e0b9e59e47"
+ dependencies:
+ has-flag "^4.0.0"
+ supports-color "^7.0.0"
+
+symbol-tree@^3.2.2:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
+
+terminal-link@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
+ dependencies:
+ ansi-escapes "^4.2.1"
+ supports-hyperlinks "^2.0.0"
+
+test-exclude@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
+ dependencies:
+ "@istanbuljs/schema" "^0.1.2"
+ glob "^7.1.4"
+ minimatch "^3.0.4"
+
+throat@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
+
+tmpl@1.0.x:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
+
+to-fast-properties@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+
+to-object-path@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+ dependencies:
+ kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+ dependencies:
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ dependencies:
+ is-number "^7.0.0"
+
+to-regex@^3.0.1, to-regex@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+ dependencies:
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ regex-not "^1.0.2"
+ safe-regex "^1.1.0"
+
+tough-cookie@^2.3.3, tough-cookie@~2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+ dependencies:
+ psl "^1.1.28"
+ punycode "^2.1.1"
+
+tough-cookie@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
+ dependencies:
+ ip-regex "^2.1.0"
+ psl "^1.1.28"
+ punycode "^2.1.1"
+
+tr46@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
+ dependencies:
+ punycode "^2.1.0"
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+
+type-check@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ dependencies:
+ prelude-ls "~1.1.2"
+
+type-detect@4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
+
+type-fest@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1"
+
+typedarray-to-buffer@^3.1.5:
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
+ dependencies:
+ is-typedarray "^1.0.0"
+
+ua-parser-js@^0.7.18:
+ version "0.7.19"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b"
+
+union-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
+ dependencies:
+ arr-union "^3.1.0"
+ get-value "^2.0.6"
+ is-extendable "^0.1.1"
+ set-value "^0.4.3"
+
+unset-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+ dependencies:
+ has-value "^0.3.1"
+ isobject "^3.0.0"
+
+uri-js@^4.2.2:
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+ dependencies:
+ punycode "^2.1.0"
+
+urix@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+
+use@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
+
+util.promisify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"
+ dependencies:
+ define-properties "^1.1.2"
+ object.getownpropertydescriptors "^2.0.3"
+
+uuid@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
+
+v8-to-istanbul@^4.0.1:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-4.1.2.tgz#387d173be5383dbec209d21af033dcb892e3ac82"
+ dependencies:
+ "@types/istanbul-lib-coverage" "^2.0.1"
+ convert-source-map "^1.6.0"
+ source-map "^0.7.3"
+
+verror@1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+ dependencies:
+ assert-plus "^1.0.0"
+ core-util-is "1.0.2"
+ extsprintf "^1.2.0"
+
+w3c-hr-time@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"
+ dependencies:
+ browser-process-hrtime "^0.1.2"
+
+w3c-xmlserializer@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz#30485ca7d70a6fd052420a3d12fd90e6339ce794"
+ dependencies:
+ domexception "^1.0.1"
+ webidl-conversions "^4.0.2"
+ xml-name-validator "^3.0.0"
+
+walker@^1.0.7, walker@~1.0.5:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
+ dependencies:
+ makeerror "1.0.x"
+
+webidl-conversions@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
+
+whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
+ dependencies:
+ iconv-lite "0.4.24"
+
+whatwg-fetch@>=0.10.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
+
+whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
+
+whatwg-url@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd"
+ dependencies:
+ lodash.sortby "^4.7.0"
+ tr46 "^1.0.1"
+ webidl-conversions "^4.0.2"
+
+which-module@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+
+which@^1.2.9, which@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ dependencies:
+ isexe "^2.0.0"
+
+which@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+ dependencies:
+ isexe "^2.0.0"
+
+wordwrap@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+
+wrap-ansi@^6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+
+write-file-atomic@^3.0.0:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
+ dependencies:
+ imurmurhash "^0.1.4"
+ is-typedarray "^1.0.0"
+ signal-exit "^3.0.2"
+ typedarray-to-buffer "^3.1.5"
+
+ws@^7.0.0:
+ version "7.2.3"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.3.tgz#a5411e1fb04d5ed0efee76d26d5c46d830c39b46"
+
+xml-name-validator@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
+
+xmlchars@^2.1.1:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
+
+y18n@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
+
+yargs-parser@^18.1.1:
+ version "18.1.1"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.1.tgz#bf7407b915427fc760fcbbccc6c82b4f0ffcbd37"
+ dependencies:
+ camelcase "^5.0.0"
+ decamelize "^1.2.0"
+
+yargs@^15.0.0:
+ version "15.3.1"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.1.tgz#9505b472763963e54afe60148ad27a330818e98b"
+ dependencies:
+ cliui "^6.0.0"
+ decamelize "^1.2.0"
+ find-up "^4.1.0"
+ get-caller-file "^2.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^2.0.0"
+ set-blocking "^2.0.0"
+ string-width "^4.2.0"
+ which-module "^2.0.0"
+ y18n "^4.0.0"
+ yargs-parser "^18.1.1"
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/sjs_code_bundle_reload_map.sjs b/devtools/client/framework/test/sjs_code_bundle_reload_map.sjs
new file mode 100644
index 0000000000..40f62034aa
--- /dev/null
+++ b/devtools/client/framework/test/sjs_code_bundle_reload_map.sjs
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* globals getState, setState */
+/* exported handleRequest */
+
+"use strict";
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+
+ // Redirect to a different file each time.
+ let counter = 1 + (+getState("counter") % 2);
+
+ let index = request.path.lastIndexOf("/");
+ let newPath = request.path.substr(0, index + 1) +
+ "code_bundle_reload_" + counter + ".js.map";
+ let newUrl = request.scheme + "://" + request.host + newPath;
+
+ response.setStatusLine(request.httpVersion, 302, "Found");
+ response.setHeader("Location", newUrl);
+ setState("counter", "" + counter);
+}
diff --git a/devtools/client/framework/test/sjs_code_reload.sjs b/devtools/client/framework/test/sjs_code_reload.sjs
new file mode 100644
index 0000000000..b48884e3df
--- /dev/null
+++ b/devtools/client/framework/test/sjs_code_reload.sjs
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* globals getState, setState */
+/* exported handleRequest */
+
+"use strict";
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+
+ // Redirect to a different file each time.
+ let counter = 1 + (+getState("counter") % 2);
+
+ let index = request.path.lastIndexOf("/");
+ let newPath = request.path.substr(0, index + 1) +
+ "code_bundle_reload_" + counter + ".js";
+ let newUrl = request.scheme + "://" + request.host + newPath;
+
+ response.setStatusLine(request.httpVersion, 302, "Found");
+ response.setHeader("Location", newUrl);
+ setState("counter", "" + counter);
+}
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 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Chrome page</title>
+<script>
+// eslint-disable-next-line no-unused-vars
+function inlineScript() {
+ console.log("foo");
+}
+</script>
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..9f2220ed81
--- /dev/null
+++ b/devtools/client/framework/test/xpcshell/test_tabs_absolute_order.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+
+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("devtools/client/framework/toolbox-tabs-order-manager");
+
+ 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.ini b/devtools/client/framework/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..6f68037a48
--- /dev/null
+++ b/devtools/client/framework/test/xpcshell/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+tags = devtools
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_tabs_absolute_order.js]
diff --git a/devtools/client/framework/toolbox-context-menu.js b/devtools/client/framework/toolbox-context-menu.js
new file mode 100644
index 0000000000..6121e98902
--- /dev/null
+++ b/devtools/client/framework/toolbox-context-menu.js
@@ -0,0 +1,119 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Menu = require("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+
+// This WeakMap will be used to know if strings have already been loaded in a given
+// window, which will be used as key.
+const stringsLoaded = new WeakMap();
+
+/**
+ * Lazily load strings for the edit menu.
+ */
+function loadEditMenuStrings(win) {
+ if (stringsLoaded.has(win)) {
+ return;
+ }
+
+ if (win.MozXULElement) {
+ stringsLoaded.set(win, true);
+ win.MozXULElement.insertFTLIfNeeded("toolkit/global/textActions.ftl");
+ }
+}
+
+/**
+ * Return an 'edit' menu for a input field. This integrates directly
+ * with docshell commands to provide the right enabled state and editor
+ * functionality.
+ *
+ * You'll need to call menu.popup() yourself, this just returns the Menu instance.
+ *
+ * @param {Window} win parent window reference
+ * @param {String} id menu ID
+ *
+ * @returns {Menu}
+ */
+function createEditContextMenu(win, id) {
+ // Localized strings for the menu are loaded lazily.
+ loadEditMenuStrings(win);
+
+ const docshell = win.docShell;
+ const menu = new Menu({ id });
+ menu.append(
+ new MenuItem({
+ id: "editmenu-undo",
+ l10nID: "text-action-undo",
+ disabled: !docshell.isCommandEnabled("cmd_undo"),
+ click: () => {
+ docshell.doCommand("cmd_undo");
+ },
+ })
+ );
+ menu.append(
+ new MenuItem({
+ type: "separator",
+ })
+ );
+ menu.append(
+ new MenuItem({
+ id: "editmenu-cut",
+ l10nID: "text-action-cut",
+ disabled: !docshell.isCommandEnabled("cmd_cut"),
+ click: () => {
+ docshell.doCommand("cmd_cut");
+ },
+ })
+ );
+ menu.append(
+ new MenuItem({
+ id: "editmenu-copy",
+ l10nID: "text-action-copy",
+ disabled: !docshell.isCommandEnabled("cmd_copy"),
+ click: () => {
+ docshell.doCommand("cmd_copy");
+ },
+ })
+ );
+ menu.append(
+ new MenuItem({
+ id: "editmenu-paste",
+ l10nID: "text-action-paste",
+ disabled: !docshell.isCommandEnabled("cmd_paste"),
+ click: () => {
+ docshell.doCommand("cmd_paste");
+ },
+ })
+ );
+ menu.append(
+ new MenuItem({
+ id: "editmenu-delete",
+ l10nID: "text-action-delete",
+ disabled: !docshell.isCommandEnabled("cmd_delete"),
+ click: () => {
+ docshell.doCommand("cmd_delete");
+ },
+ })
+ );
+ menu.append(
+ new MenuItem({
+ type: "separator",
+ })
+ );
+ menu.append(
+ new MenuItem({
+ id: "editmenu-selectAll",
+ l10nID: "text-action-select-all",
+ disabled: !docshell.isCommandEnabled("cmd_selectAll"),
+ click: () => {
+ docshell.doCommand("cmd_selectAll");
+ },
+ })
+ );
+ return menu;
+}
+
+module.exports.createEditContextMenu = createEditContextMenu;
diff --git a/devtools/client/framework/toolbox-host-manager.js b/devtools/client/framework/toolbox-host-manager.js
new file mode 100644
index 0000000000..becab6b4be
--- /dev/null
+++ b/devtools/client/framework/toolbox-host-manager.js
@@ -0,0 +1,290 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Services = require("Services");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/toolbox.properties"
+);
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const Telemetry = require("devtools/client/shared/telemetry");
+const { DOMHelpers } = require("devtools/shared/dom-helpers");
+
+// The min-width of toolbox and browser toolbox.
+const WIDTH_CHEVRON_AND_MEATBALL = 50;
+const WIDTH_CHEVRON_AND_MEATBALL_AND_CLOSE = 74;
+const ZOOM_VALUE_PREF = "devtools.toolbox.zoomValue";
+
+loader.lazyRequireGetter(
+ this,
+ "Toolbox",
+ "devtools/client/framework/toolbox",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "Hosts",
+ "devtools/client/framework/toolbox-hosts",
+ true
+);
+
+/**
+ * Implement a wrapper on the chrome side to setup a Toolbox within Firefox UI.
+ *
+ * This component handles iframe creation within Firefox, in which we are loading
+ * the toolbox document. Then both the chrome and the toolbox document communicate
+ * via "message" events.
+ *
+ * Messages sent by the toolbox to the chrome:
+ * - switch-host:
+ * Order to display the toolbox in another host (side, bottom, window, or the
+ * previously used one)
+ * - raise-host:
+ * Focus the tools
+ * - set-host-title:
+ * When using the window host, update the window title
+ *
+ * Messages sent by the chrome to the toolbox:
+ * - switched-host:
+ * The `switch-host` command sent by the toolbox is done
+ */
+
+const LAST_HOST = "devtools.toolbox.host";
+const PREVIOUS_HOST = "devtools.toolbox.previousHost";
+let ID_COUNTER = 1;
+
+function ToolboxHostManager(target, hostType, hostOptions) {
+ this.target = target;
+
+ this.frameId = ID_COUNTER++;
+
+ if (!hostType) {
+ hostType = Services.prefs.getCharPref(LAST_HOST);
+ if (!Hosts[hostType]) {
+ // If the preference value is unexpected, restore to the default value.
+ Services.prefs.clearUserPref(LAST_HOST);
+ hostType = Services.prefs.getCharPref(LAST_HOST);
+ }
+ }
+ this.host = this.createHost(hostType, hostOptions);
+ this.hostType = hostType;
+ this.telemetry = new Telemetry();
+ this.setMinWidthWithZoom = this.setMinWidthWithZoom.bind(this);
+ this._onToolboxUnload = this._onToolboxUnload.bind(this);
+ this._onMessage = this._onMessage.bind(this);
+ Services.prefs.addObserver(ZOOM_VALUE_PREF, this.setMinWidthWithZoom);
+}
+
+ToolboxHostManager.prototype = {
+ async create(toolId) {
+ await this.host.create();
+
+ this.host.frame.setAttribute("aria-label", L10N.getStr("toolbox.label"));
+ this.host.frame.ownerDocument.defaultView.addEventListener(
+ "message",
+ this._onMessage
+ );
+
+ const msSinceProcessStart = parseInt(
+ this.telemetry.msSinceProcessStart(),
+ 10
+ );
+ const toolbox = new Toolbox(
+ this.target,
+ toolId,
+ this.host.type,
+ this.host.frame.contentWindow,
+ this.frameId,
+ msSinceProcessStart
+ );
+ toolbox.once("toolbox-unload", this._onToolboxUnload);
+
+ // Prevent reloading the toolbox when loading the tools in a tab
+ // (e.g. from about:debugging)
+ const location = this.host.frame.contentWindow.location;
+ if (!location.href.startsWith("about:devtools-toolbox")) {
+ this.host.frame.setAttribute("src", "about:devtools-toolbox");
+ }
+
+ // We set an attribute on the toolbox iframe so that apps do not need
+ // access to the toolbox internals in order to get the session ID.
+ this.host.frame.setAttribute("session_id", msSinceProcessStart);
+
+ this.setMinWidthWithZoom();
+ return toolbox;
+ },
+
+ setMinWidthWithZoom: function() {
+ const zoomValue = parseFloat(Services.prefs.getCharPref(ZOOM_VALUE_PREF));
+
+ if (isNaN(zoomValue)) {
+ return;
+ }
+
+ if (
+ this.hostType === Toolbox.HostType.LEFT ||
+ this.hostType === Toolbox.HostType.RIGHT
+ ) {
+ this.host.frame.minWidth =
+ WIDTH_CHEVRON_AND_MEATBALL_AND_CLOSE * zoomValue;
+ } else if (
+ this.hostType === Toolbox.HostType.WINDOW ||
+ this.hostType === Toolbox.HostType.PAGE ||
+ this.hostType === Toolbox.HostType.BROWSERTOOLBOX
+ ) {
+ this.host.frame.minWidth = WIDTH_CHEVRON_AND_MEATBALL * zoomValue;
+ }
+ },
+
+ _onToolboxUnload() {
+ // The "toolbox-unload" event is currently emitted right before destroying
+ // the target. Run destroy() in the next tick to allow the target to be
+ // destroyed.
+ DevToolsUtils.executeSoon(() => {
+ this.destroy();
+ });
+ },
+
+ _onMessage(event) {
+ if (!event.data) {
+ return;
+ }
+ const msg = event.data;
+ // Toolbox document is still chrome and disallow identifying message
+ // origin via event.source as it is null. So use a custom id.
+ if (msg.frameId != this.frameId) {
+ return;
+ }
+ switch (msg.name) {
+ case "switch-host":
+ this.switchHost(msg.hostType);
+ break;
+ case "raise-host":
+ this.host.raise();
+ break;
+ case "set-host-title":
+ this.host.setTitle(msg.title);
+ break;
+ }
+ },
+
+ postMessage(data) {
+ const window = this.host.frame.contentWindow;
+ window.postMessage(data, "*");
+ },
+
+ destroy() {
+ Services.prefs.removeObserver(ZOOM_VALUE_PREF, this.setMinWidthWithZoom);
+ this.destroyHost();
+ this.host = null;
+ this.hostType = null;
+ this.target = null;
+ },
+
+ /**
+ * Create a host object based on the given host type.
+ *
+ * Warning: bottom and sidebar hosts require that the toolbox target provides
+ * a reference to the attached tab. Not all Targets have a tab property -
+ * make sure you correctly mix and match hosts and targets.
+ *
+ * @param {string} hostType
+ * The host type of the new host object
+ *
+ * @return {Host} host
+ * The created host object
+ */
+ createHost(hostType, options) {
+ if (!Hosts[hostType]) {
+ throw new Error("Unknown hostType: " + hostType);
+ }
+
+ const newHost = new Hosts[hostType](this.target.localTab, options);
+ return newHost;
+ },
+
+ async switchHost(hostType) {
+ if (hostType == "previous") {
+ // Switch to the last used host for the toolbox UI.
+ // This is determined by the devtools.toolbox.previousHost pref.
+ hostType = Services.prefs.getCharPref(PREVIOUS_HOST);
+
+ // Handle the case where the previous host happens to match the current
+ // host. If so, switch to bottom if it's not already used, and right side if not.
+ if (hostType === this.hostType) {
+ if (hostType === Toolbox.HostType.BOTTOM) {
+ hostType = Toolbox.HostType.RIGHT;
+ } else {
+ hostType = Toolbox.HostType.BOTTOM;
+ }
+ }
+ }
+ const iframe = this.host.frame;
+ const newHost = this.createHost(hostType);
+ const newIframe = await newHost.create();
+
+ // Load a blank document in the host frame. The new iframe must have a valid
+ // document before using swapFrameLoaders().
+ await new Promise(resolve => {
+ newIframe.setAttribute("src", "about:blank");
+ DOMHelpers.onceDOMReady(newIframe.contentWindow, resolve);
+ });
+
+ // change toolbox document's parent to the new host
+ newIframe.swapFrameLoaders(iframe);
+
+ this.destroyHost();
+
+ if (
+ this.hostType !== Toolbox.HostType.BROWSERTOOLBOX &&
+ this.hostType !== Toolbox.HostType.PAGE
+ ) {
+ Services.prefs.setCharPref(PREVIOUS_HOST, this.hostType);
+ }
+
+ this.host = newHost;
+ this.hostType = hostType;
+ this.host.setTitle(this.host.frame.contentWindow.document.title);
+ this.host.frame.ownerDocument.defaultView.addEventListener(
+ "message",
+ this._onMessage
+ );
+
+ this.setMinWidthWithZoom();
+
+ if (
+ hostType !== Toolbox.HostType.BROWSERTOOLBOX &&
+ hostType !== Toolbox.HostType.PAGE
+ ) {
+ Services.prefs.setCharPref(LAST_HOST, hostType);
+ }
+
+ // Tell the toolbox the host changed
+ this.postMessage({
+ name: "switched-host",
+ hostType,
+ });
+ },
+
+ /**
+ * Destroy the current host, and remove event listeners from its frame.
+ *
+ * @return {promise} to be resolved when the host is destroyed.
+ */
+ destroyHost() {
+ // When Firefox toplevel is closed, the frame may already be detached and
+ // the top level document gone
+ if (this.host.frame.ownerDocument.defaultView) {
+ this.host.frame.ownerDocument.defaultView.removeEventListener(
+ "message",
+ this._onMessage
+ );
+ }
+
+ return this.host.destroy();
+ },
+};
+exports.ToolboxHostManager = ToolboxHostManager;
diff --git a/devtools/client/framework/toolbox-hosts.js b/devtools/client/framework/toolbox-hosts.js
new file mode 100644
index 0000000000..19098d7fe3
--- /dev/null
+++ b/devtools/client/framework/toolbox-hosts.js
@@ -0,0 +1,443 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+const Services = require("Services");
+
+loader.lazyRequireGetter(
+ this,
+ "gDevToolsBrowser",
+ "devtools/client/framework/devtools-browser",
+ true
+);
+
+loader.lazyRequireGetter(
+ this,
+ "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm",
+ true
+);
+
+/* A host should always allow this much space for the page to be displayed.
+ * There is also a min-height on the browser, but we still don't want to set
+ * frame.height to be larger than that, since it can cause problems with
+ * resizing the toolbox and panel layout. */
+const MIN_PAGE_SIZE = 25;
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+/**
+ * A toolbox host represents an object that contains a toolbox (e.g. the
+ * sidebar or a separate window). Any host object should implement the
+ * following functions:
+ *
+ * create() - create the UI
+ * destroy() - destroy the host's UI
+ */
+
+/**
+ * Host object for the dock on the bottom of the browser
+ */
+function BottomHost(hostTab) {
+ this.hostTab = hostTab;
+
+ EventEmitter.decorate(this);
+}
+
+BottomHost.prototype = {
+ type: "bottom",
+
+ heightPref: "devtools.toolbox.footer.height",
+
+ /**
+ * Create a box at the bottom of the host tab.
+ */
+ create: async function() {
+ await gDevToolsBrowser.loadBrowserStyleSheet(this.hostTab.ownerGlobal);
+
+ const gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser;
+ const ownerDocument = gBrowser.ownerDocument;
+ this._browserContainer = gBrowser.getBrowserContainer(
+ this.hostTab.linkedBrowser
+ );
+
+ this._splitter = ownerDocument.createXULElement("splitter");
+ this._splitter.setAttribute("class", "devtools-horizontal-splitter");
+ // Avoid resizing notification containers
+ this._splitter.setAttribute("resizebefore", "flex");
+
+ this.frame = createDevToolsFrame(
+ ownerDocument,
+ "devtools-toolbox-bottom-iframe"
+ );
+ this.frame.height = Math.min(
+ Services.prefs.getIntPref(this.heightPref),
+ this._browserContainer.clientHeight - MIN_PAGE_SIZE
+ );
+
+ this._browserContainer.appendChild(this._splitter);
+ this._browserContainer.appendChild(this.frame);
+
+ focusTab(this.hostTab);
+ return this.frame;
+ },
+
+ /**
+ * Raise the host.
+ */
+ raise: function() {
+ focusTab(this.hostTab);
+ },
+
+ /**
+ * Set the toolbox title.
+ * Nothing to do for this host type.
+ */
+ setTitle: function() {},
+
+ /**
+ * Destroy the bottom dock.
+ */
+ destroy: function() {
+ if (!this._destroyed) {
+ this._destroyed = true;
+
+ Services.prefs.setIntPref(this.heightPref, this.frame.height);
+ this._browserContainer.removeChild(this._splitter);
+ this._browserContainer.removeChild(this.frame);
+ this.frame = null;
+ this._browserContainer = null;
+ this._splitter = null;
+ }
+
+ return promise.resolve(null);
+ },
+};
+
+/**
+ * Base Host object for the in-browser sidebar
+ */
+class SidebarHost {
+ constructor(hostTab, type) {
+ this.hostTab = hostTab;
+ this.type = type;
+ this.widthPref = "devtools.toolbox.sidebar.width";
+
+ EventEmitter.decorate(this);
+ }
+
+ /**
+ * Create a box in the sidebar of the host tab.
+ */
+ async create() {
+ await gDevToolsBrowser.loadBrowserStyleSheet(this.hostTab.ownerGlobal);
+ const gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser;
+ const ownerDocument = gBrowser.ownerDocument;
+ this._browserContainer = gBrowser.getBrowserContainer(
+ this.hostTab.linkedBrowser
+ );
+ this._browserPanel = gBrowser.getPanel(this.hostTab.linkedBrowser);
+
+ this._splitter = ownerDocument.createXULElement("splitter");
+ this._splitter.setAttribute("class", "devtools-side-splitter");
+
+ this.frame = createDevToolsFrame(
+ ownerDocument,
+ "devtools-toolbox-side-iframe"
+ );
+ this.frame.width = Math.min(
+ Services.prefs.getIntPref(this.widthPref),
+ this._browserPanel.clientWidth - MIN_PAGE_SIZE
+ );
+
+ // We should consider the direction when changing the dock position.
+ const topWindow = this.hostTab.ownerDocument.defaultView.top;
+ const topDoc = topWindow.document.documentElement;
+ const isLTR = topWindow.getComputedStyle(topDoc).direction === "ltr";
+
+ if ((isLTR && this.type == "right") || (!isLTR && this.type == "left")) {
+ this._browserPanel.appendChild(this._splitter);
+ this._browserPanel.appendChild(this.frame);
+ } else {
+ this._browserPanel.insertBefore(this.frame, this._browserContainer);
+ this._browserPanel.insertBefore(this._splitter, this._browserContainer);
+ }
+
+ focusTab(this.hostTab);
+ return this.frame;
+ }
+
+ /**
+ * Raise the host.
+ */
+ raise() {
+ focusTab(this.hostTab);
+ }
+
+ /**
+ * Set the toolbox title.
+ * Nothing to do for this host type.
+ */
+ setTitle() {}
+
+ /**
+ * Destroy the sidebar.
+ */
+ destroy() {
+ if (!this._destroyed) {
+ this._destroyed = true;
+
+ Services.prefs.setIntPref(this.widthPref, this.frame.width);
+ this._browserPanel.removeChild(this._splitter);
+ this._browserPanel.removeChild(this.frame);
+ }
+
+ return promise.resolve(null);
+ }
+}
+
+/**
+ * Host object for the in-browser left sidebar
+ */
+class LeftHost extends SidebarHost {
+ constructor(hostTab) {
+ super(hostTab, "left");
+ }
+}
+
+/**
+ * Host object for the in-browser right sidebar
+ */
+class RightHost extends SidebarHost {
+ constructor(hostTab) {
+ super(hostTab, "right");
+ }
+}
+
+/**
+ * Host object for the toolbox in a separate window
+ */
+function WindowHost(hostTab) {
+ this._boundUnload = this._boundUnload.bind(this);
+ this.hostTab = hostTab;
+ EventEmitter.decorate(this);
+}
+
+WindowHost.prototype = {
+ type: "window",
+
+ WINDOW_URL: "chrome://devtools/content/framework/toolbox-window.xhtml",
+
+ /**
+ * Create a new xul window to contain the toolbox.
+ */
+ create: function() {
+ return new Promise(resolve => {
+ let flags = "chrome,centerscreen,resizable,dialog=no";
+
+ // If we are debugging a tab which is in a Private window, we must also
+ // set the private flag on the DevTools host window. Otherwise switching
+ // hosts between docked and window modes can fail due to incompatible
+ // docshell origin attributes. See 1581093.
+ if (
+ this.hostTab &&
+ PrivateBrowsingUtils.isWindowPrivate(this.hostTab.ownerGlobal)
+ ) {
+ flags += ",private";
+ }
+
+ // If the current window is a non-fission window, force the non-fission
+ // flag. Otherwise switching to window host from a non-fission window in
+ // a fission Firefox (!) will attempt to swapFrameLoaders between fission
+ // and non-fission frames. See Bug 1650963.
+ if (this.hostTab && !this.hostTab.ownerGlobal.gFissionBrowser) {
+ flags += ",non-fission";
+ }
+
+ const win = Services.ww.openWindow(
+ null,
+ this.WINDOW_URL,
+ "_blank",
+ flags,
+ null
+ );
+
+ const frameLoad = () => {
+ win.removeEventListener("load", frameLoad, true);
+ win.focus();
+
+ this.frame = createDevToolsFrame(
+ win.document,
+ "devtools-toolbox-window-iframe"
+ );
+ win.document
+ .getElementById("devtools-toolbox-window")
+ .appendChild(this.frame);
+
+ // The forceOwnRefreshDriver attribute is set to avoid Windows only issues with
+ // CSS transitions when switching from docked to window hosts.
+ // Added in Bug 832920, should be reviewed in Bug 1542468.
+ this.frame.setAttribute("forceOwnRefreshDriver", "");
+ resolve(this.frame);
+ };
+
+ win.addEventListener("load", frameLoad, true);
+ win.addEventListener("unload", this._boundUnload);
+
+ this._window = win;
+ });
+ },
+
+ /**
+ * Catch the user closing the window.
+ */
+ _boundUnload: function(event) {
+ if (event.target.location != this.WINDOW_URL) {
+ return;
+ }
+ this._window.removeEventListener("unload", this._boundUnload);
+
+ this.emit("window-closed");
+ },
+
+ /**
+ * Raise the host.
+ */
+ raise: function() {
+ this._window.focus();
+ },
+
+ /**
+ * Set the toolbox title.
+ */
+ setTitle: function(title) {
+ this._window.document.title = title;
+ },
+
+ /**
+ * Destroy the window.
+ */
+ destroy: function() {
+ if (!this._destroyed) {
+ this._destroyed = true;
+
+ this._window.removeEventListener("unload", this._boundUnload);
+ this._window.close();
+ }
+
+ return promise.resolve(null);
+ },
+};
+
+/**
+ * Host object for the Browser Toolbox
+ */
+function BrowserToolboxHost(hostTab, options) {
+ this.doc = options.doc;
+ EventEmitter.decorate(this);
+}
+
+BrowserToolboxHost.prototype = {
+ type: "browsertoolbox",
+
+ create: async function() {
+ this.frame = createDevToolsFrame(
+ this.doc,
+ "devtools-toolbox-browsertoolbox-iframe"
+ );
+
+ this.doc.body.appendChild(this.frame);
+
+ return this.frame;
+ },
+
+ /**
+ * Raise the host.
+ */
+ raise: function() {
+ this.doc.defaultView.focus();
+ },
+
+ /**
+ * Set the toolbox title.
+ */
+ setTitle: function(title) {
+ this.doc.title = title;
+ },
+
+ // Do nothing. The BrowserToolbox is destroyed by quitting the application.
+ destroy: function() {
+ return promise.resolve(null);
+ },
+};
+
+/**
+ * Host object for the toolbox as a page.
+ * This is typically used by `about:debugging`, when opening toolbox in a new tab,
+ * via `about:devtools-toolbox` URLs.
+ * The `iframe` ends up being the tab's browser element.
+ */
+function PageHost(hostTab, options) {
+ this.frame = options.customIframe;
+}
+
+PageHost.prototype = {
+ type: "page",
+
+ create: function() {
+ return promise.resolve(this.frame);
+ },
+
+ // Do nothing.
+ raise: function() {},
+
+ // Do nothing.
+ setTitle: function(title) {},
+
+ // Do nothing.
+ destroy: function() {
+ return promise.resolve(null);
+ },
+};
+
+/**
+ * Switch to the given tab in a browser and focus the browser window
+ */
+function focusTab(tab) {
+ const browserWindow = tab.ownerDocument.defaultView;
+ browserWindow.focus();
+ browserWindow.gBrowser.selectedTab = tab;
+}
+
+/**
+ * Create an iframe that can be used to load DevTools via about:devtools-toolbox.
+ */
+function createDevToolsFrame(doc, className) {
+ const frame = doc.createXULElement("browser");
+ frame.setAttribute("type", "content");
+ frame.flex = 1; // Required to be able to shrink when the window shrinks
+ frame.className = className;
+
+ const inXULDocument = doc.documentElement.namespaceURI === XUL_NS;
+ if (inXULDocument) {
+ // When the toolbox frame is loaded in a XUL document, tooltips rely on a
+ // special XUL <tooltip id="aHTMLTooltip"> element.
+ // This attribute should not be set when the frame is loaded in a HTML
+ // document (for instance: Browser Toolbox).
+ frame.tooltip = "aHTMLTooltip";
+ }
+ return frame;
+}
+
+exports.Hosts = {
+ bottom: BottomHost,
+ left: LeftHost,
+ right: RightHost,
+ window: WindowHost,
+ browsertoolbox: BrowserToolboxHost,
+ page: PageHost,
+};
diff --git a/devtools/client/framework/toolbox-init.js b/devtools/client/framework/toolbox-init.js
new file mode 100644
index 0000000000..b1dabcdfd7
--- /dev/null
+++ b/devtools/client/framework/toolbox-init.js
@@ -0,0 +1,165 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env browser */
+/* global XPCNativeWrapper */
+
+"use strict";
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+
+// URL constructor doesn't support about: scheme
+const href = window.location.href.replace("about:", "http://");
+const url = new window.URL(href);
+
+// `host` is the frame element loading the toolbox.
+let host = window.browsingContext.embedderElement;
+
+// If there's no containerElement (which happens when loading about:devtools-toolbox as
+// a top level document), use the current window.
+if (!host) {
+ host = {
+ contentWindow: window,
+ contentDocument: document,
+ // toolbox-host-manager.js wants to set attributes on the frame that contains it,
+ // but that is fine to skip and doesn't make sense when using the current window.
+ setAttribute() {},
+ ownerDocument: document,
+ // toolbox-host-manager.js wants to listen for unload events from outside the frame,
+ // but this is fine to skip since the toolbox code listens inside the frame as well,
+ // and there is no outer document in this case.
+ addEventListener() {},
+ };
+}
+
+const onLoad = new Promise(r => {
+ host.contentWindow.addEventListener("DOMContentLoaded", r, { once: true });
+});
+
+async function showErrorPage(doc, errorMessage) {
+ const win = doc.defaultView;
+ const { BrowserLoader } = ChromeUtils.import(
+ "resource://devtools/client/shared/browser-loader.js"
+ );
+ const browserRequire = BrowserLoader({
+ window: win,
+ useOnlyShared: true,
+ }).require;
+
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const DebugTargetErrorPage = React.createFactory(
+ require("devtools/client/framework/components/DebugTargetErrorPage")
+ );
+ const { LocalizationHelper } = browserRequire("devtools/shared/l10n");
+ const L10N = new LocalizationHelper(
+ "devtools/client/locales/toolbox.properties"
+ );
+
+ // mount the React component into our XUL container once the DOM is ready
+ await onLoad;
+
+ // Update the tab title.
+ document.title = L10N.getStr("toolbox.debugTargetInfo.tabTitleError");
+
+ const mountEl = doc.querySelector("#toolbox-error-mount");
+ const element = DebugTargetErrorPage({
+ errorMessage,
+ L10N,
+ });
+ ReactDOM.render(element, mountEl);
+
+ // make sure we unmount the component when the page is destroyed
+ win.addEventListener(
+ "unload",
+ () => {
+ ReactDOM.unmountComponentAtNode(mountEl);
+ },
+ { once: true }
+ );
+}
+
+async function initToolbox(url, host) {
+ const { gDevTools } = require("devtools/client/framework/devtools");
+ const {
+ targetFromURL,
+ } = require("devtools/client/framework/target-from-url");
+ const { Toolbox } = require("devtools/client/framework/toolbox");
+ const { DevToolsServer } = require("devtools/server/devtools-server");
+ const { DevToolsClient } = require("devtools/client/devtools-client");
+
+ // Specify the default tool to open
+ const tool = url.searchParams.get("tool");
+
+ try {
+ let target;
+ if (url.searchParams.has("target")) {
+ // Attach toolbox to a given browser iframe (<xul:browser> or <html:iframe
+ // mozbrowser>) whose reference is set on the host iframe.
+ // Note that so far, this is no real usage of it. It is only used by a test.
+
+ // `iframe` is the targeted document to debug
+ let iframe = host.wrappedJSObject
+ ? host.wrappedJSObject.target
+ : host.target;
+ if (!iframe) {
+ throw new Error("Unable to find the targeted iframe to debug");
+ }
+
+ // Need to use a xray to have attributes and behavior expected by
+ // devtools codebase
+ iframe = XPCNativeWrapper(iframe);
+
+ // Fake a xul:tab object as we don't have one.
+ // linkedBrowser is the only one attribute being queried by client.getTab
+ const tab = { linkedBrowser: iframe };
+
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+
+ await client.connect();
+ // Creates a target for a given browser iframe.
+ const tabDescriptor = await client.mainRoot.getTab({ tab });
+ target = await tabDescriptor.getTarget();
+ // Instruct the Target to automatically close the client on destruction.
+ target.shouldCloseClient = true;
+ } else {
+ target = await targetFromURL(url);
+ const toolbox = gDevTools.getToolbox(target);
+ if (toolbox && toolbox.isDestroying()) {
+ // If a toolbox already exists for the target, wait for current toolbox destroy to
+ // be finished and retrieve a new valid target. The ongoing toolbox destroy will
+ // destroy the target, so it can not be reused.
+ await toolbox.destroy();
+ target = await targetFromURL(url);
+ }
+ }
+
+ // Display an error page if we are connected to a remote target and we lose it
+ const onTargetDestroyed = function() {
+ target.off("close", onTargetDestroyed);
+ // Prevent trying to display the error page if the toolbox tab is being destroyed
+ if (host.contentDocument) {
+ const error = new Error("Debug target was disconnected");
+ showErrorPage(host.contentDocument, `${error}`);
+ }
+ };
+ target.on("close", onTargetDestroyed);
+
+ const options = { customIframe: host };
+ await gDevTools.showToolbox(target, tool, Toolbox.HostType.PAGE, options);
+ } catch (error) {
+ // When an error occurs, show error page with message.
+ console.error("Exception while loading the toolbox", error);
+ showErrorPage(host.contentDocument, `${error}`);
+ }
+}
+
+// Only use this method to attach the toolbox if some query parameters are given
+if (url.search.length > 1) {
+ initToolbox(url, host);
+}
+// TODO: handle no params in about:devtool-toolbox
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1526996
diff --git a/devtools/client/framework/toolbox-options.html b/devtools/client/framework/toolbox-options.html
new file mode 100644
index 0000000000..1ccabd6916
--- /dev/null
+++ b/devtools/client/framework/toolbox-options.html
@@ -0,0 +1,180 @@
+<!-- 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/. -->
+<!DOCTYPE html>
+<html dir="">
+ <head>
+ <title>Toolbox option</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="stylesheet" href="chrome://devtools/content/framework/options-panel.css">
+ <script src="chrome://devtools/content/shared/theme-switching.js"></script>
+ <link rel="localization" href="devtools/client/toolbox-options.ftl"/>
+ </head>
+ <body role="application" class="theme-body">
+ <form id="options-panel">
+ <div id="tools-box" class="options-vertical-pane">
+ <fieldset id="default-tools-box" class="options-groupbox">
+ <legend data-l10n-id="options-select-default-tools-label"></legend>
+ <span id="tools-not-supported-label" class="options-citation-label theme-comment" data-l10n-id="options-tool-not-supported-label"></span>
+ </fieldset>
+
+ <fieldset id="additional-tools-box" class="options-groupbox">
+ <legend data-l10n-id="options-select-additional-tools-label"></legend>
+ </fieldset>
+
+ <fieldset id="enabled-toolbox-buttons-box" class="options-groupbox">
+ <legend data-l10n-id="options-select-enabled-toolbox-buttons-label"></legend>
+ </fieldset>
+ </div>
+
+ <div class="options-vertical-pane">
+ <fieldset id="devtools-theme-box"
+ class="options-groupbox
+ horizontal-options-groupbox
+ radiogroup"
+ data-pref="devtools.theme">
+ <legend data-l10n-id="options-select-dev-tools-theme-label"></legend>
+ </fieldset>
+
+ <fieldset id="inspector-options" class="options-groupbox">
+ <legend data-l10n-id="options-context-inspector"></legend>
+ <label data-l10n-id="options-show-user-agent-styles-tooltip">
+ <input type="checkbox"
+ data-pref="devtools.inspector.showUserAgentStyles"/>
+ <span data-l10n-id="options-show-user-agent-styles-label"></span>
+ </label>
+ <label data-l10n-id="options-collapse-attrs-tooltip">
+ <input type="checkbox"
+ data-pref="devtools.markup.collapseAttributes"/>
+ <span data-l10n-id="options-collapse-attrs-label"></span>
+ </label>
+ <label>
+ <span data-l10n-id="options-default-color-unit-label"></span>
+ <select id="defaultColorUnitMenuList"
+ data-pref="devtools.defaultColorUnit">
+ <option value="authored" data-l10n-id="options-default-color-unit-authored"></option>
+ <option value="hex" data-l10n-id="options-default-color-unit-hex"></option>
+ <option value="hsl" data-l10n-id="options-default-color-unit-hsl"></option>
+ <option value="rgb" data-l10n-id="options-default-color-unit-rgb"></option>
+ <option value="name" data-l10n-id="options-default-color-unit-name"></option>
+ </select>
+ </label>
+ </fieldset>
+
+ <fieldset id="styleeditor-options" class="options-groupbox">
+ <legend data-l10n-id="options-styleeditor-label"></legend>
+ <label data-l10n-id="options-stylesheet-autocompletion-tooltip">
+ <input type="checkbox"
+ data-pref="devtools.styleeditor.autocompletion-enabled"/>
+ <span data-l10n-id="options-stylesheet-autocompletion-label"></span>
+ </label>
+ </fieldset>
+
+ <fieldset id="screenshot-options" class="options-groupbox">
+ <legend data-l10n-id="options-screenshot-label"></legend>
+ <label data-l10n-id="options-screenshot-clipboard-tooltip">
+ <input type="checkbox"
+ id="devtools-screenshot-clipboard"
+ data-pref="devtools.screenshot.clipboard.enabled"/>
+ <span data-l10n-id="options-screenshot-clipboard-label"></span>
+ </label>
+ <label data-l10n-id="options-screenshot-audio-tooltip">
+ <input type="checkbox"
+ id="devtools-screenshot-audio"
+ data-pref="devtools.screenshot.audio.enabled"/>
+ <span data-l10n-id="options-screenshot-audio-label"></span>
+ </label>
+ </fieldset>
+ </div>
+
+ <div class="options-vertical-pane">
+ <fieldset id="sourceeditor-options" class="options-groupbox">
+ <legend data-l10n-id="options-sourceeditor-label"></legend>
+ <label data-l10n-id="options-sourceeditor-detectindentation-tooltip">
+ <input type="checkbox"
+ id="devtools-sourceeditor-detectindentation"
+ data-pref="devtools.editor.detectindentation"/>
+ <span data-l10n-id="options-sourceeditor-detectindentation-label"></span>
+ </label>
+ <label data-l10n-id="options-sourceeditor-autoclosebrackets-tooltip">
+ <input type="checkbox"
+ id="devtools-sourceeditor-autoclosebrackets"
+ data-pref="devtools.editor.autoclosebrackets"/>
+ <span data-l10n-id="options-sourceeditor-autoclosebrackets-label"></span>
+ </label>
+ <label data-l10n-id="options-sourceeditor-expandtab-tooltip">
+ <input type="checkbox"
+ id="devtools-sourceeditor-expandtab"
+ data-pref="devtools.editor.expandtab"/>
+ <span data-l10n-id="options-sourceeditor-expandtab-label"></span>
+ </label>
+ <label>
+ <span data-l10n-id="options-sourceeditor-tabsize-label"></span>
+ <select id="devtools-sourceeditor-tabsize-select"
+ data-pref="devtools.editor.tabsize">
+ <option label="2">2</option>
+ <option label="4">4</option>
+ <option label="8">8</option>
+ </select>
+ </label>
+ <label>
+ <span data-l10n-id="options-sourceeditor-keybinding-label"></span>
+ <select id="devtools-sourceeditor-keybinding-select"
+ data-pref="devtools.editor.keymap">
+ <option value="default" data-l10n-id="options-sourceeditor-keybinding-default-label"></option>
+ <option value="vim">Vim</option>
+ <option value="emacs">Emacs</option>
+ <option value="sublime">Sublime Text</option>
+ </select>
+ </label>
+ </fieldset>
+
+ <fieldset id="context-options" class="options-groupbox">
+ <legend data-l10n-id="options-context-advanced-settings"></legend>
+ <label data-l10n-id="options-source-maps-tooltip">
+ <input type="checkbox"
+ data-pref="devtools.source-map.client-service.enabled"/>
+ <span data-l10n-id="options-source-maps-label"></span>
+ </label>
+ <label data-l10n-id="options-show-platform-data-tooltip">
+ <input type="checkbox"
+ id="devtools-show-gecko-data"
+ data-pref="devtools.performance.ui.show-platform-data"/>
+ <span data-l10n-id="options-show-platform-data-label"></span>
+ </label>
+ <label data-l10n-id="options-disable-http-cache-tooltip">
+ <input type="checkbox"
+ id="devtools-disable-cache"
+ data-pref="devtools.cache.disabled"/>
+ <span data-l10n-id="options-disable-http-cache-label"></span>
+ </label>
+ <label data-l10n-id="options-disable-javascript-tooltip">
+ <input type="checkbox"
+ id="devtools-disable-javascript"/>
+ <span data-l10n-id="options-disable-javascript-label"></span>
+ </label>
+ <label data-l10n-id="options-enable-service-workers-http-tooltip">
+ <input type="checkbox"
+ id="devtools-enable-serviceWorkersTesting"
+ data-pref="devtools.serviceWorkers.testing.enabled"/>
+ <span data-l10n-id="options-enable-service-workers-http-label"></span>
+ </label>
+ <label data-l10n-id="options-enable-chrome-tooltip">
+ <input type="checkbox"
+ data-pref="devtools.chrome.enabled"/>
+ <span data-l10n-id="options-enable-chrome-label"></span>
+ </label>
+ <label data-l10n-id="options-enable-remote-tooltip2">
+ <input type="checkbox"
+ data-pref="devtools.debugger.remote-enabled"/>
+ <span data-l10n-id="options-enable-remote-label"></span>
+ </label>
+ <span class="options-citation-label theme-comment"
+ id="triggers-page-refresh-label" data-l10n-id="options-context-triggers-page-refresh"
+ ></span>
+ </fieldset>
+ </div>
+
+ </form>
+ </body>
+</html>
diff --git a/devtools/client/framework/toolbox-options.js b/devtools/client/framework/toolbox-options.js
new file mode 100644
index 0000000000..73b4f7e4bd
--- /dev/null
+++ b/devtools/client/framework/toolbox-options.js
@@ -0,0 +1,640 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Services = require("Services");
+const { gDevTools } = require("devtools/client/framework/devtools");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/toolbox.properties"
+);
+
+loader.lazyRequireGetter(
+ this,
+ "AppConstants",
+ "resource://gre/modules/AppConstants.jsm",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "openDocLink",
+ "devtools/client/shared/link",
+ true
+);
+
+exports.OptionsPanel = OptionsPanel;
+
+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");
+ }
+}
+
+function SetPref(name, value) {
+ const type = Services.prefs.getPrefType(name);
+ switch (type) {
+ case Services.prefs.PREF_STRING:
+ return Services.prefs.setCharPref(name, value);
+ case Services.prefs.PREF_INT:
+ return Services.prefs.setIntPref(name, value);
+ case Services.prefs.PREF_BOOL:
+ return Services.prefs.setBoolPref(name, value);
+ default:
+ throw new Error("Unknown type");
+ }
+}
+
+function InfallibleGetBoolPref(key) {
+ try {
+ return Services.prefs.getBoolPref(key);
+ } catch (ex) {
+ return true;
+ }
+}
+
+/**
+ * Represents the Options Panel in the Toolbox.
+ */
+function OptionsPanel(iframeWindow, toolbox) {
+ this.panelDoc = iframeWindow.document;
+ this.panelWin = iframeWindow;
+
+ this.toolbox = toolbox;
+ this.telemetry = toolbox.telemetry;
+ this.isReady = false;
+
+ this.setupToolsList = this.setupToolsList.bind(this);
+ this._prefChanged = this._prefChanged.bind(this);
+ this._themeRegistered = this._themeRegistered.bind(this);
+ this._themeUnregistered = this._themeUnregistered.bind(this);
+ this._disableJSClicked = this._disableJSClicked.bind(this);
+
+ this.disableJSNode = this.panelDoc.getElementById(
+ "devtools-disable-javascript"
+ );
+
+ this._addListeners();
+
+ const EventEmitter = require("devtools/shared/event-emitter");
+ EventEmitter.decorate(this);
+}
+
+OptionsPanel.prototype = {
+ get target() {
+ return this.toolbox.target;
+ },
+
+ async open() {
+ this.setupToolsList();
+ this.setupToolbarButtonsList();
+ this.setupThemeList();
+ this.setupAdditionalOptions();
+ await this.populatePreferences();
+ this.isReady = true;
+ this.emit("ready");
+ return this;
+ },
+
+ _addListeners: function() {
+ Services.prefs.addObserver("devtools.cache.disabled", this._prefChanged);
+ Services.prefs.addObserver("devtools.theme", this._prefChanged);
+ Services.prefs.addObserver(
+ "devtools.source-map.client-service.enabled",
+ this._prefChanged
+ );
+ gDevTools.on("theme-registered", this._themeRegistered);
+ gDevTools.on("theme-unregistered", this._themeUnregistered);
+
+ // Refresh the tools list when a new tool or webextension has been
+ // registered to the toolbox.
+ this.toolbox.on("tool-registered", this.setupToolsList);
+ this.toolbox.on("webextension-registered", this.setupToolsList);
+ // Refresh the tools list when a new tool or webextension has been
+ // unregistered from the toolbox.
+ this.toolbox.on("tool-unregistered", this.setupToolsList);
+ this.toolbox.on("webextension-unregistered", this.setupToolsList);
+ },
+
+ _removeListeners: function() {
+ Services.prefs.removeObserver("devtools.cache.disabled", this._prefChanged);
+ Services.prefs.removeObserver("devtools.theme", this._prefChanged);
+ Services.prefs.removeObserver(
+ "devtools.source-map.client-service.enabled",
+ this._prefChanged
+ );
+
+ this.toolbox.off("tool-registered", this.setupToolsList);
+ this.toolbox.off("tool-unregistered", this.setupToolsList);
+ this.toolbox.off("webextension-registered", this.setupToolsList);
+ this.toolbox.off("webextension-unregistered", this.setupToolsList);
+
+ gDevTools.off("theme-registered", this._themeRegistered);
+ gDevTools.off("theme-unregistered", this._themeUnregistered);
+ },
+
+ _prefChanged: function(subject, topic, prefName) {
+ if (prefName === "devtools.cache.disabled") {
+ const cacheDisabled = GetPref(prefName);
+ const cbx = this.panelDoc.getElementById("devtools-disable-cache");
+ cbx.checked = cacheDisabled;
+ } else if (prefName === "devtools.theme") {
+ this.updateCurrentTheme();
+ } else if (prefName === "devtools.source-map.client-service.enabled") {
+ this.updateSourceMapPref();
+ }
+ },
+
+ _themeRegistered: function(themeId) {
+ this.setupThemeList();
+ },
+
+ _themeUnregistered: function(theme) {
+ const themeBox = this.panelDoc.getElementById("devtools-theme-box");
+ const themeInput = themeBox.querySelector(`[value=${theme.id}]`);
+
+ if (themeInput) {
+ themeInput.parentNode.remove();
+ }
+ },
+
+ async setupToolbarButtonsList() {
+ // Ensure the toolbox is open, and the buttons are all set up.
+ await this.toolbox.isOpen;
+
+ const enabledToolbarButtonsBox = this.panelDoc.getElementById(
+ "enabled-toolbox-buttons-box"
+ );
+
+ const toolbarButtons = this.toolbox.toolbarButtons;
+
+ if (!toolbarButtons) {
+ console.warn("The command buttons weren't initiated yet.");
+ return;
+ }
+
+ const onCheckboxClick = checkbox => {
+ const commandButton = toolbarButtons.filter(
+ toggleableButton => toggleableButton.id === checkbox.id
+ )[0];
+
+ Services.prefs.setBoolPref(
+ commandButton.visibilityswitch,
+ checkbox.checked
+ );
+ this.toolbox.updateToolboxButtonsVisibility();
+ };
+
+ const createCommandCheckbox = button => {
+ const checkboxLabel = this.panelDoc.createElement("label");
+ const checkboxSpanLabel = this.panelDoc.createElement("span");
+ checkboxSpanLabel.textContent = button.description;
+ const checkboxInput = this.panelDoc.createElement("input");
+ checkboxInput.setAttribute("type", "checkbox");
+ checkboxInput.setAttribute("id", button.id);
+
+ if (Services.prefs.getBoolPref(button.visibilityswitch, true)) {
+ checkboxInput.setAttribute("checked", true);
+ }
+ checkboxInput.addEventListener(
+ "change",
+ onCheckboxClick.bind(this, checkboxInput)
+ );
+
+ checkboxLabel.appendChild(checkboxInput);
+ checkboxLabel.appendChild(checkboxSpanLabel);
+
+ return checkboxLabel;
+ };
+
+ for (const button of toolbarButtons) {
+ if (!button.isTargetSupported(this.toolbox.target)) {
+ continue;
+ }
+
+ enabledToolbarButtonsBox.appendChild(createCommandCheckbox(button));
+ }
+ },
+
+ setupToolsList: function() {
+ const defaultToolsBox = this.panelDoc.getElementById("default-tools-box");
+ const additionalToolsBox = this.panelDoc.getElementById(
+ "additional-tools-box"
+ );
+ const toolsNotSupportedLabel = this.panelDoc.getElementById(
+ "tools-not-supported-label"
+ );
+ let atleastOneToolNotSupported = false;
+
+ // Signal tool registering/unregistering globally (for the tools registered
+ // globally) and per toolbox (for the tools registered to a single toolbox).
+ // This event handler expect this to be binded to the related checkbox element.
+ const onCheckboxClick = function(telemetry, tool) {
+ // Set the kill switch pref boolean to true
+ Services.prefs.setBoolPref(tool.visibilityswitch, this.checked);
+
+ if (!tool.isWebExtension) {
+ gDevTools.emit(
+ this.checked ? "tool-registered" : "tool-unregistered",
+ tool.id
+ );
+ // Record which tools were registered and unregistered.
+ telemetry.keyedScalarSet(
+ "devtools.tool.registered",
+ tool.id,
+ this.checked
+ );
+ }
+ };
+
+ const createToolCheckbox = tool => {
+ const checkboxLabel = this.panelDoc.createElement("label");
+ const checkboxInput = this.panelDoc.createElement("input");
+ checkboxInput.setAttribute("type", "checkbox");
+ checkboxInput.setAttribute("id", tool.id);
+ checkboxInput.setAttribute("title", tool.tooltip || "");
+
+ const checkboxSpanLabel = this.panelDoc.createElement("span");
+ if (tool.isTargetSupported(this.target)) {
+ checkboxSpanLabel.textContent = tool.label;
+ } else {
+ atleastOneToolNotSupported = true;
+ checkboxSpanLabel.textContent = L10N.getFormatStr(
+ "options.toolNotSupportedMarker",
+ tool.label
+ );
+ checkboxInput.setAttribute("data-unsupported", "true");
+ checkboxInput.setAttribute("disabled", "true");
+ }
+
+ if (InfallibleGetBoolPref(tool.visibilityswitch)) {
+ checkboxInput.setAttribute("checked", "true");
+ }
+
+ checkboxInput.addEventListener(
+ "change",
+ onCheckboxClick.bind(checkboxInput, this.telemetry, tool)
+ );
+
+ checkboxLabel.appendChild(checkboxInput);
+ checkboxLabel.appendChild(checkboxSpanLabel);
+
+ // We shouldn't have deprecated tools anymore, but we might have one in the future,
+ // when migrating the storage inspector to the application panel (Bug 1681059).
+ // Let's keep this code for now so we keep the l10n property around and avoid
+ // unnecessary translation work if we need it again in the future.
+ if (tool.deprecated) {
+ const deprecationURL = this.panelDoc.createElement("a");
+ deprecationURL.title = deprecationURL.href = tool.deprecationURL;
+ deprecationURL.textContent = L10N.getStr("options.deprecationNotice");
+ // Cannot use a real link when we are in the Browser Toolbox.
+ deprecationURL.addEventListener("click", e => {
+ e.preventDefault();
+ openDocLink(tool.deprecationURL, { relatedToCurrent: true });
+ });
+
+ const checkboxSpanDeprecated = this.panelDoc.createElement("span");
+ checkboxSpanDeprecated.className = "deprecation-notice";
+ checkboxLabel.appendChild(checkboxSpanDeprecated);
+ checkboxSpanDeprecated.appendChild(deprecationURL);
+ }
+
+ return checkboxLabel;
+ };
+
+ // Clean up any existent default tools content.
+ for (const label of defaultToolsBox.querySelectorAll("label")) {
+ label.remove();
+ }
+
+ // Populating the default tools lists
+ const toggleableTools = gDevTools.getDefaultTools().filter(tool => {
+ return tool.visibilityswitch && !tool.hiddenInOptions;
+ });
+
+ const fragment = this.panelDoc.createDocumentFragment();
+ for (const tool of toggleableTools) {
+ fragment.appendChild(createToolCheckbox(tool));
+ }
+
+ const toolsNotSupportedLabelNode = this.panelDoc.getElementById(
+ "tools-not-supported-label"
+ );
+ defaultToolsBox.insertBefore(fragment, toolsNotSupportedLabelNode);
+
+ // Clean up any existent additional tools content.
+ for (const label of additionalToolsBox.querySelectorAll("label")) {
+ label.remove();
+ }
+
+ // Populating the additional tools list.
+ let atleastOneAddon = false;
+ for (const tool of gDevTools.getAdditionalTools()) {
+ atleastOneAddon = true;
+ additionalToolsBox.appendChild(createToolCheckbox(tool));
+ }
+
+ // Populating the additional tools that came from the installed WebExtension add-ons.
+ for (const { uuid, name, pref } of this.toolbox.listWebExtensions()) {
+ atleastOneAddon = true;
+
+ additionalToolsBox.appendChild(
+ createToolCheckbox({
+ isWebExtension: true,
+
+ // Use the preference as the unified webextensions tool id.
+ id: `webext-${uuid}`,
+ tooltip: name,
+ label: name,
+ // Disable the devtools extension using the given pref name:
+ // the toolbox options for the WebExtensions are not related to a single
+ // tool (e.g. a devtools panel created from the extension devtools_page)
+ // but to the entire devtools part of a webextension which is enabled
+ // by the Addon Manager (but it may be disabled by its related
+ // devtools about:config preference), and so the following
+ visibilityswitch: pref,
+
+ // Only local tabs are currently supported as targets.
+ isTargetSupported: target => target.isLocalTab,
+ })
+ );
+ }
+
+ if (!atleastOneAddon) {
+ additionalToolsBox.style.display = "none";
+ } else {
+ additionalToolsBox.style.display = "";
+ }
+
+ if (!atleastOneToolNotSupported) {
+ toolsNotSupportedLabel.style.display = "none";
+ } else {
+ toolsNotSupportedLabel.style.display = "";
+ }
+
+ this.panelWin.focus();
+ },
+
+ setupThemeList: function() {
+ const themeBox = this.panelDoc.getElementById("devtools-theme-box");
+ const themeLabels = themeBox.querySelectorAll("label");
+ for (const label of themeLabels) {
+ label.remove();
+ }
+
+ const createThemeOption = theme => {
+ const inputLabel = this.panelDoc.createElement("label");
+ const inputRadio = this.panelDoc.createElement("input");
+ inputRadio.setAttribute("type", "radio");
+ inputRadio.setAttribute("value", theme.id);
+ inputRadio.setAttribute("name", "devtools-theme-item");
+ inputRadio.addEventListener("change", function(e) {
+ SetPref(themeBox.getAttribute("data-pref"), e.target.value);
+ });
+
+ const inputSpanLabel = this.panelDoc.createElement("span");
+ inputSpanLabel.textContent = theme.label;
+ inputLabel.appendChild(inputRadio);
+ inputLabel.appendChild(inputSpanLabel);
+
+ return inputLabel;
+ };
+
+ // Populating the default theme list
+ const themes = gDevTools.getThemeDefinitionArray();
+ for (const theme of themes) {
+ themeBox.appendChild(createThemeOption(theme));
+ }
+
+ this.updateCurrentTheme();
+ },
+
+ /**
+ * Add extra checkbox options bound to a boolean preference.
+ */
+ setupAdditionalOptions: function() {
+ const prefDefinitions = [];
+
+ const isNightly = AppConstants.NIGHTLY_BUILD;
+ if (isNightly) {
+ // Labels are hardcoded in english because this checkbox is Nightly only.
+ prefDefinitions.push({
+ pref: "devtools.performance.new-panel-enabled",
+ label: "Enable new performance recorder (then re-open DevTools)",
+ id: "devtools-new-performance",
+ parentId: "context-options",
+ });
+ }
+
+ if (this.target.isParentProcess) {
+ // The Multiprocess Browser Toolbox is only displayed in the settings
+ // panel for the Browser Toolbox, or when debugging the main process in
+ // remote debugging.
+ prefDefinitions.push({
+ pref: "devtools.browsertoolbox.fission",
+ label: L10N.getStr("options.enableMultiProcessToolbox"),
+ id: "devtools-browsertoolbox-fission",
+ parentId: "context-options",
+ // createPreferenceOption already updates the value of the preference
+ // for the current profile when the checkbox changes. Here we need a
+ // custom behavior for the Browser Toolbox, so we pass an additional
+ // onChange callback.
+ onChange: async checked => {
+ if (!this.toolbox.isBrowserToolbox()) {
+ // If we are debugging a parent process, but the toolbox is not a
+ // Browser Toolbox, it means we are remote debugging another
+ // browser. In this case, the value of devtools.browsertoolbox.fission
+ // should not be updated in the target browser.
+ return;
+ }
+
+ // When setting this preference from the BrowserToolbox, we need to
+ // update the preference on the debugged Firefox profile as well.
+ // The devtools.browsertoolbox.fission preference is copied from the
+ // regular Firefox Profile to the Browser Toolbox profile.
+ // If the preference is not updated on the regular Firefox profile, the
+ // new value will be lost on the next Browser Toolbox restart.
+ const { mainRoot } = this.target.client;
+ const preferenceFront = await mainRoot.getFront("preference");
+ preferenceFront.setBoolPref(
+ "devtools.browsertoolbox.fission",
+ checked
+ );
+ },
+ });
+ }
+
+ const createPreferenceOption = ({ pref, label, id, onChange }) => {
+ const inputLabel = this.panelDoc.createElement("label");
+ const checkbox = this.panelDoc.createElement("input");
+ checkbox.setAttribute("type", "checkbox");
+ if (GetPref(pref)) {
+ checkbox.setAttribute("checked", "checked");
+ }
+ checkbox.setAttribute("id", id);
+ checkbox.addEventListener("change", e => {
+ SetPref(pref, e.target.checked);
+ if (onChange) {
+ onChange(e.target.checked);
+ }
+ });
+
+ const inputSpanLabel = this.panelDoc.createElement("span");
+ inputSpanLabel.textContent = label;
+ inputLabel.appendChild(checkbox);
+ inputLabel.appendChild(inputSpanLabel);
+
+ return inputLabel;
+ };
+
+ for (const prefDefinition of prefDefinitions) {
+ const parent = this.panelDoc.getElementById(prefDefinition.parentId);
+ // We want to insert the new definition after the last existing
+ // definition, but before any other element.
+ // For example in the "Advanced Settings" column there's indeed a <span>
+ // text at the end, and we want that it stays at the end.
+ // The reference element can be `null` if there's no label or if there's
+ // no element after the last label. But that's OK and it will do what we
+ // want.
+ const referenceElement = parent.querySelector("label:last-of-type + *");
+ parent.insertBefore(
+ createPreferenceOption(prefDefinition),
+ referenceElement
+ );
+ }
+ },
+
+ async populatePreferences() {
+ const prefCheckboxes = this.panelDoc.querySelectorAll(
+ "input[type=checkbox][data-pref]"
+ );
+ for (const prefCheckbox of prefCheckboxes) {
+ if (GetPref(prefCheckbox.getAttribute("data-pref"))) {
+ prefCheckbox.setAttribute("checked", true);
+ }
+ prefCheckbox.addEventListener("change", function(e) {
+ const checkbox = e.target;
+ SetPref(checkbox.getAttribute("data-pref"), checkbox.checked);
+ });
+ }
+ // Themes radio inputs are handled in setupThemeList
+ const prefRadiogroups = this.panelDoc.querySelectorAll(
+ ".radiogroup[data-pref]:not(#devtools-theme-box)"
+ );
+ for (const radioGroup of prefRadiogroups) {
+ const selectedValue = GetPref(radioGroup.getAttribute("data-pref"));
+
+ for (const radioInput of radioGroup.querySelectorAll(
+ "input[type=radio]"
+ )) {
+ if (radioInput.getAttribute("value") == selectedValue) {
+ radioInput.setAttribute("checked", true);
+ }
+
+ radioInput.addEventListener("change", function(e) {
+ SetPref(radioGroup.getAttribute("data-pref"), e.target.value);
+ });
+ }
+ }
+ const prefSelects = this.panelDoc.querySelectorAll("select[data-pref]");
+ for (const prefSelect of prefSelects) {
+ const pref = GetPref(prefSelect.getAttribute("data-pref"));
+ const options = [...prefSelect.options];
+ options.some(function(option) {
+ const value = option.value;
+ // non strict check to allow int values.
+ if (value == pref) {
+ prefSelect.selectedIndex = options.indexOf(option);
+ return true;
+ }
+ return false;
+ });
+
+ prefSelect.addEventListener("change", function(e) {
+ const select = e.target;
+ SetPref(
+ select.getAttribute("data-pref"),
+ select.options[select.selectedIndex].value
+ );
+ });
+ }
+
+ if (!this.target.chrome) {
+ this.disableJSNode.checked = !this.target.configureOptions
+ .javascriptEnabled;
+ this.disableJSNode.addEventListener("click", this._disableJSClicked);
+ } else {
+ // Hide the checkbox and label
+ this.disableJSNode.parentNode.style.display = "none";
+
+ const triggersPageRefreshLabel = this.panelDoc.getElementById(
+ "triggers-page-refresh-label"
+ );
+ triggersPageRefreshLabel.style.display = "none";
+ }
+ },
+
+ updateCurrentTheme: function() {
+ const currentTheme = GetPref("devtools.theme");
+ const themeBox = this.panelDoc.getElementById("devtools-theme-box");
+ const themeRadioInput = themeBox.querySelector(`[value=${currentTheme}]`);
+
+ if (themeRadioInput) {
+ themeRadioInput.checked = true;
+ } else {
+ // If the current theme does not exist anymore, switch to light theme
+ const lightThemeInputRadio = themeBox.querySelector("[value=light]");
+ lightThemeInputRadio.checked = true;
+ }
+ },
+
+ updateSourceMapPref: function() {
+ const prefName = "devtools.source-map.client-service.enabled";
+ const enabled = GetPref(prefName);
+ const box = this.panelDoc.querySelector(`[data-pref="${prefName}"]`);
+ box.checked = enabled;
+ },
+
+ /**
+ * Disables JavaScript for the currently loaded tab. We force a page refresh
+ * here because setting docShell.allowJavascript to true fails to block JS
+ * execution from event listeners added using addEventListener(), AJAX calls
+ * and timers. The page refresh prevents these things from being added in the
+ * first place.
+ *
+ * @param {Event} event
+ * The event sent by checking / unchecking the disable JS checkbox.
+ */
+ _disableJSClicked: function(event) {
+ const checked = event.target.checked;
+
+ const options = {
+ javascriptEnabled: !checked,
+ };
+
+ this.target.reconfigure({ options });
+ },
+
+ destroy: function() {
+ if (this.destroyed) {
+ return;
+ }
+ this.destroyed = true;
+
+ this._removeListeners();
+
+ this.disableJSNode.removeEventListener("click", this._disableJSClicked);
+
+ this.panelWin = this.panelDoc = this.disableJSNode = this.toolbox = null;
+ },
+};
diff --git a/devtools/client/framework/toolbox-tabs-order-manager.js b/devtools/client/framework/toolbox-tabs-order-manager.js
new file mode 100644
index 0000000000..7133ef71e9
--- /dev/null
+++ b/devtools/client/framework/toolbox-tabs-order-manager.js
@@ -0,0 +1,285 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
+const { gDevTools } = require("devtools/client/framework/devtools");
+const Services = require("Services");
+const Telemetry = require("devtools/client/shared/telemetry");
+const TABS_REORDERED_SCALAR = "devtools.toolbox.tabs_reordered";
+const PREFERENCE_NAME = "devtools.toolbox.tabsOrder";
+
+/**
+ * Manage the order of devtools tabs.
+ */
+class ToolboxTabsOrderManager {
+ constructor(toolbox, onOrderUpdated, panelDefinitions) {
+ this.toolbox = toolbox;
+ this.onOrderUpdated = onOrderUpdated;
+ this.currentPanelDefinitions = panelDefinitions || [];
+
+ this.onMouseDown = this.onMouseDown.bind(this);
+ this.onMouseMove = this.onMouseMove.bind(this);
+ this.onMouseUp = this.onMouseUp.bind(this);
+
+ Services.prefs.addObserver(PREFERENCE_NAME, this.onOrderUpdated);
+
+ this.telemetry = new Telemetry();
+ }
+
+ async destroy() {
+ Services.prefs.removeObserver(PREFERENCE_NAME, this.onOrderUpdated);
+
+ // Call mouseUp() to clear the state to prepare for in case a dragging was in progress
+ // when the destroy() was called.
+ await this.onMouseUp();
+ }
+
+ insertBefore(target) {
+ const xBefore = this.dragTarget.offsetLeft;
+ this.toolboxTabsElement.insertBefore(this.dragTarget, target);
+ const xAfter = this.dragTarget.offsetLeft;
+ this.dragStartX += xAfter - xBefore;
+ this.isOrderUpdated = true;
+ }
+
+ isFirstTab(tabElement) {
+ return !tabElement.previousSibling;
+ }
+
+ isLastTab(tabElement) {
+ return (
+ !tabElement.nextSibling ||
+ tabElement.nextSibling.id === "tools-chevron-menu-button"
+ );
+ }
+
+ isRTL() {
+ return this.toolbox.direction === "rtl";
+ }
+
+ async saveOrderPreference() {
+ const tabs = [...this.toolboxTabsElement.querySelectorAll(".devtools-tab")];
+ const tabIds = tabs.map(tab => tab.dataset.extensionId || tab.dataset.id);
+ // Concat the overflowed tabs id since they are not contained in visible tabs.
+ // The overflowed tabs cannot be reordered so we just append the id from current
+ // panel definitions on their order.
+ const overflowedTabIds = this.currentPanelDefinitions
+ .filter(definition => !tabs.some(tab => tab.dataset.id === definition.id))
+ .map(definition => definition.extensionId || definition.id);
+ const currentTabIds = tabIds.concat(overflowedTabIds);
+ const dragTargetId =
+ this.dragTarget.dataset.extensionId || this.dragTarget.dataset.id;
+ const prefIds = getTabsOrderFromPreference();
+ const absoluteIds = toAbsoluteOrder(prefIds, currentTabIds, dragTargetId);
+
+ // Remove panel id which is not in panel definitions and addons list.
+ const extensions = await AddonManager.getAllAddons();
+ const definitions = gDevTools.getToolDefinitionArray();
+ const result = absoluteIds.filter(
+ id =>
+ definitions.find(d => id === (d.extensionId || d.id)) ||
+ extensions.find(e => id === e.id)
+ );
+
+ Services.prefs.setCharPref(PREFERENCE_NAME, result.join(","));
+ }
+
+ setCurrentPanelDefinitions(currentPanelDefinitions) {
+ this.currentPanelDefinitions = currentPanelDefinitions;
+ }
+
+ onMouseDown(e) {
+ if (!e.target.classList.contains("devtools-tab")) {
+ return;
+ }
+
+ this.dragStartX = e.pageX;
+ this.dragTarget = e.target;
+ this.previousPageX = e.pageX;
+ this.toolboxContainerElement = this.dragTarget.closest(
+ "#toolbox-container"
+ );
+ this.toolboxTabsElement = this.dragTarget.closest(".toolbox-tabs");
+ this.isOrderUpdated = false;
+ this.eventTarget = this.dragTarget.ownerGlobal.top;
+
+ this.eventTarget.addEventListener("mousemove", this.onMouseMove);
+ this.eventTarget.addEventListener("mouseup", this.onMouseUp);
+
+ this.toolboxContainerElement.classList.add("tabs-reordering");
+ }
+
+ onMouseMove(e) {
+ const diffPageX = e.pageX - this.previousPageX;
+ let dragTargetCenterX =
+ this.dragTarget.offsetLeft + diffPageX + this.dragTarget.clientWidth / 2;
+ let isDragTargetPreviousSibling = false;
+
+ const tabElements = this.toolboxTabsElement.querySelectorAll(
+ ".devtools-tab"
+ );
+
+ // Calculate the minimum and maximum X-offset that can be valid for the drag target.
+ const firstElement = tabElements[0];
+ const firstElementCenterX =
+ firstElement.offsetLeft + firstElement.clientWidth / 2;
+ const lastElement = tabElements[tabElements.length - 1];
+ const lastElementCenterX =
+ lastElement.offsetLeft + lastElement.clientWidth / 2;
+ const max = Math.max(firstElementCenterX, lastElementCenterX);
+ const min = Math.min(firstElementCenterX, lastElementCenterX);
+
+ // Normalize the target center X so to remain between the first and last tab.
+ dragTargetCenterX = Math.min(max, dragTargetCenterX);
+ dragTargetCenterX = Math.max(min, dragTargetCenterX);
+
+ for (const tabElement of tabElements) {
+ if (tabElement === this.dragTarget) {
+ isDragTargetPreviousSibling = true;
+ continue;
+ }
+
+ // Is the dragTarget near the center of the other tab?
+ const anotherCenterX = tabElement.offsetLeft + tabElement.clientWidth / 2;
+ const distanceWithDragTarget = Math.abs(
+ dragTargetCenterX - anotherCenterX
+ );
+ const isReplaceable = distanceWithDragTarget < tabElement.clientWidth / 3;
+
+ if (isReplaceable) {
+ const replaceableElement = isDragTargetPreviousSibling
+ ? tabElement.nextSibling
+ : tabElement;
+ this.insertBefore(replaceableElement);
+ break;
+ }
+ }
+
+ let distance = e.pageX - this.dragStartX;
+
+ // To accomodate for RTL locales, we cannot rely on the first/last element of the
+ // NodeList. We cannot have negative distances for the leftmost tab, and we cannot
+ // have positive distances for the rightmost tab.
+ const isFirstTab = this.isFirstTab(this.dragTarget);
+ const isLastTab = this.isLastTab(this.dragTarget);
+ const isLeftmostTab = this.isRTL() ? isLastTab : isFirstTab;
+ const isRightmostTab = this.isRTL() ? isFirstTab : isLastTab;
+
+ if ((isLeftmostTab && distance < 0) || (isRightmostTab && distance > 0)) {
+ // If the drag target is already edge of the tabs and the mouse will make the
+ // element to move to same direction more, keep the position.
+ distance = 0;
+ }
+
+ this.dragTarget.style.left = `${distance}px`;
+ this.previousPageX = e.pageX;
+ }
+
+ async onMouseUp() {
+ if (!this.dragTarget) {
+ // The case in here has two type:
+ // 1. Although destroy method was called, it was not during reordering.
+ // 2. Although mouse event occur, destroy method was called during reordering.
+ return;
+ }
+
+ if (this.isOrderUpdated) {
+ await this.saveOrderPreference();
+
+ // Log which tabs reordered. The question we want to answer is:
+ // "How frequently are the tabs re-ordered, also which tabs get re-ordered?"
+ const toolId =
+ this.dragTarget.dataset.extensionId || this.dragTarget.dataset.id;
+ this.telemetry.keyedScalarAdd(TABS_REORDERED_SCALAR, toolId, 1);
+ }
+
+ this.eventTarget.removeEventListener("mousemove", this.onMouseMove);
+ this.eventTarget.removeEventListener("mouseup", this.onMouseUp);
+
+ this.toolboxContainerElement.classList.remove("tabs-reordering");
+ this.dragTarget.style.left = null;
+ this.dragTarget = null;
+ this.toolboxContainerElement = null;
+ this.toolboxTabsElement = null;
+ this.eventTarget = null;
+ }
+}
+
+function getTabsOrderFromPreference() {
+ const pref = Services.prefs.getCharPref(PREFERENCE_NAME, "");
+ return pref ? pref.split(",") : [];
+}
+
+function sortPanelDefinitions(definitions) {
+ const toolIds = getTabsOrderFromPreference();
+
+ return definitions.sort((a, b) => {
+ let orderA = toolIds.indexOf(a.extensionId || a.id);
+ let orderB = toolIds.indexOf(b.extensionId || b.id);
+ orderA = orderA < 0 ? Number.MAX_VALUE : orderA;
+ orderB = orderB < 0 ? Number.MAX_VALUE : orderB;
+ return orderA - orderB;
+ });
+}
+
+/*
+ * This function returns absolute tab ids that were merged the both ids that are in
+ * preference and tabs.
+ * Some tabs added with add-ons etc show/hide depending on conditions.
+ * However, all of tabs that include hidden tab always keep the relationship with
+ * left side tab, except in case the left tab was target of dragging. If the left
+ * tab has been moved, it keeps its relationship with the tab next to it.
+ *
+ * Case 1: Drag a tab to left
+ * currentTabIds: [T1, T2, T3, T4, T5]
+ * prefIds : [T1, T2, T3, E1(hidden), T4, T5]
+ * drag T4 : [T1, T2, T4, T3, T5]
+ * result : [T1, T2, T4, T3, E1, T5]
+ *
+ * Case 2: Drag a tab to right
+ * currentTabIds: [T1, T2, T3, T4, T5]
+ * prefIds : [T1, T2, T3, E1(hidden), T4, T5]
+ * drag T2 : [T1, T3, T4, T2, T5]
+ * result : [T1, T3, E1, T4, T2, T5]
+ *
+ * Case 3: Hidden tab was left end and drag a tab to left end
+ * currentTabIds: [T1, T2, T3, T4, T5]
+ * prefIds : [E1(hidden), T1, T2, T3, T4, T5]
+ * drag T4 : [T4, T1, T2, T3, T5]
+ * result : [E1, T4, T1, T2, T3, T5]
+ *
+ * Case 4: Hidden tab was right end and drag a tab to right end
+ * currentTabIds: [T1, T2, T3, T4, T5]
+ * prefIds : [T1, T2, T3, T4, T5, E1(hidden)]
+ * drag T1 : [T2, T3, T4, T5, T1]
+ * result : [T2, T3, T4, T5, E1, T1]
+ *
+ * @param Array - prefIds: id array of preference
+ * @param Array - currentTabIds: id array of appearanced tabs
+ * @param String - dragTargetId: id of dragged target
+ * @return Array
+ */
+function toAbsoluteOrder(prefIds, currentTabIds, dragTargetId) {
+ currentTabIds = [...currentTabIds];
+ let indexAtCurrentTabs = 0;
+
+ for (const prefId of prefIds) {
+ if (prefId === dragTargetId) {
+ // do nothing
+ } else if (currentTabIds.includes(prefId)) {
+ indexAtCurrentTabs = currentTabIds.indexOf(prefId) + 1;
+ } else {
+ currentTabIds.splice(indexAtCurrentTabs, 0, prefId);
+ indexAtCurrentTabs += 1;
+ }
+ }
+
+ return currentTabIds;
+}
+
+module.exports.ToolboxTabsOrderManager = ToolboxTabsOrderManager;
+module.exports.sortPanelDefinitions = sortPanelDefinitions;
+module.exports.toAbsoluteOrder = toAbsoluteOrder;
diff --git a/devtools/client/framework/toolbox-window.xhtml b/devtools/client/framework/toolbox-window.xhtml
new file mode 100644
index 0000000000..c09a52f6de
--- /dev/null
+++ b/devtools/client/framework/toolbox-window.xhtml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<!-- minwidth=50 is sum width of chevron and meatball menu button. -->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="devtools-toolbox-window"
+ macanimationtype="document"
+ windowtype="devtools:toolbox"
+ width="900" height="320"
+ persist="screenX screenY width height sizemode">
+ <tooltip id="aHTMLTooltip" page="true"/>
+</window>
diff --git a/devtools/client/framework/toolbox.js b/devtools/client/framework/toolbox.js
new file mode 100644
index 0000000000..ada833ef04
--- /dev/null
+++ b/devtools/client/framework/toolbox.js
@@ -0,0 +1,4344 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const SOURCE_MAP_WORKER =
+ "resource://devtools/client/shared/source-map/worker.js";
+const SOURCE_MAP_WORKER_ASSETS =
+ "resource://devtools/client/shared/source-map/assets/";
+
+const MAX_ORDINAL = 99;
+const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsoleEnabled";
+const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight";
+const DISABLE_AUTOHIDE_PREF = "ui.popup.disable_autohide";
+const HOST_HISTOGRAM = "DEVTOOLS_TOOLBOX_HOST";
+const CURRENT_THEME_SCALAR = "devtools.current_theme";
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const REGEX_4XX_5XX = /^[4,5]\d\d$/;
+
+var { Ci, Cc } = require("chrome");
+var promise = require("promise");
+const { debounce } = require("devtools/shared/debounce");
+const { throttle } = require("devtools/shared/throttle");
+const { safeAsyncMethod } = require("devtools/shared/async-utils");
+var Services = require("Services");
+var ChromeUtils = require("ChromeUtils");
+var { gDevTools } = require("devtools/client/framework/devtools");
+var EventEmitter = require("devtools/shared/event-emitter");
+const Selection = require("devtools/client/framework/selection");
+var Telemetry = require("devtools/client/shared/telemetry");
+const { getUnicodeUrl } = require("devtools/client/shared/unicode-url");
+var { DOMHelpers } = require("devtools/shared/dom-helpers");
+const { KeyCodes } = require("devtools/client/shared/keycodes");
+var Startup = Cc["@mozilla.org/devtools/startup-clh;1"].getService(
+ Ci.nsISupports
+).wrappedJSObject;
+
+const { TargetList } = require("devtools/shared/resources/target-list");
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+const { BrowserLoader } = ChromeUtils.import(
+ "resource://devtools/client/shared/browser-loader.js"
+);
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/toolbox.properties"
+);
+
+loader.lazyRequireGetter(
+ this,
+ "registerStoreObserver",
+ "devtools/client/shared/redux/subscriber",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "createToolboxStore",
+ "devtools/client/framework/store",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ [
+ "registerWalkerListeners",
+ "registerTarget",
+ "selectTarget",
+ "unregisterTarget",
+ ],
+ "devtools/client/framework/actions/index",
+ true
+);
+
+loader.lazyRequireGetter(
+ this,
+ "AppConstants",
+ "resource://gre/modules/AppConstants.jsm",
+ true
+);
+loader.lazyRequireGetter(this, "flags", "devtools/shared/flags");
+loader.lazyRequireGetter(
+ this,
+ "KeyShortcuts",
+ "devtools/client/shared/key-shortcuts"
+);
+loader.lazyRequireGetter(this, "ZoomKeys", "devtools/client/shared/zoom-keys");
+loader.lazyRequireGetter(
+ this,
+ "settleAll",
+ "devtools/shared/ThreadSafeDevToolsUtils",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "ToolboxButtons",
+ "devtools/client/definitions",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "SourceMapURLService",
+ "devtools/client/framework/source-map-url-service",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "BrowserConsoleManager",
+ "devtools/client/webconsole/browser-console-manager",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "viewSource",
+ "devtools/client/shared/view-source"
+);
+loader.lazyRequireGetter(
+ this,
+ "buildHarLog",
+ "devtools/client/netmonitor/src/har/har-builder-utils",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "NetMonitorAPI",
+ "devtools/client/netmonitor/src/api",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "sortPanelDefinitions",
+ "devtools/client/framework/toolbox-tabs-order-manager",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "createEditContextMenu",
+ "devtools/client/framework/toolbox-context-menu",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "getSelectedTarget",
+ "devtools/client/framework/reducers/targets",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "remoteClientManager",
+ "devtools/client/shared/remote-debugging/remote-client-manager.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "ResponsiveUIManager",
+ "devtools/client/responsive/manager"
+);
+loader.lazyRequireGetter(
+ this,
+ "DevToolsUtils",
+ "devtools/shared/DevToolsUtils"
+);
+loader.lazyRequireGetter(
+ this,
+ "NodePicker",
+ "devtools/client/inspector/node-picker"
+);
+
+loader.lazyGetter(this, "domNodeConstants", () => {
+ return require("devtools/shared/dom-node-constants");
+});
+
+loader.lazyGetter(this, "DEBUG_TARGET_TYPES", () => {
+ return require("devtools/client/shared/remote-debugging/constants")
+ .DEBUG_TARGET_TYPES;
+});
+
+loader.lazyGetter(this, "registerHarOverlay", () => {
+ return require("devtools/client/netmonitor/src/har/toolbox-overlay").register;
+});
+
+loader.lazyRequireGetter(
+ this,
+ "NodeFront",
+ "devtools/client/fronts/node",
+ true
+);
+
+loader.lazyRequireGetter(
+ this,
+ "PICKER_TYPES",
+ "devtools/shared/picker-constants"
+);
+
+loader.lazyRequireGetter(
+ this,
+ "getF12SessionId",
+ "devtools/client/framework/enable-devtools-popup",
+ true
+);
+
+const DEVTOOLS_F12_DISABLED_PREF = "devtools.experiment.f12.shortcut_disabled";
+
+/**
+ * A "Toolbox" is the component that holds all the tools for one specific
+ * target. Visually, it's a document that includes the tools tabs and all
+ * the iframes where the tool panels will be living in.
+ *
+ * @param {object} target
+ * The object the toolbox is debugging.
+ * @param {string} selectedTool
+ * Tool to select initially
+ * @param {Toolbox.HostType} hostType
+ * Type of host that will host the toolbox (e.g. sidebar, window)
+ * @param {DOMWindow} contentWindow
+ * The window object of the toolbox document
+ * @param {string} frameId
+ * A unique identifier to differentiate toolbox documents from the
+ * chrome codebase when passing DOM messages
+ * @param {Number} msSinceProcessStart
+ * the number of milliseconds since process start using monotonic
+ * timestamps (unaffected by system clock changes).
+ */
+function Toolbox(
+ target,
+ selectedTool,
+ hostType,
+ contentWindow,
+ frameId,
+ msSinceProcessStart
+) {
+ this._win = contentWindow;
+ this.frameId = frameId;
+ this.selection = new Selection();
+ this.telemetry = new Telemetry();
+
+ this.targetList = new TargetList(target.client.mainRoot, target);
+ this.targetList.on(
+ "target-thread-wrong-order-on-resume",
+ this._onTargetThreadFrontResumeWrongOrder.bind(this)
+ );
+ this.resourceWatcher = new ResourceWatcher(this.targetList);
+
+ // The session ID is used to determine which telemetry events belong to which
+ // toolbox session. Because we use Amplitude to analyse the telemetry data we
+ // must use the time since the system wide epoch as the session ID.
+ this.sessionId = msSinceProcessStart;
+
+ // If the user opened the toolbox, we can now enable the F12 shortcut.
+ if (Services.prefs.getBoolPref(DEVTOOLS_F12_DISABLED_PREF, false)) {
+ // If the toolbox is opening while F12 was disabled, the user might have
+ // pressed F12 and seen the "enable devtools" notification.
+ // A telemetry session_id was generated for the f12_popup_displayed event.
+ // Reuse it here in order to link the toolbox session to the
+ // f12_popup_displayed events.
+ // getF12SessionId() might return null if the popup was never displayed.
+ // In this case, fallback on the provided `msSinceProcessStart`.
+ this.sessionId = getF12SessionId() || msSinceProcessStart;
+
+ this.telemetry.recordEvent("f12_enabled", "tools", null, {
+ session_id: this.sessionId,
+ });
+
+ // Flip the preference.
+ Services.prefs.setBoolPref(DEVTOOLS_F12_DISABLED_PREF, false);
+ }
+
+ // Map of the available DevTools WebExtensions:
+ // Map<extensionUUID, extensionName>
+ this._webExtensions = new Map();
+
+ this._toolPanels = new Map();
+ this._inspectorExtensionSidebars = new Map();
+
+ this._netMonitorAPI = null;
+
+ // Map of frames (id => frame-info) and currently selected frame id.
+ this.frameMap = new Map();
+ this.selectedFrameId = null;
+
+ // Set of paused threads to determine whether the toolbox is paused
+ this._pausedThreads = new Set();
+
+ /**
+ * KeyShortcuts instance specific to WINDOW host type.
+ * This is the key shortcuts that are only register when the toolbox
+ * is loaded in its own window. Otherwise, these shortcuts are typically
+ * registered by devtools-startup.js module.
+ */
+ this._windowHostShortcuts = null;
+
+ this._toolRegistered = this._toolRegistered.bind(this);
+ this._toolUnregistered = this._toolUnregistered.bind(this);
+ this._onWillNavigate = this._onWillNavigate.bind(this);
+ this._refreshHostTitle = this._refreshHostTitle.bind(this);
+ this.toggleNoAutohide = this.toggleNoAutohide.bind(this);
+ this._updateFrames = this._updateFrames.bind(this);
+ this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this);
+ this.closeToolbox = this.closeToolbox.bind(this);
+ this.destroy = this.destroy.bind(this);
+ this._applyCacheSettings = this._applyCacheSettings.bind(this);
+ this._applyServiceWorkersTestingSettings = this._applyServiceWorkersTestingSettings.bind(
+ this
+ );
+ this._saveSplitConsoleHeight = this._saveSplitConsoleHeight.bind(this);
+ this._onFocus = this._onFocus.bind(this);
+ this._onBrowserMessage = this._onBrowserMessage.bind(this);
+ this._onPerformanceFrontEvent = this._onPerformanceFrontEvent.bind(this);
+ this._onTabsOrderUpdated = this._onTabsOrderUpdated.bind(this);
+ this._onToolbarFocus = this._onToolbarFocus.bind(this);
+ this._onToolbarArrowKeypress = this._onToolbarArrowKeypress.bind(this);
+ this._onPickerClick = this._onPickerClick.bind(this);
+ this._onPickerKeypress = this._onPickerKeypress.bind(this);
+ this._onPickerStarting = this._onPickerStarting.bind(this);
+ this._onPickerStarted = this._onPickerStarted.bind(this);
+ this._onPickerStopped = this._onPickerStopped.bind(this);
+ this._onPickerCanceled = this._onPickerCanceled.bind(this);
+ this._onPickerPicked = this._onPickerPicked.bind(this);
+ this._onPickerPreviewed = this._onPickerPreviewed.bind(this);
+ this._onInspectObject = this._onInspectObject.bind(this);
+ this._onNewSelectedNodeFront = this._onNewSelectedNodeFront.bind(this);
+ this._onToolSelected = this._onToolSelected.bind(this);
+ this._onContextMenu = this._onContextMenu.bind(this);
+ this._onMouseDown = this._onMouseDown.bind(this);
+ this.updateToolboxButtonsVisibility = this.updateToolboxButtonsVisibility.bind(
+ this
+ );
+ this.updateToolboxButtons = this.updateToolboxButtons.bind(this);
+ this.selectTool = this.selectTool.bind(this);
+ this._pingTelemetrySelectTool = this._pingTelemetrySelectTool.bind(this);
+ this.toggleSplitConsole = this.toggleSplitConsole.bind(this);
+ this.toggleOptions = this.toggleOptions.bind(this);
+ this.togglePaintFlashing = this.togglePaintFlashing.bind(this);
+ this.toggleDragging = this.toggleDragging.bind(this);
+ this._onPausedState = this._onPausedState.bind(this);
+ this._onResumedState = this._onResumedState.bind(this);
+ this._onTargetAvailable = this._onTargetAvailable.bind(this);
+ this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
+ this._onNavigate = this._onNavigate.bind(this);
+ this._onResourceAvailable = this._onResourceAvailable.bind(this);
+ this._onResourceUpdated = this._onResourceUpdated.bind(this);
+
+ this._throttledSetToolboxButtons = throttle(
+ () => this.component.setToolboxButtons(this.toolbarButtons),
+ 500,
+ this
+ );
+
+ this.isPaintFlashing = false;
+
+ if (!selectedTool) {
+ selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
+ }
+ this._defaultToolId = selectedTool;
+
+ this._hostType = hostType;
+
+ this.isOpen = new Promise(
+ function(resolve) {
+ this._resolveIsOpen = resolve;
+ }.bind(this)
+ );
+
+ EventEmitter.decorate(this);
+
+ this.on("host-changed", this._refreshHostTitle);
+ this.on("select", this._onToolSelected);
+
+ this.selection.on("new-node-front", this._onNewSelectedNodeFront);
+
+ gDevTools.on("tool-registered", this._toolRegistered);
+ gDevTools.on("tool-unregistered", this._toolUnregistered);
+
+ /**
+ * Get text direction for the current locale direction.
+ *
+ * `getComputedStyle` forces a synchronous reflow, so use a lazy getter in order to
+ * call it only once.
+ */
+ loader.lazyGetter(this, "direction", () => {
+ const { documentElement } = this.doc;
+ const isRtl =
+ this.win.getComputedStyle(documentElement).direction === "rtl";
+ return isRtl ? "rtl" : "ltr";
+ });
+}
+exports.Toolbox = Toolbox;
+
+/**
+ * The toolbox can be 'hosted' either embedded in a browser window
+ * or in a separate window.
+ */
+Toolbox.HostType = {
+ BOTTOM: "bottom",
+ RIGHT: "right",
+ LEFT: "left",
+ WINDOW: "window",
+ BROWSERTOOLBOX: "browsertoolbox",
+ // This is typically used by `about:debugging`, when opening toolbox in a new tab,
+ // via `about:devtools-toolbox` URLs.
+ PAGE: "page",
+};
+
+Toolbox.prototype = {
+ _URL: "about:devtools-toolbox",
+
+ _prefs: {
+ LAST_TOOL: "devtools.toolbox.selectedTool",
+ SIDE_ENABLED: "devtools.toolbox.sideEnabled",
+ },
+
+ get nodePicker() {
+ if (!this._nodePicker) {
+ this._nodePicker = new NodePicker(this.targetList, this.selection);
+ this._nodePicker.on("picker-starting", this._onPickerStarting);
+ this._nodePicker.on("picker-started", this._onPickerStarted);
+ this._nodePicker.on("picker-stopped", this._onPickerStopped);
+ this._nodePicker.on("picker-node-canceled", this._onPickerCanceled);
+ this._nodePicker.on("picker-node-picked", this._onPickerPicked);
+ this._nodePicker.on("picker-node-previewed", this._onPickerPreviewed);
+ }
+
+ return this._nodePicker;
+ },
+
+ get store() {
+ if (!this._store) {
+ this._store = createToolboxStore();
+ registerStoreObserver(this._store, this._onToolboxStateChange.bind(this));
+ }
+ return this._store;
+ },
+
+ get currentToolId() {
+ return this._currentToolId;
+ },
+
+ set currentToolId(id) {
+ this._currentToolId = id;
+ this.component.setCurrentToolId(id);
+ },
+
+ get defaultToolId() {
+ return this._defaultToolId;
+ },
+
+ get panelDefinitions() {
+ return this._panelDefinitions;
+ },
+
+ set panelDefinitions(definitions) {
+ this._panelDefinitions = definitions;
+ this._combineAndSortPanelDefinitions();
+ },
+
+ get visibleAdditionalTools() {
+ if (!this._visibleAdditionalTools) {
+ this._visibleAdditionalTools = [];
+ }
+
+ return this._visibleAdditionalTools;
+ },
+
+ set visibleAdditionalTools(tools) {
+ this._visibleAdditionalTools = tools;
+ if (this.isReady) {
+ this._combineAndSortPanelDefinitions();
+ }
+ },
+
+ /**
+ * Combines the built-in panel definitions and the additional tool definitions that
+ * can be set by add-ons.
+ */
+ _combineAndSortPanelDefinitions() {
+ let definitions = [
+ ...this._panelDefinitions,
+ ...this.getVisibleAdditionalTools(),
+ ];
+ definitions = sortPanelDefinitions(definitions);
+ this.component.setPanelDefinitions(definitions);
+ },
+
+ lastUsedToolId: null,
+
+ /**
+ * Returns a *copy* of the _toolPanels collection.
+ *
+ * @return {Map} panels
+ * All the running panels in the toolbox
+ */
+ getToolPanels: function() {
+ return new Map(this._toolPanels);
+ },
+
+ /**
+ * Access the panel for a given tool
+ */
+ getPanel: function(id) {
+ return this._toolPanels.get(id);
+ },
+
+ /**
+ * Get the panel instance for a given tool once it is ready.
+ * If the tool is already opened, the promise will resolve immediately,
+ * otherwise it will wait until the tool has been opened before resolving.
+ *
+ * Note that this does not open the tool, use selectTool if you'd
+ * like to select the tool right away.
+ *
+ * @param {String} id
+ * The id of the panel, for example "jsdebugger".
+ * @returns Promise
+ * A promise that resolves once the panel is ready.
+ */
+ getPanelWhenReady: function(id) {
+ const panel = this.getPanel(id);
+ return new Promise(resolve => {
+ if (panel) {
+ resolve(panel);
+ } else {
+ this.on(id + "-ready", initializedPanel => {
+ resolve(initializedPanel);
+ });
+ }
+ });
+ },
+
+ /**
+ * This is a shortcut for getPanel(currentToolId) because it is much more
+ * likely that we're going to want to get the panel that we've just made
+ * visible
+ */
+ getCurrentPanel: function() {
+ return this._toolPanels.get(this.currentToolId);
+ },
+
+ toggleDragging: function() {
+ this.doc.querySelector("window").classList.toggle("dragging");
+ },
+
+ /**
+ * Get the current top level target the toolbox is debugging.
+ */
+ get target() {
+ return this.targetList.targetFront;
+ },
+
+ get threadFront() {
+ return this.targetList.targetFront.threadFront;
+ },
+
+ /**
+ * Get/alter the host of a Toolbox, i.e. is it in browser or in a separate
+ * tab. See HostType for more details.
+ */
+ get hostType() {
+ return this._hostType;
+ },
+
+ /**
+ * Shortcut to the window containing the toolbox UI
+ */
+ get win() {
+ return this._win;
+ },
+
+ /**
+ * When the toolbox is loaded in a frame with type="content", win.parent will not return
+ * the parent Chrome window. This getter should return the parent Chrome window
+ * regardless of the frame type. See Bug 1539979.
+ */
+ get topWindow() {
+ return DevToolsUtils.getTopWindow(this.win);
+ },
+
+ get topDoc() {
+ return this.topWindow.document;
+ },
+
+ /**
+ * Shortcut to the document containing the toolbox UI
+ */
+ get doc() {
+ return this.win.document;
+ },
+
+ /**
+ * Get the toggled state of the split console
+ */
+ get splitConsole() {
+ return this._splitConsole;
+ },
+
+ /**
+ * Get the focused state of the split console
+ */
+ isSplitConsoleFocused: function() {
+ if (!this._splitConsole) {
+ return false;
+ }
+ const focusedWin = Services.focus.focusedWindow;
+ return (
+ focusedWin &&
+ focusedWin ===
+ this.doc.querySelector("#toolbox-panel-iframe-webconsole").contentWindow
+ );
+ },
+
+ isBrowserToolbox: function() {
+ return this.hostType === Toolbox.HostType.BROWSERTOOLBOX;
+ },
+
+ /**
+ * Set a given target as selected (which may impact the console evaluation context selector).
+ *
+ * @param {String} targetActorID: The actorID of the target we want to select.
+ */
+ selectTarget(targetActorID) {
+ if (this.getSelectedTargetFront()?.actorID !== targetActorID) {
+ this.store.dispatch(selectTarget(targetActorID));
+ }
+ },
+
+ /**
+ * @returns {ThreadFront|null} The selected thread front, or null if there is none.
+ */
+ getSelectedTargetFront: function() {
+ const selectedTarget = getSelectedTarget(this.store.getState());
+ if (!selectedTarget) {
+ return null;
+ }
+
+ return this.target.client.getFrontByID(selectedTarget.actorID);
+ },
+
+ _onToolboxStateChange(state, oldState) {
+ if (getSelectedTarget(state) !== getSelectedTarget(oldState)) {
+ const dbg = this.getPanel("jsdebugger");
+ if (!dbg) {
+ return;
+ }
+
+ const threadActorID = getSelectedTarget(state)?.threadFront?.actorID;
+ if (!threadActorID) {
+ return;
+ }
+
+ dbg.selectThread(threadActorID);
+ }
+ },
+
+ _onPausedState: function(packet, threadFront) {
+ // Suppress interrupted events by default because the thread is
+ // paused/resumed a lot for various actions.
+ if (packet.why.type === "interrupted") {
+ return;
+ }
+
+ this.highlightTool("jsdebugger");
+
+ if (
+ packet.why.type === "debuggerStatement" ||
+ packet.why.type === "mutationBreakpoint" ||
+ packet.why.type === "eventBreakpoint" ||
+ packet.why.type === "breakpoint" ||
+ packet.why.type === "exception"
+ ) {
+ this.raise();
+ this.selectTool("jsdebugger", packet.why.type);
+ this._pausedThreads.add(threadFront);
+ this.emit("toolbox-paused");
+ }
+ },
+
+ _onResumedState: function(threadFront) {
+ this._pausedThreads.delete(threadFront);
+
+ if (this._pausedThreads.size == 0) {
+ this.emit("toolbox-resumed");
+ this.unhighlightTool("jsdebugger");
+ }
+ },
+
+ /**
+ * This method will be called for the top-level target, as well as any potential
+ * additional targets we may care about.
+ */
+ async _onTargetAvailable({ targetFront, isTargetSwitching }) {
+ if (targetFront.isTopLevel) {
+ if (isTargetSwitching) {
+ // Notify gDevTools that the toolbox will be hooked to another target.
+ this.emit("switch-target", targetFront);
+ }
+
+ // Attach to a new top-level target.
+ // For now, register these event listeners only on the top level target
+ targetFront.on("will-navigate", this._onWillNavigate);
+ targetFront.on("navigate", this._onNavigate);
+ targetFront.on("frame-update", this._updateFrames);
+ targetFront.on("inspect-object", this._onInspectObject);
+
+ targetFront.watchFronts("inspector", async inspectorFront => {
+ registerWalkerListeners(this.store, inspectorFront.walker);
+ });
+ }
+
+ const { threadFront } = targetFront;
+ if (threadFront) {
+ // threadFront listeners are removed when the thread is destroyed
+ threadFront.on("paused", packet =>
+ this._onPausedState(packet, threadFront)
+ );
+ threadFront.on("resumed", () => this._onResumedState(threadFront));
+ }
+
+ if (this.hostType !== Toolbox.HostType.PAGE) {
+ await this.store.dispatch(registerTarget(targetFront));
+ }
+
+ if (targetFront.isTopLevel && isTargetSwitching) {
+ // These methods expect the target to be attached, which is guaranteed by the time
+ // _onTargetAvailable is called by the TargetList.
+ await this._listFrames();
+ await this.initPerformance();
+ }
+ },
+
+ _onTargetDestroyed({ targetFront }) {
+ if (targetFront.isTopLevel) {
+ this.target.off("inspect-object", this._onInspectObject);
+ this.target.off("will-navigate", this._onWillNavigate);
+ this.target.off("navigate", this._onNavigate);
+ this.target.off("frame-update", this._updateFrames);
+ }
+
+ if (this.hostType !== Toolbox.HostType.PAGE) {
+ this.store.dispatch(unregisterTarget(targetFront));
+ }
+ },
+
+ _onTargetThreadFrontResumeWrongOrder() {
+ const box = this.getNotificationBox();
+ box.appendNotification(
+ L10N.getStr("toolbox.resumeOrderWarning"),
+ "wrong-resume-order",
+ "",
+ box.PRIORITY_WARNING_HIGH
+ );
+ },
+
+ /**
+ * Open the toolbox
+ */
+ open: function() {
+ return async function() {
+ const isToolboxURL = this.win.location.href.startsWith(this._URL);
+ if (isToolboxURL) {
+ // Update the URL so that onceDOMReady watch for the right url.
+ this._URL = this.win.location.href;
+ }
+
+ const domReady = new Promise(resolve => {
+ DOMHelpers.onceDOMReady(
+ this.win,
+ () => {
+ resolve();
+ },
+ this._URL
+ );
+ });
+
+ // Optimization: fire up a few other things before waiting on
+ // the iframe being ready (makes startup faster)
+ await this.targetList.startListening();
+ // The TargetList is created from Toolbox's constructor,
+ // and Toolbox.open (i.e. this function) is called soon after.
+ // It means that this call to TargetList.watchTargets is the first,
+ // and we are registering the first target listener, which means
+ // Toolbox._onTargetAvailable will be called first, before any other
+ // onTargetAvailable listener that might be registered on the targetList.
+ await this.targetList.watchTargets(
+ TargetList.ALL_TYPES,
+ this._onTargetAvailable,
+ this._onTargetDestroyed
+ );
+
+ // Watch for console API messages, errors and network events in order to populate
+ // the error count icon in the toolbox.
+ const onResourcesWatched = this.resourceWatcher.watchResources(
+ [
+ this.resourceWatcher.TYPES.CONSOLE_MESSAGE,
+ this.resourceWatcher.TYPES.ERROR_MESSAGE,
+ // Independently of watching network event resources for the error count icon,
+ // we need to start tracking network activity on toolbox open for targets such
+ // as tabs, in order to ensure there is always at least one listener existing
+ // for network events across the lifetime of the various panels, so stopping
+ // the resource watcher from clearing out its cache of network event resources.
+ this.resourceWatcher.TYPES.NETWORK_EVENT,
+ ],
+ {
+ onAvailable: this._onResourceAvailable,
+ onUpdated: this._onResourceUpdated,
+ }
+ );
+
+ await domReady;
+
+ this.browserRequire = BrowserLoader({
+ window: this.win,
+ useOnlyShared: true,
+ }).require;
+
+ this.isReady = true;
+
+ const framesPromise = this._listFrames();
+
+ Services.prefs.addObserver(
+ "devtools.cache.disabled",
+ this._applyCacheSettings
+ );
+ Services.prefs.addObserver(
+ "devtools.serviceWorkers.testing.enabled",
+ this._applyServiceWorkersTestingSettings
+ );
+
+ // Get the DOM element to mount the ToolboxController to.
+ this._componentMount = this.doc.getElementById("toolbox-toolbar-mount");
+
+ this._mountReactComponent();
+ this._buildDockOptions();
+ this._buildTabs();
+ this._applyCacheSettings();
+ this._applyServiceWorkersTestingSettings();
+ this._addWindowListeners();
+ this._addChromeEventHandlerEvents();
+ this._registerOverlays();
+
+ // Get the tab bar of the ToolboxController to attach the "keypress" event listener to.
+ this._tabBar = this.doc.querySelector(".devtools-tabbar");
+ this._tabBar.addEventListener("keypress", this._onToolbarArrowKeypress);
+
+ this._componentMount.setAttribute(
+ "aria-label",
+ L10N.getStr("toolbox.label")
+ );
+
+ // Set debug target data on the ToolboxController component.
+ this._setDebugTargetData();
+
+ this.webconsolePanel = this.doc.querySelector(
+ "#toolbox-panel-webconsole"
+ );
+ this.webconsolePanel.height = Services.prefs.getIntPref(
+ SPLITCONSOLE_HEIGHT_PREF
+ );
+ this.webconsolePanel.addEventListener(
+ "resize",
+ this._saveSplitConsoleHeight
+ );
+
+ this._buildButtons();
+
+ this._pingTelemetry();
+
+ // The isTargetSupported check needs to happen after the target is
+ // remoted, otherwise we could have done it in the toolbox constructor
+ // (bug 1072764).
+ const toolDef = gDevTools.getToolDefinition(this._defaultToolId);
+ if (!toolDef || !toolDef.isTargetSupported(this.target)) {
+ this._defaultToolId = "webconsole";
+ }
+
+ // Start rendering the toolbox toolbar before selecting the tool, as the tools
+ // can take a few hundred milliseconds seconds to start up.
+ //
+ // Delay React rendering as Toolbox.open is synchronous.
+ // Even if this involve promises, it is synchronous. Toolbox.open already loads
+ // react modules and freeze the event loop for a significant time.
+ // requestIdleCallback allows releasing it to allow user events to be processed.
+ // Use 16ms maximum delay to allow one frame to be rendered at 60FPS
+ // (1000ms/60FPS=16ms)
+ this.win.requestIdleCallback(
+ () => {
+ this.component.setCanRender();
+ },
+ { timeout: 16 }
+ );
+
+ await this.selectTool(this._defaultToolId, "initial_panel");
+
+ // Wait until the original tool is selected so that the split
+ // console input will receive focus.
+ let splitConsolePromise = promise.resolve();
+ if (Services.prefs.getBoolPref(SPLITCONSOLE_ENABLED_PREF)) {
+ splitConsolePromise = this.openSplitConsole();
+ this.telemetry.addEventProperty(
+ this.topWindow,
+ "open",
+ "tools",
+ null,
+ "splitconsole",
+ true
+ );
+ } else {
+ this.telemetry.addEventProperty(
+ this.topWindow,
+ "open",
+ "tools",
+ null,
+ "splitconsole",
+ false
+ );
+ }
+
+ await promise.all([
+ splitConsolePromise,
+ framesPromise,
+ onResourcesWatched,
+ ]);
+
+ // We do not expect the focus to be restored when using about:debugging toolboxes
+ // Otherwise, when reloading the toolbox, the debugged tab will be focused.
+ if (this.hostType !== Toolbox.HostType.PAGE) {
+ // Request the actor to restore the focus to the content page once the
+ // target is detached. This typically happens when the console closes.
+ // We restore the focus as it may have been stolen by the console input.
+ await this.target.reconfigure({
+ options: {
+ restoreFocus: true,
+ },
+ });
+ }
+
+ // Lazily connect to the profiler here and don't wait for it to complete,
+ // used to intercept console.profile calls before the performance tools are open.
+ const performanceFrontConnection = this.initPerformance();
+
+ // If in testing environment, wait for performance connection to finish,
+ // so we don't have to explicitly wait for this in tests; ideally, all tests
+ // will handle this on their own, but each have their own tear down function.
+ if (flags.testing) {
+ await performanceFrontConnection;
+ }
+
+ this.emit("ready");
+ this._resolveIsOpen();
+ }
+ .bind(this)()
+ .catch(e => {
+ console.error("Exception while opening the toolbox", String(e), e);
+ // While the exception stack is correctly printed in the Browser console when
+ // passing `e` to console.error, it is not on the stdout, so print it via dump.
+ dump(e.stack + "\n");
+ });
+ },
+
+ /**
+ * Retrieve the ChromeEventHandler associated to the toolbox frame.
+ * When DevTools are loaded in a content frame, this will return the containing chrome
+ * frame. Events from nested frames will bubble up to this chrome frame, which allows to
+ * listen to events from nested frames.
+ */
+ getChromeEventHandler() {
+ if (!this.win || !this.win.docShell) {
+ return null;
+ }
+ return this.win.docShell.chromeEventHandler;
+ },
+
+ /**
+ * Attach events on the chromeEventHandler for the current window. When loaded in a
+ * frame with type set to "content", events will not bubble across frames. The
+ * chromeEventHandler does not have this limitation and will catch all events triggered
+ * on any of the frames under the devtools document.
+ *
+ * Events relying on the chromeEventHandler need to be added and removed at specific
+ * moments in the lifecycle of the toolbox, so all the events relying on it should be
+ * grouped here.
+ */
+ _addChromeEventHandlerEvents: function() {
+ // win.docShell.chromeEventHandler might not be accessible anymore when removing the
+ // events, so we can't rely on a dynamic getter here.
+ // Keep a reference on the chromeEventHandler used to addEventListener to be sure we
+ // can remove the listeners afterwards.
+ this._chromeEventHandler = this.getChromeEventHandler();
+ if (!this._chromeEventHandler) {
+ return;
+ }
+
+ // Add shortcuts and window-host-shortcuts that use the ChromeEventHandler as target.
+ this._addShortcuts();
+ this._addWindowHostShortcuts();
+
+ this._chromeEventHandler.addEventListener(
+ "keypress",
+ this._splitConsoleOnKeypress
+ );
+ this._chromeEventHandler.addEventListener("focus", this._onFocus, true);
+ this._chromeEventHandler.addEventListener(
+ "contextmenu",
+ this._onContextMenu
+ );
+ this._chromeEventHandler.addEventListener("mousedown", this._onMouseDown);
+ },
+
+ _removeChromeEventHandlerEvents: function() {
+ if (!this._chromeEventHandler) {
+ return;
+ }
+
+ // Remove shortcuts and window-host-shortcuts that use the ChromeEventHandler as
+ // target.
+ this._removeShortcuts();
+ this._removeWindowHostShortcuts();
+
+ this._chromeEventHandler.removeEventListener(
+ "keypress",
+ this._splitConsoleOnKeypress
+ );
+ this._chromeEventHandler.removeEventListener("focus", this._onFocus, true);
+ this._chromeEventHandler.removeEventListener(
+ "contextmenu",
+ this._onContextMenu
+ );
+ this._chromeEventHandler.removeEventListener(
+ "mousedown",
+ this._onMouseDown
+ );
+
+ this._chromeEventHandler = null;
+ },
+
+ _addShortcuts: function() {
+ // Create shortcuts instance for the toolbox
+ if (!this.shortcuts) {
+ this.shortcuts = new KeyShortcuts({
+ window: this.doc.defaultView,
+ // The toolbox key shortcuts should be triggered from any frame in DevTools.
+ // Use the chromeEventHandler as the target to catch events from all frames.
+ target: this.getChromeEventHandler(),
+ });
+ }
+
+ // Listen for the shortcut key to show the frame list
+ this.shortcuts.on(L10N.getStr("toolbox.showFrames.key"), event => {
+ if (event.target.id === "command-button-frames") {
+ event.target.click();
+ }
+ });
+
+ // Listen for tool navigation shortcuts.
+ this.shortcuts.on(L10N.getStr("toolbox.nextTool.key"), event => {
+ this.selectNextTool();
+ event.preventDefault();
+ });
+ this.shortcuts.on(L10N.getStr("toolbox.previousTool.key"), event => {
+ this.selectPreviousTool();
+ event.preventDefault();
+ });
+ this.shortcuts.on(L10N.getStr("toolbox.toggleHost.key"), event => {
+ this.switchToPreviousHost();
+ event.preventDefault();
+ });
+
+ // List for Help/Settings key.
+ this.shortcuts.on(L10N.getStr("toolbox.help.key"), this.toggleOptions);
+
+ // Listen for Reload shortcuts
+ [
+ ["reload", false],
+ ["reload2", false],
+ ["forceReload", true],
+ ["forceReload2", true],
+ ].forEach(([id, force]) => {
+ const key = L10N.getStr("toolbox." + id + ".key");
+ this.shortcuts.on(key, event => {
+ this.reloadTarget(force);
+
+ // Prevent Firefox shortcuts from reloading the page
+ event.preventDefault();
+ });
+ });
+
+ // Add zoom-related shortcuts.
+ if (!this._hostOptions || this._hostOptions.zoom === true) {
+ ZoomKeys.register(this.win, this.shortcuts);
+ }
+ },
+
+ _removeShortcuts: function() {
+ if (this.shortcuts) {
+ this.shortcuts.destroy();
+ this.shortcuts = null;
+ }
+ },
+
+ /**
+ * Adds the keys and commands to the Toolbox Window in window mode.
+ */
+ _addWindowHostShortcuts: function() {
+ if (this.hostType != Toolbox.HostType.WINDOW) {
+ // Those shortcuts are only valid for host type WINDOW.
+ return;
+ }
+
+ if (!this._windowHostShortcuts) {
+ this._windowHostShortcuts = new KeyShortcuts({
+ window: this.win,
+ // The window host key shortcuts should be triggered from any frame in DevTools.
+ // Use the chromeEventHandler as the target to catch events from all frames.
+ target: this.getChromeEventHandler(),
+ });
+ }
+
+ const shortcuts = this._windowHostShortcuts;
+
+ for (const item of Startup.KeyShortcuts) {
+ const { id, toolId, shortcut, modifiers } = item;
+ const electronKey = KeyShortcuts.parseXulKey(modifiers, shortcut);
+
+ if (id == "browserConsole") {
+ // Add key for toggling the browser console from the detached window
+ shortcuts.on(electronKey, () => {
+ BrowserConsoleManager.toggleBrowserConsole();
+ });
+ } else if (toolId) {
+ // KeyShortcuts contain tool-specific and global key shortcuts,
+ // here we only need to copy shortcut specific to each tool.
+ shortcuts.on(electronKey, () => {
+ this.selectTool(toolId, "key_shortcut").then(() =>
+ this.fireCustomKey(toolId)
+ );
+ });
+ }
+ }
+
+ // CmdOrCtrl+W is registered only when the toolbox is running in
+ // detached window. In the other case the entire browser tab
+ // is closed when the user uses this shortcut.
+ shortcuts.on(L10N.getStr("toolbox.closeToolbox.key"), this.closeToolbox);
+
+ // The others are only registered in window host type as for other hosts,
+ // these keys are already registered by devtools-startup.js
+ shortcuts.on(
+ L10N.getStr("toolbox.toggleToolboxF12.key"),
+ this.closeToolbox
+ );
+ if (AppConstants.platform == "macosx") {
+ shortcuts.on(
+ L10N.getStr("toolbox.toggleToolboxOSX.key"),
+ this.closeToolbox
+ );
+ } else {
+ shortcuts.on(L10N.getStr("toolbox.toggleToolbox.key"), this.closeToolbox);
+ }
+ },
+
+ _removeWindowHostShortcuts: function() {
+ if (this._windowHostShortcuts) {
+ this._windowHostShortcuts.destroy();
+ this._windowHostShortcuts = null;
+ }
+ },
+
+ _onContextMenu: function(e) {
+ // Handle context menu events in standard input elements: <input> and <textarea>.
+ // Also support for custom input elements using .devtools-input class
+ // (e.g. CodeMirror instances).
+ const isInInput =
+ e.originalTarget.closest("input[type=text]") ||
+ e.originalTarget.closest("input[type=search]") ||
+ e.originalTarget.closest("input:not([type])") ||
+ e.originalTarget.closest(".devtools-input") ||
+ e.originalTarget.closest("textarea");
+
+ const doc = e.originalTarget.ownerDocument;
+ const isHTMLPanel = doc.documentElement.namespaceURI === HTML_NS;
+
+ if (
+ // Context-menu events on input elements will use a custom context menu.
+ isInInput ||
+ // Context-menu events from HTML panels should not trigger the default
+ // browser context menu for HTML documents.
+ isHTMLPanel
+ ) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ if (isInInput) {
+ this.openTextBoxContextMenu(e.screenX, e.screenY);
+ }
+ },
+
+ _onMouseDown: function(e) {
+ const isMiddleClick = e.button === 1;
+ if (isMiddleClick) {
+ // Middle clicks will trigger the scroll lock feature to turn on.
+ // When the DevTools toolbox was running in an <iframe>, this behavior was
+ // disabled by default. When running in a <browser> element, we now need
+ // to catch and preventDefault() on those events.
+ e.preventDefault();
+ }
+ },
+
+ _getDebugTargetData: function() {
+ const url = new URL(this.win.location);
+ const searchParams = new this.win.URLSearchParams(url.search);
+
+ const targetType = searchParams.get("type") || DEBUG_TARGET_TYPES.TAB;
+
+ const remoteId = searchParams.get("remoteId");
+ const runtimeInfo = remoteClientManager.getRuntimeInfoByRemoteId(remoteId);
+ const connectionType = remoteClientManager.getConnectionTypeByRemoteId(
+ remoteId
+ );
+
+ return {
+ connectionType,
+ runtimeInfo,
+ targetType,
+ };
+ },
+
+ /**
+ * loading React modules when needed (to avoid performance penalties
+ * during Firefox start up time).
+ */
+ get React() {
+ return this.browserRequire("devtools/client/shared/vendor/react");
+ },
+
+ get ReactDOM() {
+ return this.browserRequire("devtools/client/shared/vendor/react-dom");
+ },
+
+ get ReactRedux() {
+ return this.browserRequire("devtools/client/shared/vendor/react-redux");
+ },
+
+ get ToolboxController() {
+ return this.browserRequire(
+ "devtools/client/framework/components/ToolboxController"
+ );
+ },
+
+ /**
+ * Unconditionally create and get the source map service.
+ */
+ _createSourceMapService: function() {
+ if (this._sourceMapService) {
+ return this._sourceMapService;
+ }
+ // Uses browser loader to access the `Worker` global.
+ const service = this.browserRequire(
+ "devtools/client/shared/source-map/index"
+ );
+
+ // Provide a wrapper for the service that reports errors more nicely.
+ this._sourceMapService = new Proxy(service, {
+ get: (target, name) => {
+ switch (name) {
+ case "getOriginalURLs":
+ return urlInfo => {
+ return target.getOriginalURLs(urlInfo).catch(text => {
+ const message = L10N.getFormatStr(
+ "toolbox.sourceMapFailure",
+ text,
+ urlInfo.url,
+ urlInfo.sourceMapURL
+ );
+ this.target.logWarningInPage(message, "source map");
+ // It's ok to swallow errors here, because a null
+ // result just means that no source map was found.
+ return null;
+ });
+ };
+
+ case "getOriginalSourceText":
+ return originalSourceId => {
+ return target
+ .getOriginalSourceText(originalSourceId)
+ .catch(error => {
+ const message = L10N.getFormatStr(
+ "toolbox.sourceMapSourceFailure",
+ error.message,
+ error.metadata ? error.metadata.url : "<unknown>"
+ );
+ this.target.logWarningInPage(message, "source map");
+ // Also replace the result with the error text.
+ // Note that this result has to have the same form
+ // as whatever the upstream getOriginalSourceText
+ // returns.
+ return {
+ text: message,
+ contentType: "text/plain",
+ };
+ });
+ };
+
+ case "applySourceMap":
+ return (generatedId, url, code, mappings) => {
+ return target
+ .applySourceMap(generatedId, url, code, mappings)
+ .then(async result => {
+ // If a tool has changed or introduced a source map
+ // (e.g, by pretty-printing a source), tell the
+ // source map URL service about the change, so that
+ // subscribers to that service can be updated as
+ // well.
+ if (this._sourceMapURLService) {
+ await this._sourceMapURLService.newSourceMapCreated(
+ generatedId
+ );
+ }
+ return result;
+ });
+ };
+
+ default:
+ return target[name];
+ }
+ },
+ });
+
+ this._sourceMapService.startSourceMapWorker(
+ SOURCE_MAP_WORKER,
+ SOURCE_MAP_WORKER_ASSETS
+ );
+ return this._sourceMapService;
+ },
+
+ /**
+ * A common access point for the client-side mapping service for source maps that
+ * any panel can use. This is a "low-level" API that connects to
+ * the source map worker.
+ */
+ get sourceMapService() {
+ return this._createSourceMapService();
+ },
+
+ /**
+ * A common access point for the client-side parser service that any panel can use.
+ */
+ get parserService() {
+ if (this._parserService) {
+ return this._parserService;
+ }
+
+ const {
+ ParserDispatcher,
+ } = require("devtools/client/debugger/src/workers/parser/index");
+
+ this._parserService = new ParserDispatcher();
+ this._parserService.start(
+ "resource://devtools/client/debugger/dist/parser-worker.js",
+ this.win
+ );
+ return this._parserService;
+ },
+
+ /**
+ * Clients wishing to use source maps but that want the toolbox to
+ * track the source and style sheet actor mapping can use this
+ * source map service. This is a higher-level service than the one
+ * returned by |sourceMapService|, in that it automatically tracks
+ * source and style sheet actor IDs.
+ */
+ get sourceMapURLService() {
+ if (this._sourceMapURLService) {
+ return this._sourceMapURLService;
+ }
+ const sourceMaps = this._createSourceMapService();
+ this._sourceMapURLService = new SourceMapURLService(this, sourceMaps);
+ return this._sourceMapURLService;
+ },
+
+ // Return HostType id for telemetry
+ _getTelemetryHostId: function() {
+ switch (this.hostType) {
+ case Toolbox.HostType.BOTTOM:
+ return 0;
+ case Toolbox.HostType.RIGHT:
+ return 1;
+ case Toolbox.HostType.WINDOW:
+ return 2;
+ case Toolbox.HostType.BROWSERTOOLBOX:
+ return 3;
+ case Toolbox.HostType.LEFT:
+ return 4;
+ case Toolbox.HostType.PAGE:
+ return 5;
+ default:
+ return 9;
+ }
+ },
+
+ // Return HostType string for telemetry
+ _getTelemetryHostString: function() {
+ switch (this.hostType) {
+ case Toolbox.HostType.BOTTOM:
+ return "bottom";
+ case Toolbox.HostType.LEFT:
+ return "left";
+ case Toolbox.HostType.RIGHT:
+ return "right";
+ case Toolbox.HostType.WINDOW:
+ return "window";
+ case Toolbox.HostType.PAGE:
+ return "page";
+ case Toolbox.HostType.BROWSERTOOLBOX:
+ return "other";
+ default:
+ return "bottom";
+ }
+ },
+
+ _pingTelemetry: function() {
+ this.telemetry.toolOpened("toolbox", this.sessionId, this);
+
+ this.telemetry
+ .getHistogramById(HOST_HISTOGRAM)
+ .add(this._getTelemetryHostId());
+
+ // Log current theme. The question we want to answer is:
+ // "What proportion of users use which themes?"
+ const currentTheme = Services.prefs.getCharPref("devtools.theme");
+ this.telemetry.keyedScalarAdd(CURRENT_THEME_SCALAR, currentTheme, 1);
+
+ const browserWin = this.topWindow;
+ this.telemetry.preparePendingEvent(browserWin, "open", "tools", null, [
+ "entrypoint",
+ "first_panel",
+ "host",
+ "shortcut",
+ "splitconsole",
+ "width",
+ "session_id",
+ ]);
+ this.telemetry.addEventProperty(
+ browserWin,
+ "open",
+ "tools",
+ null,
+ "host",
+ this._getTelemetryHostString()
+ );
+ },
+
+ /**
+ * Create a simple object to store the state of a toolbox button. The checked state of
+ * a button can be updated arbitrarily outside of the scope of the toolbar and its
+ * controllers. In order to simplify this interaction this object emits an
+ * "updatechecked" event any time the isChecked value is updated, allowing any consuming
+ * components to listen and respond to updates.
+ *
+ * @param {Object} options:
+ *
+ * @property {String} id - The id of the button or command.
+ * @property {String} className - An optional additional className for the button.
+ * @property {String} description - The value that will display as a tooltip and in
+ * the options panel for enabling/disabling.
+ * @property {Boolean} disabled - An optional disabled state for the button.
+ * @property {Function} onClick - The function to run when the button is activated by
+ * click or keyboard shortcut. First argument will be the 'click'
+ * event, and second argument is the toolbox instance.
+ * @property {Boolean} isInStartContainer - Buttons can either be placed at the start
+ * of the toolbar, or at the end.
+ * @property {Function} setup - Function run immediately to listen for events changing
+ * whenever the button is checked or unchecked. The toolbox object
+ * is passed as first argument and a callback is passed as second
+ * argument, to be called whenever the checked state changes.
+ * @property {Function} teardown - Function run on toolbox close to let a chance to
+ * unregister listeners set when `setup` was called and avoid
+ * memory leaks. The same arguments than `setup` function are
+ * passed to `teardown`.
+ * @property {Function} isTargetSupported - Function to automatically enable/disable
+ * the button based on the target. If the target don't support
+ * the button feature, this method should return false.
+ * @property {Function} isCurrentlyVisible - Function to automatically
+ * hide/show the button based on current state.
+ * @property {Function} isChecked - Optional function called to known if the button
+ * is toggled or not. The function should return true when
+ * the button should be displayed as toggled on.
+ */
+ _createButtonState: function(options) {
+ let isCheckedValue = false;
+ const {
+ id,
+ className,
+ description,
+ disabled,
+ onClick,
+ isInStartContainer,
+ setup,
+ teardown,
+ isTargetSupported,
+ isCurrentlyVisible,
+ isChecked,
+ onKeyDown,
+ experimentalURL,
+ } = options;
+ const toolbox = this;
+ const button = {
+ id,
+ className,
+ description,
+ disabled,
+ async onClick(event) {
+ if (typeof onClick == "function") {
+ await onClick(event, toolbox);
+ button.emit("updatechecked");
+ }
+ },
+ onKeyDown(event) {
+ if (typeof onKeyDown == "function") {
+ onKeyDown(event, toolbox);
+ }
+ },
+ isTargetSupported,
+ isCurrentlyVisible,
+ get isChecked() {
+ if (typeof isChecked == "function") {
+ return isChecked(toolbox);
+ }
+ return isCheckedValue;
+ },
+ set isChecked(value) {
+ // Note that if options.isChecked is given, this is ignored
+ isCheckedValue = value;
+ this.emit("updatechecked");
+ },
+ // The preference for having this button visible.
+ visibilityswitch: `devtools.${id}.enabled`,
+ // The toolbar has a container at the start and end of the toolbar for
+ // holding buttons. By default the buttons are placed in the end container.
+ isInStartContainer: !!isInStartContainer,
+ experimentalURL,
+ };
+ if (typeof setup == "function") {
+ const onChange = () => {
+ button.emit("updatechecked");
+ };
+ setup(this, onChange);
+ // Save a reference to the cleanup method that will unregister the onChange
+ // callback. Immediately bind the function argument so that we don't have to
+ // also save a reference to them.
+ button.teardown = teardown.bind(options, this, onChange);
+ }
+ button.isVisible = this._commandIsVisible(button);
+
+ EventEmitter.decorate(button);
+
+ return button;
+ },
+
+ _splitConsoleOnKeypress: function(e) {
+ if (e.keyCode === KeyCodes.DOM_VK_ESCAPE) {
+ this.toggleSplitConsole();
+ // If the debugger is paused, don't let the ESC key stop any pending navigation.
+ // If the host is page, don't let the ESC stop the load of the webconsole frame.
+ if (
+ this.threadFront.state == "paused" ||
+ this.hostType === Toolbox.HostType.PAGE
+ ) {
+ e.preventDefault();
+ }
+ }
+ },
+
+ /**
+ * Add a shortcut key that should work when a split console
+ * has focus to the toolbox.
+ *
+ * @param {String} key
+ * The electron key shortcut.
+ * @param {Function} handler
+ * The callback that should be called when the provided key shortcut is pressed.
+ * @param {String} whichTool
+ * The tool the key belongs to. The corresponding handler will only be triggered
+ * if this tool is active.
+ */
+ useKeyWithSplitConsole: function(key, handler, whichTool) {
+ this.shortcuts.on(key, event => {
+ if (this.currentToolId === whichTool && this.isSplitConsoleFocused()) {
+ handler();
+ event.preventDefault();
+ }
+ });
+ },
+
+ _addWindowListeners: function() {
+ this.win.addEventListener("unload", this.destroy);
+ this.win.addEventListener("message", this._onBrowserMessage, true);
+ },
+
+ _removeWindowListeners: function() {
+ // The host iframe's contentDocument may already be gone.
+ if (this.win) {
+ this.win.removeEventListener("unload", this.destroy);
+ this.win.removeEventListener("message", this._onBrowserMessage, true);
+ }
+ },
+
+ // Called whenever the chrome send a message
+ _onBrowserMessage: function(event) {
+ if (event.data && event.data.name === "switched-host") {
+ this._onSwitchedHost(event.data);
+ }
+ },
+
+ _registerOverlays: function() {
+ registerHarOverlay(this);
+ },
+
+ _saveSplitConsoleHeight: function() {
+ Services.prefs.setIntPref(
+ SPLITCONSOLE_HEIGHT_PREF,
+ this.webconsolePanel.height
+ );
+ },
+
+ /**
+ * Make sure that the console is showing up properly based on all the
+ * possible conditions.
+ * 1) If the console tab is selected, then regardless of split state
+ * it should take up the full height of the deck, and we should
+ * hide the deck and splitter.
+ * 2) If the console tab is not selected and it is split, then we should
+ * show the splitter, deck, and console.
+ * 3) If the console tab is not selected and it is *not* split,
+ * then we should hide the console and splitter, and show the deck
+ * at full height.
+ */
+ _refreshConsoleDisplay: function() {
+ const deck = this.doc.getElementById("toolbox-deck");
+ const webconsolePanel = this.webconsolePanel;
+ const splitter = this.doc.getElementById("toolbox-console-splitter");
+ const openedConsolePanel = this.currentToolId === "webconsole";
+
+ if (openedConsolePanel) {
+ deck.setAttribute("collapsed", "true");
+ splitter.setAttribute("hidden", "true");
+ webconsolePanel.removeAttribute("collapsed");
+ } else {
+ deck.removeAttribute("collapsed");
+ if (this.splitConsole) {
+ webconsolePanel.removeAttribute("collapsed");
+ splitter.removeAttribute("hidden");
+ } else {
+ webconsolePanel.setAttribute("collapsed", "true");
+ splitter.setAttribute("hidden", "true");
+ }
+ }
+ },
+
+ /**
+ * Handle any custom key events. Returns true if there was a custom key
+ * binding run.
+ * @param {string} toolId Which tool to run the command on (skip if not
+ * current)
+ */
+ fireCustomKey: function(toolId) {
+ const toolDefinition = gDevTools.getToolDefinition(toolId);
+
+ if (
+ toolDefinition.onkey &&
+ (this.currentToolId === toolId ||
+ (toolId == "webconsole" && this.splitConsole))
+ ) {
+ toolDefinition.onkey(this.getCurrentPanel(), this);
+ }
+ },
+
+ /**
+ * Build the notification box as soon as needed.
+ */
+ get notificationBox() {
+ if (!this._notificationBox) {
+ let { NotificationBox, PriorityLevels } = this.browserRequire(
+ "devtools/client/shared/components/NotificationBox"
+ );
+
+ NotificationBox = this.React.createFactory(NotificationBox);
+
+ // Render NotificationBox and assign priority levels to it.
+ const box = this.doc.getElementById("toolbox-notificationbox");
+ this._notificationBox = Object.assign(
+ this.ReactDOM.render(NotificationBox({}), box),
+ PriorityLevels
+ );
+ }
+ return this._notificationBox;
+ },
+
+ /**
+ * Build the options for changing hosts. Called every time
+ * the host changes.
+ */
+ _buildDockOptions: function() {
+ if (!this.target.isLocalTab) {
+ this.component.setDockOptionsEnabled(false);
+ this.component.setCanCloseToolbox(false);
+ return;
+ }
+
+ this.component.setDockOptionsEnabled(true);
+ this.component.setCanCloseToolbox(
+ this.hostType !== Toolbox.HostType.WINDOW
+ );
+
+ const sideEnabled = Services.prefs.getBoolPref(this._prefs.SIDE_ENABLED);
+
+ const hostTypes = [];
+ for (const type in Toolbox.HostType) {
+ const position = Toolbox.HostType[type];
+ if (
+ position == Toolbox.HostType.BROWSERTOOLBOX ||
+ position == Toolbox.HostType.PAGE ||
+ (!sideEnabled &&
+ (position == Toolbox.HostType.LEFT ||
+ position == Toolbox.HostType.RIGHT))
+ ) {
+ continue;
+ }
+
+ hostTypes.push({
+ position,
+ switchHost: this.switchHost.bind(this, position),
+ });
+ }
+
+ this.component.setCurrentHostType(this.hostType);
+ this.component.setHostTypes(hostTypes);
+ },
+
+ postMessage: function(msg) {
+ // We sometime try to send messages in middle of destroy(), where the
+ // toolbox iframe may already be detached.
+ if (!this._destroyer) {
+ // Toolbox document is still chrome and disallow identifying message
+ // origin via event.source as it is null. So use a custom id.
+ msg.frameId = this.frameId;
+ this.topWindow.postMessage(msg, "*");
+ }
+ },
+
+ /**
+ * Initiate ToolboxTabs React component and all it's properties. Do the initial render.
+ */
+ _buildTabs: async function() {
+ // Get the initial list of tab definitions. This list can be amended at a later time
+ // by tools registering themselves.
+ const definitions = gDevTools.getToolDefinitionArray();
+ definitions.forEach(definition => this._buildPanelForTool(definition));
+
+ // Get the definitions that will only affect the main tab area.
+ this.panelDefinitions = definitions.filter(
+ definition =>
+ definition.isTargetSupported(this.target) && definition.id !== "options"
+ );
+
+ // Do async lookup of disable pop-up auto-hide state.
+ if (this.disableAutohideAvailable) {
+ const disable = await this._isDisableAutohideEnabled();
+ this.component.setDisableAutohide(disable);
+ }
+ },
+
+ _mountReactComponent: function() {
+ // Ensure the toolbar doesn't try to render until the tool is ready.
+ const element = this.React.createElement(this.ToolboxController, {
+ L10N,
+ currentToolId: this.currentToolId,
+ selectTool: this.selectTool,
+ toggleOptions: this.toggleOptions,
+ toggleSplitConsole: this.toggleSplitConsole,
+ toggleNoAutohide: this.toggleNoAutohide,
+ closeToolbox: this.closeToolbox,
+ focusButton: this._onToolbarFocus,
+ toolbox: this,
+ onTabsOrderUpdated: this._onTabsOrderUpdated,
+ });
+
+ this.component = this.ReactDOM.render(element, this._componentMount);
+ },
+
+ /**
+ * Reset tabindex attributes across all focusable elements inside the toolbar.
+ * Only have one element with tabindex=0 at a time to make sure that tabbing
+ * results in navigating away from the toolbar container.
+ * @param {FocusEvent} event
+ */
+ _onToolbarFocus: function(id) {
+ this.component.setFocusedButton(id);
+ },
+
+ /**
+ * On left/right arrow press, attempt to move the focus inside the toolbar to
+ * the previous/next focusable element. This is not in the React component
+ * as it is difficult to coordinate between different component elements.
+ * The components are responsible for setting the correct tabindex value
+ * for if they are the focused element.
+ * @param {KeyboardEvent} event
+ */
+ _onToolbarArrowKeypress: function(event) {
+ const { key, target, ctrlKey, shiftKey, altKey, metaKey } = event;
+
+ // If any of the modifier keys are pressed do not attempt navigation as it
+ // might conflict with global shortcuts (Bug 1327972).
+ if (ctrlKey || shiftKey || altKey || metaKey) {
+ return;
+ }
+
+ const buttons = [...this._tabBar.querySelectorAll("button")];
+ const curIndex = buttons.indexOf(target);
+
+ if (curIndex === -1) {
+ console.warn(
+ target +
+ " is not found among Developer Tools tab bar " +
+ "focusable elements."
+ );
+ return;
+ }
+
+ let newTarget;
+ const firstTabIndex = 0;
+ const lastTabIndex = buttons.length - 1;
+ const nextOrLastTabIndex = Math.min(lastTabIndex, curIndex + 1);
+ const previousOrFirstTabIndex = Math.max(firstTabIndex, curIndex - 1);
+ const ltr = this.direction === "ltr";
+
+ if (key === "ArrowLeft") {
+ // Do nothing if already at the beginning.
+ if (
+ (ltr && curIndex === firstTabIndex) ||
+ (!ltr && curIndex === lastTabIndex)
+ ) {
+ return;
+ }
+ newTarget = buttons[ltr ? previousOrFirstTabIndex : nextOrLastTabIndex];
+ } else if (key === "ArrowRight") {
+ // Do nothing if already at the end.
+ if (
+ (ltr && curIndex === lastTabIndex) ||
+ (!ltr && curIndex === firstTabIndex)
+ ) {
+ return;
+ }
+ newTarget = buttons[ltr ? nextOrLastTabIndex : previousOrFirstTabIndex];
+ } else {
+ return;
+ }
+
+ newTarget.focus();
+
+ event.preventDefault();
+ event.stopPropagation();
+ },
+
+ /**
+ * Add buttons to the UI as specified in devtools/client/definitions.js
+ */
+ _buildButtons() {
+ // Beyond the normal preference filtering
+ this.toolbarButtons = [
+ this._buildErrorCountButton(),
+ this._buildPickerButton(),
+ this._buildFrameButton(),
+ ];
+
+ ToolboxButtons.forEach(definition => {
+ const button = this._createButtonState(definition);
+ this.toolbarButtons.push(button);
+ });
+
+ this.component.setToolboxButtons(this.toolbarButtons);
+ },
+
+ /**
+ * Button to select a frame for the inspector to target.
+ */
+ _buildFrameButton() {
+ this.frameButton = this._createButtonState({
+ id: "command-button-frames",
+ description: L10N.getStr("toolbox.frames.tooltip"),
+ isTargetSupported: target => {
+ return target.traits.frames;
+ },
+ isCurrentlyVisible: () => {
+ const hasFrames = this.frameMap.size > 1;
+ const isOnOptionsPanel = this.currentToolId === "options";
+ return hasFrames || isOnOptionsPanel;
+ },
+ });
+
+ return this.frameButton;
+ },
+
+ /**
+ * Button to display the number of errors.
+ */
+ _buildErrorCountButton() {
+ this.errorCountButton = this._createButtonState({
+ id: "command-button-errorcount",
+ isInStartContainer: false,
+ isTargetSupported: target => true,
+ description: L10N.getStr("toolbox.errorCountButton.description"),
+ });
+ // Use updateErrorCountButton to set some properties so we don't have to repeat
+ // the logic here.
+ this.updateErrorCountButton();
+
+ return this.errorCountButton;
+ },
+
+ /**
+ * Toggle the picker, but also decide whether or not the highlighter should
+ * focus the window. This is only desirable when the toolbox is mounted to the
+ * window. When devtools is free floating, then the target window should not
+ * pop in front of the viewer when the picker is clicked.
+ *
+ * Note: Toggle picker can be overwritten by panel other than the inspector to
+ * allow for custom picker behaviour.
+ */
+ _onPickerClick: async function() {
+ const focus =
+ this.hostType === Toolbox.HostType.BOTTOM ||
+ this.hostType === Toolbox.HostType.LEFT ||
+ this.hostType === Toolbox.HostType.RIGHT;
+ const currentPanel = this.getCurrentPanel();
+ if (currentPanel.togglePicker) {
+ currentPanel.togglePicker(focus);
+ } else {
+ this.nodePicker.togglePicker(focus);
+ }
+ },
+
+ /**
+ * If the picker is activated, then allow the Escape key to deactivate the
+ * functionality instead of the default behavior of toggling the console.
+ */
+ _onPickerKeypress: function(event) {
+ if (event.keyCode === KeyCodes.DOM_VK_ESCAPE) {
+ const currentPanel = this.getCurrentPanel();
+ if (currentPanel.cancelPicker) {
+ currentPanel.cancelPicker();
+ } else {
+ this.nodePicker.cancel();
+ }
+ // Stop the console from toggling.
+ event.stopImmediatePropagation();
+ }
+ },
+
+ _onPickerStarting: async function() {
+ this.tellRDMAboutPickerState(true, PICKER_TYPES.ELEMENT);
+ this.pickerButton.isChecked = true;
+ await this.selectTool("inspector", "inspect_dom");
+ // turn off color picker when node picker is starting
+ this.getPanel("inspector").hideEyeDropper();
+ this.on("select", this.nodePicker.stop);
+ },
+
+ _onPickerStarted: async function() {
+ this.doc.addEventListener("keypress", this._onPickerKeypress, true);
+ },
+
+ _onPickerStopped: function() {
+ this.tellRDMAboutPickerState(false, PICKER_TYPES.ELEMENT);
+ this.off("select", this.nodePicker.stop);
+ this.doc.removeEventListener("keypress", this._onPickerKeypress, true);
+ this.pickerButton.isChecked = false;
+ },
+
+ /**
+ * When the picker is canceled, make sure the toolbox
+ * gets the focus.
+ */
+ _onPickerCanceled: function() {
+ if (this.hostType !== Toolbox.HostType.WINDOW) {
+ this.win.focus();
+ }
+ },
+
+ _onPickerPicked: function(nodeFront) {
+ this.selection.setNodeFront(nodeFront, { reason: "picker-node-picked" });
+ },
+
+ _onPickerPreviewed: function(nodeFront) {
+ this.selection.setNodeFront(nodeFront, { reason: "picker-node-previewed" });
+ },
+
+ /**
+ * RDM sometimes simulates touch events. For this to work correctly at all times, it
+ * needs to know when the picker is active or not.
+ * This method communicates with the RDM Manager if it exists.
+ *
+ * @param {Boolean} state
+ * @param {String} pickerType
+ * One of devtools/shared/picker-constants
+ */
+ tellRDMAboutPickerState: async function(state, pickerType) {
+ const { localTab } = this.target;
+
+ if (!ResponsiveUIManager.isActiveForTab(localTab)) {
+ return;
+ }
+
+ const ui = ResponsiveUIManager.getResponsiveUIForTab(localTab);
+ await ui.responsiveFront.setElementPickerState(state, pickerType);
+ },
+
+ /**
+ * The element picker button enables the ability to select a DOM node by clicking
+ * it on the page.
+ */
+ _buildPickerButton() {
+ this.pickerButton = this._createButtonState({
+ id: "command-button-pick",
+ description: this._getPickerTooltip(),
+ onClick: this._onPickerClick,
+ isInStartContainer: true,
+ isTargetSupported: target => {
+ return target.traits.frames;
+ },
+ });
+
+ return this.pickerButton;
+ },
+
+ /**
+ * Get the tooltip for the element picker button.
+ * It has multiple possible keyboard shortcuts for macOS.
+ *
+ * @return {String}
+ */
+ _getPickerTooltip() {
+ let shortcut = L10N.getStr("toolbox.elementPicker.key");
+ shortcut = KeyShortcuts.parseElectronKey(this.win, shortcut);
+ shortcut = KeyShortcuts.stringify(shortcut);
+ const shortcutMac = L10N.getStr("toolbox.elementPicker.mac.key");
+ const isMac = Services.appinfo.OS === "Darwin";
+ const label = isMac
+ ? "toolbox.elementPicker.mac.tooltip"
+ : "toolbox.elementPicker.tooltip";
+
+ return isMac
+ ? L10N.getFormatStr(label, shortcut, shortcutMac)
+ : L10N.getFormatStr(label, shortcut);
+ },
+
+ /**
+ * Apply the current cache setting from devtools.cache.disabled to this
+ * toolbox's tab.
+ */
+ _applyCacheSettings: async function() {
+ const pref = "devtools.cache.disabled";
+ const cacheDisabled = Services.prefs.getBoolPref(pref);
+
+ await this.target.reconfigure({
+ options: {
+ cacheDisabled: cacheDisabled,
+ },
+ });
+
+ // This event is only emitted for tests in order to know when to reload
+ if (flags.testing) {
+ this.emit("cache-reconfigured");
+ }
+ },
+
+ /**
+ * Apply the current service workers testing setting from
+ * devtools.serviceWorkers.testing.enabled to this toolbox's tab.
+ */
+ _applyServiceWorkersTestingSettings: function() {
+ const pref = "devtools.serviceWorkers.testing.enabled";
+ const serviceWorkersTestingEnabled =
+ Services.prefs.getBoolPref(pref) || false;
+
+ this.target.reconfigure({
+ options: {
+ serviceWorkersTestingEnabled: serviceWorkersTestingEnabled,
+ },
+ });
+ },
+
+ /**
+ * Update the visibility of the buttons.
+ */
+ updateToolboxButtonsVisibility() {
+ this.toolbarButtons.forEach(button => {
+ button.isVisible = this._commandIsVisible(button);
+ });
+ this.component.setToolboxButtons(this.toolbarButtons);
+ },
+
+ /**
+ * Update the buttons.
+ */
+ updateToolboxButtons() {
+ const inspectorFront = this.target.getCachedFront("inspector");
+ // two of the buttons have highlighters that need to be cleared
+ // on will-navigate, otherwise we hold on to the stale highlighter
+ const hasHighlighters =
+ inspectorFront &&
+ (inspectorFront.hasHighlighter("RulersHighlighter") ||
+ inspectorFront.hasHighlighter("MeasuringToolHighlighter"));
+ if (hasHighlighters || this.isPaintFlashing) {
+ if (this.isPaintFlashing) {
+ this.togglePaintFlashing();
+ }
+ if (hasHighlighters) {
+ inspectorFront.destroyHighlighters();
+ }
+ this.component.setToolboxButtons(this.toolbarButtons);
+ }
+ },
+
+ /**
+ * Set paintflashing to enabled or disabled for this toolbox's tab.
+ */
+ togglePaintFlashing: function() {
+ if (this.isPaintFlashing) {
+ this.telemetry.toolOpened("paintflashing", this.sessionId, this);
+ } else {
+ this.telemetry.toolClosed("paintflashing", this.sessionId, this);
+ }
+ this.isPaintFlashing = !this.isPaintFlashing;
+ return this.target.reconfigure({
+ options: {
+ paintFlashing: this.isPaintFlashing,
+ },
+ });
+ },
+
+ /**
+ * Visually update picker button.
+ * This function is called on every "select" event. Newly selected panel can
+ * update the visual state of the picker button such as disabled state,
+ * additional CSS classes (className), and tooltip (description).
+ */
+ updatePickerButton() {
+ const button = this.pickerButton;
+ const currentPanel = this.getCurrentPanel();
+
+ if (currentPanel?.updatePickerButton) {
+ currentPanel.updatePickerButton();
+ } else {
+ // If the current panel doesn't define a custom updatePickerButton,
+ // revert the button to its default state
+ button.description = this._getPickerTooltip();
+ button.className = null;
+ button.disabled = null;
+ }
+ },
+
+ /**
+ * Update the visual state of the Frame picker button.
+ */
+ updateFrameButton() {
+ if (this.currentToolId === "options" && this.frameMap.size <= 1) {
+ // If the button is only visible because the user is on the Options panel, disable
+ // the button and set an appropriate description.
+ this.frameButton.disabled = true;
+ this.frameButton.description = L10N.getStr(
+ "toolbox.frames.disabled.tooltip"
+ );
+ } else {
+ // Otherwise, enable the button and update the description.
+ this.frameButton.disabled = false;
+ this.frameButton.description = L10N.getStr("toolbox.frames.tooltip");
+ }
+
+ // Highlight the button when a child frame is selected and visible.
+ const selectedFrame = this.frameMap.get(this.selectedFrameId) || {};
+ const isVisible = this._commandIsVisible(this.frameButton);
+
+ this.frameButton.isVisible = isVisible;
+
+ if (isVisible) {
+ this.frameButton.isChecked = selectedFrame.parentID != null;
+ }
+ },
+
+ updateErrorCountButton() {
+ this.errorCountButton.isVisible =
+ this._commandIsVisible(this.errorCountButton) && this._errorCount > 0;
+ this.errorCountButton.errorCount = this._errorCount;
+ },
+
+ /**
+ * Ensure the visibility of each toolbox button matches the preference value.
+ */
+ _commandIsVisible: function(button) {
+ const { isTargetSupported, isCurrentlyVisible, visibilityswitch } = button;
+
+ if (!Services.prefs.getBoolPref(visibilityswitch, true)) {
+ return false;
+ }
+
+ if (isTargetSupported && !isTargetSupported(this.target)) {
+ return false;
+ }
+
+ if (isCurrentlyVisible && !isCurrentlyVisible()) {
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Build a panel for a tool definition.
+ *
+ * @param {string} toolDefinition
+ * Tool definition of the tool to build a tab for.
+ */
+ _buildPanelForTool: function(toolDefinition) {
+ if (!toolDefinition.isTargetSupported(this.target)) {
+ return;
+ }
+
+ const deck = this.doc.getElementById("toolbox-deck");
+ const id = toolDefinition.id;
+
+ if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) {
+ toolDefinition.ordinal = MAX_ORDINAL;
+ }
+
+ if (!toolDefinition.bgTheme) {
+ toolDefinition.bgTheme = "theme-toolbar";
+ }
+ const panel = this.doc.createXULElement("vbox");
+ panel.className = "toolbox-panel " + toolDefinition.bgTheme;
+
+ // There is already a container for the webconsole frame.
+ if (!this.doc.getElementById("toolbox-panel-" + id)) {
+ panel.id = "toolbox-panel-" + id;
+ }
+
+ deck.appendChild(panel);
+ },
+
+ /**
+ * Lazily created map of the additional tools registered to this toolbox.
+ *
+ * @returns {Map<string, object>}
+ * a map of the tools definitions registered to this
+ * particular toolbox (the key is the toolId string, the value
+ * is the tool definition plain javascript object).
+ */
+ get additionalToolDefinitions() {
+ if (!this._additionalToolDefinitions) {
+ this._additionalToolDefinitions = new Map();
+ }
+
+ return this._additionalToolDefinitions;
+ },
+
+ /**
+ * Retrieve the array of the additional tools registered to this toolbox.
+ *
+ * @return {Array<object>}
+ * the array of additional tool definitions registered on this toolbox.
+ */
+ getAdditionalTools() {
+ if (this._additionalToolDefinitions) {
+ return Array.from(this.additionalToolDefinitions.values());
+ }
+ return [];
+ },
+
+ /**
+ * Get the additional tools that have been registered and are visible.
+ *
+ * @return {Array<object>}
+ * the array of additional tool definitions registered on this toolbox.
+ */
+ getVisibleAdditionalTools() {
+ return this.visibleAdditionalTools.map(toolId =>
+ this.additionalToolDefinitions.get(toolId)
+ );
+ },
+
+ /**
+ * Test the existence of a additional tools registered to this toolbox by tool id.
+ *
+ * @param {string} toolId
+ * the id of the tool to test for existence.
+ *
+ * @return {boolean}
+ *
+ */
+ hasAdditionalTool(toolId) {
+ return this.additionalToolDefinitions.has(toolId);
+ },
+
+ /**
+ * Register and load an additional tool on this particular toolbox.
+ *
+ * @param {object} definition
+ * the additional tool definition to register and add to this toolbox.
+ */
+ addAdditionalTool(definition) {
+ if (!definition.id) {
+ throw new Error("Tool definition id is missing");
+ }
+
+ if (this.isToolRegistered(definition.id)) {
+ throw new Error("Tool definition already registered: " + definition.id);
+ }
+
+ this.additionalToolDefinitions.set(definition.id, definition);
+ this.visibleAdditionalTools = [
+ ...this.visibleAdditionalTools,
+ definition.id,
+ ];
+
+ const buildPanel = () => this._buildPanelForTool(definition);
+
+ if (this.isReady) {
+ buildPanel();
+ } else {
+ this.once("ready", buildPanel);
+ }
+ },
+
+ /**
+ * Retrieve the registered inspector extension sidebars
+ * (used by the inspector panel during its deferred initialization).
+ */
+ get inspectorExtensionSidebars() {
+ return this._inspectorExtensionSidebars;
+ },
+
+ /**
+ * Register an extension sidebar for the inspector panel.
+ *
+ * @param {String} id
+ * An unique sidebar id
+ * @param {Object} options
+ * @param {String} options.title
+ * A title for the sidebar
+ */
+ async registerInspectorExtensionSidebar(id, options) {
+ this._inspectorExtensionSidebars.set(id, options);
+
+ // Defer the extension sidebar creation if the inspector
+ // has not been created yet (and do not create the inspector
+ // only to register an extension sidebar).
+ if (!this.target.getCachedFront("inspector")) {
+ return;
+ }
+
+ const inspector = this.getPanel("inspector");
+ inspector.addExtensionSidebar(id, options);
+ },
+
+ /**
+ * Unregister an extension sidebar for the inspector panel.
+ *
+ * @param {String} id
+ * An unique sidebar id
+ */
+ unregisterInspectorExtensionSidebar(id) {
+ // Unregister the sidebar from the toolbox if the toolbox is not already
+ // being destroyed (otherwise we would trigger a re-rendering of the
+ // inspector sidebar tabs while the toolbox is going away).
+ if (this._destroyer) {
+ return;
+ }
+
+ const sidebarDef = this._inspectorExtensionSidebars.get(id);
+ if (!sidebarDef) {
+ return;
+ }
+
+ this._inspectorExtensionSidebars.delete(id);
+
+ // Remove the created sidebar instance if the inspector panel
+ // has been already created.
+ if (!this.target.getCachedFront("inspector")) {
+ return;
+ }
+
+ const inspector = this.getPanel("inspector");
+ inspector.removeExtensionSidebar(id);
+ },
+
+ /**
+ * Unregister and unload an additional tool from this particular toolbox.
+ *
+ * @param {string} toolId
+ * the id of the additional tool to unregister and remove.
+ */
+ removeAdditionalTool(toolId) {
+ // Early exit if the toolbox is already destroying itself.
+ if (this._destroyer) {
+ return;
+ }
+
+ if (!this.hasAdditionalTool(toolId)) {
+ throw new Error(
+ "Tool definition not registered to this toolbox: " + toolId
+ );
+ }
+
+ this.additionalToolDefinitions.delete(toolId);
+ this.visibleAdditionalTools = this.visibleAdditionalTools.filter(
+ id => id !== toolId
+ );
+ this.unloadTool(toolId);
+ },
+
+ /**
+ * Ensure the tool with the given id is loaded.
+ *
+ * @param {string} id
+ * The id of the tool to load.
+ */
+ loadTool: function(id) {
+ let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
+ if (iframe) {
+ const panel = this._toolPanels.get(id);
+ return new Promise(resolve => {
+ if (panel) {
+ resolve(panel);
+ } else {
+ this.once(id + "-ready", initializedPanel => {
+ resolve(initializedPanel);
+ });
+ }
+ });
+ }
+
+ return new Promise((resolve, reject) => {
+ // Retrieve the tool definition (from the global or the per-toolbox tool maps)
+ const definition = this.getToolDefinition(id);
+
+ if (!definition) {
+ reject(new Error("no such tool id " + id));
+ return;
+ }
+
+ iframe = this.doc.createXULElement("iframe");
+ iframe.className = "toolbox-panel-iframe";
+ iframe.id = "toolbox-panel-iframe-" + id;
+ iframe.setAttribute("flex", 1);
+ iframe.setAttribute("forceOwnRefreshDriver", "");
+ iframe.tooltip = "aHTMLTooltip";
+ iframe.style.visibility = "hidden";
+
+ gDevTools.emit(id + "-init", this, iframe);
+ this.emit(id + "-init", iframe);
+
+ // If no parent yet, append the frame into default location.
+ if (!iframe.parentNode) {
+ const vbox = this.doc.getElementById("toolbox-panel-" + id);
+ vbox.appendChild(iframe);
+ vbox.visibility = "visible";
+ }
+
+ const onLoad = async () => {
+ // Prevent flicker while loading by waiting to make visible until now.
+ iframe.style.visibility = "visible";
+
+ // Try to set the dir attribute as early as possible.
+ this.setIframeDocumentDir(iframe);
+
+ // The build method should return a panel instance, so events can
+ // be fired with the panel as an argument. However, in order to keep
+ // backward compatibility with existing extensions do a check
+ // for a promise return value.
+ let built = definition.build(iframe.contentWindow, this);
+
+ if (!(typeof built.then == "function")) {
+ const panel = built;
+ iframe.panel = panel;
+
+ // The panel instance is expected to fire (and listen to) various
+ // framework events, so make sure it's properly decorated with
+ // appropriate API (on, off, once, emit).
+ // In this case we decorate panel instances directly returned by
+ // the tool definition 'build' method.
+ if (typeof panel.emit == "undefined") {
+ EventEmitter.decorate(panel);
+ }
+
+ gDevTools.emit(id + "-build", this, panel);
+ this.emit(id + "-build", panel);
+
+ // The panel can implement an 'open' method for asynchronous
+ // initialization sequence.
+ if (typeof panel.open == "function") {
+ built = panel.open();
+ } else {
+ built = new Promise(resolve => {
+ resolve(panel);
+ });
+ }
+ }
+
+ // Wait till the panel is fully ready and fire 'ready' events.
+ promise.resolve(built).then(panel => {
+ this._toolPanels.set(id, panel);
+
+ // Make sure to decorate panel object with event API also in case
+ // where the tool definition 'build' method returns only a promise
+ // and the actual panel instance is available as soon as the
+ // promise is resolved.
+ if (typeof panel.emit == "undefined") {
+ EventEmitter.decorate(panel);
+ }
+
+ gDevTools.emit(id + "-ready", this, panel);
+ this.emit(id + "-ready", panel);
+
+ resolve(panel);
+ }, console.error);
+ };
+
+ iframe.setAttribute("src", definition.url);
+ if (definition.panelLabel) {
+ iframe.setAttribute("aria-label", definition.panelLabel);
+ }
+
+ // Depending on the host, iframe.contentWindow is not always
+ // defined at this moment. If it is not defined, we use an
+ // event listener on the iframe DOM node. If it's defined,
+ // we use the chromeEventHandler. We can't use a listener
+ // on the DOM node every time because this won't work
+ // if the (xul chrome) iframe is loaded in a content docshell.
+ if (iframe.contentWindow) {
+ DOMHelpers.onceDOMReady(iframe.contentWindow, onLoad);
+ } else {
+ const callback = () => {
+ iframe.removeEventListener("DOMContentLoaded", callback);
+ onLoad();
+ };
+
+ iframe.addEventListener("DOMContentLoaded", callback);
+ }
+ });
+ },
+
+ /**
+ * Set the dir attribute on the content document element of the provided iframe.
+ *
+ * @param {IFrameElement} iframe
+ */
+ setIframeDocumentDir: function(iframe) {
+ const docEl = iframe.contentWindow?.document.documentElement;
+ if (!docEl || docEl.namespaceURI !== HTML_NS) {
+ // Bail out if the content window or document is not ready or if the document is not
+ // HTML.
+ return;
+ }
+
+ if (docEl.hasAttribute("dir")) {
+ // Set the dir attribute value only if dir is already present on the document.
+ docEl.setAttribute("dir", this.direction);
+ }
+ },
+
+ /**
+ * Mark all in collection as unselected; and id as selected
+ * @param {string} collection
+ * DOM collection of items
+ * @param {string} id
+ * The Id of the item within the collection to select
+ */
+ selectSingleNode: function(collection, id) {
+ [...collection].forEach(node => {
+ if (node.id === id) {
+ node.setAttribute("selected", "true");
+ node.setAttribute("aria-selected", "true");
+ } else {
+ node.removeAttribute("selected");
+ node.removeAttribute("aria-selected");
+ }
+ // The webconsole panel is in a special location due to split console
+ if (!node.id) {
+ node = this.webconsolePanel;
+ }
+
+ const iframe = node.querySelector(".toolbox-panel-iframe");
+ if (iframe) {
+ let visible = node.id == id;
+ // Prevents hiding the split-console if it is currently enabled
+ if (node == this.webconsolePanel && this.splitConsole) {
+ visible = true;
+ }
+ this.setIframeVisible(iframe, visible);
+ }
+ });
+ },
+
+ /**
+ * Make a privileged iframe visible/hidden.
+ *
+ * For now, XUL Iframes loading chrome documents (i.e. <iframe type!="content" />)
+ * can't be hidden at platform level. And so don't support 'visibilitychange' event.
+ *
+ * This helper workarounds that by at least being able to send these kind of events.
+ * It will help panel react differently depending on them being displayed or in
+ * background.
+ */
+ setIframeVisible: function(iframe, visible) {
+ const state = visible ? "visible" : "hidden";
+ const win = iframe.contentWindow;
+ const doc = win.document;
+ if (doc.visibilityState != state) {
+ // 1) Overload document's `visibilityState` attribute
+ // Use defineProperty, as by default `document.visbilityState` is read only.
+ Object.defineProperty(doc, "visibilityState", {
+ value: state,
+ configurable: true,
+ });
+
+ // 2) Fake the 'visibilitychange' event
+ doc.dispatchEvent(new win.Event("visibilitychange"));
+ }
+ },
+
+ /**
+ * Switch to the tool with the given id
+ *
+ * @param {string} id
+ * The id of the tool to switch to
+ * @param {string} reason
+ * Reason the tool was opened
+ */
+ selectTool: function(id, reason = "unknown") {
+ this.emit("panel-changed");
+
+ if (this.currentToolId == id) {
+ const panel = this._toolPanels.get(id);
+ if (panel) {
+ // We have a panel instance, so the tool is already fully loaded.
+
+ // re-focus tool to get key events again
+ this.focusTool(id);
+
+ // Return the existing panel in order to have a consistent return value.
+ return promise.resolve(panel);
+ }
+ // Otherwise, if there is no panel instance, it is still loading,
+ // so we are racing another call to selectTool with the same id.
+ return this.once("select").then(() =>
+ promise.resolve(this._toolPanels.get(id))
+ );
+ }
+
+ if (!this.isReady) {
+ throw new Error("Can't select tool, wait for toolbox 'ready' event");
+ }
+
+ // Check if the tool exists.
+ if (
+ this.panelDefinitions.find(definition => definition.id === id) ||
+ id === "options" ||
+ this.additionalToolDefinitions.get(id)
+ ) {
+ if (this.currentToolId) {
+ this.telemetry.toolClosed(this.currentToolId, this.sessionId, this);
+ }
+
+ this._pingTelemetrySelectTool(id, reason);
+ } else {
+ throw new Error("No tool found");
+ }
+
+ // and select the right iframe
+ const toolboxPanels = this.doc.querySelectorAll(".toolbox-panel");
+ this.selectSingleNode(toolboxPanels, "toolbox-panel-" + id);
+
+ this.lastUsedToolId = this.currentToolId;
+ this.currentToolId = id;
+ this._refreshConsoleDisplay();
+ if (id != "options") {
+ Services.prefs.setCharPref(this._prefs.LAST_TOOL, id);
+ }
+
+ return this.loadTool(id).then(panel => {
+ // focus the tool's frame to start receiving key events
+ this.focusTool(id);
+
+ this.emit("select", id);
+ this.emit(id + "-selected", panel);
+ return panel;
+ });
+ },
+
+ _pingTelemetrySelectTool(id, reason) {
+ const width = Math.ceil(this.win.outerWidth / 50) * 50;
+ const panelName = this.getTelemetryPanelNameOrOther(id);
+ const prevPanelName = this.getTelemetryPanelNameOrOther(this.currentToolId);
+ const cold = !this.getPanel(id);
+ const pending = [
+ "host",
+ "width",
+ "start_state",
+ "panel_name",
+ "cold",
+ "session_id",
+ ];
+
+ // On first load this.currentToolId === undefined so we need to skip sending
+ // a devtools.main.exit telemetry event.
+ if (this.currentToolId) {
+ this.telemetry.recordEvent("exit", prevPanelName, null, {
+ host: this._hostType,
+ width: width,
+ panel_name: prevPanelName,
+ next_panel: panelName,
+ reason: reason,
+ session_id: this.sessionId,
+ });
+ }
+
+ this.telemetry.addEventProperties(this.topWindow, "open", "tools", null, {
+ width: width,
+ session_id: this.sessionId,
+ });
+
+ if (id === "webconsole") {
+ pending.push("message_count");
+ }
+
+ this.telemetry.preparePendingEvent(this, "enter", panelName, null, pending);
+
+ this.telemetry.addEventProperties(this, "enter", panelName, null, {
+ host: this._hostType,
+ start_state: reason,
+ panel_name: panelName,
+ cold: cold,
+ session_id: this.sessionId,
+ });
+
+ if (reason !== "initial_panel") {
+ const width = Math.ceil(this.win.outerWidth / 50) * 50;
+ this.telemetry.addEventProperty(
+ this,
+ "enter",
+ panelName,
+ null,
+ "width",
+ width
+ );
+ }
+
+ // Cold webconsole event message_count is handled in
+ // devtools/client/webconsole/webconsole-wrapper.js
+ if (!cold && id === "webconsole") {
+ this.telemetry.addEventProperty(
+ this,
+ "enter",
+ "webconsole",
+ null,
+ "message_count",
+ 0
+ );
+ }
+
+ this.telemetry.toolOpened(id, this.sessionId, this);
+ },
+
+ /**
+ * Focus a tool's panel by id
+ * @param {string} id
+ * The id of tool to focus
+ */
+ focusTool: function(id, state = true) {
+ const iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
+
+ if (state) {
+ iframe.focus();
+ } else {
+ iframe.blur();
+ }
+ },
+
+ /**
+ * Focus split console's input line
+ */
+ focusConsoleInput: function() {
+ const consolePanel = this.getPanel("webconsole");
+ if (consolePanel) {
+ consolePanel.focusInput();
+ }
+ },
+
+ /**
+ * If the console is split and we are focusing an element outside
+ * of the console, then store the newly focused element, so that
+ * it can be restored once the split console closes.
+ */
+ _onFocus: function({ originalTarget }) {
+ // Ignore any non element nodes, or any elements contained
+ // within the webconsole frame.
+ const webconsoleURL = gDevTools.getToolDefinition("webconsole").url;
+ if (
+ originalTarget.nodeType !== 1 ||
+ originalTarget.baseURI === webconsoleURL
+ ) {
+ return;
+ }
+
+ this._lastFocusedElement = originalTarget;
+ },
+
+ _onTabsOrderUpdated: function() {
+ this._combineAndSortPanelDefinitions();
+ },
+
+ /**
+ * Opens the split console.
+ *
+ * @returns {Promise} a promise that resolves once the tool has been
+ * loaded and focused.
+ */
+ openSplitConsole: function() {
+ this._splitConsole = true;
+ Services.prefs.setBoolPref(SPLITCONSOLE_ENABLED_PREF, true);
+ this._refreshConsoleDisplay();
+
+ // Ensure split console is visible if console was already loaded in background
+ const iframe = this.webconsolePanel.querySelector(".toolbox-panel-iframe");
+ if (iframe) {
+ this.setIframeVisible(iframe, true);
+ }
+
+ return this.loadTool("webconsole").then(() => {
+ this.component.setIsSplitConsoleActive(true);
+ this.telemetry.recordEvent("activate", "split_console", null, {
+ host: this._getTelemetryHostString(),
+ width: Math.ceil(this.win.outerWidth / 50) * 50,
+ session_id: this.sessionId,
+ });
+ this.emit("split-console");
+ this.focusConsoleInput();
+ });
+ },
+
+ /**
+ * Closes the split console.
+ *
+ * @returns {Promise} a promise that resolves once the tool has been
+ * closed.
+ */
+ closeSplitConsole: function() {
+ this._splitConsole = false;
+ Services.prefs.setBoolPref(SPLITCONSOLE_ENABLED_PREF, false);
+ this._refreshConsoleDisplay();
+ this.component.setIsSplitConsoleActive(false);
+
+ this.telemetry.recordEvent("deactivate", "split_console", null, {
+ host: this._getTelemetryHostString(),
+ width: Math.ceil(this.win.outerWidth / 50) * 50,
+ session_id: this.sessionId,
+ });
+
+ this.emit("split-console");
+
+ if (this._lastFocusedElement) {
+ this._lastFocusedElement.focus();
+ }
+ return promise.resolve();
+ },
+
+ /**
+ * Toggles the split state of the webconsole. If the webconsole panel
+ * is already selected then this command is ignored.
+ *
+ * @returns {Promise} a promise that resolves once the tool has been
+ * opened or closed.
+ */
+ toggleSplitConsole: function() {
+ if (this.currentToolId !== "webconsole") {
+ return this.splitConsole
+ ? this.closeSplitConsole()
+ : this.openSplitConsole();
+ }
+
+ return promise.resolve();
+ },
+
+ /**
+ * Toggles the options panel.
+ * If the option panel is already selected then select the last selected panel.
+ */
+ toggleOptions: function(event) {
+ // Flip back to the last used panel if we are already
+ // on the options panel.
+ if (
+ this.currentToolId === "options" &&
+ gDevTools.getToolDefinition(this.lastUsedToolId)
+ ) {
+ this.selectTool(this.lastUsedToolId, "toggle_settings_off");
+ } else {
+ this.selectTool("options", "toggle_settings_on");
+ }
+
+ // preventDefault will avoid a Linux only bug when the focus is on a text input
+ // See Bug 1519087.
+ event.preventDefault();
+ },
+
+ /**
+ * Tells the target tab to reload.
+ */
+ reloadTarget: function(force) {
+ this.target.reload({ options: { force } });
+ },
+
+ /**
+ * Loads the tool next to the currently selected tool.
+ */
+ selectNextTool: function() {
+ const definitions = this.component.panelDefinitions;
+ const index = definitions.findIndex(({ id }) => id === this.currentToolId);
+ const definition =
+ index === -1 || index >= definitions.length - 1
+ ? definitions[0]
+ : definitions[index + 1];
+ return this.selectTool(definition.id, "select_next_key");
+ },
+
+ /**
+ * Loads the tool just left to the currently selected tool.
+ */
+ selectPreviousTool: function() {
+ const definitions = this.component.panelDefinitions;
+ const index = definitions.findIndex(({ id }) => id === this.currentToolId);
+ const definition =
+ index === -1 || index < 1
+ ? definitions[definitions.length - 1]
+ : definitions[index - 1];
+ return this.selectTool(definition.id, "select_prev_key");
+ },
+
+ /**
+ * Highlights the tool's tab if it is not the currently selected tool.
+ *
+ * @param {string} id
+ * The id of the tool to highlight
+ */
+ async highlightTool(id) {
+ if (!this.component) {
+ await this.isOpen;
+ }
+ this.component.highlightTool(id);
+ },
+
+ /**
+ * De-highlights the tool's tab.
+ *
+ * @param {string} id
+ * The id of the tool to unhighlight
+ */
+ async unhighlightTool(id) {
+ if (!this.component) {
+ await this.isOpen;
+ }
+ this.component.unhighlightTool(id);
+ },
+
+ /**
+ * Raise the toolbox host.
+ */
+ raise: function() {
+ this.postMessage({ name: "raise-host" });
+ },
+
+ /**
+ * Fired when user just started navigating away to another web page.
+ */
+ async _onWillNavigate() {
+ // Clearing the error count as soon as we navigate
+ this.setErrorCount(0);
+ this.updateToolboxButtons();
+ const toolId = this.currentToolId;
+ // For now, only inspector, webconsole and netmonitor fire "reloaded" event
+ if (
+ toolId != "inspector" &&
+ toolId != "webconsole" &&
+ toolId != "netmonitor"
+ ) {
+ return;
+ }
+
+ const start = this.win.performance.now();
+ const panel = this.getPanel(toolId);
+ // Ignore the timing if the panel is still loading
+ if (!panel) {
+ return;
+ }
+
+ await panel.once("reloaded");
+ const delay = this.win.performance.now() - start;
+
+ const telemetryKey = "DEVTOOLS_TOOLBOX_PAGE_RELOAD_DELAY_MS";
+ this.telemetry.getKeyedHistogramById(telemetryKey).add(toolId, delay);
+ },
+
+ /**
+ * Refresh the host's title.
+ */
+ _refreshHostTitle: function() {
+ let title;
+
+ const isMultiProcessBrowserToolbox =
+ this.target.isParentProcess &&
+ Services.prefs.getBoolPref("devtools.browsertoolbox.fission", false);
+
+ if (isMultiProcessBrowserToolbox) {
+ title = L10N.getStr("toolbox.multiProcessBrowserToolboxTitle");
+ } else if (this.target.name && this.target.name != this.target.url) {
+ const url = this.target.isWebExtension
+ ? this.target.getExtensionPathName(this.target.url)
+ : getUnicodeUrl(this.target.url);
+ title = L10N.getFormatStr(
+ "toolbox.titleTemplate2",
+ this.target.name,
+ url
+ );
+ } else {
+ title = L10N.getFormatStr(
+ "toolbox.titleTemplate1",
+ getUnicodeUrl(this.target.url)
+ );
+ }
+ this.postMessage({
+ name: "set-host-title",
+ title,
+ });
+ },
+
+ /**
+ * Returns an instance of the preference actor. This is a lazily initialized root
+ * actor that persists preferences to the debuggee, instead of just to the DevTools
+ * client. See the definition of the preference actor for more information.
+ */
+ get preferenceFront() {
+ const frontPromise = this.target.client.mainRoot.getFront("preference");
+ frontPromise.then(front => {
+ // Set the _preferenceFront property to allow the resetPreferences toolbox method
+ // to cleanup the preference set when the toolbox is closed.
+ this._preferenceFront = front;
+ });
+
+ return frontPromise;
+ },
+
+ // Is the disable auto-hide of pop-ups feature available in this context?
+ get disableAutohideAvailable() {
+ return this.target.chrome;
+ },
+
+ async toggleNoAutohide() {
+ const front = await this.preferenceFront;
+
+ const toggledValue = !(await this._isDisableAutohideEnabled());
+
+ front.setBoolPref(DISABLE_AUTOHIDE_PREF, toggledValue);
+
+ if (this.disableAutohideAvailable) {
+ this.component.setDisableAutohide(toggledValue);
+ }
+ this._autohideHasBeenToggled = true;
+ },
+
+ async _isDisableAutohideEnabled() {
+ // Ensure that the tools are open and the feature is available in this
+ // context.
+ await this.isOpen;
+ if (!this.disableAutohideAvailable) {
+ return false;
+ }
+
+ const prefFront = await this.preferenceFront;
+ return prefFront.getBoolPref(DISABLE_AUTOHIDE_PREF);
+ },
+
+ _listFrames: async function(event) {
+ if (!this.target.traits.frames) {
+ // We are not targetting a regular BrowsingContextTargetActor
+ // it can be either an addon or browser toolbox actor
+ return promise.resolve();
+ }
+
+ try {
+ const { frames } = await this.target.listFrames();
+ this._updateFrames({ frames });
+ } catch (e) {
+ console.error("Error while listing frames", e);
+ }
+ },
+
+ /**
+ * Select a frame by sending 'switchToFrame' packet to the backend.
+ */
+ onSelectFrame: function(frameId) {
+ // Send packet to the backend to select specified frame and
+ // wait for 'frameUpdate' event packet to update the UI.
+ this.target.switchToFrame({ windowId: frameId });
+ },
+
+ /**
+ * Highlight a frame in the page
+ */
+ onHighlightFrame: async function(frameId) {
+ const inspectorFront = await this.target.getFront("inspector");
+ const highlighter = this.getHighlighter();
+
+ // Only enable frame highlighting when the top level document is targeted
+ if (this.rootFrameSelected) {
+ const nodeFront = await inspectorFront.walker.getNodeActorFromWindowID(
+ frameId
+ );
+ return highlighter.highlight(nodeFront);
+ }
+ },
+
+ /**
+ * A handler for 'frameUpdate' packets received from the backend.
+ * Following properties might be set on the packet:
+ *
+ * destroyAll {Boolean}: All frames have been destroyed.
+ * selected {Number}: A frame has been selected
+ * frames {Array}: list of frames. Every frame can have:
+ * id {Number}: frame ID
+ * url {String}: frame URL
+ * title {String}: frame title
+ * destroy {Boolean}: Set to true if destroyed
+ * parentID {Number}: ID of the parent frame (not set
+ * for top level window)
+ */
+ _updateFrames: function(data) {
+ // We may receive this event before the toolbox is ready.
+ if (!this.isReady) {
+ return;
+ }
+
+ // Store (synchronize) data about all existing frames on the backend
+ if (data.destroyAll) {
+ this.frameMap.clear();
+ this.selectedFrameId = null;
+ } else if (data.selected) {
+ this.selectedFrameId = data.selected;
+ } else if (data.frames) {
+ data.frames.forEach(frame => {
+ if (frame.destroy) {
+ this.frameMap.delete(frame.id);
+
+ // Reset the currently selected frame if it's destroyed.
+ if (this.selectedFrameId == frame.id) {
+ this.selectedFrameId = null;
+ }
+ } else {
+ this.frameMap.set(frame.id, frame);
+ }
+ });
+ }
+
+ // If there is no selected frame select the first top level
+ // frame by default. Note that there might be more top level
+ // frames in case of the BrowserToolbox.
+ if (!this.selectedFrameId) {
+ const frames = [...this.frameMap.values()];
+ const topFrames = frames.filter(frame => !frame.parentID);
+ this.selectedFrameId = topFrames.length ? topFrames[0].id : null;
+ }
+
+ // We may need to hide/show the frames button now.
+ const wasVisible = this.frameButton.isVisible;
+ const wasDisabled = this.frameButton.disabled;
+ this.updateFrameButton();
+
+ const toolbarUpdate = () => {
+ if (
+ this.frameButton.isVisible === wasVisible &&
+ this.frameButton.disabled === wasDisabled
+ ) {
+ return;
+ }
+ this.component.setToolboxButtons(this.toolbarButtons);
+ };
+
+ // If we are navigating/reloading, however (in which case data.destroyAll
+ // will be true), we should debounce the update to avoid unnecessary
+ // flickering/rendering.
+ if (data.destroyAll && !this.debouncedToolbarUpdate) {
+ this.debouncedToolbarUpdate = debounce(
+ () => {
+ toolbarUpdate();
+ this.debouncedToolbarUpdate = null;
+ },
+ 200,
+ this
+ );
+ }
+
+ if (this.debouncedToolbarUpdate) {
+ this.debouncedToolbarUpdate();
+ } else {
+ toolbarUpdate();
+ }
+ },
+
+ /**
+ * Returns a 0-based selected frame depth.
+ *
+ * For example, if the root frame is selected, the returned value is 0. For a sub-frame
+ * of the root document, the returned value is 1, and so on.
+ */
+ get selectedFrameDepth() {
+ // If the frame switcher is disabled, we won't have a selected frame ID.
+ // In this case, we're always showing the root frame.
+ if (!this.selectedFrameId) {
+ return 0;
+ }
+ let depth = 0;
+ let frame = this.frameMap.get(this.selectedFrameId);
+ while (frame) {
+ depth++;
+ frame = this.frameMap.get(frame.parentID);
+ }
+ return depth - 1;
+ },
+
+ /**
+ * Returns whether a root frame (with no parent frame) is selected.
+ */
+ get rootFrameSelected() {
+ return this.selectedFrameDepth == 0;
+ },
+
+ /**
+ * Switch to the last used host for the toolbox UI.
+ */
+ switchToPreviousHost: function() {
+ return this.switchHost("previous");
+ },
+
+ /**
+ * Switch to a new host for the toolbox UI. E.g. bottom, sidebar, window,
+ * and focus the window when done.
+ *
+ * @param {string} hostType
+ * The host type of the new host object
+ */
+ switchHost: function(hostType) {
+ if (hostType == this.hostType || !this.target.isLocalTab) {
+ return null;
+ }
+
+ // chromeEventHandler will change after swapping hosts, remove events relying on it.
+ this._removeChromeEventHandlerEvents();
+
+ this.emit("host-will-change", hostType);
+
+ // ToolboxHostManager is going to call swapFrameLoaders which mess up with
+ // focus. We have to blur before calling it in order to be able to restore
+ // the focus after, in _onSwitchedHost.
+ this.focusTool(this.currentToolId, false);
+
+ // Host code on the chrome side will send back a message once the host
+ // switched
+ this.postMessage({
+ name: "switch-host",
+ hostType,
+ });
+
+ return this.once("host-changed");
+ },
+
+ _onSwitchedHost: function({ hostType }) {
+ this._hostType = hostType;
+
+ this._buildDockOptions();
+
+ // chromeEventHandler changed after swapping hosts, add again events relying on it.
+ this._addChromeEventHandlerEvents();
+
+ // We blurred the tools at start of switchHost, but also when clicking on
+ // host switching button. We now have to restore the focus.
+ this.focusTool(this.currentToolId, true);
+
+ this.emit("host-changed");
+ this.telemetry
+ .getHistogramById(HOST_HISTOGRAM)
+ .add(this._getTelemetryHostId());
+
+ this.component.setCurrentHostType(hostType);
+ },
+
+ /**
+ * Test the availability of a tool (both globally registered tools and
+ * additional tools registered to this toolbox) by tool id.
+ *
+ * @param {string} toolId
+ * Id of the tool definition to search in the per-toolbox or globally
+ * registered tools.
+ *
+ * @returns {bool}
+ * Returns true if the tool is registered globally or on this toolbox.
+ */
+ isToolRegistered: function(toolId) {
+ return !!this.getToolDefinition(toolId);
+ },
+
+ /**
+ * Return the tool definition registered globally or additional tools registered
+ * to this toolbox.
+ *
+ * @param {string} toolId
+ * Id of the tool definition to retrieve for the per-toolbox and globally
+ * registered tools.
+ *
+ * @returns {object}
+ * The plain javascript object that represents the requested tool definition.
+ */
+ getToolDefinition: function(toolId) {
+ return (
+ gDevTools.getToolDefinition(toolId) ||
+ this.additionalToolDefinitions.get(toolId)
+ );
+ },
+
+ /**
+ * Internal helper that removes a loaded tool from the toolbox,
+ * it removes a loaded tool panel and tab from the toolbox without removing
+ * its definition, so that it can still be listed in options and re-added later.
+ *
+ * @param {string} toolId
+ * Id of the tool to be removed.
+ */
+ unloadTool: function(toolId) {
+ if (typeof toolId != "string") {
+ throw new Error("Unexpected non-string toolId received.");
+ }
+
+ if (this._toolPanels.has(toolId)) {
+ const instance = this._toolPanels.get(toolId);
+ instance.destroy();
+ this._toolPanels.delete(toolId);
+ }
+
+ const panel = this.doc.getElementById("toolbox-panel-" + toolId);
+
+ // Select another tool.
+ if (this.currentToolId == toolId) {
+ const index = this.panelDefinitions.findIndex(({ id }) => id === toolId);
+ const nextTool = this.panelDefinitions[index + 1];
+ const previousTool = this.panelDefinitions[index - 1];
+ let toolNameToSelect;
+
+ if (nextTool) {
+ toolNameToSelect = nextTool.id;
+ }
+ if (previousTool) {
+ toolNameToSelect = previousTool.id;
+ }
+ if (toolNameToSelect) {
+ this.selectTool(toolNameToSelect, "tool_unloaded");
+ }
+ }
+
+ // Remove this tool from the current panel definitions.
+ this.panelDefinitions = this.panelDefinitions.filter(
+ ({ id }) => id !== toolId
+ );
+ this.visibleAdditionalTools = this.visibleAdditionalTools.filter(
+ id => id !== toolId
+ );
+ this._combineAndSortPanelDefinitions();
+
+ if (panel) {
+ panel.remove();
+ }
+
+ if (this.hostType == Toolbox.HostType.WINDOW) {
+ const doc = this.win.parent.document;
+ const key = doc.getElementById("key_" + toolId);
+ if (key) {
+ key.remove();
+ }
+ }
+ },
+
+ /**
+ * Handler for the tool-registered event.
+ * @param {string} toolId
+ * Id of the tool that was registered
+ */
+ _toolRegistered: function(toolId) {
+ // Tools can either be in the global devtools, or added to this specific toolbox
+ // as an additional tool.
+ let definition = gDevTools.getToolDefinition(toolId);
+ let isAdditionalTool = false;
+ if (!definition) {
+ definition = this.additionalToolDefinitions.get(toolId);
+ isAdditionalTool = true;
+ }
+
+ if (definition.isTargetSupported(this.target)) {
+ if (isAdditionalTool) {
+ this.visibleAdditionalTools = [...this.visibleAdditionalTools, toolId];
+ this._combineAndSortPanelDefinitions();
+ } else {
+ this.panelDefinitions = this.panelDefinitions.concat(definition);
+ }
+ this._buildPanelForTool(definition);
+
+ // Emit the event so tools can listen to it from the toolbox level
+ // instead of gDevTools.
+ this.emit("tool-registered", toolId);
+ }
+ },
+
+ /**
+ * Handler for the tool-unregistered event.
+ * @param {string} toolId
+ * id of the tool that was unregistered
+ */
+ _toolUnregistered: function(toolId) {
+ this.unloadTool(toolId);
+
+ // Emit the event so tools can listen to it from the toolbox level
+ // instead of gDevTools
+ this.emit("tool-unregistered", toolId);
+ },
+
+ /**
+ * A helper function that returns an object containing methods to show and hide the
+ * Box Model Highlighter on a given NodeFront or node grip (object with metadata which
+ * can be used to obtain a NodeFront for a node), as well as helpers to listen to the
+ * higligher show and hide events. The event helpers are used in tests where it is
+ * cumbersome to load the Inspector panel in order to listen to highlighter events.
+ *
+ * @returns {Object} an object of the following shape:
+ * - {AsyncFunction} highlight: A function that will show a Box Model Highlighter
+ * for the provided NodeFront or node grip.
+ * - {AsyncFunction} unhighlight: A function that will hide any Box Model Highlighter
+ * that is visible. If the `highlight` promise isn't settled yet,
+ * it will wait until it's done and then unhighlight to prevent
+ * zombie highlighters.
+ * - {AsyncFunction} waitForHighlighterShown: Returns a promise which resolves with
+ * the "highlighter-shown" event data once the highlighter is shown.
+ * - {AsyncFunction} waitForHighlighterHidden: Returns a promise which resolves with
+ * the "highlighter-hidden" event data once the highlighter is
+ * hidden.
+ *
+ */
+ getHighlighter() {
+ let pendingHighlight;
+
+ /**
+ * Return a promise wich resolves with a reference to the Inspector panel.
+ */
+ const _getInspector = async () => {
+ const inspector = this.getPanel("inspector");
+ if (inspector) {
+ return inspector;
+ }
+
+ return this.loadTool("inspector");
+ };
+
+ /**
+ * Returns a promise which resolves when a Box Model Highlighter emits the given event
+ *
+ * @param {String} eventName
+ * Name of the event to listen to.
+ * @return {Promise}
+ * Promise which resolves when the highlighter event occurs.
+ * Resolves with the data payload attached to the event.
+ */
+ async function _waitForHighlighterEvent(eventName) {
+ const inspector = await _getInspector();
+ return new Promise(resolve => {
+ function _handler(data) {
+ if (data.type === inspector.highlighters.TYPES.BOXMODEL) {
+ inspector.highlighters.off(eventName, _handler);
+ resolve(data);
+ }
+ }
+
+ inspector.highlighters.on(eventName, _handler);
+ });
+ }
+
+ return {
+ // highlight might be triggered right before a test finishes. Wrap it
+ // with safeAsyncMethod to avoid intermittents.
+ highlight: this._safeAsyncAfterDestroy(async (object, options) => {
+ pendingHighlight = (async () => {
+ let nodeFront = object;
+
+ if (!(nodeFront instanceof NodeFront)) {
+ const inspectorFront = await this.target.getFront("inspector");
+ nodeFront = await inspectorFront.getNodeFrontFromNodeGrip(object);
+ }
+
+ if (!nodeFront) {
+ return null;
+ }
+
+ const inspector = await _getInspector();
+ return inspector.highlighters.showHighlighterTypeForNode(
+ inspector.highlighters.TYPES.BOXMODEL,
+ nodeFront,
+ options
+ );
+ })();
+ return pendingHighlight;
+ }),
+ unhighlight: this._safeAsyncAfterDestroy(async () => {
+ if (pendingHighlight) {
+ await pendingHighlight;
+ pendingHighlight = null;
+ }
+
+ const inspector = await _getInspector();
+ return inspector.highlighters.hideHighlighterType(
+ inspector.highlighters.TYPES.BOXMODEL
+ );
+ }),
+
+ waitForHighlighterShown: this._safeAsyncAfterDestroy(async () => {
+ return _waitForHighlighterEvent("highlighter-shown");
+ }),
+
+ waitForHighlighterHidden: this._safeAsyncAfterDestroy(async () => {
+ return _waitForHighlighterEvent("highlighter-hidden");
+ }),
+ };
+ },
+
+ /**
+ * Shortcut to avoid throwing errors when an async method fails after toolbox
+ * destroy. Should be used with methods that might be triggered by a user
+ * input, regardless of the toolbox lifecycle.
+ */
+ _safeAsyncAfterDestroy(fn) {
+ return safeAsyncMethod(fn, () => !!this._destroyer);
+ },
+
+ _onNewSelectedNodeFront: async function() {
+ // Emit a "selection-changed" event when the toolbox.selection has been set
+ // to a new node (or cleared). Currently used in the WebExtensions APIs (to
+ // provide the `devtools.panels.elements.onSelectionChanged` event).
+ this.emit("selection-changed");
+
+ const targetFrontActorID = this.selection?.nodeFront?.targetFront?.actorID;
+ if (targetFrontActorID) {
+ this.selectTarget(targetFrontActorID);
+ }
+ },
+
+ _onInspectObject: function(packet) {
+ this.inspectObjectActor(packet.objectActor, packet.inspectFromAnnotation);
+ },
+
+ _onToolSelected: function() {
+ this._refreshHostTitle();
+
+ this.updatePickerButton();
+ this.updateFrameButton();
+ this.updateErrorCountButton();
+
+ // Calling setToolboxButtons in case the visibility of a button changed.
+ this.component.setToolboxButtons(this.toolbarButtons);
+ },
+
+ inspectObjectActor: async function(objectActor, inspectFromAnnotation) {
+ const objectGrip = objectActor?.getGrip
+ ? objectActor.getGrip()
+ : objectActor;
+
+ if (
+ objectGrip.preview &&
+ objectGrip.preview.nodeType === domNodeConstants.ELEMENT_NODE
+ ) {
+ return this.viewElementInInspector(objectGrip, inspectFromAnnotation);
+ }
+
+ if (objectGrip.class == "Function") {
+ if (!objectGrip.location) {
+ console.error("Missing location in Function objectGrip", objectGrip);
+ return;
+ }
+
+ const { url, line, column } = objectGrip.location;
+ return this.viewSourceInDebugger(url, line, column);
+ }
+
+ if (objectGrip.type !== "null" && objectGrip.type !== "undefined") {
+ // Open then split console and inspect the object in the variables view,
+ // when the objectActor doesn't represent an undefined or null value.
+ if (this.currentToolId != "webconsole") {
+ await this.openSplitConsole();
+ }
+
+ const panel = this.getPanel("webconsole");
+ panel.hud.ui.inspectObjectActor(objectActor);
+ }
+ },
+
+ /**
+ * Get the toolbox's notification component
+ *
+ * @return The notification box component.
+ */
+ getNotificationBox: function() {
+ return this.notificationBox;
+ },
+
+ closeToolbox: async function() {
+ await this.destroy();
+ },
+
+ /**
+ * Public API to check is the current toolbox is currently being destroyed.
+ */
+ isDestroying: function() {
+ return this._destroyer;
+ },
+
+ /**
+ * Remove all UI elements, detach from target and clear up
+ */
+ destroy: function() {
+ // If several things call destroy then we give them all the same
+ // destruction promise so we're sure to destroy only once
+ if (this._destroyer) {
+ return this._destroyer;
+ }
+
+ // This pattern allows to immediately return the destroyer promise.
+ // See Bug 1602727 for more details.
+ let destroyerResolve;
+ this._destroyer = new Promise(r => (destroyerResolve = r));
+ this._destroyToolbox().then(destroyerResolve);
+
+ return this._destroyer;
+ },
+
+ _destroyToolbox: async function() {
+ this.emit("destroy");
+
+ // This flag will be checked by Fronts in order to decide if they should
+ // skip their destroy.
+ if (this.target.client) {
+ // Note: this.target.client might be null if the target was already
+ // destroyed (eg: tab is closed during remote debugging).
+ this.target.client.isToolboxDestroy = true;
+ }
+
+ this.off("select", this._onToolSelected);
+ this.off("host-changed", this._refreshHostTitle);
+
+ gDevTools.off("tool-registered", this._toolRegistered);
+ gDevTools.off("tool-unregistered", this._toolUnregistered);
+
+ Services.prefs.removeObserver(
+ "devtools.cache.disabled",
+ this._applyCacheSettings
+ );
+ Services.prefs.removeObserver(
+ "devtools.serviceWorkers.testing.enabled",
+ this._applyServiceWorkersTestingSettings
+ );
+
+ // We normally handle toolClosed from selectTool() but in the event of the
+ // toolbox closing we need to handle it here instead.
+ this.telemetry.toolClosed(this.currentToolId, this.sessionId, this);
+
+ this._lastFocusedElement = null;
+ this._pausedThreads = null;
+
+ if (this._sourceMapService) {
+ this._sourceMapService.stopSourceMapWorker();
+ this._sourceMapService = null;
+ }
+
+ if (this._parserService) {
+ this._parserService.stop();
+ this._parserService = null;
+ }
+
+ if (this.webconsolePanel) {
+ this._saveSplitConsoleHeight();
+ this.webconsolePanel.removeEventListener(
+ "resize",
+ this._saveSplitConsoleHeight
+ );
+ this.webconsolePanel = null;
+ }
+ if (this._componentMount) {
+ this._tabBar.removeEventListener(
+ "keypress",
+ this._onToolbarArrowKeypress
+ );
+ this.ReactDOM.unmountComponentAtNode(this._componentMount);
+ this._componentMount = null;
+ this._tabBar = null;
+ }
+
+ const outstanding = [];
+ for (const [id, panel] of this._toolPanels) {
+ try {
+ gDevTools.emit(id + "-destroy", this, panel);
+ this.emit(id + "-destroy", panel);
+
+ outstanding.push(panel.destroy());
+ } catch (e) {
+ // We don't want to stop here if any panel fail to close.
+ console.error("Panel " + id + ":", e);
+ }
+ }
+
+ this.browserRequire = null;
+ this._toolNames = null;
+
+ // Reset preferences set by the toolbox
+ outstanding.push(this.resetPreference());
+
+ this.targetList.unwatchTargets(
+ TargetList.ALL_TYPES,
+ this._onTargetAvailable,
+ this._onTargetDestroyed
+ );
+ this.resourceWatcher.unwatchResources(
+ [
+ this.resourceWatcher.TYPES.CONSOLE_MESSAGE,
+ this.resourceWatcher.TYPES.ERROR_MESSAGE,
+ this.resourceWatcher.TYPES.NETWORK_EVENT,
+ ],
+ { onAvailable: this._onResourceAvailable }
+ );
+
+ this.targetList.destroy();
+
+ // Unregister buttons listeners
+ this.toolbarButtons.forEach(button => {
+ if (typeof button.teardown == "function") {
+ // teardown arguments have already been bound in _createButtonState
+ button.teardown();
+ }
+ });
+
+ // We need to grab a reference to win before this._host is destroyed.
+ const win = this.win;
+ const host = this._getTelemetryHostString();
+ const width = Math.ceil(win.outerWidth / 50) * 50;
+ const prevPanelName = this.getTelemetryPanelNameOrOther(this.currentToolId);
+
+ this.telemetry.toolClosed("toolbox", this.sessionId, this);
+ this.telemetry.recordEvent("exit", prevPanelName, null, {
+ host: host,
+ width: width,
+ panel_name: this.getTelemetryPanelNameOrOther(this.currentToolId),
+ next_panel: "none",
+ reason: "toolbox_close",
+ session_id: this.sessionId,
+ });
+ this.telemetry.recordEvent("close", "tools", null, {
+ host: host,
+ width: width,
+ session_id: this.sessionId,
+ });
+
+ // Finish all outstanding tasks (which means finish destroying panels and
+ // then destroying the host, successfully or not) before destroying the
+ // target.
+ const onceDestroyed = new Promise(resolve => {
+ resolve(
+ settleAll(outstanding)
+ .catch(console.error)
+ .then(async () => {
+ this.selection.destroy();
+ this.selection = null;
+
+ if (this._netMonitorAPI) {
+ this._netMonitorAPI.destroy();
+ this._netMonitorAPI = null;
+ }
+
+ if (this._sourceMapURLService) {
+ await this._sourceMapURLService.waitForSourcesLoading();
+ this._sourceMapURLService.destroy();
+ this._sourceMapURLService = null;
+ }
+
+ this._removeWindowListeners();
+ this._removeChromeEventHandlerEvents();
+
+ // Notify toolbox-host-manager that the host can be destroyed.
+ this.emit("toolbox-unload");
+
+ // Targets need to be notified that the toolbox is being torn down.
+ // This is done after other destruction tasks since it may tear down
+ // fronts and the debugger transport which earlier destroy methods may
+ // require to complete.
+ return this.target.destroy();
+ }, console.error)
+ .then(() => {
+ this.emit("destroyed");
+
+ // Free _host after the call to destroyed in order to let a chance
+ // to destroyed listeners to still query toolbox attributes
+ this._host = null;
+ this._win = null;
+ this._toolPanels.clear();
+
+ // Force GC to prevent long GC pauses when running tests and to free up
+ // memory in general when the toolbox is closed.
+ if (flags.testing) {
+ win.windowUtils.garbageCollect();
+ }
+ })
+ .catch(console.error)
+ );
+ });
+
+ const leakCheckObserver = ({ wrappedJSObject: barrier }) => {
+ // Make the leak detector wait until this toolbox is properly destroyed.
+ barrier.client.addBlocker(
+ "DevTools: Wait until toolbox is destroyed",
+ onceDestroyed
+ );
+ };
+
+ const topic = "shutdown-leaks-before-check";
+ Services.obs.addObserver(leakCheckObserver, topic);
+
+ await onceDestroyed;
+
+ Services.obs.removeObserver(leakCheckObserver, topic);
+ },
+
+ /**
+ * Open the textbox context menu at given coordinates.
+ * Panels in the toolbox can call this on contextmenu events with event.screenX/Y
+ * instead of having to implement their own copy/paste/selectAll menu.
+ * @param {Number} x
+ * @param {Number} y
+ */
+ openTextBoxContextMenu: function(x, y) {
+ const menu = createEditContextMenu(this.topWindow, "toolbox-menu");
+
+ // Fire event for tests
+ menu.once("open", () => this.emit("menu-open"));
+ menu.once("close", () => this.emit("menu-close"));
+
+ menu.popup(x, y, this.doc);
+ },
+
+ /**
+ * Retrieve the current textbox context menu, if available.
+ */
+ getTextBoxContextMenu: function() {
+ return this.topDoc.getElementById("toolbox-menu");
+ },
+
+ /**
+ * Connects to the Gecko Profiler when the developer tools are open. This is
+ * necessary because of the WebConsole's `profile` and `profileEnd` methods.
+ */
+ async initPerformance() {
+ // If:
+ // - target does not have performance actor (addons)
+ // - or client uses the new performance panel (incompatible with console.profile())
+ // do not even register the shared performance connection.
+ const isNewPerfPanel = Services.prefs.getBoolPref(
+ "devtools.performance.new-panel-enabled",
+ false
+ );
+ if (isNewPerfPanel || !this.target.hasActor("performance")) {
+ return promise.resolve();
+ }
+
+ const performanceFront = await this.target.getFront("performance");
+ performanceFront.once("console-profile-start", () =>
+ this._onPerformanceFrontEvent(performanceFront)
+ );
+
+ return performanceFront;
+ },
+
+ /**
+ * Called when a "console-profile-start" event comes from the PerformanceFront. If
+ * the performance tool is already loaded when the first event comes in, immediately
+ * unbind this handler, as this is only used to load the tool for the first time when
+ * `console.profile()` recordings are started before the tool loads.
+ */
+ async _onPerformanceFrontEvent(performanceFront) {
+ if (this.getPanel("performance")) {
+ // the performance panel is already recording all performance, we no longer
+ // need the queue, if it was started
+ performanceFront.flushQueuedRecordings();
+ return;
+ }
+
+ // Before any console recordings, we'll get a `console-profile-start` event
+ // warning us that a recording will come later (via `recording-started`), so
+ // start to boot up the tool and populate the tool with any other recordings
+ // observed during that time.
+ const panel = await this.loadTool("performance");
+ const recordings = performanceFront.flushQueuedRecordings();
+ panel.panelWin.PerformanceController.populateWithRecordings(recordings);
+ await panel.open();
+ },
+
+ /**
+ * Reset preferences set by the toolbox.
+ */
+ async resetPreference() {
+ if (!this._preferenceFront) {
+ return;
+ }
+
+ // Only reset the autohide pref in the Browser Toolbox if it's been toggled
+ // in the UI (don't reset the pref if it was already set before opening)
+ if (this._autohideHasBeenToggled) {
+ await this._preferenceFront.clearUserPref(DISABLE_AUTOHIDE_PREF);
+ }
+
+ this._preferenceFront = null;
+ },
+
+ /**
+ * Returns gViewSourceUtils for viewing source.
+ */
+ get gViewSourceUtils() {
+ return this.win.gViewSourceUtils;
+ },
+
+ /**
+ * Open a CSS file when there is no line or column information available.
+ *
+ * @param {string} url The URL of the CSS file to open.
+ */
+ viewGeneratedSourceInStyleEditor: async function(url) {
+ if (typeof url !== "string") {
+ console.warn("Failed to open generated source, no url given");
+ return;
+ }
+
+ // The style editor hides the generated file if the file has original
+ // sources, so we have no choice but to open whichever original file
+ // corresponds to the first line of the generated file.
+ return viewSource.viewSourceInStyleEditor(this, url, 1);
+ },
+
+ /**
+ * Given a URL for a stylesheet (generated or original), open in the style
+ * editor if possible. Falls back to plain "view-source:".
+ * If the stylesheet has a sourcemap, we will attempt to open the original
+ * version of the file instead of the generated version.
+ */
+ viewSourceInStyleEditorByURL: async function(url, line, column) {
+ if (typeof url !== "string") {
+ console.warn("Failed to open source, no url given");
+ return;
+ }
+ if (typeof line !== "number") {
+ console.warn(
+ "No line given when navigating to source. If you're seeing this, there is a bug."
+ );
+
+ // This is a fallback in case of programming errors, but in a perfect
+ // world, viewSourceInStyleEditorByURL would always get a line/colum.
+ line = 1;
+ column = null;
+ }
+
+ return viewSource.viewSourceInStyleEditor(this, url, line, column);
+ },
+
+ /**
+ * Opens source in style editor. Falls back to plain "view-source:".
+ * If the stylesheet has a sourcemap, we will attempt to open the original
+ * version of the file instead of the generated version.
+ */
+ viewSourceInStyleEditorByFront: async function(
+ stylesheetFront,
+ line,
+ column
+ ) {
+ if (!stylesheetFront || typeof stylesheetFront !== "object") {
+ console.warn("Failed to open source, no stylesheet given");
+ return;
+ }
+ if (typeof line !== "number") {
+ console.warn(
+ "No line given when navigating to source. If you're seeing this, there is a bug."
+ );
+
+ // This is a fallback in case of programming errors, but in a perfect
+ // world, viewSourceInStyleEditorByFront would always get a line/colum.
+ line = 1;
+ column = null;
+ }
+
+ return viewSource.viewSourceInStyleEditor(
+ this,
+ stylesheetFront,
+ line,
+ column
+ );
+ },
+
+ viewElementInInspector: async function(objectGrip, reason) {
+ // Open the inspector and select the DOM Element.
+ await this.loadTool("inspector");
+ const inspector = this.getPanel("inspector");
+ const nodeFound = await inspector.inspectNodeActor(objectGrip, reason);
+ if (nodeFound) {
+ await this.selectTool("inspector", reason);
+ }
+ },
+
+ /**
+ * Open a JS file when there is no line or column information available.
+ *
+ * @param {string} url The URL of the JS file to open.
+ */
+ viewGeneratedSourceInDebugger: async function(url) {
+ if (typeof url !== "string") {
+ console.warn("Failed to open generated source, no url given");
+ return;
+ }
+
+ return viewSource.viewSourceInDebugger(this, url, null, null, null, null);
+ },
+
+ /**
+ * Opens source in debugger, the sourcemapped location will be selected in
+ * the debugger panel, if the given location resolves to a know sourcemapped one.
+ *
+ * Falls back to plain "view-source:".
+ *
+ * @see devtools/client/shared/source-utils.js
+ */
+ viewSourceInDebugger: async function(
+ sourceURL,
+ sourceLine,
+ sourceColumn,
+ sourceId,
+ reason
+ ) {
+ if (typeof sourceURL !== "string" && typeof sourceId !== "string") {
+ console.warn("Failed to open generated source, no url/id given");
+ return;
+ }
+ if (typeof sourceLine !== "number") {
+ console.warn(
+ "No line given when navigating to source. If you're seeing this, there is a bug."
+ );
+
+ // This is a fallback in case of programming errors, but in a perfect
+ // world, viewSourceInDebugger would always get a line/colum.
+ sourceLine = 1;
+ sourceColumn = null;
+ }
+
+ return viewSource.viewSourceInDebugger(
+ this,
+ sourceURL,
+ sourceLine,
+ sourceColumn,
+ sourceId,
+ reason
+ );
+ },
+
+ /**
+ * Opens source in plain "view-source:".
+ * @see devtools/client/shared/source-utils.js
+ */
+ viewSource: function(sourceURL, sourceLine) {
+ return viewSource.viewSource(this, sourceURL, sourceLine);
+ },
+
+ // Support for WebExtensions API (`devtools.network.*`)
+
+ /**
+ * Return Netmonitor API object. This object offers Network monitor
+ * public API that can be consumed by other panels or WE API.
+ */
+ getNetMonitorAPI: async function() {
+ const netPanel = this.getPanel("netmonitor");
+
+ // Return Net panel if it exists.
+ if (netPanel) {
+ return netPanel.panelWin.Netmonitor.api;
+ }
+
+ if (this._netMonitorAPI) {
+ return this._netMonitorAPI;
+ }
+
+ // Create and initialize Network monitor API object.
+ // This object is only connected to the backend - not to the UI.
+ this._netMonitorAPI = new NetMonitorAPI();
+ await this._netMonitorAPI.connect(this);
+
+ return this._netMonitorAPI;
+ },
+
+ /**
+ * Returns data (HAR) collected by the Network panel.
+ */
+ getHARFromNetMonitor: async function() {
+ const netMonitor = await this.getNetMonitorAPI();
+ let har = await netMonitor.getHar();
+
+ // Return default empty HAR file if needed.
+ har = har || buildHarLog(Services.appinfo);
+
+ // Return the log directly to be compatible with
+ // Chrome WebExtension API.
+ return har.log;
+ },
+
+ /**
+ * Add listener for `onRequestFinished` events.
+ *
+ * @param {Object} listener
+ * The listener to be called it's expected to be
+ * a function that takes ({harEntry, requestId})
+ * as first argument.
+ */
+ addRequestFinishedListener: async function(listener) {
+ const netMonitor = await this.getNetMonitorAPI();
+ netMonitor.addRequestFinishedListener(listener);
+ },
+
+ removeRequestFinishedListener: async function(listener) {
+ const netMonitor = await this.getNetMonitorAPI();
+ netMonitor.removeRequestFinishedListener(listener);
+
+ // Destroy Network monitor API object if the following is true:
+ // 1) there is no listener
+ // 2) the Net panel doesn't exist/use the API object (if the panel
+ // exists it's also responsible for destroying it,
+ // see `NetMonitorPanel.open` for more details)
+ const netPanel = this.getPanel("netmonitor");
+ const hasListeners = netMonitor.hasRequestFinishedListeners();
+ if (this._netMonitorAPI && !hasListeners && !netPanel) {
+ this._netMonitorAPI.destroy();
+ this._netMonitorAPI = null;
+ }
+ },
+
+ /**
+ * Used to lazily fetch HTTP response content within
+ * `onRequestFinished` event listener.
+ *
+ * @param {String} requestId
+ * Id of the request for which the response content
+ * should be fetched.
+ */
+ fetchResponseContent: async function(requestId) {
+ const netMonitor = await this.getNetMonitorAPI();
+ return netMonitor.fetchResponseContent(requestId);
+ },
+
+ // Support management of installed WebExtensions that provide a devtools_page.
+
+ /**
+ * List the subset of the active WebExtensions which have a devtools_page (used by
+ * toolbox-options.js to create the list of the tools provided by the enabled
+ * WebExtensions).
+ * @see devtools/client/framework/toolbox-options.js
+ */
+ listWebExtensions: function() {
+ // Return the array of the enabled webextensions (we can't use the prefs list here,
+ // because some of them may be disabled by the Addon Manager and still have a devtools
+ // preference).
+ return Array.from(this._webExtensions).map(([uuid, { name, pref }]) => {
+ return { uuid, name, pref };
+ });
+ },
+
+ /**
+ * Add a WebExtension to the list of the active extensions (given the extension UUID,
+ * a unique id assigned to an extension when it is installed, and its name),
+ * and emit a "webextension-registered" event to allow toolbox-options.js
+ * to refresh the listed tools accordingly.
+ * @see browser/components/extensions/ext-devtools.js
+ */
+ registerWebExtension: function(extensionUUID, { name, pref }) {
+ // Ensure that an installed extension (active in the AddonManager) which
+ // provides a devtools page is going to be listed in the toolbox options
+ // (and refresh its name if it was already listed).
+ this._webExtensions.set(extensionUUID, { name, pref });
+ this.emit("webextension-registered", extensionUUID);
+ },
+
+ /**
+ * Remove an active WebExtension from the list of the active extensions (given the
+ * extension UUID, a unique id assigned to an extension when it is installed, and its
+ * name), and emit a "webextension-unregistered" event to allow toolbox-options.js
+ * to refresh the listed tools accordingly.
+ * @see browser/components/extensions/ext-devtools.js
+ */
+ unregisterWebExtension: function(extensionUUID) {
+ // Ensure that an extension that has been disabled/uninstalled from the AddonManager
+ // is going to be removed from the toolbox options.
+ this._webExtensions.delete(extensionUUID);
+ this.emit("webextension-unregistered", extensionUUID);
+ },
+
+ /**
+ * A helper function which returns true if the extension with the given UUID is listed
+ * as active for the toolbox and has its related devtools about:config preference set
+ * to true.
+ * @see browser/components/extensions/ext-devtools.js
+ */
+ isWebExtensionEnabled: function(extensionUUID) {
+ const extInfo = this._webExtensions.get(extensionUUID);
+ return extInfo && Services.prefs.getBoolPref(extInfo.pref, false);
+ },
+
+ /**
+ * Returns a panel id in the case of built in panels or "other" in the case of
+ * third party panels. This is necessary due to limitations in addon id strings,
+ * the permitted length of event telemetry property values and what we actually
+ * want to see in our telemetry.
+ *
+ * @param {String} id
+ * The panel id we would like to process.
+ */
+ getTelemetryPanelNameOrOther: function(id) {
+ if (!this._toolNames) {
+ const definitions = gDevTools.getToolDefinitionArray();
+ const definitionIds = definitions.map(definition => definition.id);
+
+ this._toolNames = new Set(definitionIds);
+ }
+
+ if (!this._toolNames.has(id)) {
+ return "other";
+ }
+
+ return id;
+ },
+
+ /**
+ * Fired when the user navigates to another page.
+ */
+ _onNavigate: function() {
+ this._refreshHostTitle();
+ this._setDebugTargetData();
+ },
+
+ /**
+ * Sets basic information on the DebugTargetInfo component
+ */
+ _setDebugTargetData() {
+ if (this.hostType === Toolbox.HostType.PAGE) {
+ // Displays DebugTargetInfo which shows the basic information of debug target,
+ // if `about:devtools-toolbox` URL opens directly.
+ // DebugTargetInfo requires this._debugTargetData to be populated
+ this.component.setDebugTargetData(this._getDebugTargetData());
+ }
+ },
+
+ _onResourceAvailable(resources) {
+ let errors = this._errorCount || 0;
+
+ for (const resource of resources) {
+ if (
+ resource.resourceType === this.resourceWatcher.TYPES.ERROR_MESSAGE &&
+ // ERROR_MESSAGE resources can be warnings/info, but here we only want to count errors
+ resource.pageError.error
+ ) {
+ errors++;
+ continue;
+ }
+
+ if (
+ resource.resourceType === this.resourceWatcher.TYPES.CONSOLE_MESSAGE
+ ) {
+ const { level } = resource.message;
+ if (level === "error" || level === "exception" || level === "assert") {
+ errors++;
+ }
+
+ // Reset the count on console.clear
+ if (level === "clear") {
+ errors = 0;
+ }
+ }
+ }
+
+ this.setErrorCount(errors);
+ },
+
+ _onResourceUpdated(resources) {
+ let errors = this._errorCount || 0;
+
+ for (const { update } of resources) {
+ // In order to match webconsole behaviour, we treat 4xx and 5xx network calls as errors.
+ if (
+ update.resourceType === this.resourceWatcher.TYPES.NETWORK_EVENT &&
+ update.resourceUpdates.status &&
+ update.resourceUpdates.status.toString().match(REGEX_4XX_5XX)
+ ) {
+ errors++;
+ }
+ }
+
+ this.setErrorCount(errors);
+ },
+
+ /**
+ * Set the number of errors in the toolbar icon.
+ *
+ * @param {Number} count
+ */
+ setErrorCount(count) {
+ // Don't re-render if the number of errors changed
+ if (!this.component || this._errorCount === count) {
+ return;
+ }
+
+ this._errorCount = count;
+
+ // Update button properties and trigger a render of the toolbox
+ this.updateErrorCountButton();
+ this._throttledSetToolboxButtons();
+ },
+};
diff --git a/devtools/client/framework/toolbox.xhtml b/devtools/client/framework/toolbox.xhtml
new file mode 100644
index 0000000000..8c020aa620
--- /dev/null
+++ b/devtools/client/framework/toolbox.xhtml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/toolbox.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/components/NotificationBox.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/framework/components/DebugTargetErrorPage.css" type="text/css"?>
+
+
+<!DOCTYPE window>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ csp="default-src chrome: resource:; img-src chrome: resource: data:; object-src 'none'" role="application">
+ <linkset>
+ <html:link rel="localization" href="devtools/client/tooltips.ftl"/>
+ </linkset>
+
+ <html:link href="chrome://browser/skin/window.svg" rel="shortcut icon"/>
+ <script src="chrome://devtools/content/shared/theme-switching.js"/>
+ <script src="chrome://global/content/viewSourceUtils.js"/>
+
+ <script src="chrome://devtools/content/framework/toolbox-init.js"/>
+
+ <vbox id="toolbox-container" flex="1" role="group">
+ <div xmlns="http://www.w3.org/1999/xhtml" id="toolbox-error-mount"/>
+ <div xmlns="http://www.w3.org/1999/xhtml" id="toolbox-notificationbox"/>
+ <div xmlns="http://www.w3.org/1999/xhtml" id="toolbox-toolbar-mount"
+ role="toolbar" />
+ <vbox flex="1" class="theme-body">
+ <!-- Set large flex to allow the toolbox-panel-webconsole to have a
+ height set to a small value without flexing to fill up extra
+ space. There must be a flex on both to ensure that the console
+ panel itself is sized properly -->
+ <box id="toolbox-deck" flex="10000" minheight="75" />
+ <splitter id="toolbox-console-splitter" class="devtools-horizontal-splitter" hidden="true" />
+ <box minheight="75" flex="1" id="toolbox-panel-webconsole" collapsed="true" />
+ </vbox>
+ <tooltip id="aHTMLTooltip" page="true" />
+ </vbox>
+</window>