summaryrefslogtreecommitdiffstats
path: root/devtools/client/aboutdebugging
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/client/aboutdebugging
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/aboutdebugging')
-rw-r--r--devtools/client/aboutdebugging/README.md86
-rw-r--r--devtools/client/aboutdebugging/aboutdebugging.css35
-rw-r--r--devtools/client/aboutdebugging/aboutdebugging.js201
-rw-r--r--devtools/client/aboutdebugging/documentation/ANDROID_BUILD.md60
-rw-r--r--devtools/client/aboutdebugging/documentation/GECKOVIEW_EXAMPLE_BUILD.md66
-rw-r--r--devtools/client/aboutdebugging/documentation/GECKOVIEW_REFERENCE_BROWSER_BUILD.md152
-rw-r--r--devtools/client/aboutdebugging/documentation/TESTS.md124
-rw-r--r--devtools/client/aboutdebugging/documentation/TESTS_REAL_DEVICES.md67
-rw-r--r--devtools/client/aboutdebugging/index.html30
-rw-r--r--devtools/client/aboutdebugging/initializer.js23
-rw-r--r--devtools/client/aboutdebugging/moz.build20
-rw-r--r--devtools/client/aboutdebugging/src/actions/debug-targets.js356
-rw-r--r--devtools/client/aboutdebugging/src/actions/index.js12
-rw-r--r--devtools/client/aboutdebugging/src/actions/moz.build11
-rw-r--r--devtools/client/aboutdebugging/src/actions/runtimes.js515
-rw-r--r--devtools/client/aboutdebugging/src/actions/telemetry.js23
-rw-r--r--devtools/client/aboutdebugging/src/actions/ui.js202
-rw-r--r--devtools/client/aboutdebugging/src/base.css521
-rw-r--r--devtools/client/aboutdebugging/src/components/App.css71
-rw-r--r--devtools/client/aboutdebugging/src/components/App.js213
-rw-r--r--devtools/client/aboutdebugging/src/components/CompatibilityWarning.js110
-rw-r--r--devtools/client/aboutdebugging/src/components/ConnectionPromptSetting.js55
-rw-r--r--devtools/client/aboutdebugging/src/components/ProfilerDialog.css63
-rw-r--r--devtools/client/aboutdebugging/src/components/ProfilerDialog.js168
-rw-r--r--devtools/client/aboutdebugging/src/components/RuntimeActions.css9
-rw-r--r--devtools/client/aboutdebugging/src/components/RuntimeActions.js82
-rw-r--r--devtools/client/aboutdebugging/src/components/RuntimeInfo.css42
-rw-r--r--devtools/client/aboutdebugging/src/components/RuntimeInfo.js89
-rw-r--r--devtools/client/aboutdebugging/src/components/RuntimePage.js306
-rw-r--r--devtools/client/aboutdebugging/src/components/ServiceWorkersWarning.js52
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/ConnectPage.css50
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/ConnectPage.js315
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/ConnectSection.css50
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/ConnectSection.js69
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/ConnectSteps.css13
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/ConnectSteps.js51
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.css23
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.js148
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.css20
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.js67
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/moz.build11
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.css97
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.js91
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.css7
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.js80
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.css43
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.js147
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.css27
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.js243
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.css29
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.js49
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/InspectAction.js58
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/ProcessDetail.js32
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.css26
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.js124
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAdditionalActions.js176
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/TabAction.js52
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/TabDetail.js34
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionAdditionalActions.js182
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionDetail.js67
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.css8
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.js101
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstaller.js52
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/WorkerDetail.js120
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/moz.build22
-rw-r--r--devtools/client/aboutdebugging/src/components/moz.build21
-rw-r--r--devtools/client/aboutdebugging/src/components/shared/DetailsLog.js64
-rw-r--r--devtools/client/aboutdebugging/src/components/shared/IconLabel.css27
-rw-r--r--devtools/client/aboutdebugging/src/components/shared/IconLabel.js48
-rw-r--r--devtools/client/aboutdebugging/src/components/shared/Message.css79
-rw-r--r--devtools/client/aboutdebugging/src/components/shared/Message.js109
-rw-r--r--devtools/client/aboutdebugging/src/components/shared/moz.build9
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/RefreshDevicesButton.js46
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/Sidebar.css43
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/Sidebar.js259
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.css29
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.js60
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.css71
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.js81
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.css41
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.js216
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/moz.build11
-rw-r--r--devtools/client/aboutdebugging/src/constants.js185
-rw-r--r--devtools/client/aboutdebugging/src/create-store.js78
-rw-r--r--devtools/client/aboutdebugging/src/middleware/debug-target-listener.js111
-rw-r--r--devtools/client/aboutdebugging/src/middleware/error-logging.js35
-rw-r--r--devtools/client/aboutdebugging/src/middleware/event-recording.js268
-rw-r--r--devtools/client/aboutdebugging/src/middleware/extension-component-data.js84
-rw-r--r--devtools/client/aboutdebugging/src/middleware/moz.build13
-rw-r--r--devtools/client/aboutdebugging/src/middleware/process-component-data.js55
-rw-r--r--devtools/client/aboutdebugging/src/middleware/tab-component-data.js51
-rw-r--r--devtools/client/aboutdebugging/src/middleware/worker-component-data.js82
-rw-r--r--devtools/client/aboutdebugging/src/modules/client-wrapper.js217
-rw-r--r--devtools/client/aboutdebugging/src/modules/debug-target-collapsibilities.js46
-rw-r--r--devtools/client/aboutdebugging/src/modules/debug-target-support.js98
-rw-r--r--devtools/client/aboutdebugging/src/modules/extensions-helper.js92
-rw-r--r--devtools/client/aboutdebugging/src/modules/l10n.js12
-rw-r--r--devtools/client/aboutdebugging/src/modules/moz.build16
-rw-r--r--devtools/client/aboutdebugging/src/modules/network-locations.js69
-rw-r--r--devtools/client/aboutdebugging/src/modules/runtime-client-factory.js68
-rw-r--r--devtools/client/aboutdebugging/src/modules/runtime-default-preferences.js101
-rw-r--r--devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js37
-rw-r--r--devtools/client/aboutdebugging/src/modules/usb-runtimes.js122
-rw-r--r--devtools/client/aboutdebugging/src/moz.build17
-rw-r--r--devtools/client/aboutdebugging/src/reducers/debug-targets-state.js155
-rw-r--r--devtools/client/aboutdebugging/src/reducers/index.js24
-rw-r--r--devtools/client/aboutdebugging/src/reducers/moz.build10
-rw-r--r--devtools/client/aboutdebugging/src/reducers/runtimes-state.js178
-rw-r--r--devtools/client/aboutdebugging/src/reducers/ui-state.js115
-rw-r--r--devtools/client/aboutdebugging/src/types/debug-target.js71
-rw-r--r--devtools/client/aboutdebugging/src/types/index.js18
-rw-r--r--devtools/client/aboutdebugging/src/types/moz.build10
-rw-r--r--devtools/client/aboutdebugging/src/types/runtime.js164
-rw-r--r--devtools/client/aboutdebugging/src/types/ui.js85
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser.ini167
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_console.js440
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_debugger.js87
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_inspector.js86
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_nobg.js78
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_popup.js246
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_reload.js136
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_storage.js90
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_toolbox.js122
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_eventpage_actions_and_status.js153
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_eventpage_terminate_on_idle.js200
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_manifest_url.js73
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_popup_picker.js99
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_remote_runtime.js145
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_addon_buttons.js116
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_id_message.js70
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_install_error.js89
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_install_path.js65
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_reload_error.js68
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_warnings.js49
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_connect_networklocations.js103
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_connect_toggle_usb_devices.js81
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_connection_prompt_setting.js70
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_debug-target-pane_collapsibilities_interaction.js62
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_debug-target-pane_collapsibilities_preference.js40
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_debug-target-pane_empty.js83
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_debug-target-pane_usb_runtime.js72
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtools.js51
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_breakpoint.js74
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_contextmenu.js102
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_contextmenu_markupview.js45
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_focus.js122
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_menubar.js74
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_navigate_back_forward.js63
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_navigate_reload_button.js53
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_navigate_to_url.js56
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_reload.js74
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_shortcuts.js106
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_splitconsole_key.js46
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_target_destroyed.js36
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_tooltip_markupview.js102
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_zoom.js65
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_fenix_runtime_display.js141
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_fenix_runtime_node_picker.js83
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_hidden_addons.js106
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_message_close.js82
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_navigate.js112
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_persist_connection.js91
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_process_category.js53
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_process_main.js87
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_process_main_local.js44
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_profiler_dialog.js238
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_real_usb_runtime_page_runtime_info.js62
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_real_usb_sidebar.js34
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_routes.js114
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_rtl.js62
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_runtime_compatibility_warning.js90
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_runtime_disconnect_remote_runtime.js65
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_runtime_remote_runtime_buttons.js56
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_runtime_usbclient_closed.js109
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_select_network_runtime.js52
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_select_page_with_serviceworker.js78
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_console.js131
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_fetch_flag.js68
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_not_compatible.js124
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_push.js61
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_pushservice_url.js118
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_runtime-page.js70
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_start.js71
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_status.js104
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_timeout.js104
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_unregister.js48
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_connection_state.js192
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_network_runtimes.js43
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_runtime.js38
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_runtime_connect.js54
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_runtime_refresh.js50
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_runtime_select.js44
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_status.js50
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_unavailable_runtime.js65
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_unplugged_device.js69
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_tab_favicons.js57
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_tab_navigate.js31
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_tab_zombietab.js108
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_basic.js46
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_connection_attempt.js274
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_inspect.js67
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_navigate.js67
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_actions.js97
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_connected_details.js69
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_updates.js154
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_updates_multi.js111
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_updates_network.js90
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_thisfirefox.js121
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_thisfirefox_runtime_info.js54
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_thisfirefox_worker_inspection.js68
-rw-r--r--devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_workers_remote_runtime.js160
-rw-r--r--devtools/client/aboutdebugging/test/browser/empty.html1
-rw-r--r--devtools/client/aboutdebugging/test/browser/head.js505
-rw-r--r--devtools/client/aboutdebugging/test/browser/helper-adb.js59
-rw-r--r--devtools/client/aboutdebugging/test/browser/helper-addons.js262
-rw-r--r--devtools/client/aboutdebugging/test/browser/helper-collapsibilities.js54
-rw-r--r--devtools/client/aboutdebugging/test/browser/helper-mocks.js262
-rw-r--r--devtools/client/aboutdebugging/test/browser/helper-real-usb.js54
-rw-r--r--devtools/client/aboutdebugging/test/browser/helper-serviceworker.js111
-rw-r--r--devtools/client/aboutdebugging/test/browser/helper-telemetry.js115
-rw-r--r--devtools/client/aboutdebugging/test/browser/mocks/helper-adb-mock.js137
-rw-r--r--devtools/client/aboutdebugging/test/browser/mocks/helper-client-wrapper-mock.js138
-rw-r--r--devtools/client/aboutdebugging/test/browser/mocks/helper-runtime-client-factory-mock.js76
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/bad-extensions/invalid-json/manifest.json1
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/bad-extensions/invalid-property/manifest.json23
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/doc_aboutdebugging_devtoolstoolbox_breakpoint.html9
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/packaged-extension/packaged-extension.xpibin0 -> 4580 bytes
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/real/usb-runtimes-sample.json14
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/script_aboutdebugging_devtoolstoolbox_breakpoint.js12
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/service-workers/controlled-sw.html38
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/service-workers/controlled-sw.js31
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/service-workers/empty-sw.html30
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/service-workers/empty-sw.js1
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/service-workers/fetch-sw.html30
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/service-workers/fetch-sw.js6
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/service-workers/push-sw.html61
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/service-workers/push-sw.js35
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/test-adb-extension/adb-extension-linux.xpibin0 -> 495578 bytes
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/test-adb-extension/adb-extension-linux64.xpibin0 -> 824421 bytes
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/test-adb-extension/adb-extension-mac64.xpibin0 -> 782572 bytes
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/test-adb-extension/adb-extension-win32.xpibin0 -> 713900 bytes
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/test-temporary-extension/manifest.json13
-rw-r--r--devtools/client/aboutdebugging/test/browser/resources/test-temporary-extension/script.js8
-rw-r--r--devtools/client/aboutdebugging/test/browser/test-tab-favicons.html8
-rw-r--r--devtools/client/aboutdebugging/test/node/.eslintrc.js10
-rw-r--r--devtools/client/aboutdebugging/test/node/README.md22
-rw-r--r--devtools/client/aboutdebugging/test/node/babel.config.js8
-rw-r--r--devtools/client/aboutdebugging/test/node/components/__snapshots__/shared-message.test.js.snap91
-rw-r--r--devtools/client/aboutdebugging/test/node/components/shared-message.test.js69
-rw-r--r--devtools/client/aboutdebugging/test/node/jest.config.js14
-rw-r--r--devtools/client/aboutdebugging/test/node/package.json22
-rw-r--r--devtools/client/aboutdebugging/test/node/setup.js15
-rw-r--r--devtools/client/aboutdebugging/test/node/yarn.lock4133
-rw-r--r--devtools/client/aboutdebugging/test/xpcshell/.eslintrc.js6
-rw-r--r--devtools/client/aboutdebugging/test/xpcshell/test_extensions_path.js27
-rw-r--r--devtools/client/aboutdebugging/test/xpcshell/test_runtime_default_preferences.js203
-rw-r--r--devtools/client/aboutdebugging/test/xpcshell/xpcshell-head.js10
-rw-r--r--devtools/client/aboutdebugging/test/xpcshell/xpcshell.ini8
258 files changed, 26077 insertions, 0 deletions
diff --git a/devtools/client/aboutdebugging/README.md b/devtools/client/aboutdebugging/README.md
new file mode 100644
index 0000000000..589b708c33
--- /dev/null
+++ b/devtools/client/aboutdebugging/README.md
@@ -0,0 +1,86 @@
+# about:debugging-new
+
+## What is about:debugging-new
+The purpose of about:debugging is to be a debugging hub to start inspecting your addons, processes, tabs and workers. This new version of about:debugging will also allow you to debug remote devices (Firefox for Android on a smartphone). The user should be able to connect either via USB or WiFi.
+
+To try out about:debugging, type `about:debugging` in the Firefox URL bar.
+
+## Technical overview
+
+The about:debugging-new UI is built using React and Redux. The various React/Redux files should be organized as follows:
+- devtools/client/aboutdebugging/src/actions
+- devtools/client/aboutdebugging/src/components
+- devtools/client/aboutdebugging/src/middleware
+- devtools/client/aboutdebugging/src/reducers
+
+The folder `devtools/client/aboutdebugging/src/modules` contains various helpers and classes that are not related to React/Redux. For instance modules/usb-runtimes.js provides an abstraction layer to enable USB runtimes scanning, to list USB runtimes etc...
+
+### Firefox Component Registration
+about:debugging-new is an "about" page registered via a component manifest that is located in `/devtools/startup/aboutdebugging.manifest`. The component registration code is at `/devtools/startup/aboutdebugging-registration.js`.
+
+### Actions
+Actions should cover all user or external events that change the UI.
+
+#### asynchronous actions
+For asynchronous actions, we will use the thunk middleware, similar to what it done in the webconsole and netmonitor. An asynchronous action should be split in three actions:
+- start
+- success
+- failure
+
+As we will implement asynchronous, we should aim to keep a consistent naming for those actions.
+
+A typical usecase for an asynchronous action here would be selecting a runtime. The selection will be immediate but should trigger actions to retrieve tabs, addons, workers etc… which will all be asynchronous.
+
+### Components
+Components should avoid dealing with specialized objects as much as possible.
+
+They should never use any of the lifecycle methods that will be deprecated in React 17 (`componentWillMount`, `componentWillReceiveProps`, and `componentWillUpdate`).
+
+When it comes to updating the state, components should do it via an action if the result of this action is something that should be preserved when reloading the UI.
+
+### Middlewares
+We use several middlewares in the application. The middlewares are required in the create-store module `devtools/client/aboutdebugging/src/create-store.js`.
+
+#### thunk
+This is a shared middleware used by other DevTools modules that allows to dispatch actions and allows to create multiple asynchronous actions.
+
+#### debug-target-listener
+This middleware is responsible for monitoring a client. It will add various listeners on the client when receiving the `WATCH_RUNTIME_SUCCESS` action and will remove the listeners when receiving `UNWATCH_RUNTIME_SUCCESS`. Could probably be ported to an action, no real added value as a middleware.
+
+#### error-logging
+This middleware will forward any error object dispatched by an action to the console. Typically, all the `FAILURE` actions of our asynchronous actions should dispatch an error object.
+
+#### extension/tab/worker-component-data
+This middleware takes the raw data of a debugging target returned by the devtools client and transforms it to presentation data that can be used by the debugtarget components. Could probably be ported to an action, no real added value as a middleware.
+
+#### waitUntilService
+This is a shared middleware used by other DevTools modules that allows to wait until a given action is dispatched. We use it in mochitests.
+
+### state
+The state represents the model for the UI of the application.
+
+##### ui
+Holds the global state of the application. This can contain generic preferences such as "is feature X enabled" or purely UI related "is category X collapsed".
+
+##### runtimes
+Holds the current list of runtimes known by the application as well as the currently selected runtime.
+
+##### debug-targets
+Holds all the debug targets (ie tabs, addons, workers etc...) for the currently monitored runtime. There is at most one monitored runtime at a given time.
+
+### Constants
+Constants can be found at `devtools/client/aboutdebugging/src/constants.js`. It contains all the actions available in about:debugging as well as several other "packages" of constants used in the application.
+
+If a constant (string, number, etc...) is only used in a single module it can be defined as a `const` in this module. However as soon as the constant is shared by several modules, it should move to `devtools/client/aboutdebugging/src/constants.js`.
+
+### Types
+Types can be found at `devtools/client/aboutdebugging/src/types.js`. They serve both as documentation as well as validation. We do not aim at having types for every single element of the state, but only for complex objects.
+
+## Contact
+If you have any questions about the code, features, planning, the active team is:
+- engineering: Belén Albeza (:ladybenko)
+- engineering: Daisuke Akatsuka (:daisuke)
+- engineering: Julian Descottes (:jdescottes)
+- product management: Harald Kischner (:digitarald)
+
+You can find us on [Matrix](https://chat.mozilla.org/#/room/#devtools:mozilla.org).
diff --git a/devtools/client/aboutdebugging/aboutdebugging.css b/devtools/client/aboutdebugging/aboutdebugging.css
new file mode 100644
index 0000000000..1ab9c2ad22
--- /dev/null
+++ b/devtools/client/aboutdebugging/aboutdebugging.css
@@ -0,0 +1,35 @@
+/* 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/. */
+
+/*
+* Global styles
+*/
+@import "chrome://devtools/skin/variables.css";
+@import "chrome://devtools/content/aboutdebugging/src/base.css";
+
+/*
+* Components
+*/
+@import "chrome://devtools/content/aboutdebugging/src/components/App.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/ProfilerDialog.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/RuntimeActions.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/RuntimeInfo.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/connect/ConnectPage.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/connect/ConnectSection.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/connect/ConnectSteps.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/connect/NetworkLocationsForm.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/connect/NetworkLocationsList.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/debugtarget/DebugTargetItem.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/debugtarget/DebugTargetList.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/debugtarget/DebugTargetPane.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/debugtarget/ExtensionDetail.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/debugtarget/FieldPair.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/shared/IconLabel.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/shared/Message.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/sidebar/Sidebar.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/sidebar/SidebarFixedItem.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/sidebar/SidebarItem.css";
+@import "chrome://devtools/content/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.css";
diff --git a/devtools/client/aboutdebugging/aboutdebugging.js b/devtools/client/aboutdebugging/aboutdebugging.js
new file mode 100644
index 0000000000..7079ff7f7a
--- /dev/null
+++ b/devtools/client/aboutdebugging/aboutdebugging.js
@@ -0,0 +1,201 @@
+/* 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 {
+ bindActionCreators,
+} = require("resource://devtools/client/shared/vendor/redux.js");
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ render,
+ unmountComponentAtNode,
+} = require("resource://devtools/client/shared/vendor/react-dom.js");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const LocalizationProvider = createFactory(FluentReact.LocalizationProvider);
+
+const actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const {
+ configureStore,
+} = require("resource://devtools/client/aboutdebugging/src/create-store.js");
+const {
+ setDebugTargetCollapsibilities,
+} = require("resource://devtools/client/aboutdebugging/src/modules/debug-target-collapsibilities.js");
+
+const {
+ l10n,
+} = require("resource://devtools/client/aboutdebugging/src/modules/l10n.js");
+
+const {
+ addNetworkLocationsObserver,
+ getNetworkLocations,
+ removeNetworkLocationsObserver,
+} = require("resource://devtools/client/aboutdebugging/src/modules/network-locations.js");
+const {
+ addUSBRuntimesObserver,
+ getUSBRuntimes,
+ removeUSBRuntimesObserver,
+} = require("resource://devtools/client/aboutdebugging/src/modules/usb-runtimes.js");
+
+loader.lazyRequireGetter(
+ this,
+ "adb",
+ "resource://devtools/client/shared/remote-debugging/adb/adb.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "adbAddon",
+ "resource://devtools/client/shared/remote-debugging/adb/adb-addon.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "adbProcess",
+ "resource://devtools/client/shared/remote-debugging/adb/adb-process.js",
+ true
+);
+
+const Router = createFactory(
+ require("resource://devtools/client/shared/vendor/react-router-dom.js")
+ .HashRouter
+);
+const App = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/App.js")
+);
+
+const AboutDebugging = {
+ async init() {
+ const direction = Services.locale.isAppLocaleRTL ? "rtl" : "ltr";
+ document.documentElement.setAttribute("dir", direction);
+
+ this.onAdbAddonUpdated = this.onAdbAddonUpdated.bind(this);
+ this.onAdbProcessReady = this.onAdbProcessReady.bind(this);
+ this.onNetworkLocationsUpdated = this.onNetworkLocationsUpdated.bind(this);
+ this.onUSBRuntimesUpdated = this.onUSBRuntimesUpdated.bind(this);
+
+ this.store = configureStore();
+ this.actions = bindActionCreators(actions, this.store.dispatch);
+
+ const width = this.getRoundedViewportWidth();
+ this.actions.recordTelemetryEvent("open_adbg", { width });
+
+ await l10n.init([
+ "branding/brand.ftl",
+ "devtools/client/aboutdebugging.ftl",
+ ]);
+
+ this.actions.createThisFirefoxRuntime();
+
+ // Listen to Network locations updates and retrieve the initial list of locations.
+ addNetworkLocationsObserver(this.onNetworkLocationsUpdated);
+ await this.onNetworkLocationsUpdated();
+
+ // Listen to USB runtime updates and retrieve the initial list of runtimes.
+
+ // If ADB is already started, wait for the initial runtime list to be able to restore
+ // already connected runtimes.
+ const isProcessStarted = await adb.isProcessStarted();
+ const onAdbRuntimesReady = isProcessStarted
+ ? adb.once("runtime-list-ready")
+ : null;
+ addUSBRuntimesObserver(this.onUSBRuntimesUpdated);
+ await onAdbRuntimesReady;
+
+ await this.onUSBRuntimesUpdated();
+
+ render(
+ Provider(
+ {
+ store: this.store,
+ },
+ LocalizationProvider(
+ { bundles: l10n.getBundles() },
+ Router({}, App({}))
+ )
+ ),
+ this.mount
+ );
+
+ adbAddon.on("update", this.onAdbAddonUpdated);
+ this.onAdbAddonUpdated();
+ adbProcess.on("adb-ready", this.onAdbProcessReady);
+ // get the initial status of adb process, in case it's already started
+ this.onAdbProcessReady();
+ },
+
+ onAdbAddonUpdated() {
+ this.actions.updateAdbAddonStatus(adbAddon.status);
+ },
+
+ onAdbProcessReady() {
+ this.actions.updateAdbReady(adbProcess.ready);
+ },
+
+ onNetworkLocationsUpdated() {
+ return this.actions.updateNetworkLocations(getNetworkLocations());
+ },
+
+ async onUSBRuntimesUpdated() {
+ const runtimes = await getUSBRuntimes();
+ return this.actions.updateUSBRuntimes(runtimes);
+ },
+
+ async destroy() {
+ const width = this.getRoundedViewportWidth();
+ this.actions.recordTelemetryEvent("close_adbg", { width });
+
+ const state = this.store.getState();
+ const currentRuntimeId = state.runtimes.selectedRuntimeId;
+ if (currentRuntimeId) {
+ await this.actions.unwatchRuntime(currentRuntimeId);
+ }
+
+ // Remove all client listeners.
+ this.actions.removeRuntimeListeners();
+
+ removeNetworkLocationsObserver(this.onNetworkLocationsUpdated);
+ removeUSBRuntimesObserver(this.onUSBRuntimesUpdated);
+ adbAddon.off("update", this.onAdbAddonUpdated);
+ adbProcess.off("adb-ready", this.onAdbProcessReady);
+ setDebugTargetCollapsibilities(state.ui.debugTargetCollapsibilities);
+ unmountComponentAtNode(this.mount);
+ },
+
+ get mount() {
+ return document.getElementById("mount");
+ },
+
+ /**
+ * Computed viewport width, rounded at 50px. Used for telemetry events.
+ */
+ getRoundedViewportWidth() {
+ return Math.ceil(window.outerWidth / 50) * 50;
+ },
+};
+
+window.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ AboutDebugging.init();
+ },
+ { once: true }
+);
+
+window.addEventListener(
+ "unload",
+ () => {
+ AboutDebugging.destroy();
+ },
+ { once: true }
+);
+
+// Expose AboutDebugging to tests so that they can access to the store.
+window.AboutDebugging = AboutDebugging;
diff --git a/devtools/client/aboutdebugging/documentation/ANDROID_BUILD.md b/devtools/client/aboutdebugging/documentation/ANDROID_BUILD.md
new file mode 100644
index 0000000000..42d1a52520
--- /dev/null
+++ b/devtools/client/aboutdebugging/documentation/ANDROID_BUILD.md
@@ -0,0 +1,60 @@
+# Build Firefox for Android
+
+## When do you need to build Firefox for Android
+
+If a remote debugging change impacts the server (file located in `devtools/server` or in `devtools/shared`), you will not be able to test it with a device which runs the release version of Firefox for Android. For the purpose of remote debugging, your local build of Firefox Desktop where you will test about:debugging runs the content of `devtools/client` (including `devtools/client/aboutdebugging`). And `devtools/server` runs on the device. So as soon as you are developing or testing a patch that needs to update the server and is about USB debugging, you need to build Firefox for Android and deploy it on a test device.
+
+## Setup your environment
+
+This will be a short documentation focused on the typical patches you may write for about:debugging. For a more complete documentation, you can refer to https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions/Simple_Firefox_for_Android_build.
+
+The whole setup needs to download several gigabytes of dependencies so try to have a fast internet connection to follow those steps.
+
+### Clone mozilla-central
+
+It is recommended to create a new clone of mozilla-central for your Firefox for Android builds.
+
+```
+ hg clone https://hg.mozilla.org/mozilla-central mozilla-central-android
+ cd mozilla-central-android
+```
+
+### Run bootstrap
+
+Next simply run `mach bootstrap` and select the third option `3. Firefox for Android Artifact Mode`
+
+```
+ > ./mach bootstrap
+ Please choose the version of Firefox you want to build:
+ 1. Firefox for Desktop Artifact Mode
+ 2. Firefox for Desktop
+ 3. Firefox for Android Artifact Mode
+ 4. Firefox for Android
+ > 3
+```
+
+Follow the instructions, it will take some time as it needs to download a lot of dependencies. At the end it will provide you with a template you should use to create a `.mozconfig` file. You can use the proposed content without changing anything.
+
+### Enable USB debugging on your phone
+
+If you already used your device for USB debugging, this should already be enabled, but we will repeat the steps here.
+
+In the Settings menu, choose "About" and scroll down to the Build Number option. There's a hidden option there to activate "developer mode": tap the Build Number option seven times. You’ll see a countdown, and then a "Developer Options" menu will appear in your Settings. Don’t worry — you can turn this off whenever you like. The last step is to enable USB Debugging in the Developer Options menu.
+
+## Build and deploy to your phone
+
+Connect your phone to your computer with a USB cable. Then run:
+
+```
+ ./mach build
+ ./mach package
+ ./mach install
+```
+
+At this step if you go to the list of applications on your phone, you should be able to spot a "Fennec" application. The fullname will be slightly different, for instance for me it is called "Fennec jdescottes". You can then run the application from your desktop command-line:
+
+```
+ ./mach run
+```
+
+Sometimes this will fail with `WARNING: unable to launch Firefox for Android`. In that case you can simply start the application on your phone, as you would start any other application.
diff --git a/devtools/client/aboutdebugging/documentation/GECKOVIEW_EXAMPLE_BUILD.md b/devtools/client/aboutdebugging/documentation/GECKOVIEW_EXAMPLE_BUILD.md
new file mode 100644
index 0000000000..d34c9b2dfe
--- /dev/null
+++ b/devtools/client/aboutdebugging/documentation/GECKOVIEW_EXAMPLE_BUILD.md
@@ -0,0 +1,66 @@
+# Build GeckoView Example
+
+## When do you need to build GeckoView Example
+
+If a remote debugging change impacts the server (file located in `devtools/server` or in `devtools/shared`), you will not be able to test it without building your GeckoView Example. For the purpose of remote debugging, your local build of Firefox Desktop where you will test about:debugging runs the content of `devtools/client` (including `devtools/client/aboutdebugging`). And `devtools/server` runs on the device. So as soon as you are developing or testing a patch that needs to update the server and is about USB debugging, you need to build GeckoView Example and deploy it on a test device.
+
+## Setup your environment
+
+This will be a short documentation focused on the typical patches you may write for about:debugging. For a more complete documentation, you can refer to https://mozilla.github.io/geckoview/ and https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions/Simple_Firefox_for_Android_build.
+
+The whole setup needs to download several gigabytes of dependencies so try to have a fast internet connection to follow those steps.
+
+### Clone mozilla-central
+
+It is recommended to create a new clone of mozilla-central for your GeckoView Example builds.
+
+```
+ hg clone https://hg.mozilla.org/mozilla-central mozilla-central-gecko-view
+ cd mozilla-central-gecko-view
+```
+
+### Run bootstrap
+
+Next simply run `mach bootstrap` and select the third option `3. GeckoView/Firefox for Android Artifact Mode`
+
+```
+ > ./mach bootstrap
+ Please choose the version of Firefox you want to build:
+ 1. Firefox for Desktop Artifact Mode
+ 2. Firefox for Desktop
+ 3. GeckoView/Firefox for Android Artifact Mode
+ 4. GeckoView/Firefox for Android
+ > 3
+```
+
+Follow the instructions, it will take some time as it needs to download a lot of dependencies. At the end it will provide you with a template you should use to create a `.mozconfig` file. You can use the proposed content without changing anything.
+
+### Enable USB debugging on your phone
+
+If you already used your device for USB debugging, this should already be enabled, but we will repeat the steps here.
+
+In the Settings menu, choose "About" and scroll down to the Build Number option. There's a hidden option there to activate "developer mode": tap the Build Number option seven times. You’ll see a countdown, and then a "Developer Options" menu will appear in your Settings. Don’t worry — you can turn this off whenever you like. The last step is to enable USB Debugging in the Developer Options menu.
+
+## Build and deploy to your phone
+
+Connect your phone to your computer with a USB cable. Then run:
+
+```
+ ./mach build
+ ./mach package
+ ./mach android install-geckoview_example
+```
+
+At this step if you go to the list of applications on your phone, you should be able to spot a "GeckoView Example" application.
+
+## Reflect changes
+
+If you change server files that impact GeckoView, you need to build and install again. Reflect the changes by:
+
+```
+ ./mach build faster
+ ./mach package
+ ./mach android install-geckoview_example
+```
+
+Once you built all, the changes under `devtools/server` and `devtools/shared` can be built with `faster` option. This should be faster.
diff --git a/devtools/client/aboutdebugging/documentation/GECKOVIEW_REFERENCE_BROWSER_BUILD.md b/devtools/client/aboutdebugging/documentation/GECKOVIEW_REFERENCE_BROWSER_BUILD.md
new file mode 100644
index 0000000000..0036080a04
--- /dev/null
+++ b/devtools/client/aboutdebugging/documentation/GECKOVIEW_REFERENCE_BROWSER_BUILD.md
@@ -0,0 +1,152 @@
+# Build GeckoView Reference Browser
+
+## When do you need to build GeckoView Reference Browser
+
+If a remote debugging change impacts the server (file located in `devtools/server` or in `devtools/shared`), you will not be able to test it without building your GeckoView Reference Browser. For the purpose of remote debugging, your local build of Firefox Desktop where you will test about:debugging runs the content of `devtools/client` (including `devtools/client/aboutdebugging`). And `devtools/server` runs on the device. So as soon as you are developing or testing a patch that needs to update the server and is about USB debugging, you need to build GeckoView Reference Browser and deploy it on a test device. To build custom Reference Browser, need two modules of GeckoView and Reference Browser.
+
+## Setup your environment and build
+
+This will be a short documentation focused on the typical patches you may write for about:debugging. For a more complete documentation, you can refer to https://mozilla.github.io/geckoview/ and https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions/Simple_Firefox_for_Android_build.
+
+The whole setup needs to download several gigabytes of dependencies so try to have a fast internet connection to follow those steps.
+
+### Make empty directory
+
+It is recommended to create a new directory to build your GeckoView and Reference Browser.
+
+```
+ mkdir geckoview-reference-browser
+ cd geckoview-reference-browser
+```
+
+### Build GeckoView
+
+#### Clone mozilla-central
+
+It is recommended to create a new clone of mozilla-central for your GeckoView builds.
+
+```
+ hg clone https://hg.mozilla.org/mozilla-central mozilla-central-gecko-view
+ cd mozilla-central-gecko-view
+```
+
+#### Run bootstrap
+
+Next simply run `mach bootstrap` and select the third option `3. GeckoView/Firefox for Android Artifact Mode`
+
+```
+ > ./mach bootstrap
+ Please choose the version of Firefox you want to build:
+ 1. Firefox for Desktop Artifact Mode
+ 2. Firefox for Desktop
+ 3. GeckoView/Firefox for Android Artifact Mode
+ 4. GeckoView/Firefox for Android
+ > 3
+```
+
+Follow the instructions, it will take some time as it needs to download a lot of dependencies. At the end it will provide you with a template you should use to create a `.mozconfig` file. You can use the proposed content without changing anything.
+
+### Build
+
+Execute command below to build.
+
+```
+ ./mach build
+ ./mach package
+ ./mach android archive-geckoview
+```
+
+If the build has finished successfully, the GeckoView AAR file will be created in your build output directory. You can find this file with following command:
+
+```
+ > ls mozilla-central-gecko-view/<your-output-directory>/gradle/build/mobile/android/geckoview/outputs/aar
+ geckoview-withGeckoBinaries-debug.aar
+```
+
+
+### Build Reference Browser
+
+#### Clone reference-browser
+
+It is recommended to create a new clone of reference-browser for your Reference Browser builds.
+
+```
+ cd ../
+ git clone https://github.com/mozilla-mobile/reference-browser
+ cd reference-browser
+```
+
+#### Create `local.properties` file
+
+`local.properties` file is necessary to specify the location of the Android SDK. Please write the absolute path of Android SDK with `sdk.dir` key. If you did `./mach bootstrap` once, Android SDK should already installed. You can find the directory whose name is like `android-sdk-<os-name>` under `~/.mozconfig/`. Thus, in mac osx case, the content of `local.properties` should be like below:
+
+```
+sdk.dir=/Users/xxxxxx/.mozbuild/android-sdk-osx
+```
+
+#### Edit `app/build.gradle` to build Reference Browser with above GeckoView
+
+You need to edit two places in `app/build.gradle`.
+
+1. Add `repositories` block with following content to bottom of file. <absolute path to AAR> is the directory which was created by `./mach android archive-geckoview` to build GeckoView. This should be like `/User/xxxxxx/mozilla-central-gecko-view/<your-output-directory>/gradle/build/mobile/android/geckoview/outputs/aar`.
+
+```
+repositories {
+ flatDir(
+ name: "localBuild",
+ dirs: "<absolute path to AAR>"
+ )
+}
+```
+
+2. Edit `geckoNightlyArmImplementation`
+
+```
+dependencies {
+ // ...
+
+ //geckoNightlyArmImplementation Gecko.geckoview_nightly_arm
+ geckoNightlyArmImplementation(
+ name: 'geckoview-withGeckoBinaries-debug',
+ ext: 'aar'
+ )
+
+ // ...
+}
+```
+
+#### Build and deploy to your phone
+
+Connect your phone to your computer with a USB cable. Then run:
+
+```
+ ./gradlew build
+ ./gradlew installGeckoNightlyArmDebug
+```
+
+At this step if you go to the list of applications on your phone, you should be able to spot a "Reference Browser" application.
+
+### Enable USB debugging on your phone
+
+If you already used your device for USB debugging, this should already be enabled, but we will repeat the steps here.
+
+In the Settings menu, choose "About" and scroll down to the Build Number option. There's a hidden option there to activate "developer mode": tap the Build Number option seven times. You’ll see a countdown, and then a "Developer Options" menu will appear in your Settings. Don’t worry — you can turn this off whenever you like. The last step is to enable USB Debugging in the Developer Options menu on Reference Browser on your device.
+
+And, you can test with Reference Browser with custom GeckoView!
+
+## Reflect changes
+
+If you change codes in GeckoView, need to build and install again. Reflect the changes by:
+
+```
+ cd mozilla-central-gecko-view
+ ./mach build faster
+ ./mach package
+ ./mach android archive-geckoview
+ cd ../
+ cd reference-browser
+ ./gradlew build
+ ./gradlew installGeckoNightlyArmDebug
+```
+
+Once you built all, the changes under `devtools/server` and `devtools/shared` can build with `faster` option. This should be faster.
diff --git a/devtools/client/aboutdebugging/documentation/TESTS.md b/devtools/client/aboutdebugging/documentation/TESTS.md
new file mode 100644
index 0000000000..637e0cd18e
--- /dev/null
+++ b/devtools/client/aboutdebugging/documentation/TESTS.md
@@ -0,0 +1,124 @@
+# Running Tests for the new about:debugging
+
+## Tests overview
+
+Tests are located in `devtools/client/aboutdebugging/test`. There are two subfolders, `browser` and `xpcshell`. `browser` contains our [browser mochitests](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Mochitest). Most of our tests are browser mochitests. `xpcshell` contains our [xpc-shell unit tests](https://developer.mozilla.org/en-US/docs/Mozilla/QA/Writing_xpcshell-based_unit_tests). At the moment of writing we only have one.
+
+## Test coverage
+
+You can get some code coverage information at https://codecov.io/gh/mozilla/gecko-dev/tree/master/devtools/client/aboutdebugging/src . The service is sometimes very slow, be patient! You might have to reload the page several times to get a result.
+
+## Running tests
+
+To run tests, you can use `./mach test {path}`. The path argument can be:
+- relative/absolute path to a single test file: will run only this test
+- relative/absolute path to a folder: will run all tests in the folder
+- just a string: will match all the tests that contain this string
+
+A few examples below:
+
+```
+# Run browser_aboutdebugging_addons_manifest_url.js only
+
+./mach test devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_manifest_url.js
+
+# or
+
+./mach test browser_aboutdebugging_addons_manifest_url.js
+```
+
+```
+# Run all aboutdebugging tests
+
+./mach test devtools/client/aboutdebugging/test/browser/
+
+# or (this works because all our tests start with "browser_aboutdebugging...")
+
+./mach test browser_aboutdebugging
+```
+
+Having consistent names for our tests can be very helpful to quickly run subset of tests:
+```
+# Run all sidebar tests (will just run all the tests that start with browser_aboutdebugging_sidebar)
+
+./mach test browser_aboutdebugging_sidebar
+```
+
+## Troubleshooting
+
+### Fix the error "ADB process is already running"
+
+Some tests for about:debugging rely on starting and stopping the ADB (android debug bridge) process. However if the process is already running on your machine, the tests have no way to proceed and will fail with the message:
+
+```
+Error: The ADB process is already running on this machine, it should be stopped before running this test
+```
+
+In this case try to kill the process named `adb` in your process manager. If the adb process keeps coming back up, there must be an application that spawns the process. It might be a Firefox instance. Stop all your Firefox instances, then kill the `adb` process again and restart Firefox. (Note that in theory we should always stop adb correctly, but it seems there are still scenarios where this doesn't happen).
+
+### Pause a test
+
+If a test is not behaving as expected, it can be helpful to pause it at a certain step to have the time to investigate. You can add an await such as:
+
+```
+await new Promise(r => setTimeout(r, TIME)); // eg, replace TIME by 60000 to wait for 1 minute
+```
+
+Note that if you really need to wait for a long time, tests will timeout after some time and shutdown automatically. To avoid that, call `requestLongerTimeout(N);` somewhere in your test. `requestLongerTimeout()` takes an integer factor that is a multiplier for the the default 45 seconds timeout. So a factor of 2 means: Wait for at last 90s.
+
+### Attach a JS debugger to mochitests
+
+You can set debug tests with the DevTools debugger by passing the `--jsdebugger` argument to your tests.
+
+At the moment, you need to use `./mach mochitest` instead of `./mach test`, because of [Bug 1519369](https://bugzilla.mozilla.org/show_bug.cgi?id=1519369). This command is less flexible than `./mach test` so you will need to absolutely pass a relative path here.
+
+```
+./mach mochitest relative/path/to/test.js --jsdebugger
+```
+
+This will open a browser toolbox, with the debugger selected, before starting your test. Feel free to browse the files in the debugger and to add breakpoints. However your file is most likely not loaded yet, so the best is usually to add `debugger` statements in your code directly.
+
+This time the tests will wait for you to click on the "Browser chrome test" window to start. Do not be fooled by the "Run all tests" button on this window, clicking anywhere in the window will actually start the tests.
+
+## Other Tips
+
+### Headless mode
+
+Headless mode allows to run tests without opening a Firefox window and therefore blocking your computer.
+
+```
+./mach test browser_aboutdebugging --headless
+```
+
+### Memory leaks and Debug mode
+
+Running tests in debug mode is simply done by using a debug build (build with `ac_add_options --enable-debug`). The added value of debug mode is that it will also assert leaks. It can be very useful to run tests in debug mode if you modified things related to event listeners for instance, and you are not sure if you are cleanly removing all the listeners.
+
+### Test verify mode
+
+The test-verify mode - shortened as "TV" on our continuous integration platforms - will run a single test in a loop with some different flavors. The intent is to make it easier to catch intermittents. If you added or modified a test significantly, it is usually a good idea, to run it in test-verify mode at least once.
+
+```
+# Keeping the --headless argument, because the tests can be pretty slow
+./mach test browser_aboutdebugging_addons_manifest_url.js --headless --test-verify
+```
+
+## Try server
+
+You can push your local changesets to our remote continuous integration server, try. This is useful if you made some significant changes and you would like to make sure nothing is broken in the whole DevTools tests suite, on any platform.
+
+There are many topics to cover here, but none are specific to about:debugging. Here are a few pointers:
+- [Try overview](https://firefox-source-docs.mozilla.org/tools/try/index.html)
+- [Selectors integrated with ./mach](https://firefox-source-docs.mozilla.org/tools/try/selectors/index.html)
+- [Try syntax selector](https://firefox-source-docs.mozilla.org/tools/try/selectors/syntax.html)
+- [Try fuzzy selector](https://firefox-source-docs.mozilla.org/tools/try/selectors/fuzzy.html)
+
+Below is an example of pushing to try using the try syntax selector. As the documentation says, this syntax is obscure and can be difficult to remember, but it is still widely used by developers in mozilla-central.
+
+```
+./mach try -b do -p linux64 -u xpcshell,mochitest-dt,mochitest-chrome --artifact
+```
+
+Refer to the [try syntax documentation](https://firefox-source-docs.mozilla.org/tools/try/selectors/syntax.html) to learn what the various parameters mean.
+
+Note that you need committer access level 1 in order to push to try.
diff --git a/devtools/client/aboutdebugging/documentation/TESTS_REAL_DEVICES.md b/devtools/client/aboutdebugging/documentation/TESTS_REAL_DEVICES.md
new file mode 100644
index 0000000000..f10709fb21
--- /dev/null
+++ b/devtools/client/aboutdebugging/documentation/TESTS_REAL_DEVICES.md
@@ -0,0 +1,67 @@
+# Running Tests with real USB connected devices for the new about:debugging
+
+This document explains how to test with real USB connected devices.
+
+## Tests overview
+
+The tests that use a real device are located in `devtools/client/aboutdebugging/test/browser/`and the name of tests starts with `browser_aboutdebugging_real`. These are normal mochitest, but we need to setup the environment before starting tests.
+
+## Setup environment
+### Real device side
+1. Enable USB debugging on your device
+2. Launch Firefox
+3. Enable USB debugging on your Firefox
+4. Connect to your PC via USB
+
+You can refer to https://firefox-source-docs.mozilla.org/devtools-user/about_colon_debugging/index.html#connecting-to-a-remote-device
+
+### PC side
+Setup the real device information to evaluate the validity in tests.
+
+1. Copy a sample file which is located at `devtools/client/aboutdebugging/test/browser/real/usb-runtimes-sample.json` and rename it for example to `devtools/client/aboutdebugging/test/browser/real/local-usb-runtimes.json`.
+2. Edit the file.
+
+ This is a JSON file like below, write your real device information in here. This example indicates that there should be one USB device and should be displayed `Pixel 2` as device name and `Firefox Nightly` as short name on the sidebar of about:debugging. Regarding the other information, please see `Detail of config file` section of this document.
+
+```
+[
+ {
+ "sidebarInfo": {
+ "deviceName": "Pixel 2",
+ "shortName": "Firefox Nightly"
+ },
+ ...
+ },
+ ...
+]
+```
+
+## Test
+Normally, although we do test `./mach mochitest <path>`, to load the real device information created, specify the path as `USB_RUNTIME` environment variable, then do test.
+For example, if the name of the saved file is `devtools/client/aboutdebugging/test/browser/real/local-usb-runtimes.json`, run all real device test with a command like the one below:
+
+```
+USB_RUNTIMES=devtools/client/aboutdebugging/test/browser/real/local-usb-runtimes.json ./mach mochitest devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_real
+```
+
+If there is no `USB_RUNTIMES` environment variable, the tests will not run. This is also to avoid to run on try-server and so on.
+
+## Detail of config file
+
+```
+[
+ {
+ "sidebarInfo": {
+ "deviceName": "Pixel 2", // This should display as device name on the sidebar.
+ "shortName": "Firefox Nightly" // This should display as short name on the sidebar.
+ },
+ "runtimeDetails": {
+ "info": {
+ "name": "Mozilla Nightly", // This should display on the runtime info of runtime page.
+ "version": "64.0a1" // This should display on the runtime info of runtime page.
+ }
+ }
+ }
+ // Of course, you can append additional USB devices. Some tests can do with multiple devices.
+]
+```
diff --git a/devtools/client/aboutdebugging/index.html b/devtools/client/aboutdebugging/index.html
new file mode 100644
index 0000000000..e83bbf4a88
--- /dev/null
+++ b/devtools/client/aboutdebugging/index.html
@@ -0,0 +1,30 @@
+<!-- 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>
+<!-- Use force-theme="auto" to force the performance panel UI to use the default
+ Firefox color scheme when loaded in a popup in about:debugging -->
+<html force-theme="auto">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="color-scheme" content="light dark" />
+ <title>Debugging</title>
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src chrome: resource:; img-src data: chrome: resource: https:; object-src 'none'"
+ />
+ <link
+ rel="icon"
+ type="image/png"
+ href="chrome://global/skin/icons/developer.svg"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/aboutdebugging/aboutdebugging.css"
+ />
+ <script src="resource://devtools/client/aboutdebugging/initializer.js"></script>
+ </head>
+ <body>
+ <div id="mount"></div>
+ </body>
+</html>
diff --git a/devtools/client/aboutdebugging/initializer.js b/devtools/client/aboutdebugging/initializer.js
new file mode 100644
index 0000000000..a541df46b9
--- /dev/null
+++ b/devtools/client/aboutdebugging/initializer.js
@@ -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/. */
+
+"use strict";
+
+const { BrowserLoader } = ChromeUtils.import(
+ "resource://devtools/shared/loader/browser-loader.js"
+);
+const { require } = BrowserLoader({
+ baseURI: "resource://devtools/client/aboutdebugging/",
+ window,
+});
+
+// The only purpose of this module is to load the real aboutdebugging module via the
+// BrowserLoader.
+// This cannot be done using an inline script tag in index.html because we are applying
+// CSP for about: pages in Bug 1492063.
+// And this module cannot be merged with aboutdebugging.js because modules loaded with
+// script tags are using Promises bound to the lifecycle of the document, while modules
+// loaded with a devtools loader use Promises that will still resolve if the document is
+// destroyed. This is particularly useful to ensure asynchronous destroy() calls succeed.
+require("resource://devtools/client/aboutdebugging/aboutdebugging.js");
diff --git a/devtools/client/aboutdebugging/moz.build b/devtools/client/aboutdebugging/moz.build
new file mode 100644
index 0000000000..0ab3b80613
--- /dev/null
+++ b/devtools/client/aboutdebugging/moz.build
@@ -0,0 +1,20 @@
+# 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(
+ "aboutdebugging.js",
+ "initializer.js",
+)
+
+DIRS += [
+ "src",
+]
+
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.ini"]
+
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"]
+
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "about:debugging")
diff --git a/devtools/client/aboutdebugging/src/actions/debug-targets.js b/devtools/client/aboutdebugging/src/actions/debug-targets.js
new file mode 100644
index 0000000000..33b2f1cbea
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/debug-targets.js
@@ -0,0 +1,356 @@
+/* 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 } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs",
+ // AddonManager is a singleton, never create two instances of it.
+ { loadInDevToolsLoader: false }
+);
+const {
+ remoteClientManager,
+} = require("resource://devtools/client/shared/remote-debugging/remote-client-manager.js");
+
+const {
+ l10n,
+} = require("resource://devtools/client/aboutdebugging/src/modules/l10n.js");
+
+const {
+ isSupportedDebugTargetPane,
+} = require("resource://devtools/client/aboutdebugging/src/modules/debug-target-support.js");
+
+const {
+ openTemporaryExtension,
+} = require("resource://devtools/client/aboutdebugging/src/modules/extensions-helper.js");
+
+const {
+ getCurrentClient,
+ getCurrentRuntime,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
+
+const {
+ gDevTools,
+} = require("resource://devtools/client/framework/devtools.js");
+
+const {
+ DEBUG_TARGETS,
+ DEBUG_TARGET_PANE,
+ REQUEST_EXTENSIONS_FAILURE,
+ REQUEST_EXTENSIONS_START,
+ REQUEST_EXTENSIONS_SUCCESS,
+ REQUEST_PROCESSES_FAILURE,
+ REQUEST_PROCESSES_START,
+ REQUEST_PROCESSES_SUCCESS,
+ REQUEST_TABS_FAILURE,
+ REQUEST_TABS_START,
+ REQUEST_TABS_SUCCESS,
+ REQUEST_WORKERS_FAILURE,
+ REQUEST_WORKERS_START,
+ REQUEST_WORKERS_SUCCESS,
+ TEMPORARY_EXTENSION_INSTALL_FAILURE,
+ TEMPORARY_EXTENSION_INSTALL_START,
+ TEMPORARY_EXTENSION_INSTALL_SUCCESS,
+ TEMPORARY_EXTENSION_RELOAD_FAILURE,
+ TEMPORARY_EXTENSION_RELOAD_START,
+ TEMPORARY_EXTENSION_RELOAD_SUCCESS,
+ TERMINATE_EXTENSION_BGSCRIPT_FAILURE,
+ TERMINATE_EXTENSION_BGSCRIPT_SUCCESS,
+ TERMINATE_EXTENSION_BGSCRIPT_START,
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+
+function getTabForUrl(url) {
+ for (const navigator of Services.wm.getEnumerator("navigator:browser")) {
+ for (const browser of navigator.gBrowser.browsers) {
+ if (
+ browser.contentWindow &&
+ browser.contentWindow.location.href === url
+ ) {
+ return navigator.gBrowser.getTabForBrowser(browser);
+ }
+ }
+ }
+
+ return null;
+}
+
+function inspectDebugTarget(type, id) {
+ return async ({ dispatch, getState }) => {
+ const runtime = getCurrentRuntime(getState().runtimes);
+
+ if (
+ type == DEBUG_TARGETS.EXTENSION &&
+ runtime.id === RUNTIMES.THIS_FIREFOX
+ ) {
+ // Bug 1780912: To avoid UX issues when debugging local web extensions,
+ // we are opening the toolbox in an independant window.
+ // Whereas all others are opened in new tabs.
+ gDevTools.showToolboxForWebExtension(id);
+ } else {
+ const urlParams = {
+ type,
+ };
+ // Main process may not provide any ID.
+ if (id) {
+ urlParams.id = id;
+ }
+
+ if (runtime.id !== RUNTIMES.THIS_FIREFOX) {
+ urlParams.remoteId = remoteClientManager.getRemoteId(
+ runtime.id,
+ runtime.type
+ );
+ }
+
+ const url = `about:devtools-toolbox?${new window.URLSearchParams(
+ urlParams
+ )}`;
+
+ const existingTab = getTabForUrl(url);
+ if (existingTab) {
+ const navigator = existingTab.ownerGlobal;
+ navigator.gBrowser.selectedTab = existingTab;
+ navigator.focus();
+ } else {
+ window.open(url);
+ }
+ }
+
+ dispatch(
+ Actions.recordTelemetryEvent("inspect", {
+ target_type: type.toUpperCase(),
+ runtime_type: runtime.type,
+ })
+ );
+ };
+}
+
+function installTemporaryExtension() {
+ const message = l10n.getString(
+ "about-debugging-tmp-extension-install-message"
+ );
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: TEMPORARY_EXTENSION_INSTALL_START });
+ const file = await openTemporaryExtension(window, message);
+ try {
+ await AddonManager.installTemporaryAddon(file);
+ dispatch({ type: TEMPORARY_EXTENSION_INSTALL_SUCCESS });
+ } catch (e) {
+ dispatch({ type: TEMPORARY_EXTENSION_INSTALL_FAILURE, error: e });
+ }
+ };
+}
+
+function pushServiceWorker(id, registrationFront) {
+ return async ({ dispatch, getState }) => {
+ try {
+ // The push button is only available if canDebugServiceWorkers is true.
+ // With this configuration, `push` should always be called on the
+ // registration front, and not on the (service) WorkerTargetActor.
+ await registrationFront.push();
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+function reloadTemporaryExtension(id) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: TEMPORARY_EXTENSION_RELOAD_START, id });
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ const addonTargetFront = await clientWrapper.getAddon({ id });
+ await addonTargetFront.reload();
+ dispatch({ type: TEMPORARY_EXTENSION_RELOAD_SUCCESS, id });
+ } catch (e) {
+ const error = typeof e === "string" ? new Error(e) : e;
+ dispatch({ type: TEMPORARY_EXTENSION_RELOAD_FAILURE, id, error });
+ }
+ };
+}
+
+function removeTemporaryExtension(id) {
+ return async ({ getState }) => {
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ await clientWrapper.uninstallAddon({ id });
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+function terminateExtensionBackgroundScript(id) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: TERMINATE_EXTENSION_BGSCRIPT_START, id });
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ const addonTargetFront = await clientWrapper.getAddon({ id });
+ await addonTargetFront.terminateBackgroundScript();
+ dispatch({ type: TERMINATE_EXTENSION_BGSCRIPT_SUCCESS, id });
+ } catch (e) {
+ const error = typeof e === "string" ? new Error(e) : e;
+ dispatch({ type: TERMINATE_EXTENSION_BGSCRIPT_FAILURE, id, error });
+ }
+ };
+}
+
+function requestTabs() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: REQUEST_TABS_START });
+
+ const runtime = getCurrentRuntime(getState().runtimes);
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ const isSupported = isSupportedDebugTargetPane(
+ runtime.runtimeDetails.info.type,
+ DEBUG_TARGET_PANE.TAB
+ );
+ const tabs = isSupported ? await clientWrapper.listTabs() : [];
+
+ // Fetch the favicon for all tabs.
+ await Promise.all(
+ tabs.map(descriptorFront => descriptorFront.retrieveFavicon())
+ );
+
+ dispatch({ type: REQUEST_TABS_SUCCESS, tabs });
+ } catch (e) {
+ dispatch({ type: REQUEST_TABS_FAILURE, error: e });
+ }
+ };
+}
+
+function requestExtensions() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: REQUEST_EXTENSIONS_START });
+
+ const runtime = getCurrentRuntime(getState().runtimes);
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ const isIconDataURLRequired = runtime.type !== RUNTIMES.THIS_FIREFOX;
+ const addons = await clientWrapper.listAddons({
+ iconDataURL: isIconDataURLRequired,
+ });
+
+ const showHiddenAddons = getState().ui.showHiddenAddons;
+
+ // Filter out non-debuggable addons as well as hidden ones, unless the dedicated
+ // preference is set to true.
+ const extensions = addons.filter(
+ a => a.debuggable && (!a.hidden || showHiddenAddons)
+ );
+
+ const installedExtensions = extensions.filter(
+ e => !e.temporarilyInstalled
+ );
+ const temporaryExtensions = extensions.filter(
+ e => e.temporarilyInstalled
+ );
+
+ dispatch({
+ type: REQUEST_EXTENSIONS_SUCCESS,
+ installedExtensions,
+ temporaryExtensions,
+ });
+ } catch (e) {
+ dispatch({ type: REQUEST_EXTENSIONS_FAILURE, error: e });
+ }
+ };
+}
+
+function requestProcesses() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: REQUEST_PROCESSES_START });
+
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ const mainProcessDescriptorFront = await clientWrapper.getMainProcess();
+ dispatch({
+ type: REQUEST_PROCESSES_SUCCESS,
+ mainProcess: {
+ id: 0,
+ processFront: mainProcessDescriptorFront,
+ },
+ });
+ } catch (e) {
+ dispatch({ type: REQUEST_PROCESSES_FAILURE, error: e });
+ }
+ };
+}
+
+function requestWorkers() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: REQUEST_WORKERS_START });
+
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ const { otherWorkers, serviceWorkers, sharedWorkers } =
+ await clientWrapper.listWorkers();
+
+ for (const serviceWorker of serviceWorkers) {
+ const { registrationFront } = serviceWorker;
+ if (!registrationFront) {
+ continue;
+ }
+
+ const subscription = await registrationFront.getPushSubscription();
+ serviceWorker.subscription = subscription;
+ }
+
+ dispatch({
+ type: REQUEST_WORKERS_SUCCESS,
+ otherWorkers,
+ serviceWorkers,
+ sharedWorkers,
+ });
+ } catch (e) {
+ dispatch({ type: REQUEST_WORKERS_FAILURE, error: e });
+ }
+ };
+}
+
+function startServiceWorker(registrationFront) {
+ return async () => {
+ try {
+ await registrationFront.start();
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+function unregisterServiceWorker(registrationFront) {
+ return async () => {
+ try {
+ await registrationFront.unregister();
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+module.exports = {
+ inspectDebugTarget,
+ installTemporaryExtension,
+ pushServiceWorker,
+ reloadTemporaryExtension,
+ removeTemporaryExtension,
+ requestTabs,
+ requestExtensions,
+ requestProcesses,
+ requestWorkers,
+ startServiceWorker,
+ terminateExtensionBackgroundScript,
+ unregisterServiceWorker,
+};
diff --git a/devtools/client/aboutdebugging/src/actions/index.js b/devtools/client/aboutdebugging/src/actions/index.js
new file mode 100644
index 0000000000..797d2c5831
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/index.js
@@ -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/. */
+
+"use strict";
+
+const debugTargets = require("resource://devtools/client/aboutdebugging/src/actions/debug-targets.js");
+const runtimes = require("resource://devtools/client/aboutdebugging/src/actions/runtimes.js");
+const telemetry = require("resource://devtools/client/aboutdebugging/src/actions/telemetry.js");
+const ui = require("resource://devtools/client/aboutdebugging/src/actions/ui.js");
+
+Object.assign(exports, ui, runtimes, telemetry, debugTargets);
diff --git a/devtools/client/aboutdebugging/src/actions/moz.build b/devtools/client/aboutdebugging/src/actions/moz.build
new file mode 100644
index 0000000000..a750640d06
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/moz.build
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "debug-targets.js",
+ "index.js",
+ "runtimes.js",
+ "telemetry.js",
+ "ui.js",
+)
diff --git a/devtools/client/aboutdebugging/src/actions/runtimes.js b/devtools/client/aboutdebugging/src/actions/runtimes.js
new file mode 100644
index 0000000000..fba620951e
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/runtimes.js
@@ -0,0 +1,515 @@
+/* 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 Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+
+const {
+ getAllRuntimes,
+ getCurrentRuntime,
+ findRuntimeById,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
+
+const {
+ l10n,
+} = require("resource://devtools/client/aboutdebugging/src/modules/l10n.js");
+const {
+ setDefaultPreferencesIfNeeded,
+ DEFAULT_PREFERENCES,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtime-default-preferences.js");
+const {
+ createClientForRuntime,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtime-client-factory.js");
+const {
+ isSupportedDebugTargetPane,
+} = require("resource://devtools/client/aboutdebugging/src/modules/debug-target-support.js");
+
+const {
+ remoteClientManager,
+} = require("resource://devtools/client/shared/remote-debugging/remote-client-manager.js");
+
+const {
+ CONNECT_RUNTIME_CANCEL,
+ CONNECT_RUNTIME_FAILURE,
+ CONNECT_RUNTIME_NOT_RESPONDING,
+ CONNECT_RUNTIME_START,
+ CONNECT_RUNTIME_SUCCESS,
+ DEBUG_TARGET_PANE,
+ DISCONNECT_RUNTIME_FAILURE,
+ DISCONNECT_RUNTIME_START,
+ DISCONNECT_RUNTIME_SUCCESS,
+ PAGE_TYPES,
+ REMOTE_RUNTIMES_UPDATED,
+ RUNTIME_PREFERENCE,
+ RUNTIMES,
+ THIS_FIREFOX_RUNTIME_CREATED,
+ UNWATCH_RUNTIME_FAILURE,
+ UNWATCH_RUNTIME_START,
+ UNWATCH_RUNTIME_SUCCESS,
+ UPDATE_CONNECTION_PROMPT_SETTING_FAILURE,
+ UPDATE_CONNECTION_PROMPT_SETTING_START,
+ UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS,
+ WATCH_RUNTIME_FAILURE,
+ WATCH_RUNTIME_START,
+ WATCH_RUNTIME_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const CONNECTION_TIMING_OUT_DELAY = 3000;
+const CONNECTION_CANCEL_DELAY = 13000;
+
+async function getRuntimeIcon(runtime, channel) {
+ if (runtime.isFenix) {
+ switch (channel) {
+ case "release":
+ case "beta":
+ return "chrome://devtools/skin/images/aboutdebugging-fenix.svg";
+ case "aurora":
+ default:
+ return "chrome://devtools/skin/images/aboutdebugging-fenix-nightly.svg";
+ }
+ }
+
+ return channel === "release" || channel === "beta" || channel === "aurora"
+ ? `chrome://devtools/skin/images/aboutdebugging-firefox-${channel}.svg`
+ : "chrome://devtools/skin/images/aboutdebugging-firefox-nightly.svg";
+}
+
+function onRemoteDevToolsClientClosed() {
+ window.AboutDebugging.onNetworkLocationsUpdated();
+ window.AboutDebugging.onUSBRuntimesUpdated();
+}
+
+function connectRuntime(id) {
+ // Create a random connection id to track the connection attempt in telemetry.
+ const connectionId = (Math.random() * 100000) | 0;
+
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: CONNECT_RUNTIME_START, connectionId, id });
+
+ // The preferences test-connection-timing-out-delay and test-connection-cancel-delay
+ // don't have a default value but will be overridden during our tests.
+ const connectionTimingOutDelay = Services.prefs.getIntPref(
+ "devtools.aboutdebugging.test-connection-timing-out-delay",
+ CONNECTION_TIMING_OUT_DELAY
+ );
+ const connectionCancelDelay = Services.prefs.getIntPref(
+ "devtools.aboutdebugging.test-connection-cancel-delay",
+ CONNECTION_CANCEL_DELAY
+ );
+
+ const connectionNotRespondingTimer = setTimeout(() => {
+ // If connecting to the runtime takes time over CONNECTION_TIMING_OUT_DELAY,
+ // we assume the connection prompt is showing on the runtime, show a dialog
+ // to let user know that.
+ dispatch({ type: CONNECT_RUNTIME_NOT_RESPONDING, connectionId, id });
+ }, connectionTimingOutDelay);
+ const connectionCancelTimer = setTimeout(() => {
+ // Connect button of the runtime will be disabled during connection, but the status
+ // continues till the connection was either succeed or failed. This may have a
+ // possibility that the disabling continues unless page reloading, user will not be
+ // able to click again. To avoid this, revert the connect button status after
+ // CONNECTION_CANCEL_DELAY ms.
+ dispatch({ type: CONNECT_RUNTIME_CANCEL, connectionId, id });
+ }, connectionCancelDelay);
+
+ try {
+ const runtime = findRuntimeById(id, getState().runtimes);
+ const clientWrapper = await createClientForRuntime(runtime);
+
+ await setDefaultPreferencesIfNeeded(clientWrapper, DEFAULT_PREFERENCES);
+
+ const deviceDescription = await clientWrapper.getDeviceDescription();
+ const compatibilityReport =
+ await clientWrapper.checkVersionCompatibility();
+ const icon = await getRuntimeIcon(runtime, deviceDescription.channel);
+
+ const {
+ CONNECTION_PROMPT,
+ PERMANENT_PRIVATE_BROWSING,
+ SERVICE_WORKERS_ENABLED,
+ } = RUNTIME_PREFERENCE;
+ const connectionPromptEnabled = await clientWrapper.getPreference(
+ CONNECTION_PROMPT,
+ false
+ );
+ const privateBrowsing = await clientWrapper.getPreference(
+ PERMANENT_PRIVATE_BROWSING,
+ false
+ );
+ const serviceWorkersEnabled = await clientWrapper.getPreference(
+ SERVICE_WORKERS_ENABLED,
+ true
+ );
+ const serviceWorkersAvailable = serviceWorkersEnabled && !privateBrowsing;
+
+ // Fenix specific workarounds are needed until we can get proper server side APIs
+ // to detect Fenix and get the proper application names and versions.
+ // See https://github.com/mozilla-mobile/fenix/issues/2016.
+
+ // For Fenix runtimes, the ADB runtime name is more accurate than the one returned
+ // by the Device actor.
+ const runtimeName = runtime.isFenix
+ ? runtime.name
+ : deviceDescription.name;
+
+ // For Fenix runtimes, the version we should display is the application version
+ // retrieved from ADB, and not the Gecko version returned by the Device actor.
+ const version = runtime.isFenix
+ ? runtime.extra.adbPackageVersion
+ : deviceDescription.version;
+
+ const runtimeDetails = {
+ canDebugServiceWorkers: deviceDescription.canDebugServiceWorkers,
+ clientWrapper,
+ compatibilityReport,
+ connectionPromptEnabled,
+ info: {
+ deviceName: deviceDescription.deviceName,
+ icon,
+ isFenix: runtime.isFenix,
+ name: runtimeName,
+ os: deviceDescription.os,
+ type: runtime.type,
+ version,
+ },
+ serviceWorkersAvailable,
+ };
+
+ if (runtime.type !== RUNTIMES.THIS_FIREFOX) {
+ // `closed` event will be emitted when disabling remote debugging
+ // on the connected remote runtime.
+ clientWrapper.once("closed", onRemoteDevToolsClientClosed);
+ }
+
+ dispatch({
+ type: CONNECT_RUNTIME_SUCCESS,
+ connectionId,
+ runtime: {
+ id,
+ runtimeDetails,
+ type: runtime.type,
+ },
+ });
+ } catch (e) {
+ dispatch({ type: CONNECT_RUNTIME_FAILURE, connectionId, id, error: e });
+ } finally {
+ clearTimeout(connectionNotRespondingTimer);
+ clearTimeout(connectionCancelTimer);
+ }
+ };
+}
+
+function createThisFirefoxRuntime() {
+ return ({ dispatch, getState }) => {
+ const thisFirefoxRuntime = {
+ id: RUNTIMES.THIS_FIREFOX,
+ isConnecting: false,
+ isConnectionFailed: false,
+ isConnectionNotResponding: false,
+ isConnectionTimeout: false,
+ isUnavailable: false,
+ isUnplugged: false,
+ name: l10n.getString("about-debugging-this-firefox-runtime-name"),
+ type: RUNTIMES.THIS_FIREFOX,
+ };
+ dispatch({
+ type: THIS_FIREFOX_RUNTIME_CREATED,
+ runtime: thisFirefoxRuntime,
+ });
+ };
+}
+
+function disconnectRuntime(id, shouldRedirect = false) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: DISCONNECT_RUNTIME_START });
+ try {
+ const runtime = findRuntimeById(id, getState().runtimes);
+ const { clientWrapper } = runtime.runtimeDetails;
+
+ if (runtime.type !== RUNTIMES.THIS_FIREFOX) {
+ clientWrapper.off("closed", onRemoteDevToolsClientClosed);
+ }
+ await clientWrapper.close();
+ if (shouldRedirect) {
+ await dispatch(
+ Actions.selectPage(PAGE_TYPES.RUNTIME, RUNTIMES.THIS_FIREFOX)
+ );
+ }
+
+ dispatch({
+ type: DISCONNECT_RUNTIME_SUCCESS,
+ runtime: {
+ id,
+ type: runtime.type,
+ },
+ });
+ } catch (e) {
+ dispatch({ type: DISCONNECT_RUNTIME_FAILURE, error: e });
+ }
+ };
+}
+
+function updateConnectionPromptSetting(connectionPromptEnabled) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: UPDATE_CONNECTION_PROMPT_SETTING_START });
+ try {
+ const runtime = getCurrentRuntime(getState().runtimes);
+ const { clientWrapper } = runtime.runtimeDetails;
+ const promptPrefName = RUNTIME_PREFERENCE.CONNECTION_PROMPT;
+ await clientWrapper.setPreference(
+ promptPrefName,
+ connectionPromptEnabled
+ );
+ // Re-get actual value from the runtime.
+ connectionPromptEnabled = await clientWrapper.getPreference(
+ promptPrefName,
+ connectionPromptEnabled
+ );
+
+ dispatch({
+ type: UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS,
+ connectionPromptEnabled,
+ runtime,
+ });
+ } catch (e) {
+ dispatch({ type: UPDATE_CONNECTION_PROMPT_SETTING_FAILURE, error: e });
+ }
+ };
+}
+
+function watchRuntime(id) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: WATCH_RUNTIME_START });
+
+ try {
+ if (id === RUNTIMES.THIS_FIREFOX) {
+ // THIS_FIREFOX connects and disconnects on the fly when opening the page.
+ await dispatch(connectRuntime(RUNTIMES.THIS_FIREFOX));
+ }
+
+ // The selected runtime should already have a connected client assigned.
+ const runtime = findRuntimeById(id, getState().runtimes);
+ await dispatch({ type: WATCH_RUNTIME_SUCCESS, runtime });
+
+ dispatch(Actions.requestExtensions());
+ // we have to wait for tabs, otherwise the requests to getTarget may interfer
+ // with listProcesses
+ await dispatch(Actions.requestTabs());
+ dispatch(Actions.requestWorkers());
+
+ if (
+ isSupportedDebugTargetPane(
+ runtime.runtimeDetails.info.type,
+ DEBUG_TARGET_PANE.PROCESSES
+ )
+ ) {
+ dispatch(Actions.requestProcesses());
+ }
+ } catch (e) {
+ dispatch({ type: WATCH_RUNTIME_FAILURE, error: e });
+ }
+ };
+}
+
+function unwatchRuntime(id) {
+ return async ({ dispatch, getState }) => {
+ const runtime = findRuntimeById(id, getState().runtimes);
+
+ dispatch({ type: UNWATCH_RUNTIME_START, runtime });
+
+ try {
+ if (id === RUNTIMES.THIS_FIREFOX) {
+ // THIS_FIREFOX connects and disconnects on the fly when opening the page.
+ await dispatch(disconnectRuntime(RUNTIMES.THIS_FIREFOX));
+ }
+
+ dispatch({ type: UNWATCH_RUNTIME_SUCCESS });
+ } catch (e) {
+ dispatch({ type: UNWATCH_RUNTIME_FAILURE, error: e });
+ }
+ };
+}
+
+function updateNetworkRuntimes(locations) {
+ const runtimes = locations.map(location => {
+ const [host, port] = location.split(":");
+ return {
+ id: location,
+ extra: {
+ connectionParameters: { host, port: parseInt(port, 10) },
+ },
+ isConnecting: false,
+ isConnectionFailed: false,
+ isConnectionNotResponding: false,
+ isConnectionTimeout: false,
+ isFenix: false,
+ isUnavailable: false,
+ isUnplugged: false,
+ isUnknown: false,
+ name: location,
+ type: RUNTIMES.NETWORK,
+ };
+ });
+ return updateRemoteRuntimes(runtimes, RUNTIMES.NETWORK);
+}
+
+function updateUSBRuntimes(adbRuntimes) {
+ const runtimes = adbRuntimes.map(adbRuntime => {
+ // Set connectionParameters only for known runtimes.
+ const socketPath = adbRuntime.socketPath;
+ const deviceId = adbRuntime.deviceId;
+ const connectionParameters = socketPath ? { deviceId, socketPath } : null;
+ return {
+ id: adbRuntime.id,
+ extra: {
+ connectionParameters,
+ deviceName: adbRuntime.deviceName,
+ adbPackageVersion: adbRuntime.versionName,
+ },
+ isConnecting: false,
+ isConnectionFailed: false,
+ isConnectionNotResponding: false,
+ isConnectionTimeout: false,
+ isFenix: adbRuntime.isFenix,
+ isUnavailable: adbRuntime.isUnavailable,
+ isUnplugged: adbRuntime.isUnplugged,
+ name: adbRuntime.shortName,
+ type: RUNTIMES.USB,
+ };
+ });
+ return updateRemoteRuntimes(runtimes, RUNTIMES.USB);
+}
+
+/**
+ * Check that a given runtime can still be found in the provided array of runtimes, and
+ * that the connection of the associated DevToolsClient is still valid.
+ * Note that this check is only valid for runtimes which match the type of the runtimes
+ * in the array.
+ */
+function _isRuntimeValid(runtime, runtimes) {
+ const isRuntimeAvailable = runtimes.some(r => r.id === runtime.id);
+ const isConnectionValid =
+ runtime.runtimeDetails && !runtime.runtimeDetails.clientWrapper.isClosed();
+ return isRuntimeAvailable && isConnectionValid;
+}
+
+function updateRemoteRuntimes(runtimes, type) {
+ return async ({ dispatch, getState }) => {
+ const currentRuntime = getCurrentRuntime(getState().runtimes);
+
+ // Check if the updated remote runtimes should trigger a navigation out of the current
+ // runtime page.
+ if (
+ currentRuntime &&
+ currentRuntime.type === type &&
+ !_isRuntimeValid(currentRuntime, runtimes)
+ ) {
+ // Since current remote runtime is invalid, move to this firefox page.
+ // This case is considered as followings and so on:
+ // * Remove ADB addon
+ // * (Physically) Disconnect USB runtime
+ //
+ // The reason we call selectPage before REMOTE_RUNTIMES_UPDATED is fired is below.
+ // Current runtime can not be retrieved after REMOTE_RUNTIMES_UPDATED action, since
+ // that updates runtime state. So, before that we fire selectPage action to execute
+ // `unwatchRuntime` correctly.
+ await dispatch(
+ Actions.selectPage(PAGE_TYPES.RUNTIME, RUNTIMES.THIS_FIREFOX)
+ );
+ }
+
+ // For existing runtimes, transfer all properties that are not available in the
+ // runtime objects passed to this method:
+ // - runtimeDetails (set by about:debugging after a successful connection)
+ // - isConnecting (set by about:debugging during the connection)
+ // - isConnectionFailed (set by about:debugging if connection was failed)
+ // - isConnectionNotResponding
+ // (set by about:debugging if connection is taking too much time)
+ // - isConnectionTimeout (set by about:debugging if connection was timeout)
+ runtimes.forEach(runtime => {
+ const existingRuntime = findRuntimeById(runtime.id, getState().runtimes);
+ const isConnectionValid =
+ existingRuntime?.runtimeDetails &&
+ !existingRuntime.runtimeDetails.clientWrapper.isClosed();
+ runtime.runtimeDetails = isConnectionValid
+ ? existingRuntime.runtimeDetails
+ : null;
+ runtime.isConnecting = existingRuntime
+ ? existingRuntime.isConnecting
+ : false;
+ runtime.isConnectionFailed = existingRuntime
+ ? existingRuntime.isConnectionFailed
+ : false;
+ runtime.isConnectionNotResponding = existingRuntime
+ ? existingRuntime.isConnectionNotResponding
+ : false;
+ runtime.isConnectionTimeout = existingRuntime
+ ? existingRuntime.isConnectionTimeout
+ : false;
+ });
+
+ const existingRuntimes = getAllRuntimes(getState().runtimes);
+ for (const runtime of existingRuntimes) {
+ // Runtime was connected before.
+ const isConnected = runtime.runtimeDetails;
+ // Runtime is of the same type as the updated runtimes array, so we should check it.
+ const isSameType = runtime.type === type;
+ if (isConnected && isSameType && !_isRuntimeValid(runtime, runtimes)) {
+ // Disconnect runtimes that were no longer valid.
+ await dispatch(disconnectRuntime(runtime.id));
+ }
+ }
+
+ dispatch({ type: REMOTE_RUNTIMES_UPDATED, runtimes, runtimeType: type });
+
+ for (const runtime of getAllRuntimes(getState().runtimes)) {
+ if (runtime.type !== type) {
+ continue;
+ }
+
+ // Reconnect clients already available in the RemoteClientManager.
+ const isConnected = !!runtime.runtimeDetails;
+ const hasConnectedClient = remoteClientManager.hasClient(
+ runtime.id,
+ runtime.type
+ );
+ if (!isConnected && hasConnectedClient) {
+ await dispatch(connectRuntime(runtime.id));
+ }
+ }
+ };
+}
+
+/**
+ * Remove all the listeners added on client objects. Since those objects are persisted
+ * regardless of the about:debugging lifecycle, all the added events should be removed
+ * before leaving about:debugging.
+ */
+function removeRuntimeListeners() {
+ return ({ dispatch, getState }) => {
+ const allRuntimes = getAllRuntimes(getState().runtimes);
+ const remoteRuntimes = allRuntimes.filter(
+ r => r.type !== RUNTIMES.THIS_FIREFOX
+ );
+ for (const runtime of remoteRuntimes) {
+ if (runtime.runtimeDetails) {
+ const { clientWrapper } = runtime.runtimeDetails;
+ clientWrapper.off("closed", onRemoteDevToolsClientClosed);
+ }
+ }
+ };
+}
+
+module.exports = {
+ connectRuntime,
+ createThisFirefoxRuntime,
+ disconnectRuntime,
+ removeRuntimeListeners,
+ unwatchRuntime,
+ updateConnectionPromptSetting,
+ updateNetworkRuntimes,
+ updateUSBRuntimes,
+ watchRuntime,
+};
diff --git a/devtools/client/aboutdebugging/src/actions/telemetry.js b/devtools/client/aboutdebugging/src/actions/telemetry.js
new file mode 100644
index 0000000000..b418c77a50
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/telemetry.js
@@ -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/. */
+
+"use strict";
+
+const {
+ TELEMETRY_RECORD,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * If a given event cannot be mapped to an existing action, use this action that will only
+ * be processed by the event recording middleware.
+ */
+function recordTelemetryEvent(method, details) {
+ return ({ dispatch, getState }) => {
+ dispatch({ type: TELEMETRY_RECORD, method, details });
+ };
+}
+
+module.exports = {
+ recordTelemetryEvent,
+};
diff --git a/devtools/client/aboutdebugging/src/actions/ui.js b/devtools/client/aboutdebugging/src/actions/ui.js
new file mode 100644
index 0000000000..fb676cefd6
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/ui.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 {
+ ADB_ADDON_INSTALL_START,
+ ADB_ADDON_INSTALL_SUCCESS,
+ ADB_ADDON_INSTALL_FAILURE,
+ ADB_ADDON_UNINSTALL_START,
+ ADB_ADDON_UNINSTALL_SUCCESS,
+ ADB_ADDON_UNINSTALL_FAILURE,
+ ADB_ADDON_STATUS_UPDATED,
+ ADB_READY_UPDATED,
+ DEBUG_TARGET_COLLAPSIBILITY_UPDATED,
+ HIDE_PROFILER_DIALOG,
+ NETWORK_LOCATIONS_UPDATE_FAILURE,
+ NETWORK_LOCATIONS_UPDATE_START,
+ NETWORK_LOCATIONS_UPDATE_SUCCESS,
+ PAGE_TYPES,
+ SELECT_PAGE_FAILURE,
+ SELECT_PAGE_START,
+ SELECT_PAGE_SUCCESS,
+ SELECTED_RUNTIME_ID_UPDATED,
+ SHOW_PROFILER_DIALOG,
+ SWITCH_PROFILER_CONTEXT,
+ USB_RUNTIMES_SCAN_START,
+ USB_RUNTIMES_SCAN_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const NetworkLocationsModule = require("resource://devtools/client/aboutdebugging/src/modules/network-locations.js");
+const {
+ adbAddon,
+} = require("resource://devtools/client/shared/remote-debugging/adb/adb-addon.js");
+const {
+ refreshUSBRuntimes,
+} = require("resource://devtools/client/aboutdebugging/src/modules/usb-runtimes.js");
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+
+function selectPage(page, runtimeId) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: SELECT_PAGE_START });
+
+ try {
+ const isSamePage = (oldPage, newPage) => {
+ if (newPage === PAGE_TYPES.RUNTIME && oldPage === PAGE_TYPES.RUNTIME) {
+ return runtimeId === getState().runtimes.selectedRuntimeId;
+ }
+ return newPage === oldPage;
+ };
+
+ if (!page) {
+ throw new Error("No page provided.");
+ }
+
+ const currentPage = getState().ui.selectedPage;
+ // Nothing to dispatch if the page is the same as the current page
+ if (isSamePage(currentPage, page)) {
+ return;
+ }
+
+ // Stop showing the profiler dialog if we are navigating to another page.
+ if (getState().ui.showProfilerDialog) {
+ await dispatch({ type: HIDE_PROFILER_DIALOG });
+ }
+
+ // Stop watching current runtime, if currently on a RUNTIME page.
+ if (currentPage === PAGE_TYPES.RUNTIME) {
+ const currentRuntimeId = getState().runtimes.selectedRuntimeId;
+ await dispatch(Actions.unwatchRuntime(currentRuntimeId));
+ }
+
+ // Always update the selected runtime id.
+ // If we are navigating to a non-runtime page, the Runtime page components are no
+ // longer rendered so it is safe to nullify the runtimeId.
+ // If we are navigating to a runtime page, the runtime corresponding to runtimeId
+ // is already connected, so components can safely get runtimeDetails on this new
+ // runtime.
+ dispatch({ type: SELECTED_RUNTIME_ID_UPDATED, runtimeId });
+
+ // Start watching current runtime, if moving to a RUNTIME page.
+ if (page === PAGE_TYPES.RUNTIME) {
+ await dispatch(Actions.watchRuntime(runtimeId));
+ }
+
+ dispatch({ type: SELECT_PAGE_SUCCESS, page });
+ } catch (e) {
+ dispatch({ type: SELECT_PAGE_FAILURE, error: e });
+ }
+ };
+}
+
+function updateDebugTargetCollapsibility(key, isCollapsed) {
+ return { type: DEBUG_TARGET_COLLAPSIBILITY_UPDATED, key, isCollapsed };
+}
+
+function addNetworkLocation(location) {
+ return ({ dispatch, getState }) => {
+ NetworkLocationsModule.addNetworkLocation(location);
+ };
+}
+
+function removeNetworkLocation(location) {
+ return ({ dispatch, getState }) => {
+ NetworkLocationsModule.removeNetworkLocation(location);
+ };
+}
+
+function showProfilerDialog() {
+ return { type: SHOW_PROFILER_DIALOG };
+}
+
+/**
+ * The profiler can switch between "devtools-remote" and "aboutprofiling-remote"
+ * page contexts.
+ */
+function switchProfilerContext(profilerContext) {
+ return { type: SWITCH_PROFILER_CONTEXT, profilerContext };
+}
+
+function hideProfilerDialog() {
+ return { type: HIDE_PROFILER_DIALOG };
+}
+
+function updateAdbAddonStatus(adbAddonStatus) {
+ return { type: ADB_ADDON_STATUS_UPDATED, adbAddonStatus };
+}
+
+function updateAdbReady(isAdbReady) {
+ return { type: ADB_READY_UPDATED, isAdbReady };
+}
+
+function updateNetworkLocations(locations) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: NETWORK_LOCATIONS_UPDATE_START });
+ try {
+ await dispatch(Actions.updateNetworkRuntimes(locations));
+ dispatch({ type: NETWORK_LOCATIONS_UPDATE_SUCCESS, locations });
+ } catch (e) {
+ dispatch({ type: NETWORK_LOCATIONS_UPDATE_FAILURE, error: e });
+ }
+ };
+}
+
+function installAdbAddon() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: ADB_ADDON_INSTALL_START });
+
+ try {
+ // "aboutdebugging" will be forwarded to telemetry as the installation source
+ // for the addon.
+ await adbAddon.install("about:debugging");
+ dispatch({ type: ADB_ADDON_INSTALL_SUCCESS });
+ } catch (e) {
+ dispatch({ type: ADB_ADDON_INSTALL_FAILURE, error: e });
+ }
+ };
+}
+
+function uninstallAdbAddon() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: ADB_ADDON_UNINSTALL_START });
+
+ try {
+ await adbAddon.uninstall();
+ dispatch({ type: ADB_ADDON_UNINSTALL_SUCCESS });
+ } catch (e) {
+ dispatch({ type: ADB_ADDON_UNINSTALL_FAILURE, error: e });
+ }
+ };
+}
+
+function scanUSBRuntimes() {
+ return async ({ dispatch, getState }) => {
+ // do not re-scan if we are already doing it
+ if (getState().ui.isScanningUsb) {
+ return;
+ }
+
+ dispatch({ type: USB_RUNTIMES_SCAN_START });
+ await refreshUSBRuntimes();
+ dispatch({ type: USB_RUNTIMES_SCAN_SUCCESS });
+ };
+}
+
+module.exports = {
+ addNetworkLocation,
+ hideProfilerDialog,
+ installAdbAddon,
+ removeNetworkLocation,
+ scanUSBRuntimes,
+ selectPage,
+ showProfilerDialog,
+ switchProfilerContext,
+ uninstallAdbAddon,
+ updateAdbAddonStatus,
+ updateAdbReady,
+ updateDebugTargetCollapsibility,
+ updateNetworkLocations,
+};
diff --git a/devtools/client/aboutdebugging/src/base.css b/devtools/client/aboutdebugging/src/base.css
new file mode 100644
index 0000000000..728cbeed0c
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/base.css
@@ -0,0 +1,521 @@
+/* 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 {
+ /* Colors from common.css */
+ --in-content-background-color: f9f9fa;
+ --in-content-border-color: #d7d7db;
+ --in-content-primary-button-background: rgb(0, 97, 224);
+ --in-content-primary-button-background-active: rgb(5, 62, 148);
+ --in-content-primary-button-background-hover: rgb(2, 80, 187);
+ --in-content-text-color: #0c0c0d;
+
+ --bg-color: var(--in-content-background-color);
+ --text-color: var(--in-content-text-color);
+ --secondary-text-color: var(--grey-50);
+
+ --border-color: var(--in-content-border-color);
+
+ --box-background: #fff;
+ --box-border-color: var(--in-content-border-color);
+
+ --button-background-color: var(--grey-90-a10); /* Note: this is from Photon Default button */
+ --button-color: var(--grey-90); /* Note: this is from Photon Default button */
+ --button-hover-background-color: var(--grey-90-a20); /* Note: this is from Photon Default button */
+ --button-active-background-color: var(--grey-90-a30); /* Note: this is from Photon Default button */
+
+ --category-background-hover: rgba(12,12,13,0.1);
+ --category-text: rgba(12,12,13);
+ --category-text-selected: var(--in-content-primary-button-background);
+
+ --fieldpair-text-color: var(--grey-50);
+
+ --sidebar-text-color: var(--category-text);
+ --sidebar-selected-color: var(--category-text-selected);
+ --sidebar-background-hover: var(--category-background-hover);
+
+ --card-background-color: var(--white-100);
+ --card-separator-color: var(--grey-20);
+
+ /* Dimensions from common.css #categories > .category */
+ /* TODO: Values are not based on photon's 4px base distance, see bug 1501638 */
+ --category-height: 48px;
+ --category-padding: 10px;
+ --category-transition-duration: 150ms;
+
+ --icon-ok-color: var(--green-70);
+ --icon-info-color: var(--grey-90);
+
+ --link-color: var(--in-content-primary-button-background);
+ --link-color-active: var(--in-content-primary-button-background-active);
+ --link-color-hover: var(--in-content-primary-button-background-hover);
+
+ --primary-button-background-color: var(--blue-60);
+ --primary-button-color: var(--white-100);
+ --primary-button-hover-background-color: var(--blue-70);
+ --primary-button-active-background-color: var(--blue-80);
+
+ --popup-header-background-color: var(--grey-20);
+ --popup-header-color: var(--grey-90);
+
+ /* Colors from Photon */
+ --success-background: #30e60b;
+ --warning-background: #fffbd6; /* from the Web Console */
+ --warning-border: rgba(164, 127, 0, 0.27); /* yellow-70(#a47f00) at 27% */
+ --warning-icon: var(--yellow-65); /* from the Web Console */
+ --warning-text: var(--yellow-80); /* from the Web Console */
+ --error-background: #fdf2f5; /* from the Web Console */
+ --error-border: rgba(90, 0, 2, 0.16); /* red-80(#5a0002) at 16% */
+ --error-icon: var(--red-60); /* from the Web Console */
+ --error-text: var(--red-70); /* from the Web Console */
+ --highlight-50: #0a84ff;
+ --grey-20: #ededf0; /* for ui, no special semantic */
+ --grey-30: #d7d7db; /* for ui, no special semantic */
+ --grey-50: #737373; /* for ui, no special semantic */
+ --grey-90: #0c0c0d; /* for ui, no special semantic */
+ --grey-90-a10: rgba(12, 12, 13, 0.1);
+ --grey-90-a20: rgba(12, 12, 13, 0.2);
+ --grey-90-a30: rgba(12, 12, 13, 0.3);
+ --grey-90-a60: rgba(12, 12, 13, 0.6);
+ --red-70: #a4000f; /* for ui, no special semantic */
+ --white-100: #fff; /* for ui, no special semantic */
+ --yellow-60: #d7b600; /* for ui, no special semantic */
+ --yellow-70: #a47f00; /* for ui, no special semantic */
+
+ /* Typography from Photon */
+ /* See https://firefox-dev.tools/photon/visuals/typography.html */
+ --body-10-font-size: 13px;
+ --body-10-font-weight: 400;
+ --body-20-font-size: 15px;
+ --body-20-font-weight: 400;
+ --body-20-font-weight-bold: 700;
+ --caption-10-font-size: 11px;
+ --caption-10-font-weight: 400;
+ --caption-20-font-size: 13px;
+ --caption-20-font-weight: 400;
+ --display-10-font-size: 28px;
+ --display-10-font-weight: 200;
+ --title-20-font-size: 17px;
+ --title-20-font-weight: 600;
+ --title-30-font-size: 22px;
+ --title-30-font-weight: 300;
+
+ /* Global layout vars */
+ --page-width: 664px;
+ --base-unit: 4px;
+
+ /* Global styles */
+ --base-font-style: message-box;
+ --base-font-size: var(--body-10-font-size);
+ --base-font-weight: var(--body-10-font-weight);
+ --base-line-height: 1.8;
+ --icon-label-font-size: var(--body-10-font-size);
+ --message-font-size: var(--body-10-font-size);
+ --button-font-size: var(--base-font-size);
+ --micro-font-size: 11px;
+ --monospace-font-family: monospace;
+
+ --card-shadow-blur-radius: var(--base-unit);
+
+
+ /*
+ * Variables particular to about:debugging
+ */
+ --alt-heading-icon-size: calc(var(--base-unit) * 6);
+ --alt-heading-icon-gap: var(--base-unit);
+ --main-heading-icon-size: calc(var(--base-unit) * 17); /* 4px * 17 = 68px */
+ --main-heading-icon-gap: calc(var(--base-unit) * 3);
+ --main-subheading-icon-size: calc(var(--base-unit) * 4);
+ --main-subheading-heading-icon-gap: calc(var(--base-unit) * 2);
+}
+
+/* Dark Theme variables */
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --in-content-background-color: rgb(28, 27, 34);
+ --in-content-border-color: rgba(249,249,250,0.2);
+ --in-content-primary-button-background: #00ddff;
+ --in-content-primary-button-background-active: rgb(170,242,255);
+ --in-content-primary-button-background-hover: rgb(128,235,255);
+ --in-content-text-color: #eee;
+
+ --secondary-text-color: rgb(168, 168, 168);
+
+ --box-background: rgb(35, 34, 43);
+
+ --button-background-color: rgb(72, 72, 84);
+ --button-color: var(--white-100);
+ --button-hover-background-color: rgb(92, 92, 106);
+
+ --category-background-hover: rgba(12,12,13,0.1);
+ --category-text: var(--text-color);
+
+ --fieldpair-text-color: var(--text-color);
+
+ --sidebar-text-color: var(--text-color);
+ --sidebar-background-hover: rgb(92, 92, 106);
+
+ --card-background-color: rgb(35, 34, 43);
+ --card-separator-color: var(--grey-50);
+
+ --icon-ok-color: var(--white-100);
+ --icon-info-color: var(--white-100);
+
+ --popup-header-background-color: var(--grey-50);
+ --popup-header-color: var(--white-100);
+
+ /*
+ * From common.inc.css
+ * https://searchfox.org/mozilla-central/rev/b52cf6bbe214bd9d93ed9333d0403f7d556ad7c8/toolkit/themes/shared/in-content/common.inc.css#165-168
+ */
+ --primary-button-background-color: #00ddff;
+ --primary-button-color: rgb(43,42,51);
+ --primary-button-active-background-color: rgb(170,242,255);
+ --primary-button-hover-background-color: rgb(128,235,255);
+ }
+}
+
+/*
+* Reset some tags
+*/
+
+html {
+ font: var(--base-font-style);
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ color: var(--text-color);
+ font-size: var(--base-font-size);
+ font-weight: var(--base-font-weight);
+ line-height: var(--base-line-height);
+ background: var(--bg-color);
+}
+
+dd {
+ margin: 0;
+ padding: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+a {
+ color: var(--link-color);
+}
+a:hover {
+ color: var(--link-color-hover);
+}
+a:active {
+ color: var(--link-color-active);
+}
+
+p, h1 {
+ margin: 0;
+}
+
+/*
+* Utils
+*/
+
+/* text that needs to be cut with … */
+.ellipsis-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Technical text that should use a monospace font, such as code, error messages. */
+.technical-text {
+ font-family: var(--monospace-font-family);
+}
+
+/* Links that need to look like current text */
+.undecorated-link,
+.undecorated-link:hover {
+ text-decoration: none;
+ color: currentColor;
+}
+
+/* Text needs to wrap anywhere */
+.word-wrap-anywhere {
+ word-wrap: anywhere;
+}
+
+/*
+* Typography
+*/
+
+/* Main style for heading (i.e. h1) */
+.main-heading {
+ font-size: var(--display-10-font-size);
+ font-weight: var(--display-10-font-weight);
+ line-height: 1.2;
+}
+
+.main-heading__icon {
+ width: 100%;
+}
+
+.main-heading-subtitle {
+ font-size: var(--title-30-font-size);
+ font-weight: var(--title-30-font-weight);
+}
+
+/* Main style for a subheading (i.e. h2). It features an icon */
+/* +--------+-------------+
+* | [Icon] | Lorem ipsum |
+* +--------+-------------+
+*/
+.main-subheading {
+ margin-block: calc(var(--base-unit) * 4) 0;
+ font-size: var(--title-20-font-size); /* Note: this is from Photon Title 20 */
+ font-weight: var(--title-20-font-weight); /* Note: this is from Photon Title 20 */
+
+ display: grid;
+ grid-template-columns: var(--main-subheading-icon-size) 1fr;
+ grid-column-gap: var(--main-subheading-heading-icon-gap);
+ align-items: center;
+}
+
+.main-subheading__icon {
+ width: 100%;
+ fill: currentColor;
+ -moz-context-properties: fill;
+}
+
+/* Alternative style for a heading (i.e. h1) */
+.alt-heading {
+ font-weight: var(--title-20-font-weight);
+ font-size: var(--title-20-font-size);
+
+ margin-block-start: 0;
+ margin-block-end: calc(var(--base-unit) * 4);
+}
+
+.alt-heading--larger {
+ font-size: var(--title-30-font-size);
+ font-weight: var(--title-30-font-weight);
+}
+
+/* Alternative style for a subheading (i.e. h2). It features an icon */
+/* +--------+-------------+
+* | [Icon] | Lorem ipsum |
+* +--------+-------------+
+*/
+.alt-subheading {
+ margin-block-start: calc(var(--base-unit) * 4);
+ font-weight: 600;
+ font-size: 1.14em;
+ line-height: 1.4em; /* odd value - from common.inc.css */
+
+ display: grid;
+ grid-template-columns: var(--alt-heading-icon-size) 1fr;
+ grid-column-gap: var(--alt-heading-icon-gap);
+ align-items: center;
+}
+
+.alt-subheading__icon {
+ width: 100%;
+ fill: currentColor;
+ -moz-context-properties: fill;
+}
+
+/*
+* Layout elements
+*/
+
+/* for horizontal rules / separators */
+.separator {
+ border-style: solid none none none;
+ border-color: var(--border-color);
+}
+
+/* adds breathing space to the separator */
+.separator--breathe {
+ margin: calc(var(--base-unit) * 5) 0;
+}
+
+/* a series of button-like elements, layed out horizontally */
+.toolbar {
+ display: flex;
+ column-gap: calc(var(--base-unit) * 3);
+}
+
+.toolbar--right-align {
+ justify-content: end;
+}
+
+/*
+Form controls
+*/
+.default-button, .default-input {
+ box-sizing: border-box;
+ font-size: 1em;
+}
+
+/* Buttons from Photon */
+.default-button, .primary-button {
+ appearance: none;
+ margin: 0;
+ height: calc(var(--base-unit) * 8);
+ padding-inline-start: calc(var(--base-unit) * 5);
+ padding-inline-end: calc(var(--base-unit) * 5);
+
+ border: none;
+ border-radius: calc(var(--base-unit) / 2);
+
+ font-size: var(--button-font-size);
+}
+
+/* Disabled state for buttons from Photon */
+.default-button:disabled, .primary-button:disabled {
+ opacity: 0.4;
+}
+
+/* Smaller variant size for buttons, from Photon */
+.default-button--micro, .primary-button--micro {
+ padding-inline-start: calc(2 * var(--base-unit));
+ padding-inline-end: calc(2 * var(--base-unit));
+ font-size: var(--micro-font-size);
+ height: calc(var(--base-unit) * 6);
+}
+
+/* Photon button representing a primary action */
+.primary-button {
+ color: var(--primary-button-color);
+ background-color: var(--primary-button-background-color);
+}
+
+.primary-button:enabled:hover {
+ background: var(--primary-button-hover-background-color);
+}
+
+.primary-button:enabled:active {
+ background: var(--primary-button-active-background-color);
+}
+
+/* Photon standard button */
+.default-button {
+ color: var(--button-color);
+ background-color: var(--button-background-color);
+}
+
+.default-button:enabled:hover {
+ background: var(--button-hover-background-color);
+}
+
+.default-button:enabled:active {
+ background: var(--button-active-background-color);
+}
+
+@media (prefers-contrast) {
+ .default-button,
+ .ghost-button,
+ .primary-button {
+ background-color: ButtonFace;
+ /* Add a border to make buttons visible in high contrast */
+ border: 1px solid ButtonText;
+ color: ButtonText;
+ }
+
+ .ghost-button {
+ fill: ButtonText;
+ }
+
+ :is(
+ .default-button,
+ .ghost-button,
+ .primary-button
+ ):enabled:is(:hover, :active) {
+ background-color: ButtonText;
+ color: ButtonFace;
+ }
+}
+
+/* Photon ghost button. Icon button with no background */
+.ghost-button {
+ background: transparent;
+ border: none;
+ border-radius: calc(var(--base-unit) / 2);
+ fill: var(--button-color);
+ height: calc(var(--base-unit) * 6);
+ padding: calc(var(--base-unit));
+ width: calc(var(--base-unit) * 6);
+
+ -moz-context-properties: fill;
+}
+
+.ghost-button:hover {
+ background: var(--button-hover-background-color);
+}
+
+.ghost-button:active {
+ background: var(--button-active-background-color);
+}
+
+/* Standard inputs */
+.default-input {
+ line-height: unset;
+ padding: 0 calc(var(--base-unit) * 2);
+ height: 100%;
+
+ border: 1px solid var(--box-border-color);
+ border-radius: 2px;
+ color: var(--text-color);
+ background-color: var(--box-background);
+}
+
+/*
+* Other UI components
+*/
+
+/*
+* A small, colored badge.
+* NOTE: styles borrowed from Photon's micro buttons (there aren't badges)
+*/
+.badge {
+ background: var(--grey-30);
+ border-radius: calc(var(--base-unit) / 2);
+ font-size: var(--micro-font-size);
+ padding: var(--base-unit) calc(2 * var(--base-unit));
+}
+
+.badge--info {
+ background: var(--highlight-50);
+}
+
+.badge--success {
+ background: var(--success-background);
+}
+
+.badge--warning {
+ background: var(--warning-background);
+}
+
+.badge--error {
+ background: var(--error-background);
+}
+
+/*
+ * Card UI, from Photon
+ */
+.card {
+ background-color: var(--card-background-color); /* from common.inc.css */
+ border-radius: var(--card-shadow-blur-radius); /* from common.inc.css */
+ box-shadow: 0 1px 4px var(--grey-90-a10); /* from common.inc.css */
+ box-sizing: border-box;
+ min-width: min-content;
+ padding-block: calc(var(--base-unit) * 5);
+}
+
+.card__heading {
+ font-size: var(--title-20-font-size); /* Note: this is from Photon Title 20 */
+ font-weight: var(--title-20-font-weight); /* Note: this is from Photon Title 20 */
+}
diff --git a/devtools/client/aboutdebugging/src/components/App.css b/devtools/client/aboutdebugging/src/components/App.css
new file mode 100644
index 0000000000..5196ce8e2e
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/App.css
@@ -0,0 +1,71 @@
+/* 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/. */
+
+/*
+ * The current layout of about:debugging is
+ *
+ * +-------------+-------------------------------+
+ * | Sidebar | Page (Runtime or Connect) |
+ * | (240px) | |
+ * | | |
+ * +-------------+-------------------------------+
+ *
+ * Some of the values (font sizes, widths, etc.) are the same as
+ * about:preferences, which uses the shared common.css
+ */
+
+.app {
+ /* from common */
+ --sidebar-width: 280px;
+ --app-top-padding: 70px;
+ --app-bottom-padding: 40px;
+ --app-left-padding: 32px;
+ --app-right-padding: 32px;
+
+ box-sizing: border-box;
+ width: 100vw;
+ height: 100vh;
+ overflow: hidden; /* we don't want the sidebar to scroll, only the main content */
+
+ display: grid;
+ grid-column-gap: 40px;
+ grid-template-columns: var(--sidebar-width) auto;
+
+ font-size: var(--base-font-size);
+ font-weight: var(--base-font-weight);
+ line-height: var(--base-line-height);
+}
+
+.app__sidebar {
+ padding-block-start: var(--app-top-padding);
+ padding-block-end: var(--app-bottom-padding);
+ padding-inline-start: var(--app-left-padding);
+}
+
+.app__content {
+ /* we want to scroll only the main content, not the sidebar */
+ overflow-y: auto;
+
+ /* padding will give space for card shadow to appear and
+ margin will correct the alignment */
+ margin-inline-start: calc(var(--card-shadow-blur-radius) * -1);
+ padding-inline: var(--card-shadow-blur-radius);
+ padding-block-start: var(--app-top-padding);
+}
+
+/* Workaround for Gecko clipping the padding-bottom of a scrollable container;
+ we create a block to act as the bottom padding instead. */
+.app__content::after {
+ content: "";
+ display: block;
+ height: var(--app-bottom-padding);
+}
+
+.page {
+ max-width: var(--page-width);
+ min-width: min-content;
+ font-size: var(--body-20-font-size);
+ font-weight: var(--body-20-font-weight);
+ padding-inline-end: var(--app-right-padding);
+}
diff --git a/devtools/client/aboutdebugging/src/components/App.js b/devtools/client/aboutdebugging/src/components/App.js
new file mode 100644
index 0000000000..7bdf3eb0c5
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/App.js
@@ -0,0 +1,213 @@
+/* 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 {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Route = createFactory(
+ require("resource://devtools/client/shared/vendor/react-router-dom.js").Route
+);
+const Switch = createFactory(
+ require("resource://devtools/client/shared/vendor/react-router-dom.js").Switch
+);
+const Redirect = createFactory(
+ require("resource://devtools/client/shared/vendor/react-router-dom.js")
+ .Redirect
+);
+
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+const {
+ PAGE_TYPES,
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const ConnectPage = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/connect/ConnectPage.js")
+);
+const RuntimePage = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/RuntimePage.js")
+);
+const Sidebar = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/sidebar/Sidebar.js")
+);
+
+class App extends PureComponent {
+ static get propTypes() {
+ return {
+ adbAddonStatus: Types.adbAddonStatus,
+ // The "dispatch" helper is forwarded to the App component via connect.
+ // From that point, components are responsible for forwarding the dispatch
+ // property to all components who need to dispatch actions.
+ dispatch: PropTypes.func.isRequired,
+ // getString prop is injected by the withLocalization wrapper
+ getString: PropTypes.func.isRequired,
+ isAdbReady: PropTypes.bool.isRequired,
+ isScanningUsb: PropTypes.bool.isRequired,
+ networkLocations: PropTypes.arrayOf(Types.location).isRequired,
+ networkRuntimes: PropTypes.arrayOf(Types.runtime).isRequired,
+ selectedPage: Types.page,
+ selectedRuntimeId: PropTypes.string,
+ usbRuntimes: PropTypes.arrayOf(Types.runtime).isRequired,
+ };
+ }
+
+ componentDidUpdate() {
+ this.updateTitle();
+ }
+
+ updateTitle() {
+ const { getString, selectedPage, selectedRuntimeId } = this.props;
+
+ const pageTitle =
+ selectedPage === PAGE_TYPES.RUNTIME
+ ? getString("about-debugging-page-title-runtime-page", {
+ selectedRuntimeId,
+ })
+ : getString("about-debugging-page-title-setup-page");
+
+ document.title = pageTitle;
+ }
+
+ renderConnect() {
+ const { adbAddonStatus, dispatch, networkLocations } = this.props;
+
+ return ConnectPage({
+ adbAddonStatus,
+ dispatch,
+ networkLocations,
+ });
+ }
+
+ // The `match` object here is passed automatically by the Route object.
+ // We are using it to read the route path.
+ // See react-router docs:
+ // https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/match.md
+ renderRuntime({ match }) {
+ const isRuntimeAvailable = id => {
+ const runtimes = [
+ ...this.props.networkRuntimes,
+ ...this.props.usbRuntimes,
+ ];
+ const runtime = runtimes.find(x => x.id === id);
+ return runtime?.runtimeDetails;
+ };
+
+ const { dispatch } = this.props;
+
+ let runtimeId = match.params.runtimeId || RUNTIMES.THIS_FIREFOX;
+ if (match.params.runtimeId !== RUNTIMES.THIS_FIREFOX) {
+ const rawId = decodeURIComponent(match.params.runtimeId);
+ if (isRuntimeAvailable(rawId)) {
+ runtimeId = rawId;
+ } else {
+ // Also redirect to "This Firefox" if runtime is not found
+ return Redirect({ to: `/runtime/${RUNTIMES.THIS_FIREFOX}` });
+ }
+ }
+
+ // we need to pass a key so the component updates when we want to showcase
+ // a different runtime
+ return RuntimePage({ dispatch, key: runtimeId, runtimeId });
+ }
+
+ renderRoutes() {
+ return Switch(
+ {},
+ Route({
+ path: "/setup",
+ render: () => this.renderConnect(),
+ }),
+ Route({
+ path: "/runtime/:runtimeId",
+ render: routeProps => this.renderRuntime(routeProps),
+ }),
+ // default route when there's no match which includes "/"
+ // TODO: the url does not match "/" means invalid URL,
+ // in this case maybe we'd like to do something else than a redirect.
+ // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1509897
+ Route({
+ render: routeProps => {
+ const { pathname } = routeProps.location;
+ // The old about:debugging supported the following routes:
+ // about:debugging#workers, about:debugging#addons and about:debugging#tabs.
+ // Such links can still be found in external documentation pages.
+ // We redirect to This Firefox rather than the Setup Page here.
+ if (
+ pathname === "/workers" ||
+ pathname === "/addons" ||
+ pathname === "/tabs"
+ ) {
+ return Redirect({ to: `/runtime/${RUNTIMES.THIS_FIREFOX}` });
+ }
+ return Redirect({ to: "/setup" });
+ },
+ })
+ );
+ }
+
+ render() {
+ const {
+ adbAddonStatus,
+ dispatch,
+ isAdbReady,
+ isScanningUsb,
+ networkRuntimes,
+ selectedPage,
+ selectedRuntimeId,
+ usbRuntimes,
+ } = this.props;
+
+ return Localized(
+ {},
+ dom.div(
+ { className: "app" },
+ Sidebar({
+ adbAddonStatus,
+ className: "app__sidebar",
+ dispatch,
+ isAdbReady,
+ isScanningUsb,
+ networkRuntimes,
+ selectedPage,
+ selectedRuntimeId,
+ usbRuntimes,
+ }),
+ dom.main({ className: "app__content" }, this.renderRoutes())
+ )
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ adbAddonStatus: state.ui.adbAddonStatus,
+ isAdbReady: state.ui.isAdbReady,
+ isScanningUsb: state.ui.isScanningUsb,
+ networkLocations: state.ui.networkLocations,
+ networkRuntimes: state.runtimes.networkRuntimes,
+ selectedPage: state.ui.selectedPage,
+ selectedRuntimeId: state.runtimes.selectedRuntimeId,
+ usbRuntimes: state.runtimes.usbRuntimes,
+ };
+};
+
+const mapDispatchToProps = dispatch => ({
+ dispatch,
+});
+
+module.exports = FluentReact.withLocalization(
+ connect(mapStateToProps, mapDispatchToProps)(App)
+);
diff --git a/devtools/client/aboutdebugging/src/components/CompatibilityWarning.js b/devtools/client/aboutdebugging/src/components/CompatibilityWarning.js
new file mode 100644
index 0000000000..42284fa672
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/CompatibilityWarning.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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Message = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/Message.js")
+);
+
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const {
+ COMPATIBILITY_STATUS,
+} = require("resource://devtools/client/shared/remote-debugging/version-checker.js");
+
+const TROUBLESHOOTING_URL =
+ "https://firefox-source-docs.mozilla.org/devtools-user/about_colon_debugging/";
+const FENNEC_TROUBLESHOOTING_URL =
+ "https://firefox-source-docs.mozilla.org/devtools-user/about_colon_debugging/index.html#connection-to-firefox-for-android-68";
+
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+class CompatibilityWarning extends PureComponent {
+ static get propTypes() {
+ return {
+ compatibilityReport: Types.compatibilityReport.isRequired,
+ };
+ }
+
+ render() {
+ const {
+ localID,
+ localVersion,
+ minVersion,
+ runtimeID,
+ runtimeVersion,
+ status,
+ } = this.props.compatibilityReport;
+
+ if (status === COMPATIBILITY_STATUS.COMPATIBLE) {
+ return null;
+ }
+
+ let localizationId, statusClassName;
+ switch (status) {
+ case COMPATIBILITY_STATUS.TOO_OLD:
+ statusClassName = "qa-compatibility-warning-too-old";
+ localizationId = "about-debugging-browser-version-too-old";
+ break;
+ case COMPATIBILITY_STATUS.TOO_RECENT:
+ statusClassName = "qa-compatibility-warning-too-recent";
+ localizationId = "about-debugging-browser-version-too-recent";
+ break;
+ case COMPATIBILITY_STATUS.TOO_OLD_FENNEC:
+ statusClassName = "qa-compatibility-warning-too-old-fennec";
+ localizationId = "about-debugging-browser-version-too-old-fennec";
+ break;
+ }
+
+ const troubleshootingUrl =
+ status === COMPATIBILITY_STATUS.TOO_OLD_FENNEC
+ ? FENNEC_TROUBLESHOOTING_URL
+ : TROUBLESHOOTING_URL;
+
+ const messageLevel =
+ status === COMPATIBILITY_STATUS.TOO_OLD_FENNEC
+ ? MESSAGE_LEVEL.ERROR
+ : MESSAGE_LEVEL.WARNING;
+
+ return Message(
+ {
+ level: messageLevel,
+ isCloseable: true,
+ },
+ Localized(
+ {
+ id: localizationId,
+ a: dom.a({
+ href: troubleshootingUrl,
+ target: "_blank",
+ }),
+ $localID: localID,
+ $localVersion: localVersion,
+ $minVersion: minVersion,
+ $runtimeID: runtimeID,
+ $runtimeVersion: runtimeVersion,
+ },
+ dom.p(
+ {
+ className: `qa-compatibility-warning ${statusClassName}`,
+ },
+ localizationId
+ )
+ )
+ );
+ }
+}
+
+module.exports = CompatibilityWarning;
diff --git a/devtools/client/aboutdebugging/src/components/ConnectionPromptSetting.js b/devtools/client/aboutdebugging/src/components/ConnectionPromptSetting.js
new file mode 100644
index 0000000000..d4773bb298
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/ConnectionPromptSetting.js
@@ -0,0 +1,55 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+
+class ConnectionPromptSetting extends PureComponent {
+ static get propTypes() {
+ return {
+ className: PropTypes.string,
+ connectionPromptEnabled: PropTypes.bool.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ };
+ }
+
+ onToggleClick() {
+ const { connectionPromptEnabled, dispatch } = this.props;
+ dispatch(Actions.updateConnectionPromptSetting(!connectionPromptEnabled));
+ }
+
+ render() {
+ const { className, connectionPromptEnabled } = this.props;
+
+ const localizedState = connectionPromptEnabled
+ ? "about-debugging-connection-prompt-disable-button"
+ : "about-debugging-connection-prompt-enable-button";
+
+ return Localized(
+ {
+ id: localizedState,
+ },
+ dom.button(
+ {
+ className: `${className} default-button qa-connection-prompt-toggle-button`,
+ onClick: () => this.onToggleClick(),
+ },
+ localizedState
+ )
+ );
+ }
+}
+
+module.exports = ConnectionPromptSetting;
diff --git a/devtools/client/aboutdebugging/src/components/ProfilerDialog.css b/devtools/client/aboutdebugging/src/components/ProfilerDialog.css
new file mode 100644
index 0000000000..d5352bbea2
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/ProfilerDialog.css
@@ -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/. */
+
+.profiler-dialog__frame {
+ border: none;
+ height: 100%;
+ width: 100%;
+}
+
+/*
+ * The current layout of the dialog header is
+ *
+ * +-----------------------------+---+
+ * | dialog title (auto) | X |
+ * +-----------------------------+---+
+ */
+.profiler-dialog__header {
+ align-items: center;
+ background-color: var(--popup-header-background-color);
+ color: var(--popup-header-color);
+ display: grid;
+ grid-template-columns: 1fr max-content;
+ padding: var(--base-unit);
+}
+
+.profiler-dialog__header__title {
+ margin-inline-start: calc(var(--base-unit) * 2);
+
+ /* Reset <h1> styles */
+ font-size: 15px;
+ font-weight: normal;
+}
+
+.profiler-dialog__inner {
+ background-color: var(--box-background);
+ display: grid;
+ grid-template-rows: max-content auto;
+ max-height: calc(100% - calc(var(--base-unit) * 25)); /* 100% - 100px */
+ position: fixed;
+}
+
+.profiler-dialog__inner--medium {
+ width: calc(var(--base-unit) * 150); /* 600px */
+ height: calc(var(--base-unit) * 150); /* 600px */
+}
+
+.profiler-dialog__inner--large {
+ width: calc(var(--base-unit) * 200); /* 800px */
+ height: calc(var(--base-unit) * 175); /* 700px */
+}
+
+.profiler-dialog__mask {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: var(--grey-90-a60);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
diff --git a/devtools/client/aboutdebugging/src/components/ProfilerDialog.js b/devtools/client/aboutdebugging/src/components/ProfilerDialog.js
new file mode 100644
index 0000000000..f4bb583464
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/ProfilerDialog.js
@@ -0,0 +1,168 @@
+/* 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 {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+const {
+ PROFILER_PAGE_CONTEXT,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This component is a modal dialog containing the performance profiler UI. It uses
+ * the simplified DevTools panel located in devtools/client/performance-new. When
+ * using a custom preset, and editing the settings, the page context switches
+ * to about:profiling, which receives the PerfFront of the remote debuggee.
+ */
+class ProfilerDialog extends PureComponent {
+ static get propTypes() {
+ return {
+ runtimeDetails: Types.runtimeDetails.isRequired,
+ profilerContext: PropTypes.string.isRequired,
+ hideProfilerDialog: PropTypes.func.isRequired,
+ switchProfilerContext: PropTypes.func.isRequired,
+ };
+ }
+
+ hide() {
+ this.props.hideProfilerDialog();
+ }
+
+ setProfilerIframeDirection(frameWindow) {
+ // Set iframe direction according to the parent document direction.
+ const { documentElement } = document;
+ const dir = window.getComputedStyle(documentElement).direction;
+ frameWindow.document.documentElement.setAttribute("dir", dir);
+ }
+
+ /**
+ * The profiler iframe can either be the simplified devtools recording panel,
+ * or the more detailed about:profiling settings page.
+ */
+ renderProfilerIframe() {
+ const {
+ runtimeDetails: { clientWrapper },
+ switchProfilerContext,
+ profilerContext,
+ } = this.props;
+
+ let src, onLoad;
+
+ switch (profilerContext) {
+ case PROFILER_PAGE_CONTEXT.DEVTOOLS_REMOTE:
+ src = clientWrapper.getPerformancePanelUrl();
+ onLoad = e => {
+ const frameWindow = e.target.contentWindow;
+ this.setProfilerIframeDirection(frameWindow);
+
+ clientWrapper.loadPerformanceProfiler(frameWindow, () => {
+ switchProfilerContext(PROFILER_PAGE_CONTEXT.ABOUTPROFILING_REMOTE);
+ });
+ };
+ break;
+
+ case PROFILER_PAGE_CONTEXT.ABOUTPROFILING_REMOTE:
+ src = "about:profiling#remote";
+ onLoad = e => {
+ const frameWindow = e.target.contentWindow;
+ this.setProfilerIframeDirection(frameWindow);
+
+ clientWrapper.loadAboutProfiling(frameWindow, () => {
+ switchProfilerContext(PROFILER_PAGE_CONTEXT.DEVTOOLS_REMOTE);
+ });
+ };
+ break;
+
+ default:
+ throw new Error(`Unhandled profiler context: "${profilerContext}"`);
+ }
+
+ return dom.iframe({
+ key: profilerContext,
+ className: "profiler-dialog__frame",
+ src,
+ onLoad,
+ });
+ }
+
+ render() {
+ const { profilerContext, switchProfilerContext } = this.props;
+ const dialogSizeClassName =
+ profilerContext === PROFILER_PAGE_CONTEXT.DEVTOOLS_REMOTE
+ ? "profiler-dialog__inner--medium"
+ : "profiler-dialog__inner--large";
+
+ return dom.div(
+ {
+ className: "profiler-dialog__mask qa-profiler-dialog-mask",
+ onClick: () => this.hide(),
+ },
+ dom.article(
+ {
+ className: `profiler-dialog__inner ${dialogSizeClassName} qa-profiler-dialog`,
+ onClick: e => e.stopPropagation(),
+ },
+ dom.header(
+ {
+ className: "profiler-dialog__header",
+ },
+ Localized(
+ {
+ id: "about-debugging-profiler-dialog-title2",
+ },
+ dom.h1(
+ {
+ className: "profiler-dialog__header__title",
+ },
+ "about-debugging-profiler-dialog-title2"
+ )
+ ),
+ dom.button(
+ {
+ className: "ghost-button qa-profiler-dialog-close",
+ onClick: () => {
+ if (profilerContext === PROFILER_PAGE_CONTEXT.DEVTOOLS_REMOTE) {
+ this.hide();
+ } else {
+ switchProfilerContext(PROFILER_PAGE_CONTEXT.DEVTOOLS_REMOTE);
+ }
+ },
+ },
+ dom.img({
+ src: "chrome://devtools/skin/images/close.svg",
+ })
+ )
+ ),
+ this.renderProfilerIframe()
+ )
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ profilerContext: state.ui.profilerContext,
+ };
+};
+
+const mapDispatchToProps = {
+ hideProfilerDialog: Actions.hideProfilerDialog,
+ switchProfilerContext: Actions.switchProfilerContext,
+};
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(ProfilerDialog);
diff --git a/devtools/client/aboutdebugging/src/components/RuntimeActions.css b/devtools/client/aboutdebugging/src/components/RuntimeActions.css
new file mode 100644
index 0000000000..6333560d4b
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/RuntimeActions.css
@@ -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/. */
+
+.runtime-actions__toolbar {
+ column-gap: var(--base-unit);
+ display: flex;
+ justify-content: end;
+}
diff --git a/devtools/client/aboutdebugging/src/components/RuntimeActions.js b/devtools/client/aboutdebugging/src/components/RuntimeActions.js
new file mode 100644
index 0000000000..eefa8b500b
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/RuntimeActions.js
@@ -0,0 +1,82 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const ConnectionPromptSetting = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/ConnectionPromptSetting.js")
+);
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const {
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+class RuntimeActions extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ runtimeDetails: Types.runtimeDetails,
+ runtimeId: PropTypes.string.isRequired,
+ };
+ }
+
+ onProfilerButtonClick() {
+ this.props.dispatch(Actions.showProfilerDialog());
+ }
+
+ renderConnectionPromptSetting() {
+ const { dispatch, runtimeDetails, runtimeId } = this.props;
+ const { connectionPromptEnabled } = runtimeDetails;
+ // do not show the connection prompt setting in 'This Firefox'
+ return runtimeId !== RUNTIMES.THIS_FIREFOX
+ ? ConnectionPromptSetting({
+ connectionPromptEnabled,
+ dispatch,
+ })
+ : null;
+ }
+
+ renderProfileButton() {
+ const { runtimeId } = this.props;
+
+ return runtimeId !== RUNTIMES.THIS_FIREFOX
+ ? Localized(
+ {
+ id: "about-debugging-runtime-profile-button2",
+ },
+ dom.button(
+ {
+ className: "default-button qa-profile-runtime-button",
+ onClick: () => this.onProfilerButtonClick(),
+ },
+ "about-debugging-runtime-profile-button2"
+ )
+ )
+ : null;
+ }
+
+ render() {
+ return dom.div(
+ {
+ className: "runtime-actions__toolbar",
+ },
+ this.renderProfileButton(),
+ this.renderConnectionPromptSetting()
+ );
+ }
+}
+
+module.exports = RuntimeActions;
diff --git a/devtools/client/aboutdebugging/src/components/RuntimeInfo.css b/devtools/client/aboutdebugging/src/components/RuntimeInfo.css
new file mode 100644
index 0000000000..e6fcd9dd7e
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/RuntimeInfo.css
@@ -0,0 +1,42 @@
+/* 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/. */
+
+
+/**
+ * Layout for the runtime info container is:
+ *
+ * <- 68px --x--------- 1fr ----------><---- max ---->
+ * ∧ +---------+------------------------+--------------+
+ * 1fr | | Runtime Info | [Action] |
+ * | | Icon | eg "Firefox (70.0a1)" | |
+ * x | +------------------------+ |
+ * max | | Device Name (optional) | |
+ * ∨ +---------+------------------------+--------------+
+ */
+.runtime-info {
+ align-items: center;
+ display: grid;
+
+ grid-column-gap: var(--main-heading-icon-gap);
+ grid-template-areas:
+ "icon title action"
+ "icon subtitle .";
+ grid-template-columns: var(--main-heading-icon-size) 1fr max-content;
+ grid-template-rows: 1fr max-content;
+
+ margin-block-end: calc(var(--base-unit) * 5);
+}
+
+.runtime-info__icon {
+ grid-area: icon;
+}
+.runtime-info__title {
+ grid-area: title;
+}
+.runtime-info__subtitle {
+ grid-area: subtitle;
+}
+.runtime-info__action {
+ grid-area: action;
+}
diff --git a/devtools/client/aboutdebugging/src/components/RuntimeInfo.js b/devtools/client/aboutdebugging/src/components/RuntimeInfo.js
new file mode 100644
index 0000000000..6a8c67dd33
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/RuntimeInfo.js
@@ -0,0 +1,89 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This component displays runtime information.
+ */
+class RuntimeInfo extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ icon: PropTypes.string.isRequired,
+ deviceName: PropTypes.string,
+ name: PropTypes.string.isRequired,
+ version: PropTypes.string.isRequired,
+ runtimeId: PropTypes.string.isRequired,
+ };
+ }
+ render() {
+ const { icon, deviceName, name, version, runtimeId, dispatch } = this.props;
+
+ return dom.h1(
+ {
+ className: "main-heading runtime-info",
+ },
+ dom.img({
+ className: "main-heading__icon runtime-info__icon qa-runtime-icon",
+ src: icon,
+ }),
+ Localized(
+ {
+ id: "about-debugging-runtime-name",
+ $name: name,
+ $version: version,
+ },
+ dom.label(
+ {
+ className: "qa-runtime-name runtime-info__title",
+ },
+ `${name} (${version})`
+ )
+ ),
+ deviceName
+ ? dom.label(
+ {
+ className: "main-heading-subtitle runtime-info__subtitle",
+ },
+ deviceName
+ )
+ : null,
+ runtimeId !== RUNTIMES.THIS_FIREFOX
+ ? Localized(
+ {
+ id: "about-debugging-runtime-disconnect-button",
+ },
+ dom.button(
+ {
+ className:
+ "default-button runtime-info__action qa-runtime-info__action",
+ onClick() {
+ dispatch(Actions.disconnectRuntime(runtimeId, true));
+ },
+ },
+ "Disconnect"
+ )
+ )
+ : null
+ );
+ }
+}
+
+module.exports = RuntimeInfo;
diff --git a/devtools/client/aboutdebugging/src/components/RuntimePage.js b/devtools/client/aboutdebugging/src/components/RuntimePage.js
new file mode 100644
index 0000000000..e2dae9b0cd
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/RuntimePage.js
@@ -0,0 +1,306 @@
+/* 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 {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const CompatibilityWarning = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/CompatibilityWarning.js")
+);
+const DebugTargetPane = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.js")
+);
+const ExtensionDetail = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.js")
+);
+const InspectAction = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/InspectAction.js")
+);
+const ProfilerDialog = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/ProfilerDialog.js")
+);
+const RuntimeActions = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/RuntimeActions.js")
+);
+const RuntimeInfo = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/RuntimeInfo.js")
+);
+const ServiceWorkerAction = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.js")
+);
+const ServiceWorkerAdditionalActions = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAdditionalActions.js")
+);
+const ServiceWorkersWarning = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/ServiceWorkersWarning.js")
+);
+const ProcessDetail = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/ProcessDetail.js")
+);
+const TabAction = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/TabAction.js")
+);
+const TabDetail = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/TabDetail.js")
+);
+const TemporaryExtensionAdditionalActions = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionAdditionalActions.js")
+);
+const TemporaryExtensionDetail = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionDetail.js")
+);
+const TemporaryExtensionInstallSection = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.js")
+);
+const WorkerDetail = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/WorkerDetail.js")
+);
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const {
+ DEBUG_TARGETS,
+ DEBUG_TARGET_PANE,
+ PAGE_TYPES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+const {
+ getCurrentRuntimeDetails,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
+const {
+ isSupportedDebugTargetPane,
+ supportsTemporaryExtensionInstaller,
+} = require("resource://devtools/client/aboutdebugging/src/modules/debug-target-support.js");
+
+class RuntimePage extends PureComponent {
+ static get propTypes() {
+ return {
+ collapsibilities: Types.collapsibilities.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ installedExtensions: PropTypes.arrayOf(PropTypes.object).isRequired,
+ otherWorkers: PropTypes.arrayOf(PropTypes.object).isRequired,
+ runtimeDetails: Types.runtimeDetails,
+ runtimeId: PropTypes.string.isRequired,
+ processes: PropTypes.arrayOf(PropTypes.object).isRequired,
+ serviceWorkers: PropTypes.arrayOf(PropTypes.object).isRequired,
+ sharedWorkers: PropTypes.arrayOf(PropTypes.object).isRequired,
+ showProfilerDialog: PropTypes.bool.isRequired,
+ tabs: PropTypes.arrayOf(PropTypes.object).isRequired,
+ temporaryExtensions: PropTypes.arrayOf(PropTypes.object).isRequired,
+ temporaryInstallError: PropTypes.object,
+ };
+ }
+
+ // TODO: avoid the use of this method
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillMount() {
+ const { dispatch, runtimeId } = this.props;
+ dispatch(Actions.selectPage(PAGE_TYPES.RUNTIME, runtimeId));
+ }
+
+ getIconByType(type) {
+ switch (type) {
+ case DEBUG_TARGETS.EXTENSION:
+ return "chrome://devtools/skin/images/debugging-addons.svg";
+ case DEBUG_TARGETS.PROCESS:
+ return "chrome://devtools/skin/images/aboutdebugging-process-icon.svg";
+ case DEBUG_TARGETS.TAB:
+ return "chrome://devtools/skin/images/debugging-tabs.svg";
+ case DEBUG_TARGETS.WORKER:
+ return "chrome://devtools/skin/images/debugging-workers.svg";
+ }
+
+ throw new Error(`Unsupported type [${type}]`);
+ }
+
+ renderDebugTargetPane({
+ actionComponent,
+ additionalActionsComponent,
+ children,
+ detailComponent,
+ icon,
+ localizationId,
+ name,
+ paneKey,
+ targets,
+ }) {
+ const { collapsibilities, dispatch, runtimeDetails } = this.props;
+
+ if (!isSupportedDebugTargetPane(runtimeDetails.info.type, paneKey)) {
+ return null;
+ }
+
+ return Localized(
+ {
+ id: localizationId,
+ attrs: { name: true },
+ },
+ DebugTargetPane(
+ {
+ actionComponent,
+ additionalActionsComponent,
+ collapsibilityKey: paneKey,
+ detailComponent,
+ dispatch,
+ icon,
+ isCollapsed: collapsibilities.get(paneKey),
+ name,
+ targets,
+ },
+ children
+ )
+ );
+ }
+
+ renderTemporaryExtensionInstallSection() {
+ const runtimeType = this.props.runtimeDetails.info.type;
+ if (
+ !isSupportedDebugTargetPane(
+ runtimeType,
+ DEBUG_TARGET_PANE.TEMPORARY_EXTENSION
+ ) ||
+ !supportsTemporaryExtensionInstaller(runtimeType)
+ ) {
+ return null;
+ }
+
+ const { dispatch, temporaryInstallError } = this.props;
+ return TemporaryExtensionInstallSection({
+ dispatch,
+ temporaryInstallError,
+ });
+ }
+
+ render() {
+ const {
+ dispatch,
+ installedExtensions,
+ otherWorkers,
+ processes,
+ runtimeDetails,
+ runtimeId,
+ serviceWorkers,
+ sharedWorkers,
+ showProfilerDialog,
+ tabs,
+ temporaryExtensions,
+ } = this.props;
+
+ if (!runtimeDetails) {
+ // runtimeInfo can be null when the selectPage action navigates from a runtime A
+ // to a runtime B (between unwatchRuntime and watchRuntime).
+ return null;
+ }
+
+ const { compatibilityReport } = runtimeDetails;
+
+ return dom.article(
+ {
+ className: "page qa-runtime-page",
+ },
+ RuntimeInfo({ ...runtimeDetails.info, runtimeId, dispatch }),
+ RuntimeActions({ dispatch, runtimeId, runtimeDetails }),
+ runtimeDetails.serviceWorkersAvailable ? null : ServiceWorkersWarning(),
+ CompatibilityWarning({ compatibilityReport }),
+ this.renderDebugTargetPane({
+ actionComponent: TabAction,
+ detailComponent: TabDetail,
+ icon: this.getIconByType(DEBUG_TARGETS.TAB),
+ localizationId: "about-debugging-runtime-tabs",
+ name: "Tabs",
+ paneKey: DEBUG_TARGET_PANE.TAB,
+ targets: tabs,
+ }),
+ this.renderDebugTargetPane({
+ actionComponent: InspectAction,
+ additionalActionsComponent: TemporaryExtensionAdditionalActions,
+ children: this.renderTemporaryExtensionInstallSection(),
+ detailComponent: TemporaryExtensionDetail,
+ icon: this.getIconByType(DEBUG_TARGETS.EXTENSION),
+ localizationId: "about-debugging-runtime-temporary-extensions",
+ name: "Temporary Extensions",
+ paneKey: DEBUG_TARGET_PANE.TEMPORARY_EXTENSION,
+ targets: temporaryExtensions,
+ }),
+ this.renderDebugTargetPane({
+ actionComponent: InspectAction,
+ detailComponent: ExtensionDetail,
+ icon: this.getIconByType(DEBUG_TARGETS.EXTENSION),
+ localizationId: "about-debugging-runtime-extensions",
+ name: "Extensions",
+ paneKey: DEBUG_TARGET_PANE.INSTALLED_EXTENSION,
+ targets: installedExtensions,
+ }),
+ this.renderDebugTargetPane({
+ actionComponent: ServiceWorkerAction,
+ additionalActionsComponent: ServiceWorkerAdditionalActions,
+ detailComponent: WorkerDetail,
+ icon: this.getIconByType(DEBUG_TARGETS.WORKER),
+ localizationId: "about-debugging-runtime-service-workers",
+ name: "Service Workers",
+ paneKey: DEBUG_TARGET_PANE.SERVICE_WORKER,
+ targets: serviceWorkers,
+ }),
+ this.renderDebugTargetPane({
+ actionComponent: InspectAction,
+ detailComponent: WorkerDetail,
+ icon: this.getIconByType(DEBUG_TARGETS.WORKER),
+ localizationId: "about-debugging-runtime-shared-workers",
+ name: "Shared Workers",
+ paneKey: DEBUG_TARGET_PANE.SHARED_WORKER,
+ targets: sharedWorkers,
+ }),
+ this.renderDebugTargetPane({
+ actionComponent: InspectAction,
+ detailComponent: WorkerDetail,
+ icon: this.getIconByType(DEBUG_TARGETS.WORKER),
+ localizationId: "about-debugging-runtime-other-workers",
+ name: "Other Workers",
+ paneKey: DEBUG_TARGET_PANE.OTHER_WORKER,
+ targets: otherWorkers,
+ }),
+ this.renderDebugTargetPane({
+ actionComponent: InspectAction,
+ detailComponent: ProcessDetail,
+ icon: this.getIconByType(DEBUG_TARGETS.PROCESS),
+ localizationId: "about-debugging-runtime-processes",
+ name: "Processes",
+ paneKey: DEBUG_TARGET_PANE.PROCESSES,
+ targets: processes,
+ }),
+
+ showProfilerDialog ? ProfilerDialog({ dispatch, runtimeDetails }) : null
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ collapsibilities: state.ui.debugTargetCollapsibilities,
+ installedExtensions: state.debugTargets.installedExtensions,
+ processes: state.debugTargets.processes,
+ otherWorkers: state.debugTargets.otherWorkers,
+ runtimeDetails: getCurrentRuntimeDetails(state.runtimes),
+ serviceWorkers: state.debugTargets.serviceWorkers,
+ sharedWorkers: state.debugTargets.sharedWorkers,
+ showProfilerDialog: state.ui.showProfilerDialog,
+ tabs: state.debugTargets.tabs,
+ temporaryExtensions: state.debugTargets.temporaryExtensions,
+ temporaryInstallError: state.ui.temporaryInstallError,
+ };
+};
+
+module.exports = connect(mapStateToProps)(RuntimePage);
diff --git a/devtools/client/aboutdebugging/src/components/ServiceWorkersWarning.js b/devtools/client/aboutdebugging/src/components/ServiceWorkersWarning.js
new file mode 100644
index 0000000000..4f9dc93d7f
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/ServiceWorkersWarning.js
@@ -0,0 +1,52 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Message = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/Message.js")
+);
+
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const DOC_URL =
+ "https://firefox-source-docs.mozilla.org/devtools-user/about_colon_debugging/index.html#service-workers-not-compatible";
+
+class ServiceWorkersWarning extends PureComponent {
+ render() {
+ return Message(
+ {
+ level: MESSAGE_LEVEL.WARNING,
+ isCloseable: true,
+ },
+ Localized(
+ {
+ id: "about-debugging-runtime-service-workers-not-compatible",
+ a: dom.a({
+ href: DOC_URL,
+ target: "_blank",
+ }),
+ },
+ dom.p(
+ {
+ className: "qa-service-workers-warning",
+ },
+ "about-debugging-runtime-service-workers-not-compatible"
+ )
+ )
+ );
+ }
+}
+
+module.exports = ServiceWorkersWarning;
diff --git a/devtools/client/aboutdebugging/src/components/connect/ConnectPage.css b/devtools/client/aboutdebugging/src/components/connect/ConnectPage.css
new file mode 100644
index 0000000000..a693bf4113
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/ConnectPage.css
@@ -0,0 +1,50 @@
+/* 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/. */
+
+.connect-page__breather {
+ margin-block-start: calc(var(--base-unit) * 6);
+}
+
+/*
+ * +--------+----------------------+
+ * | USB | |<button> |
+ * +--------+ | |
+ * | status | | |
+ * +--------+----------------------+
+ */
+.connect-page__usb-section__heading {
+ display: grid;
+ align-items: center;
+ grid-template-areas: "title . toggle"
+ "status . toggle";
+ grid-template-columns: auto 1fr auto;
+ grid-column-gap: calc(var(--base-unit) * 2);
+ grid-row-gap: var(--base-unit);
+}
+
+.connect-page__usb-section__heading__toggle {
+ grid-area: toggle;
+}
+
+.connect-page__usb-section__heading__title {
+ grid-area: title;
+ line-height: 1;
+}
+.connect-page__usb-section__heading__status {
+ grid-area: status;
+ line-height: 1;
+ font-size: var(--caption-20-font-size);
+ font-weight: var(--caption-20-font-weight);
+ color: var(--secondary-text-color);
+}
+
+.connect-page__troubleshoot {
+ font-size: var(--body-10-font-size);
+ font-weight: var(--body-10-font-weight);
+ margin-block-start: calc(var(--base-unit) * 2);
+}
+
+.connect-page__troubleshoot--network {
+ padding-inline: calc(var(--base-unit) * 6);
+}
diff --git a/devtools/client/aboutdebugging/src/components/connect/ConnectPage.js b/devtools/client/aboutdebugging/src/components/connect/ConnectPage.js
new file mode 100644
index 0000000000..97b1b01df7
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/ConnectPage.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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ USB_STATES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+
+loader.lazyRequireGetter(
+ this,
+ "ADB_ADDON_STATES",
+ "resource://devtools/client/shared/remote-debugging/adb/adb-addon.js",
+ true
+);
+
+const Link = createFactory(
+ require("resource://devtools/client/shared/vendor/react-router-dom.js").Link
+);
+const ConnectSection = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/connect/ConnectSection.js")
+);
+const ConnectSteps = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/connect/ConnectSteps.js")
+);
+const NetworkLocationsForm = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.js")
+);
+const NetworkLocationsList = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.js")
+);
+
+const {
+ PAGE_TYPES,
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+const USB_ICON_SRC =
+ "chrome://devtools/skin/images/aboutdebugging-usb-icon.svg";
+const GLOBE_ICON_SRC =
+ "chrome://devtools/skin/images/aboutdebugging-globe-icon.svg";
+
+const TROUBLESHOOT_USB_URL =
+ "https://firefox-source-docs.mozilla.org/devtools-user/about_colon_debugging/index.html#connecting-to-a-remote-device";
+const TROUBLESHOOT_NETWORK_URL =
+ "https://firefox-source-docs.mozilla.org/devtools-user/about_colon_debugging/index.html#connecting-over-the-network";
+
+class ConnectPage extends PureComponent {
+ static get propTypes() {
+ return {
+ adbAddonStatus: Types.adbAddonStatus,
+ dispatch: PropTypes.func.isRequired,
+ networkLocations: PropTypes.arrayOf(Types.location).isRequired,
+ };
+ }
+
+ // TODO: avoid the use of this method
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillMount() {
+ this.props.dispatch(Actions.selectPage(PAGE_TYPES.CONNECT));
+ }
+
+ onToggleUSBClick() {
+ const { adbAddonStatus } = this.props;
+ const isAddonInstalled = adbAddonStatus === ADB_ADDON_STATES.INSTALLED;
+ if (isAddonInstalled) {
+ this.props.dispatch(Actions.uninstallAdbAddon());
+ } else {
+ this.props.dispatch(Actions.installAdbAddon());
+ }
+ }
+
+ getUsbStatus() {
+ switch (this.props.adbAddonStatus) {
+ case ADB_ADDON_STATES.INSTALLED:
+ return USB_STATES.ENABLED_USB;
+ case ADB_ADDON_STATES.UNINSTALLED:
+ return USB_STATES.DISABLED_USB;
+ default:
+ return USB_STATES.UPDATING_USB;
+ }
+ }
+
+ renderUsbStatus() {
+ const statusTextId = {
+ [USB_STATES.ENABLED_USB]: "about-debugging-setup-usb-status-enabled",
+ [USB_STATES.DISABLED_USB]: "about-debugging-setup-usb-status-disabled",
+ [USB_STATES.UPDATING_USB]: "about-debugging-setup-usb-status-updating",
+ }[this.getUsbStatus()];
+
+ return Localized(
+ {
+ id: statusTextId,
+ },
+ dom.span(
+ {
+ className: "connect-page__usb-section__heading__status",
+ },
+ statusTextId
+ )
+ );
+ }
+
+ renderUsbToggleButton() {
+ const usbStatus = this.getUsbStatus();
+
+ const localizedStates = {
+ [USB_STATES.ENABLED_USB]: "about-debugging-setup-usb-disable-button",
+ [USB_STATES.DISABLED_USB]: "about-debugging-setup-usb-enable-button",
+ [USB_STATES.UPDATING_USB]: "about-debugging-setup-usb-updating-button",
+ };
+ const localizedState = localizedStates[usbStatus];
+
+ // Disable the button while the USB status is updating.
+ const disabled = usbStatus === USB_STATES.UPDATING_USB;
+
+ return Localized(
+ {
+ id: localizedState,
+ },
+ dom.button(
+ {
+ className:
+ "default-button connect-page__usb-section__heading__toggle " +
+ "qa-connect-usb-toggle-button",
+ disabled,
+ onClick: () => this.onToggleUSBClick(),
+ },
+ localizedState
+ )
+ );
+ }
+
+ renderUsb() {
+ const { adbAddonStatus } = this.props;
+ const isAddonInstalled = adbAddonStatus === ADB_ADDON_STATES.INSTALLED;
+ return ConnectSection(
+ {
+ icon: USB_ICON_SRC,
+ title: dom.div(
+ {
+ className: "connect-page__usb-section__heading",
+ },
+ Localized(
+ { id: "about-debugging-setup-usb-title" },
+ dom.span(
+ {
+ className: "connect-page__usb-section__heading__title",
+ },
+ "USB"
+ )
+ ),
+ this.renderUsbStatus(),
+ this.renderUsbToggleButton()
+ ),
+ },
+ isAddonInstalled
+ ? ConnectSteps({
+ steps: [
+ {
+ localizationId:
+ "about-debugging-setup-usb-step-enable-dev-menu2",
+ },
+ {
+ localizationId: "about-debugging-setup-usb-step-enable-debug2",
+ },
+ {
+ localizationId:
+ "about-debugging-setup-usb-step-enable-debug-firefox2",
+ },
+ {
+ localizationId: "about-debugging-setup-usb-step-plug-device",
+ },
+ ],
+ })
+ : Localized(
+ {
+ id: "about-debugging-setup-usb-disabled",
+ },
+ dom.aside(
+ {
+ className: "qa-connect-usb-disabled-message",
+ },
+ "Enabling this will download and add the required Android USB debugging " +
+ "components to Firefox."
+ )
+ ),
+ this.renderTroubleshootText(RUNTIMES.USB)
+ );
+ }
+
+ renderNetwork() {
+ const { dispatch, networkLocations } = this.props;
+
+ return Localized(
+ {
+ id: "about-debugging-setup-network",
+ attrs: { title: true },
+ },
+ ConnectSection({
+ icon: GLOBE_ICON_SRC,
+ title: "Network Location",
+ extraContent: dom.div(
+ {},
+ NetworkLocationsList({ dispatch, networkLocations }),
+ NetworkLocationsForm({ dispatch, networkLocations }),
+ this.renderTroubleshootText(RUNTIMES.NETWORK)
+ ),
+ })
+ );
+ }
+
+ renderTroubleshootText(connectionType) {
+ const localizationId =
+ connectionType === RUNTIMES.USB
+ ? "about-debugging-setup-usb-troubleshoot"
+ : "about-debugging-setup-network-troubleshoot";
+
+ const className =
+ "connect-page__troubleshoot connect-page__troubleshoot--" +
+ `${connectionType === RUNTIMES.USB ? "usb" : "network"}`;
+
+ const url =
+ connectionType === RUNTIMES.USB
+ ? TROUBLESHOOT_USB_URL
+ : TROUBLESHOOT_NETWORK_URL;
+
+ return dom.aside(
+ {
+ className,
+ },
+ Localized(
+ {
+ id: localizationId,
+ a: dom.a({
+ href: url,
+ target: "_blank",
+ }),
+ },
+ dom.p({}, localizationId)
+ )
+ );
+ }
+
+ render() {
+ return dom.article(
+ {
+ className: "page connect-page qa-connect-page",
+ },
+ Localized(
+ {
+ id: "about-debugging-setup-title",
+ },
+ dom.h1(
+ {
+ className: "alt-heading alt-heading--larger",
+ },
+ "Setup"
+ )
+ ),
+ Localized(
+ {
+ id: "about-debugging-setup-intro",
+ },
+ dom.p(
+ {},
+ "Configure the connection method you wish to remotely debug your device with."
+ )
+ ),
+ Localized(
+ {
+ id: "about-debugging-setup-this-firefox2",
+ a: Link({
+ to: `/runtime/${RUNTIMES.THIS_FIREFOX}`,
+ }),
+ },
+ dom.p({}, "about-debugging-setup-this-firefox")
+ ),
+ dom.section(
+ {
+ className: "connect-page__breather",
+ },
+ Localized(
+ {
+ id: "about-debugging-setup-connect-heading",
+ },
+ dom.h2(
+ {
+ className: "alt-heading",
+ },
+ "Connect a device"
+ )
+ ),
+ this.renderUsb(),
+ this.renderNetwork()
+ )
+ );
+ }
+}
+
+module.exports = FluentReact.withLocalization(ConnectPage);
diff --git a/devtools/client/aboutdebugging/src/components/connect/ConnectSection.css b/devtools/client/aboutdebugging/src/components/connect/ConnectSection.css
new file mode 100644
index 0000000000..4349b147b0
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/ConnectSection.css
@@ -0,0 +1,50 @@
+/* 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/. */
+
+.connect-section {
+ --icon-size: calc(var(--base-unit) * 9);
+ --header-col-gap: calc(var(--base-unit) * 2);
+ margin-block-end: calc(var(--base-unit) * 4);
+}
+
+/*
+ * +--------+----------------+
+ * | <icon> | <heading> 1fr |
+ * +--------+----------------+
+ */
+.connect-section__header {
+ display: grid;
+ grid-template-areas: "icon heading";
+ grid-template-columns: auto 1fr;
+ grid-template-rows: var(--icon-size);
+ grid-column-gap: var(--header-col-gap);
+ align-items: center;
+
+ padding-block-end: calc(var(--base-unit) * 4);
+ padding-inline: calc(var(--base-unit) * 5);
+}
+
+.connect-section__header__title {
+ grid-area: heading;
+}
+
+.connect-section__header__icon {
+ grid-area: icon;
+ width: var(--icon-size);
+ height: var(--icon-size);
+
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+.connect-section__content {
+ line-height: 1.5;
+ padding-inline-start: calc(var(--base-unit) * 5
+ + var(--header-col-gap) + var(--icon-size));
+ padding-inline-end: calc(var(--base-unit) * 5);
+}
+
+.connect-section__extra {
+ border-block-start: 1px solid var(--card-separator-color);
+}
diff --git a/devtools/client/aboutdebugging/src/components/connect/ConnectSection.js b/devtools/client/aboutdebugging/src/components/connect/ConnectSection.js
new file mode 100644
index 0000000000..55f8eb4e78
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/ConnectSection.js
@@ -0,0 +1,69 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+class ConnectSection extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node,
+ className: PropTypes.string,
+ extraContent: PropTypes.node,
+ icon: PropTypes.string.isRequired,
+ title: PropTypes.node.isRequired,
+ };
+ }
+
+ renderExtraContent() {
+ const { extraContent } = this.props;
+ return dom.section(
+ {
+ className: "connect-section__extra",
+ },
+ extraContent
+ );
+ }
+
+ render() {
+ const { extraContent } = this.props;
+
+ return dom.section(
+ {
+ className: `card connect-section ${this.props.className || ""}`,
+ },
+ dom.header(
+ {
+ className: "connect-section__header",
+ },
+ dom.img({
+ className: "connect-section__header__icon",
+ src: this.props.icon,
+ }),
+ dom.h1(
+ {
+ className: "card__heading connect-section__header__title",
+ },
+ this.props.title
+ )
+ ),
+ this.props.children
+ ? dom.div(
+ {
+ className: "connect-section__content",
+ },
+ this.props.children
+ )
+ : null,
+ extraContent ? this.renderExtraContent() : null
+ );
+ }
+}
+
+module.exports = ConnectSection;
diff --git a/devtools/client/aboutdebugging/src/components/connect/ConnectSteps.css b/devtools/client/aboutdebugging/src/components/connect/ConnectSteps.css
new file mode 100644
index 0000000000..bddd513aa7
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/ConnectSteps.css
@@ -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/. */
+
+.connect-page__step-list {
+ list-style-type: decimal;
+ list-style-position: outside;
+ margin-inline-start: calc(var(--base-unit) * 4);
+}
+
+.connect-page__step {
+ padding-inline-start: var(--base-unit);
+}
diff --git a/devtools/client/aboutdebugging/src/components/connect/ConnectSteps.js b/devtools/client/aboutdebugging/src/components/connect/ConnectSteps.js
new file mode 100644
index 0000000000..0e8d304108
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/ConnectSteps.js
@@ -0,0 +1,51 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+class ConnectSteps extends PureComponent {
+ static get propTypes() {
+ return {
+ steps: PropTypes.arrayOf(
+ PropTypes.shape({
+ localizationId: PropTypes.string.isRequired,
+ }).isRequired
+ ),
+ };
+ }
+
+ render() {
+ return dom.ul(
+ {
+ className: "connect-page__step-list",
+ },
+ ...this.props.steps.map(step =>
+ Localized(
+ {
+ id: step.localizationId,
+ },
+ dom.li(
+ {
+ className: "connect-page__step",
+ key: step.localizationId,
+ },
+ step.localizationId
+ )
+ )
+ )
+ );
+ }
+}
+
+module.exports = ConnectSteps;
diff --git a/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.css b/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.css
new file mode 100644
index 0000000000..5694bcf216
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.css
@@ -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/. */
+
+/*
+ * Layout of a network location form
+ *
+ * +-------------+--------------------+------------+
+ * | "Host:port" | Input | Add button |
+ * +-------------+--------------------+------------+
+ */
+.connect-page__network-form {
+ display: grid;
+ grid-column-gap: calc(var(--base-unit) * 2);
+ grid-template-columns: auto 1fr auto;
+ align-items: center;
+ padding-block-start: calc(var(--base-unit) * 4);
+ padding-inline: calc(var(--base-unit) * 6);
+}
+
+.connect-page__network-form__error-message {
+ grid-column: 1 / -1;
+}
diff --git a/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.js b/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.js
new file mode 100644
index 0000000000..347167921b
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.js
@@ -0,0 +1,148 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Message = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/Message.js")
+);
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+class NetworkLocationsForm extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ networkLocations: PropTypes.arrayOf(Types.location).isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ errorHostValue: null,
+ errorMessageId: null,
+ value: "",
+ };
+ }
+
+ onSubmit(e) {
+ const { networkLocations } = this.props;
+ const { value } = this.state;
+
+ e.preventDefault();
+
+ if (!value) {
+ return;
+ }
+
+ if (!value.match(/[^:]+:\d+/)) {
+ this.setState({
+ errorHostValue: value,
+ errorMessageId: "about-debugging-network-location-form-invalid",
+ });
+ return;
+ }
+
+ if (networkLocations.includes(value)) {
+ this.setState({
+ errorHostValue: value,
+ errorMessageId: "about-debugging-network-location-form-duplicate",
+ });
+ return;
+ }
+
+ this.props.dispatch(Actions.addNetworkLocation(value));
+ this.setState({ errorHostValue: null, errorMessageId: null, value: "" });
+ }
+
+ renderError() {
+ const { errorHostValue, errorMessageId } = this.state;
+
+ if (!errorMessageId) {
+ return null;
+ }
+
+ return Message(
+ {
+ className:
+ "connect-page__network-form__error-message " +
+ "qa-connect-page__network-form__error-message",
+ level: MESSAGE_LEVEL.ERROR,
+ isCloseable: true,
+ },
+ Localized(
+ {
+ id: errorMessageId,
+ "$host-value": errorHostValue,
+ },
+ dom.p(
+ {
+ className: "technical-text",
+ },
+ errorMessageId
+ )
+ )
+ );
+ }
+
+ render() {
+ return dom.form(
+ {
+ className: "connect-page__network-form",
+ onSubmit: e => this.onSubmit(e),
+ },
+ this.renderError(),
+ Localized(
+ {
+ id: "about-debugging-network-locations-host-input-label",
+ },
+ dom.label(
+ {
+ htmlFor: "about-debugging-network-locations-host-input",
+ },
+ "Host"
+ )
+ ),
+ dom.input({
+ id: "about-debugging-network-locations-host-input",
+ className: "default-input qa-network-form-input",
+ placeholder: "localhost:6080",
+ type: "text",
+ value: this.state.value,
+ onChange: e => {
+ const value = e.target.value;
+ this.setState({ value });
+ },
+ }),
+ Localized(
+ {
+ id: "about-debugging-network-locations-add-button",
+ },
+ dom.button(
+ {
+ className: "primary-button qa-network-form-submit-button",
+ },
+ "Add"
+ )
+ )
+ );
+ }
+}
+
+module.exports = NetworkLocationsForm;
diff --git a/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.css b/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.css
new file mode 100644
index 0000000000..e5676b784a
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.css
@@ -0,0 +1,20 @@
+/* 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/. */
+
+/*
+ * Layout of a network location item
+ *
+ * +-------------------------------------+---------------+
+ * | Location (eg localhost:8080) | Remove button |
+ * +-------------------------------------+---------------+
+ */
+.network-location {
+ display: grid;
+ grid-template-columns: auto max-content;
+ align-items: center;
+
+ padding-block: calc(var(--base-unit) * 2);
+ padding-inline: calc(var(--base-unit) * 6);
+ border-bottom: 1px solid var(--card-separator-color);
+}
diff --git a/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.js b/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.js
new file mode 100644
index 0000000000..e680fe525f
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+class NetworkLocationsList extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ networkLocations: PropTypes.arrayOf(Types.location).isRequired,
+ };
+ }
+
+ renderList() {
+ return dom.ul(
+ {},
+ this.props.networkLocations.map(location =>
+ dom.li(
+ {
+ className: "network-location qa-network-location",
+ key: location,
+ },
+ dom.span(
+ {
+ className: "ellipsis-text qa-network-location-value",
+ },
+ location
+ ),
+ Localized(
+ {
+ id: "about-debugging-network-locations-remove-button",
+ },
+ dom.button(
+ {
+ className: "default-button qa-network-location-remove-button",
+ onClick: () => {
+ this.props.dispatch(Actions.removeNetworkLocation(location));
+ },
+ },
+ "Remove"
+ )
+ )
+ )
+ )
+ );
+ }
+
+ render() {
+ return this.props.networkLocations.length ? this.renderList() : null;
+ }
+}
+
+module.exports = NetworkLocationsList;
diff --git a/devtools/client/aboutdebugging/src/components/connect/moz.build b/devtools/client/aboutdebugging/src/components/connect/moz.build
new file mode 100644
index 0000000000..9228e80125
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/moz.build
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "ConnectPage.js",
+ "ConnectSection.js",
+ "ConnectSteps.js",
+ "NetworkLocationsForm.js",
+ "NetworkLocationsList.js",
+)
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.css b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.css
new file mode 100644
index 0000000000..f049f33b23
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.css
@@ -0,0 +1,97 @@
+/* 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/. */
+
+/*
+ * The current layout of debug target item is
+ *
+ * +--------+-----------------------------+----------------+
+ * | | Name | |
+ * | [Icon] |-----------------------------| Action button |
+ * | | Subname | |
+ * +--------+-----------------------------+----------------+
+ * | Detail |
+ * | |
+ * +-------------------------------------------------------+
+ * | Additional actions |
+ * | |
+ * +-------------------------------------------------------+
+ */
+.debug-target-item {
+ display: grid;
+ grid-template-columns: calc(var(--base-unit) * 8) 1fr max-content;
+ grid-template-rows: 1fr minmax(0, auto) auto;
+ grid-column-gap: calc(var(--base-unit) * 2);
+ grid-template-areas: "icon name action"
+ "icon subname action"
+ "detail detail detail"
+ "additional_actions additional_actions additional_actions";
+ margin-block-end: calc(var(--base-unit) * 4);
+
+ padding-block: calc(var(--base-unit) * 3) calc(var(--base-unit) * 2);
+ padding-inline: calc(var(--base-unit) * 3) calc(var(--base-unit) * 2);
+}
+
+.debug-target-item__icon {
+ align-self: center;
+ grid-area: icon;
+ margin-inline-start: calc(var(--base-unit) * 3);
+ width: 100%;
+
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+.debug-target-item__name {
+ align-self: center;
+ grid-area: name;
+ font-size: var(--body-20-font-size);
+ font-weight: var(--body-20-font-weight-bold);
+ line-height: 1.5;
+ margin-inline-start: calc(var(--base-unit) * 3);
+}
+
+.debug-target-item__action {
+ grid-area: action;
+ align-self: center;
+ margin-inline-end: calc(var(--base-unit) * 2);
+}
+
+.debug-target-item__additional_actions {
+ grid-area: additional_actions;
+ border-top: 1px solid var(--card-separator-color);
+ margin-block-start: calc(var(--base-unit) * 2);
+ padding-block-start: calc(var(--base-unit) * 2);
+ padding-inline-end: calc(var(--base-unit) * 2);
+}
+
+.debug-target-item__detail {
+ grid-area: detail;
+ margin-block-start: calc(var(--base-unit) * 3);
+}
+
+.debug-target-item__detail--empty {
+ margin-block-start: var(--base-unit);
+}
+
+.debug-target-item__messages {
+ margin-inline: calc(var(--base-unit) * 3) calc(var(--base-unit) * 2);
+}
+
+.debug-target-item__subname {
+ grid-area: subname;
+ color: var(--secondary-text-color);
+ font-size: var(--caption-20-font-size);
+ font-weight: var(--caption-20-font-weight);
+ line-height: 1.5;
+}
+
+/* The subname is always LTR under the Tabs section,
+ so check its parent's direction to set the correct margin. */
+.debug-target-item:dir(ltr) > .debug-target-item__subname {
+ margin-left: calc(var(--base-unit) * 3);
+}
+
+.debug-target-item:dir(rtl) > .debug-target-item__subname {
+ margin-right: calc(var(--base-unit) * 3);
+}
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.js b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.js
new file mode 100644
index 0000000000..6f19d7be02
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.js
@@ -0,0 +1,91 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+/**
+ * This component displays debug target.
+ */
+class DebugTargetItem extends PureComponent {
+ static get propTypes() {
+ return {
+ actionComponent: PropTypes.any.isRequired,
+ additionalActionsComponent: PropTypes.any,
+ detailComponent: PropTypes.any.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ renderAction() {
+ const { actionComponent, dispatch, target } = this.props;
+ return dom.div(
+ {
+ className: "debug-target-item__action",
+ },
+ actionComponent({ dispatch, target })
+ );
+ }
+
+ renderAdditionalActions() {
+ const { additionalActionsComponent, dispatch, target } = this.props;
+
+ if (!additionalActionsComponent) {
+ return null;
+ }
+
+ return dom.section(
+ {
+ className: "debug-target-item__additional_actions",
+ },
+ additionalActionsComponent({ dispatch, target })
+ );
+ }
+
+ renderDetail() {
+ const { detailComponent, target } = this.props;
+ return detailComponent({ target });
+ }
+
+ renderIcon() {
+ return dom.img({
+ className: "debug-target-item__icon qa-debug-target-item-icon",
+ src: this.props.target.icon,
+ });
+ }
+
+ renderName() {
+ return dom.span(
+ {
+ className: "debug-target-item__name ellipsis-text",
+ title: this.props.target.name,
+ },
+ this.props.target.name
+ );
+ }
+
+ render() {
+ return dom.li(
+ {
+ className: "card debug-target-item qa-debug-target-item",
+ "data-qa-target-type": this.props.target.type,
+ },
+ this.renderIcon(),
+ this.renderName(),
+ this.renderAction(),
+ this.renderDetail(),
+ this.renderAdditionalActions()
+ );
+ }
+}
+
+module.exports = DebugTargetItem;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.css b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.css
new file mode 100644
index 0000000000..827983e2bf
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.css
@@ -0,0 +1,7 @@
+/* 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/. */
+
+.debug-target-list {
+ margin-block-start: calc(var(--base-unit) * 4);
+}
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.js b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.js
new file mode 100644
index 0000000000..ce1e7ff12c
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.js
@@ -0,0 +1,80 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const DebugTargetItem = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.js")
+);
+
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+/**
+ * This component displays list of debug target.
+ */
+class DebugTargetList extends PureComponent {
+ static get propTypes() {
+ return {
+ actionComponent: PropTypes.any.isRequired,
+ additionalActionsComponent: PropTypes.any,
+ detailComponent: PropTypes.any.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ targets: PropTypes.arrayOf(Types.debugTarget).isRequired,
+ };
+ }
+
+ renderEmptyList() {
+ return Localized(
+ {
+ id: "about-debugging-debug-target-list-empty",
+ },
+ dom.p(
+ {
+ className: "qa-debug-target-list-empty",
+ },
+ "Nothing yet."
+ )
+ );
+ }
+
+ render() {
+ const {
+ actionComponent,
+ additionalActionsComponent,
+ detailComponent,
+ dispatch,
+ targets,
+ } = this.props;
+
+ return targets.length === 0
+ ? this.renderEmptyList()
+ : dom.ul(
+ {
+ className: "debug-target-list qa-debug-target-list",
+ },
+ targets.map((target, key) =>
+ DebugTargetItem({
+ actionComponent,
+ additionalActionsComponent,
+ detailComponent,
+ dispatch,
+ key,
+ target,
+ })
+ )
+ );
+ }
+}
+
+module.exports = DebugTargetList;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.css b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.css
new file mode 100644
index 0000000000..616b4ac28c
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.css
@@ -0,0 +1,43 @@
+/* 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/. */
+
+/*
+ * Style for the heading of a debug target pane
+ * +-----------------+---------------+-----------------+
+ * | [Category icon] | Category name | [Collapse icon] |
+ * +-----------------+---------------+-----------------+
+ */
+.debug-target-pane__heading {
+ grid-template-columns: var(--main-subheading-icon-size) max-content calc(var(--base-unit) * 3);
+ user-select: none;
+}
+
+.debug-target-pane__icon {
+ transition: transform 150ms cubic-bezier(.07, .95, 0, 1);
+ transform: rotate(90deg);
+}
+
+.debug-target-pane__icon--collapsed {
+ transform: rotate(0deg);
+}
+
+.debug-target-pane__icon--collapsed:dir(rtl) {
+ transform: rotate(180deg);
+}
+
+.debug-target-pane__title {
+ cursor: pointer;
+}
+
+.debug-target-pane__collapsable {
+ overflow: hidden;
+ /* padding will give space for card shadow to appear and
+ margin will correct the alignment */
+ margin-inline: calc(var(--card-shadow-blur-radius) * -1);
+ padding-inline: var(--card-shadow-blur-radius);
+}
+
+.debug-target-pane__collapsable--collapsed {
+ max-height: 0;
+}
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.js b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.js
new file mode 100644
index 0000000000..abfa1042b8
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.js
@@ -0,0 +1,147 @@
+/* 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 {
+ createFactory,
+ createRef,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+
+const DebugTargetList = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.js")
+);
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+/**
+ * This component provides list for debug target and name area.
+ */
+class DebugTargetPane extends PureComponent {
+ static get propTypes() {
+ return {
+ actionComponent: PropTypes.any.isRequired,
+ additionalActionsComponent: PropTypes.any,
+ children: PropTypes.node,
+ collapsibilityKey: PropTypes.string.isRequired,
+ detailComponent: PropTypes.any.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ // Provided by wrapping the component with FluentReact.withLocalization.
+ getString: PropTypes.func.isRequired,
+ icon: PropTypes.string.isRequired,
+ isCollapsed: PropTypes.bool.isRequired,
+ name: PropTypes.string.isRequired,
+ targets: PropTypes.arrayOf(Types.debugTarget).isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.collapsableRef = createRef();
+ }
+
+ componentDidUpdate(prevProps, prevState, snapshot) {
+ if (snapshot === null) {
+ return;
+ }
+
+ const el = this.collapsableRef.current;
+
+ // Cancel existing animation which is collapsing/expanding.
+ for (const animation of el.getAnimations()) {
+ animation.cancel();
+ }
+
+ el.animate(
+ { maxHeight: [`${snapshot}px`, `${el.clientHeight}px`] },
+ { duration: 150, easing: "cubic-bezier(.07, .95, 0, 1)" }
+ );
+ }
+
+ getSnapshotBeforeUpdate(prevProps) {
+ if (this.props.isCollapsed !== prevProps.isCollapsed) {
+ return this.collapsableRef.current.clientHeight;
+ }
+
+ return null;
+ }
+
+ toggleCollapsibility() {
+ const { collapsibilityKey, dispatch, isCollapsed } = this.props;
+ dispatch(
+ Actions.updateDebugTargetCollapsibility(collapsibilityKey, !isCollapsed)
+ );
+ }
+
+ render() {
+ const {
+ actionComponent,
+ additionalActionsComponent,
+ children,
+ detailComponent,
+ dispatch,
+ getString,
+ icon,
+ isCollapsed,
+ name,
+ targets,
+ } = this.props;
+
+ const title = getString("about-debugging-collapse-expand-debug-targets");
+
+ return dom.section(
+ {
+ className: "qa-debug-target-pane",
+ },
+ dom.a(
+ {
+ className:
+ "undecorated-link debug-target-pane__title " +
+ "qa-debug-target-pane-title",
+ title,
+ onClick: e => this.toggleCollapsibility(),
+ },
+ dom.h2(
+ { className: "main-subheading debug-target-pane__heading" },
+ dom.img({
+ className: "main-subheading__icon",
+ src: icon,
+ }),
+ `${name} (${targets.length})`,
+ dom.img({
+ className:
+ "main-subheading__icon debug-target-pane__icon" +
+ (isCollapsed ? " debug-target-pane__icon--collapsed" : ""),
+ src: "chrome://devtools/skin/images/arrow-e.svg",
+ })
+ )
+ ),
+ dom.div(
+ {
+ className:
+ "debug-target-pane__collapsable qa-debug-target-pane__collapsable" +
+ (isCollapsed ? " debug-target-pane__collapsable--collapsed" : ""),
+ ref: this.collapsableRef,
+ },
+ children,
+ DebugTargetList({
+ actionComponent,
+ additionalActionsComponent,
+ detailComponent,
+ dispatch,
+ isCollapsed,
+ targets,
+ })
+ )
+ );
+ }
+}
+
+module.exports = FluentReact.withLocalization(DebugTargetPane);
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.css b/devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.css
new file mode 100644
index 0000000000..6a4befa76a
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.css
@@ -0,0 +1,27 @@
+/* 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/. */
+
+.extension-backgroundscript {
+ display: flex;
+ column-gap: calc(var(--base-unit) * 2);
+}
+
+.extension-backgroundscript__status {
+ display: flex;
+ align-items: center;
+ float: inline-end;
+}
+
+.extension-backgroundscript__status::before {
+ background-color: var(--grey-50);
+ border-radius: 100%;
+ content: "";
+ height: calc(var(--base-unit) * 2);
+ margin-inline-end: var(--base-unit);
+ width: calc(var(--base-unit) * 2);
+}
+
+.extension-backgroundscript__status--running::before {
+ background-color: var(--success-background);
+}
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.js b/devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.js
new file mode 100644
index 0000000000..ef14483723
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.js
@@ -0,0 +1,243 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ getCurrentRuntimeDetails,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
+
+const DetailsLog = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/DetailsLog.js")
+);
+const FieldPair = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.js")
+);
+const Message = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/Message.js")
+);
+
+const {
+ EXTENSION_BGSCRIPT_STATUSES,
+ MESSAGE_LEVEL,
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+/**
+ * This component displays detail information for extension.
+ */
+class ExtensionDetail extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node,
+ // Provided by wrapping the component with FluentReact.withLocalization.
+ getString: PropTypes.func.isRequired,
+ // Provided by redux state
+ runtimeDetails: Types.runtimeDetails.isRequired,
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ renderWarnings() {
+ const { warnings } = this.props.target.details;
+
+ if (!warnings.length) {
+ return null;
+ }
+
+ return dom.section(
+ {
+ className: "debug-target-item__messages",
+ },
+ warnings.map((warning, index) => {
+ return Message(
+ {
+ level: MESSAGE_LEVEL.WARNING,
+ isCloseable: true,
+ key: `warning-${index}`,
+ },
+ DetailsLog(
+ {
+ type: MESSAGE_LEVEL.WARNING,
+ },
+ dom.p(
+ {
+ className: "technical-text",
+ },
+ warning
+ )
+ )
+ );
+ })
+ );
+ }
+
+ renderUUID() {
+ const { uuid } = this.props.target.details;
+ if (!uuid) {
+ return null;
+ }
+
+ return Localized(
+ {
+ id: "about-debugging-extension-uuid",
+ attrs: { label: true },
+ },
+ FieldPair({
+ label: "Internal UUID",
+ value: uuid,
+ })
+ );
+ }
+
+ renderExtensionId() {
+ const { id } = this.props.target;
+
+ return Localized(
+ {
+ id: "about-debugging-extension-id",
+ attrs: { label: true },
+ },
+ FieldPair({
+ label: "Extension ID",
+ value: id,
+ })
+ );
+ }
+
+ renderLocation() {
+ const { location } = this.props.target.details;
+ if (!location) {
+ return null;
+ }
+
+ return Localized(
+ {
+ id: "about-debugging-extension-location",
+ attrs: { label: true },
+ },
+ FieldPair({
+ label: "Location",
+ value: location,
+ })
+ );
+ }
+
+ renderManifest() {
+ // Manifest links are only relevant when debugging the current Firefox
+ // instance.
+ if (this.props.runtimeDetails.info.type !== RUNTIMES.THIS_FIREFOX) {
+ return null;
+ }
+
+ const { manifestURL } = this.props.target.details;
+ const link = dom.a(
+ {
+ className: "qa-manifest-url",
+ href: manifestURL,
+ target: "_blank",
+ },
+ manifestURL
+ );
+
+ return Localized(
+ {
+ id: "about-debugging-extension-manifest-url",
+ attrs: { label: true },
+ },
+ FieldPair({
+ label: "Manifest URL",
+ value: link,
+ })
+ );
+ }
+
+ renderBackgroundScriptStatus() {
+ // The status of the background script is only relevant if it is
+ // not persistent.
+ const { persistentBackgroundScript } = this.props.target.details;
+ if (!(persistentBackgroundScript === false)) {
+ return null;
+ }
+
+ const { backgroundScriptStatus } = this.props.target.details;
+
+ let status;
+ let statusLocalizationId;
+ let statusClassName;
+
+ if (backgroundScriptStatus === EXTENSION_BGSCRIPT_STATUSES.RUNNING) {
+ status = `extension-backgroundscript__status--running`;
+ statusLocalizationId = `about-debugging-extension-backgroundscript-status-running`;
+ statusClassName = `extension-backgroundscript__status--running`;
+ } else {
+ status = `extension-backgroundscript__status--stopped`;
+ statusLocalizationId = `about-debugging-extension-backgroundscript-status-stopped`;
+ statusClassName = `extension-backgroundscript__status--stopped`;
+ }
+
+ return Localized(
+ {
+ id: "about-debugging-extension-backgroundscript",
+ attrs: { label: true },
+ },
+ FieldPair({
+ label: "Background Script",
+ value: Localized(
+ {
+ id: statusLocalizationId,
+ },
+ dom.span(
+ {
+ className: `extension-backgroundscript__status qa-extension-backgroundscript-status ${statusClassName}`,
+ },
+ status
+ )
+ ),
+ })
+ );
+ }
+
+ render() {
+ return dom.section(
+ {
+ className: "debug-target-item__detail",
+ },
+ this.renderWarnings(),
+ dom.dl(
+ {},
+ this.renderLocation(),
+ this.renderExtensionId(),
+ this.renderUUID(),
+ this.renderManifest(),
+ this.renderBackgroundScriptStatus(),
+ this.props.children
+ )
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ runtimeDetails: getCurrentRuntimeDetails(state.runtimes),
+ };
+};
+
+module.exports = FluentReact.withLocalization(
+ connect(mapStateToProps)(ExtensionDetail)
+);
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.css b/devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.css
new file mode 100644
index 0000000000..a0b290d2a3
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.css
@@ -0,0 +1,29 @@
+/* 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/. */
+
+.fieldpair {
+ display: grid;
+ grid-template-columns: auto auto;
+ border-top: 1px solid var(--card-separator-color);
+ padding-block: calc(var(--base-unit) * 2);
+ padding-inline: calc(var(--base-unit) * 4) calc(var(--base-unit) * 2);
+}
+
+.fieldpair:last-child {
+ padding-block-end: 0;
+}
+
+.fieldpair__title {
+ margin-inline-end: var(--base-unit);
+ font-size: var(--caption-20-font-size);
+ font-weight: var(--caption-20-font-weight);
+}
+
+.fieldpair__description {
+ color: var(--fieldpair-text-color);
+ flex: 1;
+ font-size: var(--caption-20-font-size);
+ font-weight: var(--caption-20-font-weight);
+ text-align: end;
+}
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.js b/devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.js
new file mode 100644
index 0000000000..5e7ae53c65
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.js
@@ -0,0 +1,49 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+/* Renders a pair of `<dt>` (label) + `<dd>` (value) field. */
+class FieldPair extends PureComponent {
+ static get propTypes() {
+ return {
+ className: PropTypes.string,
+ label: PropTypes.node.isRequired,
+ value: PropTypes.node,
+ };
+ }
+
+ render() {
+ const { label, value } = this.props;
+ return dom.div(
+ {
+ className: "fieldpair",
+ },
+ dom.dt(
+ {
+ className:
+ "fieldpair__title " +
+ (this.props.className ? this.props.className : ""),
+ },
+ label
+ ),
+ value
+ ? dom.dd(
+ {
+ className: "fieldpair__description ellipsis-text",
+ },
+ value
+ )
+ : null
+ );
+ }
+}
+
+module.exports = FieldPair;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/InspectAction.js b/devtools/client/aboutdebugging/src/components/debugtarget/InspectAction.js
new file mode 100644
index 0000000000..f7aff438a4
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/InspectAction.js
@@ -0,0 +1,58 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+/**
+ * This component provides inspect button.
+ */
+class InspectAction extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ disabled: PropTypes.bool,
+ target: Types.debugTarget.isRequired,
+ title: PropTypes.string,
+ };
+ }
+
+ inspect() {
+ const { dispatch, target } = this.props;
+ dispatch(Actions.inspectDebugTarget(target.type, target.id));
+ }
+
+ render() {
+ const { disabled, title } = this.props;
+
+ return Localized(
+ {
+ id: "about-debugging-debug-target-inspect-button",
+ },
+ dom.button(
+ {
+ onClick: e => this.inspect(),
+ className: "default-button qa-debug-target-inspect-button",
+ disabled,
+ title,
+ },
+ "Inspect"
+ )
+ );
+ }
+}
+
+module.exports = InspectAction;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/ProcessDetail.js b/devtools/client/aboutdebugging/src/components/debugtarget/ProcessDetail.js
new file mode 100644
index 0000000000..a9973e90e3
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/ProcessDetail.js
@@ -0,0 +1,32 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+/**
+ * This component displays detail information for a process.
+ */
+class ProcessDetail extends PureComponent {
+ static get propTypes() {
+ return {
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ render() {
+ const { description } = this.props.target.details;
+ return dom.p(
+ { className: "debug-target-item__subname ellipsis-text" },
+ description
+ );
+ }
+}
+
+module.exports = ProcessDetail;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.css b/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.css
new file mode 100644
index 0000000000..c3ac2dc872
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.css
@@ -0,0 +1,26 @@
+/* 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/. */
+
+.service-worker-action {
+ display: flex;
+ column-gap: calc(var(--base-unit) * 2);
+}
+
+.service-worker-action__status {
+ display: flex;
+ align-items: center;
+}
+
+.service-worker-action__status::before {
+ background-color: var(--grey-50);
+ border-radius: 100%;
+ content: "";
+ height: calc(var(--base-unit) * 2);
+ margin-inline-end: var(--base-unit);
+ width: calc(var(--base-unit) * 2);
+}
+
+.service-worker-action__status--running::before {
+ background-color: var(--success-background);
+}
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.js b/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.js
new file mode 100644
index 0000000000..38aede94f4
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.js
@@ -0,0 +1,124 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ getCurrentRuntimeDetails,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
+
+const InspectAction = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/InspectAction.js")
+);
+
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+const {
+ SERVICE_WORKER_STATUSES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This component displays buttons for service worker.
+ */
+class ServiceWorkerAction extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ // Provided by redux state
+ runtimeDetails: Types.runtimeDetails.isRequired,
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ _renderInspectAction() {
+ const { status } = this.props.target.details;
+ const shallRenderInspectAction =
+ status === SERVICE_WORKER_STATUSES.RUNNING ||
+ status === SERVICE_WORKER_STATUSES.REGISTERING;
+
+ if (!shallRenderInspectAction) {
+ return null;
+ }
+
+ const { canDebugServiceWorkers } = this.props.runtimeDetails;
+ return Localized(
+ {
+ id: "about-debugging-worker-inspect-action-disabled",
+ attrs: {
+ // Show an explanatory title only if the action is disabled.
+ title: !canDebugServiceWorkers,
+ },
+ },
+ InspectAction({
+ disabled: !canDebugServiceWorkers,
+ dispatch: this.props.dispatch,
+ target: this.props.target,
+ })
+ );
+ }
+
+ _getStatusLocalizationId(status) {
+ switch (status) {
+ case SERVICE_WORKER_STATUSES.REGISTERING.toLowerCase():
+ return "about-debugging-worker-status-registering";
+ case SERVICE_WORKER_STATUSES.RUNNING.toLowerCase():
+ return "about-debugging-worker-status-running";
+ case SERVICE_WORKER_STATUSES.STOPPED.toLowerCase():
+ return "about-debugging-worker-status-stopped";
+ default:
+ // Assume status is stopped for unknown status value.
+ return "about-debugging-worker-status-stopped";
+ }
+ }
+
+ _renderStatus() {
+ const status = this.props.target.details.status.toLowerCase();
+ const statusClassName =
+ status === SERVICE_WORKER_STATUSES.RUNNING.toLowerCase()
+ ? "service-worker-action__status--running"
+ : "";
+
+ return Localized(
+ {
+ id: this._getStatusLocalizationId(status),
+ },
+ dom.span(
+ {
+ className: `service-worker-action__status qa-worker-status ${statusClassName}`,
+ },
+ status
+ )
+ );
+ }
+
+ render() {
+ return dom.div(
+ {
+ className: "service-worker-action",
+ },
+ this._renderStatus(),
+ this._renderInspectAction()
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ runtimeDetails: getCurrentRuntimeDetails(state.runtimes),
+ };
+};
+
+module.exports = connect(mapStateToProps)(ServiceWorkerAction);
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAdditionalActions.js b/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAdditionalActions.js
new file mode 100644
index 0000000000..38262ad511
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAdditionalActions.js
@@ -0,0 +1,176 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ getCurrentRuntimeDetails,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+const {
+ SERVICE_WORKER_STATUSES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * The main purpose of this component is to expose a meaningful prop
+ * disabledTitle that can be used with fluent localization.
+ */
+class _ActionButton extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node,
+ className: PropTypes.string.isRequired,
+ disabled: PropTypes.bool,
+ disabledTitle: PropTypes.string,
+ onClick: PropTypes.func.isRequired,
+ };
+ }
+
+ render() {
+ const { className, disabled, disabledTitle, onClick } = this.props;
+ return dom.button(
+ {
+ className,
+ disabled,
+ onClick: e => onClick(),
+ title: disabled && disabledTitle ? disabledTitle : undefined,
+ },
+ this.props.children
+ );
+ }
+}
+const ActionButtonFactory = createFactory(_ActionButton);
+
+/**
+ * This component displays buttons for service worker.
+ */
+class ServiceWorkerAdditionalActions extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ // Provided by wrapping the component with FluentReact.withLocalization.
+ getString: PropTypes.func.isRequired,
+ // Provided by redux state
+ runtimeDetails: Types.runtimeDetails.isRequired,
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ push() {
+ const { dispatch, target } = this.props;
+ dispatch(
+ Actions.pushServiceWorker(target.id, target.details.registrationFront)
+ );
+ }
+
+ start() {
+ const { dispatch, target } = this.props;
+ dispatch(Actions.startServiceWorker(target.details.registrationFront));
+ }
+
+ unregister() {
+ const { dispatch, target } = this.props;
+ dispatch(Actions.unregisterServiceWorker(target.details.registrationFront));
+ }
+
+ _renderButton({ className, disabled, key, labelId, onClick }) {
+ return Localized(
+ {
+ id: labelId,
+ attrs: {
+ disabledTitle: !!disabled,
+ },
+ key,
+ },
+ ActionButtonFactory(
+ {
+ className,
+ disabled,
+ onClick: e => onClick(),
+ },
+ labelId
+ )
+ );
+ }
+
+ _renderPushButton() {
+ return this._renderButton({
+ className: "default-button default-button--micro qa-push-button",
+ disabled: !this.props.runtimeDetails.canDebugServiceWorkers,
+ key: "service-worker-push-button",
+ labelId: "about-debugging-worker-action-push2",
+ onClick: this.push.bind(this),
+ });
+ }
+
+ _renderStartButton() {
+ return this._renderButton({
+ className: "default-button default-button--micro qa-start-button",
+ disabled: !this.props.runtimeDetails.canDebugServiceWorkers,
+ key: "service-worker-start-button",
+ labelId: "about-debugging-worker-action-start2",
+ onClick: this.start.bind(this),
+ });
+ }
+
+ _renderUnregisterButton() {
+ return this._renderButton({
+ className: "default-button default-button--micro qa-unregister-button",
+ key: "service-worker-unregister-button",
+ labelId: "about-debugging-worker-action-unregister",
+ disabled: false,
+ onClick: this.unregister.bind(this),
+ });
+ }
+
+ _renderActionButtons() {
+ const { status } = this.props.target.details;
+
+ switch (status) {
+ case SERVICE_WORKER_STATUSES.RUNNING:
+ return [this._renderUnregisterButton(), this._renderPushButton()];
+ case SERVICE_WORKER_STATUSES.REGISTERING:
+ return null;
+ case SERVICE_WORKER_STATUSES.STOPPED:
+ return [this._renderUnregisterButton(), this._renderStartButton()];
+ default:
+ console.error("Unexpected service worker status: " + status);
+ return null;
+ }
+ }
+
+ render() {
+ return dom.div(
+ {
+ className: "toolbar toolbar--right-align",
+ },
+ this._renderActionButtons()
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ runtimeDetails: getCurrentRuntimeDetails(state.runtimes),
+ };
+};
+
+module.exports = FluentReact.withLocalization(
+ connect(mapStateToProps)(ServiceWorkerAdditionalActions)
+);
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/TabAction.js b/devtools/client/aboutdebugging/src/components/debugtarget/TabAction.js
new file mode 100644
index 0000000000..132f1e5f44
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/TabAction.js
@@ -0,0 +1,52 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const InspectAction = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/InspectAction.js")
+);
+
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+/**
+ * This component displays the inspect button for tabs.
+ */
+class TabAction extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ render() {
+ const isZombieTab = this.props.target.details.isZombieTab;
+ return Localized(
+ {
+ id: "about-debugging-zombie-tab-inspect-action-disabled",
+ attrs: {
+ // Show an explanatory title only if the action is disabled.
+ title: isZombieTab,
+ },
+ },
+ InspectAction({
+ disabled: isZombieTab,
+ dispatch: this.props.dispatch,
+ target: this.props.target,
+ })
+ );
+ }
+}
+
+module.exports = FluentReact.withLocalization(TabAction);
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/TabDetail.js b/devtools/client/aboutdebugging/src/components/debugtarget/TabDetail.js
new file mode 100644
index 0000000000..dcbd3f0a4d
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/TabDetail.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+/**
+ * This component displays detail information for tab.
+ */
+class TabDetail extends PureComponent {
+ static get propTypes() {
+ return {
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ render() {
+ return dom.div(
+ {
+ className: "debug-target-item__subname ellipsis-text",
+ dir: "ltr",
+ },
+ this.props.target.details.url
+ );
+ }
+}
+
+module.exports = TabDetail;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionAdditionalActions.js b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionAdditionalActions.js
new file mode 100644
index 0000000000..44b7d3e167
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionAdditionalActions.js
@@ -0,0 +1,182 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+const DetailsLog = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/DetailsLog.js")
+);
+const Message = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/Message.js")
+);
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This component provides components that reload/remove temporary extension.
+ */
+class TemporaryExtensionAdditionalActions extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ reload() {
+ const { dispatch, target } = this.props;
+ dispatch(Actions.reloadTemporaryExtension(target.id));
+ }
+
+ terminateBackgroundScript() {
+ const { dispatch, target } = this.props;
+ dispatch(Actions.terminateExtensionBackgroundScript(target.id));
+ }
+
+ remove() {
+ const { dispatch, target } = this.props;
+ dispatch(Actions.removeTemporaryExtension(target.id));
+ }
+
+ renderReloadError() {
+ const { reloadError } = this.props.target.details;
+
+ if (!reloadError) {
+ return null;
+ }
+
+ return Message(
+ {
+ className: "qa-temporary-extension-reload-error",
+ level: MESSAGE_LEVEL.ERROR,
+ key: "reload-error",
+ },
+ DetailsLog(
+ {
+ type: MESSAGE_LEVEL.ERROR,
+ },
+ dom.p(
+ {
+ className: "technical-text",
+ },
+ reloadError
+ )
+ )
+ );
+ }
+
+ renderTerminateBackgroundScriptError() {
+ const { lastTerminateBackgroundScriptError } = this.props.target.details;
+
+ if (!lastTerminateBackgroundScriptError) {
+ return null;
+ }
+
+ return Message(
+ {
+ className: "qa-temporary-extension-terminate-backgroundscript-error",
+ level: MESSAGE_LEVEL.ERROR,
+ key: "terminate-backgroundscript-error",
+ },
+ DetailsLog(
+ {
+ type: MESSAGE_LEVEL.ERROR,
+ },
+ dom.p(
+ {
+ className: "technical-text",
+ },
+ lastTerminateBackgroundScriptError
+ )
+ )
+ );
+ }
+
+ renderTerminateBackgroundScriptButton() {
+ const { persistentBackgroundScript } = this.props.target.details;
+
+ // For extensions with a non persistent background script
+ // also include a "terminate background script" action.
+ if (persistentBackgroundScript !== false) {
+ return null;
+ }
+
+ return Localized(
+ {
+ id: "about-debugging-tmp-extension-terminate-bgscript-button",
+ },
+ dom.button(
+ {
+ className:
+ "default-button default-button--micro " +
+ "qa-temporary-extension-terminate-bgscript-button",
+ onClick: e => this.terminateBackgroundScript(),
+ },
+ "Terminate Background Script"
+ )
+ );
+ }
+
+ renderRemoveButton() {
+ return Localized(
+ {
+ id: "about-debugging-tmp-extension-remove-button",
+ },
+ dom.button(
+ {
+ className:
+ "default-button default-button--micro " +
+ "qa-temporary-extension-remove-button",
+ onClick: e => this.remove(),
+ },
+ "Remove"
+ )
+ );
+ }
+
+ render() {
+ return [
+ dom.div(
+ {
+ className: "toolbar toolbar--right-align",
+ key: "actions",
+ },
+ this.renderTerminateBackgroundScriptButton(),
+ Localized(
+ {
+ id: "about-debugging-tmp-extension-reload-button",
+ },
+ dom.button(
+ {
+ className:
+ "default-button default-button--micro " +
+ "qa-temporary-extension-reload-button",
+ onClick: e => this.reload(),
+ },
+ "Reload"
+ )
+ ),
+ this.renderRemoveButton()
+ ),
+ this.renderReloadError(),
+ this.renderTerminateBackgroundScriptError(),
+ ];
+ }
+}
+
+module.exports = TemporaryExtensionAdditionalActions;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionDetail.js b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionDetail.js
new file mode 100644
index 0000000000..6c589472d6
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionDetail.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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const ExtensionDetail = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.js")
+);
+const FieldPair = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.js")
+);
+
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+const TEMP_ID_DOC_URL =
+ "https://developer.mozilla.org/Add-ons/WebExtensions/WebExtensions_and_the_Add-on_ID";
+
+/**
+ * This component displays detail information for a temporary extension.
+ */
+class TemporaryExtensionDetail extends PureComponent {
+ static get propTypes() {
+ return {
+ // Provided by wrapping the component with FluentReact.withLocalization.
+ getString: PropTypes.func.isRequired,
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ renderTemporaryIdMessage() {
+ return Localized(
+ {
+ id: "about-debugging-tmp-extension-temporary-id",
+ a: dom.a({
+ className: "qa-temporary-id-link",
+ href: TEMP_ID_DOC_URL,
+ target: "_blank",
+ }),
+ },
+ dom.div({
+ className: "qa-temporary-id-message",
+ })
+ );
+ }
+
+ render() {
+ return ExtensionDetail(
+ {
+ target: this.props.target,
+ },
+ FieldPair({ label: this.renderTemporaryIdMessage() })
+ );
+ }
+}
+
+module.exports = FluentReact.withLocalization(TemporaryExtensionDetail);
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.css b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.css
new file mode 100644
index 0000000000..9166a3b615
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.css
@@ -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/. */
+
+.temporary-extension-install-section__toolbar {
+ display: flex;
+ justify-content: end;
+}
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.js b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.js
new file mode 100644
index 0000000000..f85618f73d
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.js
@@ -0,0 +1,101 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const DetailsLog = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/DetailsLog.js")
+);
+const Message = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/Message.js")
+);
+const TemporaryExtensionInstaller = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstaller.js")
+);
+
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This component provides an installer and error message area for temporary extension.
+ */
+class TemporaryExtensionInstallSection extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ temporaryInstallError: PropTypes.object,
+ };
+ }
+
+ renderError() {
+ const { temporaryInstallError } = this.props;
+
+ if (!temporaryInstallError) {
+ return null;
+ }
+
+ const errorMessages = [
+ temporaryInstallError.message,
+ ...(temporaryInstallError.additionalErrors || []),
+ ];
+
+ const errors = errorMessages.map((message, index) => {
+ return dom.p(
+ {
+ className: "technical-text",
+ key: "tmp-extension-install-error-" + index,
+ },
+ message
+ );
+ });
+
+ return Message(
+ {
+ level: MESSAGE_LEVEL.ERROR,
+ className: "qa-tmp-extension-install-error",
+ isCloseable: true,
+ },
+ Localized(
+ {
+ id: "about-debugging-tmp-extension-install-error",
+ },
+ dom.p({}, "about-debugging-tmp-extension-install-error")
+ ),
+ DetailsLog(
+ {
+ type: MESSAGE_LEVEL.ERROR,
+ },
+ errors
+ )
+ );
+ }
+
+ render() {
+ const { dispatch } = this.props;
+
+ return dom.section(
+ {},
+ dom.div(
+ {
+ className: "temporary-extension-install-section__toolbar",
+ },
+ TemporaryExtensionInstaller({ dispatch })
+ ),
+ this.renderError()
+ );
+ }
+}
+
+module.exports = TemporaryExtensionInstallSection;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstaller.js b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstaller.js
new file mode 100644
index 0000000000..e515c647ec
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstaller.js
@@ -0,0 +1,52 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+
+/**
+ * This component provides an installer for temporary extension.
+ */
+class TemporaryExtensionInstaller extends PureComponent {
+ static get propTypes() {
+ return {
+ className: PropTypes.string,
+ dispatch: PropTypes.func.isRequired,
+ };
+ }
+
+ install() {
+ this.props.dispatch(Actions.installTemporaryExtension());
+ }
+
+ render() {
+ const { className } = this.props;
+
+ return Localized(
+ {
+ id: "about-debugging-tmp-extension-install-button",
+ },
+ dom.button(
+ {
+ className: `${className} default-button qa-temporary-extension-install-button`,
+ onClick: e => this.install(),
+ },
+ "Load Temporary Add-on…"
+ )
+ );
+ }
+}
+
+module.exports = TemporaryExtensionInstaller;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/WorkerDetail.js b/devtools/client/aboutdebugging/src/components/debugtarget/WorkerDetail.js
new file mode 100644
index 0000000000..b69252c430
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/WorkerDetail.js
@@ -0,0 +1,120 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ SERVICE_WORKER_FETCH_STATES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const FieldPair = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.js")
+);
+
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+/**
+ * This component displays detail information for worker.
+ */
+class WorkerDetail extends PureComponent {
+ static get propTypes() {
+ return {
+ // Provided by wrapping the component with FluentReact.withLocalization.
+ getString: PropTypes.func.isRequired,
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ renderFetch() {
+ const { fetch } = this.props.target.details;
+ const isListening = fetch === SERVICE_WORKER_FETCH_STATES.LISTENING;
+ const localizationId = isListening
+ ? "about-debugging-worker-fetch-listening"
+ : "about-debugging-worker-fetch-not-listening";
+
+ return Localized(
+ {
+ id: localizationId,
+ attrs: {
+ label: true,
+ value: true,
+ },
+ },
+ FieldPair({
+ className: isListening
+ ? "qa-worker-fetch-listening"
+ : "qa-worker-fetch-not-listening",
+ label: "Fetch",
+ slug: "fetch",
+ value: "about-debugging-worker-fetch-value",
+ })
+ );
+ }
+
+ renderPushService() {
+ const { pushServiceEndpoint } = this.props.target.details;
+
+ return Localized(
+ {
+ id: "about-debugging-worker-push-service",
+ attrs: { label: true },
+ },
+ FieldPair({
+ slug: "push-service",
+ label: "Push Service",
+ value: dom.span(
+ {
+ className: "qa-worker-push-service-value",
+ },
+ pushServiceEndpoint
+ ),
+ })
+ );
+ }
+
+ renderScope() {
+ const { scope } = this.props.target.details;
+
+ return Localized(
+ {
+ id: "about-debugging-worker-scope",
+ attrs: { label: true },
+ },
+ FieldPair({
+ slug: "scope",
+ label: "Scope",
+ value: scope,
+ })
+ );
+ }
+
+ render() {
+ const { fetch, pushServiceEndpoint, scope } = this.props.target.details;
+
+ const isEmptyList = !pushServiceEndpoint && !fetch && !scope && !status;
+
+ return dom.dl(
+ {
+ className:
+ "debug-target-item__detail" +
+ (isEmptyList ? " debug-target-item__detail--empty" : ""),
+ },
+ pushServiceEndpoint ? this.renderPushService() : null,
+ fetch ? this.renderFetch() : null,
+ scope ? this.renderScope() : null
+ );
+ }
+}
+
+module.exports = FluentReact.withLocalization(WorkerDetail);
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/moz.build b/devtools/client/aboutdebugging/src/components/debugtarget/moz.build
new file mode 100644
index 0000000000..981e6887a2
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/moz.build
@@ -0,0 +1,22 @@
+# 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(
+ "DebugTargetItem.js",
+ "DebugTargetList.js",
+ "DebugTargetPane.js",
+ "ExtensionDetail.js",
+ "FieldPair.js",
+ "InspectAction.js",
+ "ProcessDetail.js",
+ "ServiceWorkerAction.js",
+ "ServiceWorkerAdditionalActions.js",
+ "TabAction.js",
+ "TabDetail.js",
+ "TemporaryExtensionAdditionalActions.js",
+ "TemporaryExtensionDetail.js",
+ "TemporaryExtensionInstaller.js",
+ "TemporaryExtensionInstallSection.js",
+ "WorkerDetail.js",
+)
diff --git a/devtools/client/aboutdebugging/src/components/moz.build b/devtools/client/aboutdebugging/src/components/moz.build
new file mode 100644
index 0000000000..c48d384b3d
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/moz.build
@@ -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/.
+
+DIRS += [
+ "connect",
+ "debugtarget",
+ "shared",
+ "sidebar",
+]
+
+DevToolsModules(
+ "App.js",
+ "CompatibilityWarning.js",
+ "ConnectionPromptSetting.js",
+ "ProfilerDialog.js",
+ "RuntimeActions.js",
+ "RuntimeInfo.js",
+ "RuntimePage.js",
+ "ServiceWorkersWarning.js",
+)
diff --git a/devtools/client/aboutdebugging/src/components/shared/DetailsLog.js b/devtools/client/aboutdebugging/src/components/shared/DetailsLog.js
new file mode 100644
index 0000000000..f10c8a487d
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/shared/DetailsLog.js
@@ -0,0 +1,64 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This component is designed to wrap a warning / error log message
+ * in the details tag to hide long texts and make the message expendable
+ * out of the box.
+ */
+class DetailsLog extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node.isRequired,
+ type: PropTypes.string,
+ };
+ }
+ getLocalizationString() {
+ const { type } = this.props;
+
+ switch (type) {
+ case MESSAGE_LEVEL.WARNING:
+ return "about-debugging-message-details-label-warning";
+ case MESSAGE_LEVEL.ERROR:
+ return "about-debugging-message-details-label-error";
+ default:
+ return "about-debugging-message-details-label";
+ }
+ }
+
+ render() {
+ const { children } = this.props;
+
+ return dom.details(
+ {
+ className: "details--log",
+ },
+ Localized(
+ {
+ id: this.getLocalizationString(),
+ },
+ dom.summary({}, this.getLocalizationString())
+ ),
+ children
+ );
+ }
+}
+
+module.exports = DetailsLog;
diff --git a/devtools/client/aboutdebugging/src/components/shared/IconLabel.css b/devtools/client/aboutdebugging/src/components/shared/IconLabel.css
new file mode 100644
index 0000000000..ddb7c3929a
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/shared/IconLabel.css
@@ -0,0 +1,27 @@
+/* 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/. */
+
+.icon-label {
+ display: flex;
+ column-gap: var(--base-unit);
+ align-items: center;
+ margin: calc(var(--base-unit) * 2) 0;
+ -moz-context-properties: fill;
+
+ font-size: var(--icon-label-font-size);
+}
+
+.icon-label--ok {
+ --icon-color: var(--icon-ok-color);
+}
+.icon-label--info {
+ --icon-color: var(--icon-info-color);
+}
+
+.icon-label__icon {
+ padding: var(--base-unit);
+ fill: var(--icon-color);
+ width: calc(var(--base-unit) * 4);
+ height: calc(var(--base-unit) * 4);
+}
diff --git a/devtools/client/aboutdebugging/src/components/shared/IconLabel.js b/devtools/client/aboutdebugging/src/components/shared/IconLabel.js
new file mode 100644
index 0000000000..3141e75640
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/shared/IconLabel.js
@@ -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/. */
+
+"use strict";
+
+const {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const {
+ ICON_LABEL_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const ICONS = {
+ [ICON_LABEL_LEVEL.INFO]: "chrome://devtools/skin/images/info.svg",
+ [ICON_LABEL_LEVEL.OK]: "chrome://devtools/skin/images/check.svg",
+};
+
+/* This component displays an icon accompanied by some content. It's similar
+ to a message, but it's not interactive */
+class IconLabel extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node.isRequired,
+ className: PropTypes.string,
+ level: PropTypes.oneOf(Object.values(ICON_LABEL_LEVEL)).isRequired,
+ };
+ }
+
+ render() {
+ const { children, className, level } = this.props;
+ return dom.span(
+ {
+ className: `icon-label icon-label--${level} ${className || ""}`,
+ },
+ dom.img({
+ className: "icon-label__icon",
+ src: ICONS[level],
+ }),
+ children
+ );
+ }
+}
+
+module.exports = IconLabel;
diff --git a/devtools/client/aboutdebugging/src/components/shared/Message.css b/devtools/client/aboutdebugging/src/components/shared/Message.css
new file mode 100644
index 0000000000..e2c71c50b4
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/shared/Message.css
@@ -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/. */
+
+.message--level-error {
+ --message-background-color: var(--error-background);
+ --message-border-color: var(--error-border);
+ --message-color: var(--error-text);
+ --message-icon-color: var(--error-icon);
+}
+
+.message--level-info {
+ --message-background-color: var(--grey-20);
+ --message-border-color: transparent;
+ --message-color: var(--grey-90);
+ --message-icon-color: var(--grey-90);
+}
+
+.message--level-warning {
+ --message-background-color: var(--warning-background);
+ --message-border-color: var(--warning-border);
+ --message-color: var(--warning-text);
+ --message-icon-color: var(--warning-icon);
+}
+
+/*
+ * Layout of the message
+ *
+ * +--------+-----------------+----------+
+ * | Icon | Message content | closing |
+ * | | (several lines) | button |
+ * | | ( ... ) |(optional)|
+ * +--------+-----------------+----------+
+ */
+.message {
+ background-color: var(--message-background-color);
+ border: 1px solid var(--message-border-color);
+ border-radius: var(--base-unit);
+ color: var(--message-color);
+ display: grid;
+ grid-column-gap: var(--base-unit);
+ grid-template-columns: calc(var(--base-unit) * 6) 1fr auto;
+ grid-template-areas:
+ "icon body button";
+ margin: calc(var(--base-unit) * 2) 0;
+ padding: var(--base-unit);
+ -moz-context-properties: fill;
+}
+
+.message__icon {
+ margin: var(--base-unit);
+ fill: var(--message-icon-color);
+ grid-area: icon;
+}
+
+.message__body {
+ align-self: center;
+ font-size: var(--message-font-size);
+ font-weight: 400;
+ grid-area: body;
+ line-height: 1.6;
+}
+
+.message__button {
+ grid-area: button;
+ fill: var(--message-icon-color);
+}
+
+.message__button:hover {
+ /* reverse colors with icon when hover state */
+ background-color: var(--message-icon-color);
+ fill: var(--message-background-color);
+}
+
+.message__button:active {
+ /* reverse colors with text when active state */
+ background-color: var(--message-color);
+ fill: var(--message-background-color);
+}
diff --git a/devtools/client/aboutdebugging/src/components/shared/Message.js b/devtools/client/aboutdebugging/src/components/shared/Message.js
new file mode 100644
index 0000000000..248db58dd3
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/shared/Message.js
@@ -0,0 +1,109 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const ICONS = {
+ [MESSAGE_LEVEL.ERROR]:
+ "chrome://devtools/skin/images/aboutdebugging-error.svg",
+ [MESSAGE_LEVEL.INFO]:
+ "chrome://devtools/skin/images/aboutdebugging-information.svg",
+ [MESSAGE_LEVEL.WARNING]: "chrome://devtools/skin/images/alert.svg",
+};
+const CLOSE_ICON_SRC = "chrome://devtools/skin/images/close.svg";
+
+/**
+ * This component is designed to display a photon-style message bar. The component is
+ * responsible for displaying the message container with the appropriate icon. The content
+ * of the message should be passed as the children of this component.
+ */
+class Message extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node.isRequired,
+ className: PropTypes.string,
+ isCloseable: PropTypes.bool,
+ level: PropTypes.oneOf(Object.values(MESSAGE_LEVEL)).isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isClosed: false,
+ };
+ }
+
+ closeMessage() {
+ this.setState({ isClosed: true });
+ }
+
+ renderButton(level) {
+ return dom.button(
+ {
+ className:
+ `ghost-button message__button message__button--${level} ` +
+ `qa-message-button-close-button`,
+ onClick: () => this.closeMessage(),
+ },
+ Localized(
+ {
+ id: "about-debugging-message-close-icon",
+ attrs: {
+ alt: true,
+ },
+ },
+ dom.img({
+ className: "qa-message-button-close-icon",
+ src: CLOSE_ICON_SRC,
+ })
+ )
+ );
+ }
+
+ render() {
+ const { children, className, level, isCloseable } = this.props;
+ const { isClosed } = this.state;
+
+ if (isClosed) {
+ return null;
+ }
+
+ return dom.aside(
+ {
+ className:
+ `message message--level-${level} qa-message` +
+ (className ? ` ${className}` : ""),
+ },
+ dom.img({
+ className: "message__icon",
+ src: ICONS[level],
+ }),
+ dom.div(
+ {
+ className: "message__body",
+ },
+ children
+ ),
+ // if the message is closeable, render a closing button
+ isCloseable ? this.renderButton(level) : null
+ );
+ }
+}
+
+module.exports = Message;
diff --git a/devtools/client/aboutdebugging/src/components/shared/moz.build b/devtools/client/aboutdebugging/src/components/shared/moz.build
new file mode 100644
index 0000000000..7e0e89f2a0
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/shared/moz.build
@@ -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/.
+
+DevToolsModules(
+ "DetailsLog.js",
+ "IconLabel.js",
+ "Message.js",
+)
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/RefreshDevicesButton.js b/devtools/client/aboutdebugging/src/components/sidebar/RefreshDevicesButton.js
new file mode 100644
index 0000000000..78ebb61661
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/RefreshDevicesButton.js
@@ -0,0 +1,46 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+
+class RefreshDevicesButton extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ isScanning: PropTypes.bool.isRequired,
+ };
+ }
+
+ refreshDevices() {
+ this.props.dispatch(Actions.scanUSBRuntimes());
+ }
+
+ render() {
+ return Localized(
+ { id: "about-debugging-refresh-usb-devices-button" },
+ dom.button(
+ {
+ className: "default-button qa-refresh-devices-button",
+ disabled: this.props.isScanning,
+ onClick: () => this.refreshDevices(),
+ },
+ "Refresh devices"
+ )
+ );
+ }
+}
+
+module.exports = RefreshDevicesButton;
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/Sidebar.css b/devtools/client/aboutdebugging/src/components/sidebar/Sidebar.css
new file mode 100644
index 0000000000..afad630f6e
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/Sidebar.css
@@ -0,0 +1,43 @@
+/* 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/. */
+
+.sidebar {
+ display: grid;
+ grid-template-rows: auto auto;
+}
+
+.sidebar__label {
+ color: var(--secondary-text-color);
+ display: block;
+ padding: 12px 0;
+ text-align: center;
+ font-size: var(--message-font-size);
+}
+
+.sidebar__adb-status {
+ margin-block-end: calc(var(--base-unit) * 2);
+}
+
+.sidebar__refresh-usb {
+ text-align: center;
+}
+
+.sidebar__footer {
+ align-self: flex-end;
+}
+
+.sidebar__footer__support-help {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ column-gap: calc(var(--base-unit) * 4);
+ height: 100%;
+}
+
+.sidebar__footer__icon {
+ width: calc(var(--base-unit) * 4);
+ height: calc(var(--base-unit) * 4);
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/Sidebar.js b/devtools/client/aboutdebugging/src/components/sidebar/Sidebar.js
new file mode 100644
index 0000000000..3213b8141c
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/Sidebar.js
@@ -0,0 +1,259 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ ICON_LABEL_LEVEL,
+ PAGE_TYPES,
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+loader.lazyRequireGetter(
+ this,
+ "ADB_ADDON_STATES",
+ "resource://devtools/client/shared/remote-debugging/adb/adb-addon.js",
+ true
+);
+
+const IconLabel = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/IconLabel.js")
+);
+const SidebarItem = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.js")
+);
+const SidebarFixedItem = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.js")
+);
+const SidebarRuntimeItem = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.js")
+);
+const RefreshDevicesButton = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/sidebar/RefreshDevicesButton.js")
+);
+const FIREFOX_ICON =
+ "chrome://devtools/skin/images/aboutdebugging-firefox-logo.svg";
+const CONNECT_ICON = "chrome://devtools/skin/images/settings.svg";
+const GLOBE_ICON =
+ "chrome://devtools/skin/images/aboutdebugging-globe-icon.svg";
+const USB_ICON =
+ "chrome://devtools/skin/images/aboutdebugging-connect-icon.svg";
+
+class Sidebar extends PureComponent {
+ static get propTypes() {
+ return {
+ adbAddonStatus: Types.adbAddonStatus,
+ className: PropTypes.string,
+ dispatch: PropTypes.func.isRequired,
+ isAdbReady: PropTypes.bool.isRequired,
+ isScanningUsb: PropTypes.bool.isRequired,
+ networkRuntimes: PropTypes.arrayOf(Types.runtime).isRequired,
+ selectedPage: Types.page,
+ selectedRuntimeId: PropTypes.string,
+ usbRuntimes: PropTypes.arrayOf(Types.runtime).isRequired,
+ };
+ }
+
+ renderAdbStatus() {
+ const isUsbEnabled =
+ this.props.isAdbReady &&
+ this.props.adbAddonStatus === ADB_ADDON_STATES.INSTALLED;
+ const localizationId = isUsbEnabled
+ ? "about-debugging-sidebar-usb-enabled"
+ : "about-debugging-sidebar-usb-disabled";
+ return IconLabel(
+ {
+ level: isUsbEnabled ? ICON_LABEL_LEVEL.OK : ICON_LABEL_LEVEL.INFO,
+ },
+ Localized(
+ {
+ id: localizationId,
+ },
+ dom.span(
+ {
+ className: "qa-sidebar-usb-status",
+ },
+ localizationId
+ )
+ )
+ );
+ }
+
+ renderDevicesEmpty() {
+ return SidebarItem(
+ {},
+ Localized(
+ {
+ id: "about-debugging-sidebar-no-devices",
+ },
+ dom.aside(
+ {
+ className: "sidebar__label qa-sidebar-no-devices",
+ },
+ "No devices discovered"
+ )
+ )
+ );
+ }
+
+ renderDevices() {
+ const { networkRuntimes, usbRuntimes } = this.props;
+
+ // render a "no devices" messages when the lists are empty
+ if (!networkRuntimes.length && !usbRuntimes.length) {
+ return this.renderDevicesEmpty();
+ }
+ // render all devices otherwise
+ return [
+ ...this.renderRuntimeItems(GLOBE_ICON, networkRuntimes),
+ ...this.renderRuntimeItems(USB_ICON, usbRuntimes),
+ ];
+ }
+
+ renderRuntimeItems(icon, runtimes) {
+ const { dispatch, selectedPage, selectedRuntimeId } = this.props;
+
+ return runtimes.map(runtime => {
+ const keyId = `${runtime.type}-${runtime.id}`;
+ const runtimeHasDetails = !!runtime.runtimeDetails;
+ const isSelected =
+ selectedPage === PAGE_TYPES.RUNTIME && runtime.id === selectedRuntimeId;
+
+ let name = runtime.name;
+ if (runtime.type === RUNTIMES.USB && runtimeHasDetails) {
+ // Update the name to be same to the runtime page.
+ name = runtime.runtimeDetails.info.name;
+ }
+
+ return SidebarRuntimeItem({
+ deviceName: runtime.extra.deviceName,
+ dispatch,
+ icon,
+ key: keyId,
+ isConnected: runtimeHasDetails,
+ isConnecting: runtime.isConnecting,
+ isConnectionFailed: runtime.isConnectionFailed,
+ isConnectionNotResponding: runtime.isConnectionNotResponding,
+ isConnectionTimeout: runtime.isConnectionTimeout,
+ isSelected,
+ isUnavailable: runtime.isUnavailable,
+ isUnplugged: runtime.isUnplugged,
+ name,
+ runtimeId: runtime.id,
+ });
+ });
+ }
+
+ renderFooter() {
+ const HELP_ICON_SRC = "chrome://global/skin/icons/help.svg";
+ const SUPPORT_URL =
+ "https://firefox-source-docs.mozilla.org/devtools-user/about_colon_debugging/";
+
+ return dom.footer(
+ {
+ className: "sidebar__footer",
+ },
+ dom.ul(
+ {},
+ SidebarItem(
+ {
+ className: "sidebar-item--condensed",
+ to: SUPPORT_URL,
+ },
+ dom.span(
+ {
+ className: "sidebar__footer__support-help",
+ },
+ Localized(
+ {
+ id: "about-debugging-sidebar-support-icon",
+ attrs: {
+ alt: true,
+ },
+ },
+ dom.img({
+ className: "sidebar__footer__icon",
+ src: HELP_ICON_SRC,
+ })
+ ),
+ Localized(
+ {
+ id: "about-debugging-sidebar-support",
+ },
+ dom.span({}, "about-debugging-sidebar-support")
+ )
+ )
+ )
+ )
+ );
+ }
+
+ render() {
+ const { dispatch, selectedPage, selectedRuntimeId, isScanningUsb } =
+ this.props;
+
+ return dom.aside(
+ {
+ className: `sidebar ${this.props.className || ""}`,
+ },
+ dom.ul(
+ {},
+ Localized(
+ { id: "about-debugging-sidebar-setup", attrs: { name: true } },
+ SidebarFixedItem({
+ dispatch,
+ icon: CONNECT_ICON,
+ isSelected: PAGE_TYPES.CONNECT === selectedPage,
+ key: PAGE_TYPES.CONNECT,
+ name: "Setup",
+ to: "/setup",
+ })
+ ),
+ Localized(
+ { id: "about-debugging-sidebar-this-firefox", attrs: { name: true } },
+ SidebarFixedItem({
+ icon: FIREFOX_ICON,
+ isSelected:
+ PAGE_TYPES.RUNTIME === selectedPage &&
+ selectedRuntimeId === RUNTIMES.THIS_FIREFOX,
+ key: RUNTIMES.THIS_FIREFOX,
+ name: "This Firefox",
+ to: `/runtime/${RUNTIMES.THIS_FIREFOX}`,
+ })
+ ),
+ SidebarItem(
+ {
+ className: "sidebar__adb-status",
+ },
+ dom.hr({ className: "separator separator--breathe" }),
+ this.renderAdbStatus()
+ ),
+ this.renderDevices(),
+ SidebarItem(
+ {
+ className: "sidebar-item--breathe sidebar__refresh-usb",
+ key: "refresh-devices",
+ },
+ RefreshDevicesButton({
+ dispatch,
+ isScanning: isScanningUsb,
+ })
+ )
+ ),
+ this.renderFooter()
+ );
+ }
+}
+
+module.exports = Sidebar;
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.css b/devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.css
new file mode 100644
index 0000000000..7345b8e80f
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.css
@@ -0,0 +1,29 @@
+/* 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/. */
+
+/*
+ * Layout of a fixed sidebar item
+ *
+ * +--------+----------------+
+ * | Icon | Name |
+ * +--------+----------------+
+ */
+
+.sidebar-fixed-item__container {
+ align-items: center;
+ border-radius: 2px;
+ display: grid;
+ grid-template-columns: 34px 1fr;
+ height: 100%;
+ font-size: var(--body-20-font-size);
+ font-weight: var(--body-20-font-weight);
+}
+
+.sidebar-fixed-item__icon {
+ fill: currentColor;
+ height: 24px;
+ margin-inline-end: 9px;
+ width: 24px;
+ -moz-context-properties: fill;
+}
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.js b/devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.js
new file mode 100644
index 0000000000..cf5dbab31a
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.js
@@ -0,0 +1,60 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const SidebarItem = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.js")
+);
+
+/**
+ * This component displays a fixed item in the Sidebar component.
+ */
+class SidebarFixedItem extends PureComponent {
+ static get propTypes() {
+ return {
+ icon: PropTypes.string.isRequired,
+ isSelected: PropTypes.bool.isRequired,
+ name: PropTypes.string.isRequired,
+ to: PropTypes.string,
+ };
+ }
+
+ render() {
+ const { icon, isSelected, name, to } = this.props;
+
+ return SidebarItem(
+ {
+ className: "sidebar-item--tall",
+ isSelected,
+ to,
+ },
+ dom.div(
+ {
+ className: "sidebar-fixed-item__container",
+ },
+ dom.img({
+ className: "sidebar-fixed-item__icon",
+ src: icon,
+ }),
+ dom.span(
+ {
+ className: "ellipsis-text",
+ title: name,
+ },
+ name
+ )
+ )
+ );
+ }
+}
+
+module.exports = SidebarFixedItem;
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.css b/devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.css
new file mode 100644
index 0000000000..3f12964012
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.css
@@ -0,0 +1,71 @@
+/* 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/. */
+
+.sidebar-item {
+ color: var(--sidebar-text-color);
+ border-radius: 2px;
+ padding-inline-end: var(--category-padding);
+ padding-inline-start: var(--category-padding);
+ transition: background-color var(--category-transition-duration);
+ user-select: none;
+}
+
+.sidebar-item--tall {
+ height: var(--category-height);
+}
+
+.sidebar-item--condensed {
+ height: calc(var(--base-unit) * 9);
+}
+
+.sidebar-item__link {
+ display: block;
+ height: 100%;
+}
+
+.sidebar-item__link,
+.sidebar-item__link:hover {
+ color: inherit; /* do not apply usual link colors, but grab this element parent's */
+}
+
+.sidebar-item:not(.sidebar-item--selectable) {
+ color: var(--secondary-text-color);
+}
+
+.sidebar-item--selectable:hover {
+ background-color: var(--sidebar-background-hover);
+}
+
+.sidebar-item--selected {
+ color: var(--sidebar-selected-color);
+}
+
+.sidebar-item--breathe {
+ margin-block-start: calc(6 * var(--base-unit));
+ margin-block-end: calc(2 * var(--base-unit));
+}
+
+@media (prefers-contrast) {
+ /* Color transitions (black <-> white) look bad in high contrast */
+ .sidebar-item {
+ transition: background 0s;
+ }
+
+ .sidebar-item--selected,
+ .sidebar-item--selected:hover {
+ background-color: ButtonText;
+ }
+
+ /* `color: inherit` should not be used in high contrast mode
+ otherwise the link inherits the <a> color from ua.css */
+ .sidebar-item__link,
+ .sidebar-item__link:hover {
+ color: ButtonText;
+ }
+
+ .sidebar-item--selected .sidebar-item__link,
+ .sidebar-item--selected .sidebar-item__link:hover {
+ color: ButtonFace;
+ }
+}
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.js b/devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.js
new file mode 100644
index 0000000000..cd17d8e6bc
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.js
@@ -0,0 +1,81 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const Link = createFactory(
+ require("resource://devtools/client/shared/vendor/react-router-dom.js").Link
+);
+
+/**
+ * This component is used as a wrapper by items in the sidebar.
+ */
+class SidebarItem extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node.isRequired,
+ className: PropTypes.string,
+ isSelected: PropTypes.bool.isRequired,
+ to: PropTypes.string,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ isSelected: false,
+ };
+ }
+
+ renderContent() {
+ const { children, to } = this.props;
+
+ if (to) {
+ const isExternalUrl = /^http/.test(to);
+
+ return isExternalUrl
+ ? dom.a(
+ {
+ className: "sidebar-item__link undecorated-link",
+ href: to,
+ target: "_blank",
+ },
+ children
+ )
+ : Link(
+ {
+ className: "sidebar-item__link qa-sidebar-link undecorated-link",
+ to,
+ },
+ children
+ );
+ }
+
+ return children;
+ }
+
+ render() {
+ const { className, isSelected, to } = this.props;
+
+ return dom.li(
+ {
+ className:
+ "sidebar-item qa-sidebar-item" +
+ (className ? ` ${className}` : "") +
+ (isSelected
+ ? " sidebar-item--selected qa-sidebar-item-selected"
+ : "") +
+ (to ? " sidebar-item--selectable" : ""),
+ },
+ this.renderContent()
+ );
+ }
+}
+
+module.exports = SidebarItem;
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.css b/devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.css
new file mode 100644
index 0000000000..423723a27f
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.css
@@ -0,0 +1,41 @@
+/* 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/. */
+
+/*
+ * Layout of a runtime sidebar item
+ *
+ * +--------+----------------+---------------------------+
+ * | Icon | Runtime name | Connect button |
+ * +--------+----------------+---------------------------+
+ */
+
+.sidebar-runtime-item__container {
+ box-sizing: border-box;
+ height: var(--category-height);
+ align-items: center;
+ display: grid;
+ grid-column-gap: var(--base-unit);
+ grid-template-columns: calc(var(--base-unit) * 6) 1fr auto;
+ font-size: var(--body-20-font-size);
+ font-weight: var(--body-20-font-weight);
+}
+
+.sidebar-runtime-item__icon {
+ fill: currentColor;
+ -moz-context-properties: fill;
+}
+
+.sidebar-runtime-item__runtime {
+ line-height: 1;
+}
+
+.sidebar-runtime-item__runtime__details {
+ font-size: var(--caption-10-font-size);
+ font-weight: var(--caption-10-font-weight);
+ line-height: 1.2;
+}
+
+.sidebar-runtime-item__message:first-of-type {
+ margin-block-start: calc(var(--base-unit) * -1);
+}
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.js b/devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.js
new file mode 100644
index 0000000000..a5c5fe6d55
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.js
@@ -0,0 +1,216 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Message = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/Message.js")
+);
+const SidebarItem = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.js")
+);
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This component displays a runtime item of the Sidebar component.
+ */
+class SidebarRuntimeItem extends PureComponent {
+ static get propTypes() {
+ return {
+ deviceName: PropTypes.string,
+ dispatch: PropTypes.func.isRequired,
+ // Provided by wrapping the component with FluentReact.withLocalization.
+ getString: PropTypes.func.isRequired,
+ icon: PropTypes.string.isRequired,
+ isConnected: PropTypes.bool.isRequired,
+ isConnecting: PropTypes.bool.isRequired,
+ isConnectionFailed: PropTypes.bool.isRequired,
+ isConnectionNotResponding: PropTypes.bool.isRequired,
+ isConnectionTimeout: PropTypes.bool.isRequired,
+ isSelected: PropTypes.bool.isRequired,
+ isUnavailable: PropTypes.bool.isRequired,
+ isUnplugged: PropTypes.bool.isRequired,
+ name: PropTypes.string.isRequired,
+ runtimeId: PropTypes.string.isRequired,
+ };
+ }
+
+ renderConnectButton() {
+ const { isConnecting } = this.props;
+ const localizationId = isConnecting
+ ? "about-debugging-sidebar-item-connect-button-connecting"
+ : "about-debugging-sidebar-item-connect-button";
+ return Localized(
+ {
+ id: localizationId,
+ },
+ dom.button(
+ {
+ className: "default-button default-button--micro qa-connect-button",
+ disabled: isConnecting,
+ onClick: () => {
+ const { dispatch, runtimeId } = this.props;
+ dispatch(Actions.connectRuntime(runtimeId));
+ },
+ },
+ localizationId
+ )
+ );
+ }
+
+ renderMessage(flag, level, localizationId, className) {
+ if (!flag) {
+ return null;
+ }
+
+ return Message(
+ {
+ level,
+ className: `${className} sidebar-runtime-item__message`,
+ isCloseable: true,
+ },
+ Localized(
+ {
+ id: localizationId,
+ },
+ dom.p({ className: "word-wrap-anywhere" }, localizationId)
+ )
+ );
+ }
+
+ renderName() {
+ const { deviceName, getString, isUnavailable, isUnplugged, name } =
+ this.props;
+
+ let displayName, qaClassName;
+ if (isUnplugged) {
+ displayName = getString("about-debugging-sidebar-runtime-item-unplugged");
+ qaClassName = "qa-runtime-item-unplugged";
+ } else if (isUnavailable) {
+ displayName = getString(
+ "about-debugging-sidebar-runtime-item-waiting-for-browser"
+ );
+ qaClassName = "qa-runtime-item-waiting-for-browser";
+ } else {
+ displayName = name;
+ qaClassName = "qa-runtime-item-standard";
+ }
+
+ const localizationId = deviceName
+ ? "about-debugging-sidebar-runtime-item-name"
+ : "about-debugging-sidebar-runtime-item-name-no-device";
+
+ const className = "ellipsis-text sidebar-runtime-item__runtime";
+
+ function renderWithDevice() {
+ return dom.span(
+ {
+ className,
+ title: localizationId,
+ },
+ deviceName,
+ dom.br({}),
+ dom.span(
+ {
+ className: `sidebar-runtime-item__runtime__details ${qaClassName}`,
+ },
+ displayName
+ )
+ );
+ }
+
+ function renderNoDevice() {
+ return dom.span(
+ {
+ className,
+ title: localizationId,
+ },
+ displayName
+ );
+ }
+
+ return Localized(
+ {
+ id: localizationId,
+ attrs: { title: true },
+ $deviceName: deviceName,
+ $displayName: displayName,
+ },
+ deviceName ? renderWithDevice() : renderNoDevice()
+ );
+ }
+
+ render() {
+ const {
+ getString,
+ icon,
+ isConnected,
+ isConnectionFailed,
+ isConnectionTimeout,
+ isConnectionNotResponding,
+ isSelected,
+ isUnavailable,
+ runtimeId,
+ } = this.props;
+
+ const connectionStatus = isConnected
+ ? getString("aboutdebugging-sidebar-runtime-connection-status-connected")
+ : getString(
+ "aboutdebugging-sidebar-runtime-connection-status-disconnected"
+ );
+
+ return SidebarItem(
+ {
+ isSelected,
+ to: isConnected ? `/runtime/${encodeURIComponent(runtimeId)}` : null,
+ },
+ dom.section(
+ {
+ className: "sidebar-runtime-item__container",
+ },
+ dom.img({
+ className: "sidebar-runtime-item__icon ",
+ src: icon,
+ alt: connectionStatus,
+ title: connectionStatus,
+ }),
+ this.renderName(),
+ !isUnavailable && !isConnected ? this.renderConnectButton() : null
+ ),
+ this.renderMessage(
+ isConnectionFailed,
+ MESSAGE_LEVEL.ERROR,
+ "about-debugging-sidebar-item-connect-button-connection-failed",
+ "qa-connection-error"
+ ),
+ this.renderMessage(
+ isConnectionTimeout,
+ MESSAGE_LEVEL.ERROR,
+ "about-debugging-sidebar-item-connect-button-connection-timeout",
+ "qa-connection-timeout"
+ ),
+ this.renderMessage(
+ isConnectionNotResponding,
+ MESSAGE_LEVEL.WARNING,
+ "about-debugging-sidebar-item-connect-button-connection-not-responding",
+ "qa-connection-not-responding"
+ )
+ );
+ }
+}
+
+module.exports = FluentReact.withLocalization(SidebarRuntimeItem);
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/moz.build b/devtools/client/aboutdebugging/src/components/sidebar/moz.build
new file mode 100644
index 0000000000..081ea2a848
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/moz.build
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "RefreshDevicesButton.js",
+ "Sidebar.js",
+ "SidebarFixedItem.js",
+ "SidebarItem.js",
+ "SidebarRuntimeItem.js",
+)
diff --git a/devtools/client/aboutdebugging/src/constants.js b/devtools/client/aboutdebugging/src/constants.js
new file mode 100644
index 0000000000..fa0b847a32
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/constants.js
@@ -0,0 +1,185 @@
+/* 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 {
+ CONNECTION_TYPES,
+ DEBUG_TARGET_TYPES,
+} = require("resource://devtools/client/shared/remote-debugging/constants.js");
+
+const actionTypes = {
+ ADB_ADDON_INSTALL_START: "ADB_ADDON_INSTALL_START",
+ ADB_ADDON_INSTALL_SUCCESS: "ADB_ADDON_INSTALL_SUCCESS",
+ ADB_ADDON_INSTALL_FAILURE: "ADB_ADDON_INSTALL_FAILURE",
+ ADB_ADDON_UNINSTALL_START: "ADB_ADDON_UNINSTALL_START",
+ ADB_ADDON_UNINSTALL_SUCCESS: "ADB_ADDON_UNINSTALL_SUCCESS",
+ ADB_ADDON_UNINSTALL_FAILURE: "ADB_ADDON_UNINSTALL_FAILURE",
+ ADB_ADDON_STATUS_UPDATED: "ADB_ADDON_STATUS_UPDATED",
+ ADB_READY_UPDATED: "ADB_READY_UPDATED",
+ CONNECT_RUNTIME_CANCEL: "CONNECT_RUNTIME_CANCEL",
+ CONNECT_RUNTIME_FAILURE: "CONNECT_RUNTIME_FAILURE",
+ CONNECT_RUNTIME_NOT_RESPONDING: "CONNECT_RUNTIME_NOT_RESPONDING",
+ CONNECT_RUNTIME_START: "CONNECT_RUNTIME_START",
+ CONNECT_RUNTIME_SUCCESS: "CONNECT_RUNTIME_SUCCESS",
+ DEBUG_TARGET_COLLAPSIBILITY_UPDATED: "DEBUG_TARGET_COLLAPSIBILITY_UPDATED",
+ DISCONNECT_RUNTIME_FAILURE: "DISCONNECT_RUNTIME_FAILURE",
+ DISCONNECT_RUNTIME_START: "DISCONNECT_RUNTIME_START",
+ DISCONNECT_RUNTIME_SUCCESS: "DISCONNECT_RUNTIME_SUCCESS",
+ EXTENSION_BGSCRIPT_STATUS_UPDATED: "EXTENSION_BGSCRIPT_STATUS_UPDATED",
+ HIDE_PROFILER_DIALOG: "HIDE_PROFILER_DIALOG",
+ SWITCH_PROFILER_CONTEXT: "SWITCH_PROFILER_CONTEXT",
+ NETWORK_LOCATIONS_UPDATE_FAILURE: "NETWORK_LOCATIONS_UPDATE_FAILURE",
+ NETWORK_LOCATIONS_UPDATE_START: "NETWORK_LOCATIONS_UPDATE_START",
+ NETWORK_LOCATIONS_UPDATE_SUCCESS: "NETWORK_LOCATIONS_UPDATE_SUCCESS",
+ REMOTE_RUNTIMES_UPDATED: "REMOTE_RUNTIMES_UPDATED",
+ REQUEST_EXTENSIONS_FAILURE: "REQUEST_EXTENSIONS_FAILURE",
+ REQUEST_EXTENSIONS_START: "REQUEST_EXTENSIONS_START",
+ REQUEST_EXTENSIONS_SUCCESS: "REQUEST_EXTENSIONS_SUCCESS",
+ REQUEST_PROCESSES_FAILURE: "REQUEST_PROCESSES_FAILURE",
+ REQUEST_PROCESSES_START: "REQUEST_PROCESSES_START",
+ REQUEST_PROCESSES_SUCCESS: "REQUEST_PROCESSES_SUCCESS",
+ REQUEST_TABS_FAILURE: "REQUEST_TABS_FAILURE",
+ REQUEST_TABS_START: "REQUEST_TABS_START",
+ REQUEST_TABS_SUCCESS: "REQUEST_TABS_SUCCESS",
+ REQUEST_WORKERS_FAILURE: "REQUEST_WORKERS_FAILURE",
+ REQUEST_WORKERS_START: "REQUEST_WORKERS_START",
+ REQUEST_WORKERS_SUCCESS: "REQUEST_WORKERS_SUCCESS",
+ SELECT_PAGE_FAILURE: "SELECT_PAGE_FAILURE",
+ SELECT_PAGE_START: "SELECT_PAGE_START",
+ SELECT_PAGE_SUCCESS: "SELECT_PAGE_SUCCESS",
+ SELECTED_RUNTIME_ID_UPDATED: "SELECTED_RUNTIME_ID_UPDATED",
+ SHOW_PROFILER_DIALOG: "SHOW_PROFILER_DIALOG",
+ TELEMETRY_RECORD: "TELEMETRY_RECORD",
+ TEMPORARY_EXTENSION_INSTALL_FAILURE: "TEMPORARY_EXTENSION_INSTALL_FAILURE",
+ TEMPORARY_EXTENSION_INSTALL_START: "TEMPORARY_EXTENSION_INSTALL_START",
+ TEMPORARY_EXTENSION_INSTALL_SUCCESS: "TEMPORARY_EXTENSION_INSTALL_SUCCESS",
+ TEMPORARY_EXTENSION_RELOAD_FAILURE: "TEMPORARY_EXTENSION_RELOAD_FAILURE",
+ TEMPORARY_EXTENSION_RELOAD_START: "TEMPORARY_EXTENSION_RELOAD_START",
+ TEMPORARY_EXTENSION_RELOAD_SUCCESS: "TEMPORARY_EXTENSION_RELOAD_SUCCESS",
+ TERMINATE_EXTENSION_BGSCRIPT_FAILURE: "TERMINATE_EXTENSION_BGSCRIPT_FAILURE",
+ TERMINATE_EXTENSION_BGSCRIPT_START: "TERMINATE_EXTENSION_BGSCRIPT_START",
+ TERMINATE_EXTENSION_BGSCRIPT_SUCCESS: "TERMINATE_EXTENSION_BGSCRIPT_SUCCESS",
+ THIS_FIREFOX_RUNTIME_CREATED: "THIS_FIREFOX_RUNTIME_CREATED",
+ UNWATCH_RUNTIME_FAILURE: "UNWATCH_RUNTIME_FAILURE",
+ UNWATCH_RUNTIME_START: "UNWATCH_RUNTIME_START",
+ UNWATCH_RUNTIME_SUCCESS: "UNWATCH_RUNTIME_SUCCESS",
+ UPDATE_CONNECTION_PROMPT_SETTING_FAILURE:
+ "UPDATE_CONNECTION_PROMPT_SETTING_FAILURE",
+ UPDATE_CONNECTION_PROMPT_SETTING_START:
+ "UPDATE_CONNECTION_PROMPT_SETTING_START",
+ UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS:
+ "UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS",
+ USB_RUNTIMES_SCAN_START: "USB_RUNTIMES_SCAN_START",
+ USB_RUNTIMES_SCAN_SUCCESS: "USB_RUNTIMES_SCAN_SUCCESS",
+ WATCH_RUNTIME_FAILURE: "WATCH_RUNTIME_FAILURE",
+ WATCH_RUNTIME_START: "WATCH_RUNTIME_START",
+ WATCH_RUNTIME_SUCCESS: "WATCH_RUNTIME_SUCCESS",
+};
+
+const DEBUG_TARGETS = DEBUG_TARGET_TYPES;
+
+const DEBUG_TARGET_PANE = {
+ INSTALLED_EXTENSION: "installedExtension",
+ PROCESSES: "processes",
+ OTHER_WORKER: "otherWorker",
+ SERVICE_WORKER: "serviceWorker",
+ SHARED_WORKER: "sharedWorker",
+ TAB: "tab",
+ TEMPORARY_EXTENSION: "temporaryExtension",
+};
+
+const ICON_LABEL_LEVEL = {
+ INFO: "info",
+ OK: "ok",
+};
+
+const MESSAGE_LEVEL = {
+ ERROR: "error",
+ INFO: "info",
+ WARNING: "warning",
+};
+
+const PAGE_TYPES = {
+ RUNTIME: "runtime",
+ CONNECT: "connect",
+};
+
+const PREFERENCES = {
+ // Preference that drives the display of the "Tabs" category on This Firefox.
+ LOCAL_TAB_DEBUGGING_ENABLED: "devtools.aboutdebugging.local-tab-debugging",
+ // Preference that drives the display of the "Processes" debug target category.
+ PROCESS_DEBUGGING_ENABLED: "devtools.aboutdebugging.process-debugging",
+ // Preference that drives the display of hidden & system addons in about:debugging.
+ SHOW_HIDDEN_ADDONS: "devtools.aboutdebugging.showHiddenAddons",
+ // Preference to store the last path used for loading a temporary extension.
+ TEMPORARY_EXTENSION_PATH: "devtools.aboutdebugging.tmpExtDirPath",
+ // Preference that disables installing extensions when set to false.
+ XPINSTALL_ENABLED: "xpinstall.enabled",
+};
+
+const RUNTIME_PREFERENCE = {
+ CONNECTION_PROMPT: "devtools.debugger.prompt-connection",
+ PERMANENT_PRIVATE_BROWSING: "browser.privatebrowsing.autostart",
+ SERVICE_WORKERS_ENABLED: "dom.serviceWorkers.enabled",
+};
+
+const RUNTIMES = {
+ NETWORK: CONNECTION_TYPES.NETWORK,
+ THIS_FIREFOX: CONNECTION_TYPES.THIS_FIREFOX,
+ USB: CONNECTION_TYPES.USB,
+};
+
+const SERVICE_WORKER_FETCH_STATES = {
+ LISTENING: "LISTENING",
+ NOT_LISTENING: "NOT_LISTENING",
+};
+
+const SERVICE_WORKER_STATUSES = {
+ RUNNING: "RUNNING",
+ REGISTERING: "REGISTERING",
+ STOPPED: "STOPPED",
+};
+
+const USB_STATES = {
+ DISABLED_USB: "DISABLED_USB",
+ ENABLED_USB: "ENABLED_USB",
+ UPDATING_USB: "UPDATING_USB",
+};
+
+const EXTENSION_BGSCRIPT_STATUSES = {
+ RUNNING: "RUNNING",
+ STOPPED: "STOPPED",
+};
+
+/**
+ * These constants reference the performance-new's concept of a PageContext.
+ * These are defined in devtools/client/performance-new/@types/perf.d.ts
+ * about:debugging only uses the remote variants of the PageContexts.
+ */
+const PROFILER_PAGE_CONTEXT = {
+ DEVTOOLS_REMOTE: "devtools-remote",
+ ABOUTPROFILING_REMOTE: "aboutprofiling-remote",
+};
+
+// flatten constants
+module.exports = Object.assign(
+ {},
+ {
+ DEBUG_TARGETS,
+ DEBUG_TARGET_PANE,
+ EXTENSION_BGSCRIPT_STATUSES,
+ ICON_LABEL_LEVEL,
+ MESSAGE_LEVEL,
+ PAGE_TYPES,
+ PREFERENCES,
+ RUNTIME_PREFERENCE,
+ RUNTIMES,
+ SERVICE_WORKER_FETCH_STATES,
+ SERVICE_WORKER_STATUSES,
+ USB_STATES,
+ PROFILER_PAGE_CONTEXT,
+ },
+ actionTypes
+);
diff --git a/devtools/client/aboutdebugging/src/create-store.js b/devtools/client/aboutdebugging/src/create-store.js
new file mode 100644
index 0000000000..93fd240b00
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/create-store.js
@@ -0,0 +1,78 @@
+/* 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 {
+ applyMiddleware,
+ createStore,
+} = require("resource://devtools/client/shared/vendor/redux.js");
+const {
+ thunk,
+} = require("resource://devtools/client/shared/redux/middleware/thunk.js");
+const {
+ waitUntilService,
+} = require("resource://devtools/client/shared/redux/middleware/wait-service.js");
+
+const rootReducer = require("resource://devtools/client/aboutdebugging/src/reducers/index.js");
+const {
+ DebugTargetsState,
+} = require("resource://devtools/client/aboutdebugging/src/reducers/debug-targets-state.js");
+const {
+ RuntimesState,
+} = require("resource://devtools/client/aboutdebugging/src/reducers/runtimes-state.js");
+const {
+ UiState,
+} = require("resource://devtools/client/aboutdebugging/src/reducers/ui-state.js");
+const debugTargetListenerMiddleware = require("resource://devtools/client/aboutdebugging/src/middleware/debug-target-listener.js");
+const errorLoggingMiddleware = require("resource://devtools/client/aboutdebugging/src/middleware/error-logging.js");
+const eventRecordingMiddleware = require("resource://devtools/client/aboutdebugging/src/middleware/event-recording.js");
+const extensionComponentDataMiddleware = require("resource://devtools/client/aboutdebugging/src/middleware/extension-component-data.js");
+const processComponentDataMiddleware = require("resource://devtools/client/aboutdebugging/src/middleware/process-component-data.js");
+const tabComponentDataMiddleware = require("resource://devtools/client/aboutdebugging/src/middleware/tab-component-data.js");
+const workerComponentDataMiddleware = require("resource://devtools/client/aboutdebugging/src/middleware/worker-component-data.js");
+const {
+ getDebugTargetCollapsibilities,
+} = require("resource://devtools/client/aboutdebugging/src/modules/debug-target-collapsibilities.js");
+const {
+ getNetworkLocations,
+} = require("resource://devtools/client/aboutdebugging/src/modules/network-locations.js");
+
+const {
+ PREFERENCES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+function configureStore() {
+ const initialState = {
+ debugTargets: new DebugTargetsState(),
+ runtimes: new RuntimesState(),
+ ui: getUiState(),
+ };
+
+ const middleware = applyMiddleware(
+ thunk(),
+ debugTargetListenerMiddleware,
+ errorLoggingMiddleware,
+ eventRecordingMiddleware,
+ extensionComponentDataMiddleware,
+ processComponentDataMiddleware,
+ tabComponentDataMiddleware,
+ workerComponentDataMiddleware,
+ waitUntilService
+ );
+
+ return createStore(rootReducer, initialState, middleware);
+}
+
+function getUiState() {
+ const collapsibilities = getDebugTargetCollapsibilities();
+ const locations = getNetworkLocations();
+ const showHiddenAddons = Services.prefs.getBoolPref(
+ PREFERENCES.SHOW_HIDDEN_ADDONS,
+ false
+ );
+ return new UiState(locations, collapsibilities, showHiddenAddons);
+}
+
+exports.configureStore = configureStore;
diff --git a/devtools/client/aboutdebugging/src/middleware/debug-target-listener.js b/devtools/client/aboutdebugging/src/middleware/debug-target-listener.js
new file mode 100644
index 0000000000..655e667a6d
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/middleware/debug-target-listener.js
@@ -0,0 +1,111 @@
+/* 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 {
+ EXTENSION_BGSCRIPT_STATUSES,
+ EXTENSION_BGSCRIPT_STATUS_UPDATED,
+ UNWATCH_RUNTIME_START,
+ WATCH_RUNTIME_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+
+const RootResourceCommand = require("resource://devtools/shared/commands/root-resource/root-resource-command.js");
+
+function debugTargetListenerMiddleware(store) {
+ const onExtensionsUpdated = () => {
+ store.dispatch(Actions.requestExtensions());
+ };
+
+ const onTabsUpdated = () => {
+ store.dispatch(Actions.requestTabs());
+ };
+
+ const onWorkersUpdated = () => {
+ store.dispatch(Actions.requestWorkers());
+ };
+
+ let rootResourceCommand;
+
+ function onExtensionsBackgroundScriptStatusAvailable(resources) {
+ for (const resource of resources) {
+ const backgroundScriptStatus = resource.payload.isRunning
+ ? EXTENSION_BGSCRIPT_STATUSES.RUNNING
+ : EXTENSION_BGSCRIPT_STATUSES.STOPPED;
+
+ store.dispatch({
+ type: EXTENSION_BGSCRIPT_STATUS_UPDATED,
+ id: resource.payload.addonId,
+ backgroundScriptStatus,
+ });
+ }
+ }
+
+ return next => async action => {
+ switch (action.type) {
+ case WATCH_RUNTIME_SUCCESS: {
+ const { runtime } = action;
+ const { clientWrapper } = runtime.runtimeDetails;
+
+ rootResourceCommand = clientWrapper.createRootResourceCommand();
+
+ // Watch extensions background script status updates.
+ await rootResourceCommand
+ .watchResources(
+ [RootResourceCommand.TYPES.EXTENSIONS_BGSCRIPT_STATUS],
+ { onAvailable: onExtensionsBackgroundScriptStatusAvailable }
+ )
+ .catch(e => {
+ // Log an error if watching this resource rejects (e.g. if
+ // the promise was not resolved yet when about:debugging tab
+ // or the RDP connection to a remote target has been closed).
+ console.error(e);
+ });
+
+ // Tabs
+ clientWrapper.on("tabListChanged", onTabsUpdated);
+
+ // Addons
+ clientWrapper.on("addonListChanged", onExtensionsUpdated);
+
+ // Workers
+ clientWrapper.on("workersUpdated", onWorkersUpdated);
+ break;
+ }
+ case UNWATCH_RUNTIME_START: {
+ const { runtime } = action;
+ const { clientWrapper } = runtime.runtimeDetails;
+
+ // Stop watching extensions background script status updates.
+ try {
+ rootResourceCommand?.unwatchResources(
+ [RootResourceCommand.TYPES.EXTENSIONS_BGSCRIPT_STATUS],
+ { onAvailable: onExtensionsBackgroundScriptStatusAvailable }
+ );
+ } catch (e) {
+ // Log an error if watching this resource rejects (e.g. if
+ // the promise was not resolved yet when about:debugging tab
+ // or the RDP connection to a remote target has been closed).
+ console.error(e);
+ }
+
+ // Tabs
+ clientWrapper.off("tabListChanged", onTabsUpdated);
+
+ // Addons
+ clientWrapper.off("addonListChanged", onExtensionsUpdated);
+
+ // Workers
+ clientWrapper.off("workersUpdated", onWorkersUpdated);
+ break;
+ }
+ }
+
+ return next(action);
+ };
+}
+
+module.exports = debugTargetListenerMiddleware;
diff --git a/devtools/client/aboutdebugging/src/middleware/error-logging.js b/devtools/client/aboutdebugging/src/middleware/error-logging.js
new file mode 100644
index 0000000000..fd04f34b76
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/middleware/error-logging.js
@@ -0,0 +1,35 @@
+/* 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";
+
+/**
+ * Error logging middleware that will forward all actions that contain an error property
+ * to the console.
+ */
+function errorLoggingMiddleware() {
+ return next => action => {
+ if (action.error) {
+ const { error } = action;
+ if (error.message) {
+ console.error(`[ACTION FAILED] ${action.type}: ${error.message}`);
+ } else if (typeof error === "string") {
+ // All failure actions should dispatch an error object instead of a message.
+ // We allow some flexibility to still provide some error logging.
+ console.error(`[ACTION FAILED] ${action.type}: ${error}`);
+ console.error(
+ `[ACTION FAILED] ${action.type} should dispatch the error object!`
+ );
+ }
+
+ if (error.stack) {
+ console.error(error.stack);
+ }
+ }
+
+ return next(action);
+ };
+}
+
+module.exports = errorLoggingMiddleware;
diff --git a/devtools/client/aboutdebugging/src/middleware/event-recording.js b/devtools/client/aboutdebugging/src/middleware/event-recording.js
new file mode 100644
index 0000000000..f926100b8b
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/middleware/event-recording.js
@@ -0,0 +1,268 @@
+/* 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 Telemetry = require("resource://devtools/client/shared/telemetry.js");
+loader.lazyGetter(
+ this,
+ "telemetry",
+ () => new Telemetry({ useSessionId: true })
+);
+
+const {
+ CONNECT_RUNTIME_CANCEL,
+ CONNECT_RUNTIME_FAILURE,
+ CONNECT_RUNTIME_NOT_RESPONDING,
+ CONNECT_RUNTIME_START,
+ CONNECT_RUNTIME_SUCCESS,
+ DISCONNECT_RUNTIME_SUCCESS,
+ REMOTE_RUNTIMES_UPDATED,
+ RUNTIMES,
+ SELECT_PAGE_SUCCESS,
+ SHOW_PROFILER_DIALOG,
+ TELEMETRY_RECORD,
+ UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const {
+ findRuntimeById,
+ getAllRuntimes,
+ getCurrentRuntime,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
+
+function recordEvent(method, details) {
+ telemetry.recordEvent(method, "aboutdebugging", null, details);
+
+ // For close and open events, also ping the regular telemetry helpers used
+ // for all DevTools UIs.
+ if (method === "open_adbg") {
+ telemetry.toolOpened("aboutdebugging", window.AboutDebugging);
+ } else if (method === "close_adbg") {
+ // XXX: Note that aboutdebugging has no histogram created for
+ // TIME_ACTIVE_SECOND, so calling toolClosed will not actually
+ // record anything.
+ telemetry.toolClosed("aboutdebugging", window.AboutDebugging);
+ }
+}
+
+const telemetryRuntimeIds = new Map();
+// Create an anonymous id that will allow to track all events related to a runtime without
+// leaking personal data related to this runtime.
+function getTelemetryRuntimeId(id) {
+ if (!telemetryRuntimeIds.has(id)) {
+ const randomId = (Math.random() * 100000) | 0;
+ telemetryRuntimeIds.set(id, "runtime-" + randomId);
+ }
+ return telemetryRuntimeIds.get(id);
+}
+
+function getCurrentRuntimeIdForTelemetry(store) {
+ const id = getCurrentRuntime(store.getState().runtimes).id;
+ return getTelemetryRuntimeId(id);
+}
+
+function getRuntimeEventExtras(runtime) {
+ const { extra, runtimeDetails } = runtime;
+
+ // deviceName can be undefined for non-usb devices, but we should not log "undefined".
+ const deviceName = extra?.deviceName || "";
+ const runtimeShortName = runtime.type === RUNTIMES.USB ? runtime.name : "";
+ const runtimeName = runtimeDetails?.info.name || "";
+ return {
+ connection_type: runtime.type,
+ device_name: deviceName,
+ runtime_id: getTelemetryRuntimeId(runtime.id),
+ runtime_name: runtimeName || runtimeShortName,
+ };
+}
+
+function onConnectRuntimeSuccess(action, store) {
+ if (action.runtime.type === RUNTIMES.THIS_FIREFOX) {
+ // Only record connection and disconnection events for remote runtimes.
+ return;
+ }
+ // When we just connected to a runtime, the runtimeDetails are not in the store yet,
+ // so we merge it here to retrieve the expected telemetry data.
+ const storeRuntime = findRuntimeById(
+ action.runtime.id,
+ store.getState().runtimes
+ );
+ const runtime = Object.assign({}, storeRuntime, {
+ runtimeDetails: action.runtime.runtimeDetails,
+ });
+ const extras = Object.assign({}, getRuntimeEventExtras(runtime), {
+ runtime_os: action.runtime.runtimeDetails.info.os,
+ runtime_version: action.runtime.runtimeDetails.info.version,
+ });
+ recordEvent("runtime_connected", extras);
+}
+
+function onDisconnectRuntimeSuccess(action, store) {
+ const runtime = findRuntimeById(action.runtime.id, store.getState().runtimes);
+ if (runtime.type === RUNTIMES.THIS_FIREFOX) {
+ // Only record connection and disconnection events for remote runtimes.
+ return;
+ }
+
+ recordEvent("runtime_disconnected", getRuntimeEventExtras(runtime));
+}
+
+function onRemoteRuntimesUpdated(action, store) {
+ // Compare new runtimes with the existing runtimes to detect if runtimes, devices
+ // have been added or removed.
+ const newRuntimes = action.runtimes;
+ const allRuntimes = getAllRuntimes(store.getState().runtimes);
+ const oldRuntimes = allRuntimes.filter(r => r.type === action.runtimeType);
+
+ // Check if all the old runtimes and devices are still available in the updated
+ // array.
+ for (const oldRuntime of oldRuntimes) {
+ const runtimeRemoved = newRuntimes.every(r => r.id !== oldRuntime.id);
+ if (runtimeRemoved && !oldRuntime.isUnplugged) {
+ recordEvent("runtime_removed", getRuntimeEventExtras(oldRuntime));
+ }
+ }
+
+ // Using device names as unique IDs is inaccurate. See Bug 1544582.
+ const oldDeviceNames = new Set(oldRuntimes.map(r => r.extra.deviceName));
+ for (const oldDeviceName of oldDeviceNames) {
+ const newRuntime = newRuntimes.find(
+ r => r.extra.deviceName === oldDeviceName
+ );
+ const oldRuntime = oldRuntimes.find(
+ r => r.extra.deviceName === oldDeviceName
+ );
+ const isUnplugged = newRuntime?.isUnplugged && !oldRuntime.isUnplugged;
+ if (oldDeviceName && (!newRuntime || isUnplugged)) {
+ recordEvent("device_removed", {
+ connection_type: action.runtimeType,
+ device_name: oldDeviceName,
+ });
+ }
+ }
+
+ // Check if the new runtimes and devices were already available in the existing
+ // array.
+ for (const newRuntime of newRuntimes) {
+ const runtimeAdded = oldRuntimes.every(r => r.id !== newRuntime.id);
+ if (runtimeAdded && !newRuntime.isUnplugged) {
+ recordEvent("runtime_added", getRuntimeEventExtras(newRuntime));
+ }
+ }
+
+ // Using device names as unique IDs is inaccurate. See Bug 1544582.
+ const newDeviceNames = new Set(newRuntimes.map(r => r.extra.deviceName));
+ for (const newDeviceName of newDeviceNames) {
+ const newRuntime = newRuntimes.find(
+ r => r.extra.deviceName === newDeviceName
+ );
+ const oldRuntime = oldRuntimes.find(
+ r => r.extra.deviceName === newDeviceName
+ );
+ const isPlugged = oldRuntime?.isUnplugged && !newRuntime.isUnplugged;
+
+ if (newDeviceName && (!oldRuntime || isPlugged)) {
+ recordEvent("device_added", {
+ connection_type: action.runtimeType,
+ device_name: newDeviceName,
+ });
+ }
+ }
+}
+
+function recordConnectionAttempt(connectionId, runtimeId, status, store) {
+ const runtime = findRuntimeById(runtimeId, store.getState().runtimes);
+ if (runtime.type === RUNTIMES.THIS_FIREFOX) {
+ // Only record connection_attempt events for remote runtimes.
+ return;
+ }
+
+ recordEvent("connection_attempt", {
+ connection_id: connectionId,
+ connection_type: runtime.type,
+ runtime_id: getTelemetryRuntimeId(runtimeId),
+ status,
+ });
+}
+
+/**
+ * This middleware will record events to telemetry for some specific actions.
+ */
+function eventRecordingMiddleware(store) {
+ return next => action => {
+ switch (action.type) {
+ case CONNECT_RUNTIME_CANCEL:
+ recordConnectionAttempt(
+ action.connectionId,
+ action.id,
+ "cancelled",
+ store
+ );
+ break;
+ case CONNECT_RUNTIME_FAILURE:
+ recordConnectionAttempt(
+ action.connectionId,
+ action.id,
+ "failed",
+ store
+ );
+ break;
+ case CONNECT_RUNTIME_NOT_RESPONDING:
+ recordConnectionAttempt(
+ action.connectionId,
+ action.id,
+ "not responding",
+ store
+ );
+ break;
+ case CONNECT_RUNTIME_START:
+ recordConnectionAttempt(action.connectionId, action.id, "start", store);
+ break;
+ case CONNECT_RUNTIME_SUCCESS:
+ recordConnectionAttempt(
+ action.connectionId,
+ action.runtime.id,
+ "success",
+ store
+ );
+ onConnectRuntimeSuccess(action, store);
+ break;
+ case DISCONNECT_RUNTIME_SUCCESS:
+ onDisconnectRuntimeSuccess(action, store);
+ break;
+ case REMOTE_RUNTIMES_UPDATED:
+ onRemoteRuntimesUpdated(action, store);
+ break;
+ case SELECT_PAGE_SUCCESS:
+ recordEvent("select_page", { page_type: action.page });
+ break;
+ case SHOW_PROFILER_DIALOG:
+ recordEvent("show_profiler", {
+ runtime_id: getCurrentRuntimeIdForTelemetry(store),
+ });
+ break;
+ case TELEMETRY_RECORD:
+ const { method, details } = action;
+ if (method) {
+ recordEvent(method, details);
+ } else {
+ console.error(
+ `[RECORD EVENT FAILED] ${action.type}: no "method" property`
+ );
+ }
+ break;
+ case UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS:
+ recordEvent("update_conn_prompt", {
+ prompt_enabled: `${action.connectionPromptEnabled}`,
+ runtime_id: getCurrentRuntimeIdForTelemetry(store),
+ });
+ break;
+ }
+
+ return next(action);
+ };
+}
+
+module.exports = eventRecordingMiddleware;
diff --git a/devtools/client/aboutdebugging/src/middleware/extension-component-data.js b/devtools/client/aboutdebugging/src/middleware/extension-component-data.js
new file mode 100644
index 0000000000..5987f36398
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/middleware/extension-component-data.js
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ DEBUG_TARGETS,
+ REQUEST_EXTENSIONS_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const {
+ getExtensionUuid,
+ parseFileUri,
+} = require("resource://devtools/client/aboutdebugging/src/modules/extensions-helper.js");
+
+/**
+ * This middleware converts extensions object that get from DevToolsClient.listAddons()
+ * to data which is used in DebugTargetItem.
+ */
+const extensionComponentDataMiddleware = store => next => action => {
+ switch (action.type) {
+ case REQUEST_EXTENSIONS_SUCCESS: {
+ action.installedExtensions = toComponentData(action.installedExtensions);
+ action.temporaryExtensions = toComponentData(action.temporaryExtensions);
+ break;
+ }
+ }
+
+ return next(action);
+};
+
+function getFilePath(extension) {
+ // Only show file system paths, and only for temporarily installed add-ons.
+ if (
+ !extension.temporarilyInstalled ||
+ !extension.url ||
+ !extension.url.startsWith("file://")
+ ) {
+ return null;
+ }
+
+ return parseFileUri(extension.url);
+}
+
+function toComponentData(extensions) {
+ return extensions.map(extension => {
+ const type = DEBUG_TARGETS.EXTENSION;
+ const {
+ actor,
+ backgroundScriptStatus,
+ iconDataURL,
+ iconURL,
+ id,
+ manifestURL,
+ name,
+ persistentBackgroundScript,
+ warnings,
+ } = extension;
+ const icon =
+ iconDataURL ||
+ iconURL ||
+ "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+ const location = getFilePath(extension);
+ const uuid = getExtensionUuid(extension);
+ return {
+ name,
+ icon,
+ id,
+ type,
+ details: {
+ actor,
+ backgroundScriptStatus,
+ location,
+ manifestURL,
+ persistentBackgroundScript,
+ uuid,
+ warnings: warnings || [],
+ },
+ };
+ });
+}
+
+module.exports = extensionComponentDataMiddleware;
diff --git a/devtools/client/aboutdebugging/src/middleware/moz.build b/devtools/client/aboutdebugging/src/middleware/moz.build
new file mode 100644
index 0000000000..f50150f569
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/middleware/moz.build
@@ -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/.
+
+DevToolsModules(
+ "debug-target-listener.js",
+ "error-logging.js",
+ "event-recording.js",
+ "extension-component-data.js",
+ "process-component-data.js",
+ "tab-component-data.js",
+ "worker-component-data.js",
+)
diff --git a/devtools/client/aboutdebugging/src/middleware/process-component-data.js b/devtools/client/aboutdebugging/src/middleware/process-component-data.js
new file mode 100644
index 0000000000..d5cdc6365b
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/middleware/process-component-data.js
@@ -0,0 +1,55 @@
+/* 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 {
+ l10n,
+} = require("resource://devtools/client/aboutdebugging/src/modules/l10n.js");
+
+const {
+ DEBUG_TARGETS,
+ REQUEST_PROCESSES_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This middleware converts tabs object that get from DevToolsClient.listProcesses() to
+ * data which is used in DebugTargetItem.
+ */
+const processComponentDataMiddleware = store => next => action => {
+ switch (action.type) {
+ case REQUEST_PROCESSES_SUCCESS: {
+ const mainProcessComponentData = toMainProcessComponentData(
+ action.mainProcess
+ );
+ action.processes = [mainProcessComponentData];
+ break;
+ }
+ }
+
+ return next(action);
+};
+
+function toMainProcessComponentData(process) {
+ const type = DEBUG_TARGETS.PROCESS;
+ const icon = "chrome://devtools/skin/images/aboutdebugging-process-icon.svg";
+
+ // For now, we assume there is only one process and this is the main process
+ // So the name and title are for a remote (multiprocess) browser toolbox.
+ const name = l10n.getString("about-debugging-multiprocess-toolbox-name");
+ const description = l10n.getString(
+ "about-debugging-multiprocess-toolbox-description"
+ );
+
+ return {
+ name,
+ icon,
+ type,
+ details: {
+ description,
+ },
+ };
+}
+
+module.exports = processComponentDataMiddleware;
diff --git a/devtools/client/aboutdebugging/src/middleware/tab-component-data.js b/devtools/client/aboutdebugging/src/middleware/tab-component-data.js
new file mode 100644
index 0000000000..f468926f81
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/middleware/tab-component-data.js
@@ -0,0 +1,51 @@
+/* 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 {
+ DEBUG_TARGETS,
+ REQUEST_TABS_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This middleware converts tabs object that get from DevToolsClient.listTabs() to data
+ * which is used in DebugTargetItem.
+ */
+const tabComponentDataMiddleware = store => next => action => {
+ switch (action.type) {
+ case REQUEST_TABS_SUCCESS: {
+ action.tabs = toComponentData(action.tabs);
+ break;
+ }
+ }
+
+ return next(action);
+};
+
+function toComponentData(tabs) {
+ return tabs.map(tab => {
+ const type = DEBUG_TARGETS.TAB;
+ const id = tab.browserId;
+ const icon = tab.favicon
+ ? `data:image/png;base64,${btoa(
+ String.fromCharCode.apply(String, tab.favicon)
+ )}`
+ : "chrome://devtools/skin/images/globe.svg";
+ const name = tab.title || tab.url;
+ const { url, isZombieTab } = tab;
+ return {
+ name,
+ icon,
+ id,
+ type,
+ details: {
+ isZombieTab,
+ url,
+ },
+ };
+ });
+}
+
+module.exports = tabComponentDataMiddleware;
diff --git a/devtools/client/aboutdebugging/src/middleware/worker-component-data.js b/devtools/client/aboutdebugging/src/middleware/worker-component-data.js
new file mode 100644
index 0000000000..178c99e322
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/middleware/worker-component-data.js
@@ -0,0 +1,82 @@
+/* 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 {
+ DEBUG_TARGETS,
+ REQUEST_WORKERS_SUCCESS,
+ SERVICE_WORKER_FETCH_STATES,
+ SERVICE_WORKER_STATUSES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This middleware converts workers object that get from DevToolsClient.listAllWorkers()
+ * to data which is used in DebugTargetItem.
+ */
+const workerComponentDataMiddleware = store => next => action => {
+ switch (action.type) {
+ case REQUEST_WORKERS_SUCCESS: {
+ action.otherWorkers = toComponentData(action.otherWorkers);
+ action.serviceWorkers = toComponentData(action.serviceWorkers, true);
+ action.sharedWorkers = toComponentData(action.sharedWorkers);
+ break;
+ }
+ }
+
+ return next(action);
+};
+
+function getServiceWorkerStatus(worker) {
+ const isActive = worker.state === Ci.nsIServiceWorkerInfo.STATE_ACTIVATED;
+ const isRunning = !!worker.workerDescriptorFront;
+
+ if (isActive && isRunning) {
+ return SERVICE_WORKER_STATUSES.RUNNING;
+ } else if (isActive) {
+ return SERVICE_WORKER_STATUSES.STOPPED;
+ }
+
+ // We cannot get service worker registrations unless the registration is in
+ // ACTIVE state. Unable to know the actual state ("installing", "waiting"), we
+ // display a custom state "registering" for now. See Bug 1153292.
+ return SERVICE_WORKER_STATUSES.REGISTERING;
+}
+
+function toComponentData(workers, isServiceWorker) {
+ return workers.map(worker => {
+ // Here `worker` is the worker object created by RootFront.listAllWorkers
+ const type = DEBUG_TARGETS.WORKER;
+ const icon = "chrome://devtools/skin/images/debugging-workers.svg";
+ let { fetch } = worker;
+ const { id, name, registrationFront, scope, subscription } = worker;
+
+ let pushServiceEndpoint = null;
+ let status = null;
+
+ if (isServiceWorker) {
+ fetch = fetch
+ ? SERVICE_WORKER_FETCH_STATES.LISTENING
+ : SERVICE_WORKER_FETCH_STATES.NOT_LISTENING;
+ status = getServiceWorkerStatus(worker);
+ pushServiceEndpoint = subscription ? subscription.endpoint : null;
+ }
+
+ return {
+ details: {
+ fetch,
+ pushServiceEndpoint,
+ registrationFront,
+ scope,
+ status,
+ },
+ icon,
+ id,
+ name,
+ type,
+ };
+ });
+}
+
+module.exports = workerComponentDataMiddleware;
diff --git a/devtools/client/aboutdebugging/src/modules/client-wrapper.js b/devtools/client/aboutdebugging/src/modules/client-wrapper.js
new file mode 100644
index 0000000000..6d537b60a4
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/client-wrapper.js
@@ -0,0 +1,217 @@
+/* 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 {
+ checkVersionCompatibility,
+} = require("resource://devtools/client/shared/remote-debugging/version-checker.js");
+
+const {
+ RUNTIME_PREFERENCE,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const {
+ WorkersListener,
+} = require("resource://devtools/client/shared/workers-listener.js");
+const RootResourceCommand = require("resource://devtools/shared/commands/root-resource/root-resource-command.js");
+
+const PREF_TYPES = {
+ BOOL: "BOOL",
+};
+
+// Map of preference to preference type.
+const PREF_TO_TYPE = {
+ [RUNTIME_PREFERENCE.CHROME_DEBUG_ENABLED]: PREF_TYPES.BOOL,
+ [RUNTIME_PREFERENCE.CONNECTION_PROMPT]: PREF_TYPES.BOOL,
+ [RUNTIME_PREFERENCE.PERMANENT_PRIVATE_BROWSING]: PREF_TYPES.BOOL,
+ [RUNTIME_PREFERENCE.REMOTE_DEBUG_ENABLED]: PREF_TYPES.BOOL,
+ [RUNTIME_PREFERENCE.SERVICE_WORKERS_ENABLED]: PREF_TYPES.BOOL,
+};
+
+// Some events are fired by mainRoot rather than client.
+const MAIN_ROOT_EVENTS = ["addonListChanged", "tabListChanged"];
+
+/**
+ * The ClientWrapper class is used to isolate aboutdebugging from the DevTools client API
+ * The modules of about:debugging should never call DevTools client APIs directly.
+ */
+class ClientWrapper {
+ constructor(client) {
+ this.client = client;
+ this.workersListener = new WorkersListener(client.mainRoot);
+ }
+
+ once(evt, listener) {
+ if (MAIN_ROOT_EVENTS.includes(evt)) {
+ this.client.mainRoot.once(evt, listener);
+ } else {
+ this.client.once(evt, listener);
+ }
+ }
+
+ on(evt, listener) {
+ if (evt === "workersUpdated") {
+ this.workersListener.addListener(listener);
+ } else if (MAIN_ROOT_EVENTS.includes(evt)) {
+ this.client.mainRoot.on(evt, listener);
+ } else {
+ this.client.on(evt, listener);
+ }
+ }
+
+ off(evt, listener) {
+ if (evt === "workersUpdated") {
+ this.workersListener.removeListener(listener);
+ } else if (MAIN_ROOT_EVENTS.includes(evt)) {
+ this.client.mainRoot.off(evt, listener);
+ } else {
+ this.client.off(evt, listener);
+ }
+ }
+
+ async getFront(typeName) {
+ return this.client.mainRoot.getFront(typeName);
+ }
+
+ async getDeviceDescription() {
+ const deviceFront = await this.getFront("device");
+ const description = await deviceFront.getDescription();
+
+ // Only expose a specific set of properties.
+ return {
+ canDebugServiceWorkers: description.canDebugServiceWorkers,
+ channel: description.channel,
+ deviceName: description.deviceName,
+ name: description.brandName,
+ os: description.os,
+ version: description.version,
+ };
+ }
+
+ createRootResourceCommand() {
+ return new RootResourceCommand({ rootFront: this.client.mainRoot });
+ }
+
+ async checkVersionCompatibility() {
+ return checkVersionCompatibility(this.client);
+ }
+
+ async setPreference(prefName, value) {
+ const prefType = PREF_TO_TYPE[prefName];
+ const preferenceFront = await this.client.mainRoot.getFront("preference");
+ switch (prefType) {
+ case PREF_TYPES.BOOL:
+ return preferenceFront.setBoolPref(prefName, value);
+ default:
+ throw new Error("Unsupported preference" + prefName);
+ }
+ }
+
+ async getPreference(prefName, defaultValue) {
+ if (typeof defaultValue === "undefined") {
+ throw new Error(
+ "Default value is mandatory for getPreference, the actor will " +
+ "throw if the preference is not set on the target runtime"
+ );
+ }
+
+ const prefType = PREF_TO_TYPE[prefName];
+ const preferenceFront = await this.client.mainRoot.getFront("preference");
+ switch (prefType) {
+ case PREF_TYPES.BOOL:
+ // TODO: Add server-side trait and methods to pass a default value to getBoolPref.
+ // See Bug 1522588.
+ let prefValue;
+ try {
+ prefValue = await preferenceFront.getBoolPref(prefName);
+ } catch (e) {
+ prefValue = defaultValue;
+ }
+ return prefValue;
+ default:
+ throw new Error("Unsupported preference:" + prefName);
+ }
+ }
+
+ async listTabs() {
+ return this.client.mainRoot.listTabs();
+ }
+
+ async listAddons(options) {
+ return this.client.mainRoot.listAddons(options);
+ }
+
+ async getAddon({ id }) {
+ return this.client.mainRoot.getAddon({ id });
+ }
+
+ async uninstallAddon({ id }) {
+ const addonsFront = await this.getFront("addons");
+ return addonsFront.uninstallAddon(id);
+ }
+
+ async getMainProcess() {
+ return this.client.mainRoot.getMainProcess();
+ }
+
+ async getServiceWorkerFront({ id }) {
+ return this.client.mainRoot.getWorker(id);
+ }
+
+ async listWorkers() {
+ const { other, service, shared } =
+ await this.client.mainRoot.listAllWorkers();
+
+ return {
+ otherWorkers: other,
+ serviceWorkers: service,
+ sharedWorkers: shared,
+ };
+ }
+
+ async close() {
+ return this.client.close();
+ }
+
+ isClosed() {
+ return this.client._transportClosed;
+ }
+
+ // This method will be mocked to return a dummy URL during mochitests
+ getPerformancePanelUrl() {
+ return "chrome://devtools/content/performance-new/panel/index.xhtml";
+ }
+
+ /**
+ * @param {Window} win - The window of the dialog window.
+ * @param {Function} openAboutProfiling
+ */
+ async loadPerformanceProfiler(win, openAboutProfiling) {
+ const perfFront = await this.getFront("perf");
+ const { traits } = this.client;
+ await win.gInit(perfFront, traits, "devtools-remote", openAboutProfiling);
+ }
+
+ /**
+ * @param {Window} win - The window of the dialog window.
+ * @param {Function} openRemoteDevTools
+ */
+ async loadAboutProfiling(win, openRemoteDevTools) {
+ const perfFront = await this.getFront("perf");
+ const isSupportedPlatform = await perfFront.isSupportedPlatform();
+ const supportedFeatures = await perfFront.getSupportedFeatures();
+ await win.gInit(
+ "aboutprofiling-remote",
+ isSupportedPlatform,
+ supportedFeatures,
+ openRemoteDevTools
+ );
+ }
+
+ get traits() {
+ return { ...this.client.mainRoot.traits };
+ }
+}
+
+exports.ClientWrapper = ClientWrapper;
diff --git a/devtools/client/aboutdebugging/src/modules/debug-target-collapsibilities.js b/devtools/client/aboutdebugging/src/modules/debug-target-collapsibilities.js
new file mode 100644
index 0000000000..efba47e03a
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/debug-target-collapsibilities.js
@@ -0,0 +1,46 @@
+/* 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 PREF_PREFIX = "devtools.aboutdebugging.collapsibilities.";
+const {
+ DEBUG_TARGET_PANE,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This module provides a collection of helper methods to read and update the debug
+ * target pane's collapsibilities.
+ */
+
+/**
+ * @return {Object}
+ * {
+ * key: constants.DEBUG_TARGET_PANE
+ * value: true - collapsed
+ * false - expanded
+ * }
+ */
+function getDebugTargetCollapsibilities() {
+ const map = new Map();
+
+ for (const key of Object.values(DEBUG_TARGET_PANE)) {
+ const pref = Services.prefs.getBoolPref(PREF_PREFIX + key, false);
+ map.set(key, pref);
+ }
+
+ return map;
+}
+exports.getDebugTargetCollapsibilities = getDebugTargetCollapsibilities;
+
+/**
+ * @param collapsibilities - Same format to getDebugTargetCollapsibilities.
+ */
+function setDebugTargetCollapsibilities(collapsibilities) {
+ for (const key of Object.values(DEBUG_TARGET_PANE)) {
+ const isCollapsed = collapsibilities.get(key);
+ Services.prefs.setBoolPref(PREF_PREFIX + key, isCollapsed);
+ }
+}
+exports.setDebugTargetCollapsibilities = setDebugTargetCollapsibilities;
diff --git a/devtools/client/aboutdebugging/src/modules/debug-target-support.js b/devtools/client/aboutdebugging/src/modules/debug-target-support.js
new file mode 100644
index 0000000000..2b8d4afa5b
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/debug-target-support.js
@@ -0,0 +1,98 @@
+/* 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 {
+ DEBUG_TARGET_PANE,
+ PREFERENCES,
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+// Process target debugging is disabled by default.
+function isProcessDebuggingSupported() {
+ return Services.prefs.getBoolPref(
+ PREFERENCES.PROCESS_DEBUGGING_ENABLED,
+ false
+ );
+}
+
+// Local tab debugging is disabled by default.
+function isLocalTabDebuggingSupported() {
+ return Services.prefs.getBoolPref(
+ PREFERENCES.LOCAL_TAB_DEBUGGING_ENABLED,
+ false
+ );
+}
+
+// Local process debugging is disabled by default.
+// This preference has no default value in
+// devtools/client/preferences/devtools-client.js
+// because it is only intended for tests.
+function isLocalProcessDebuggingSupported() {
+ return Services.prefs.getBoolPref(
+ "devtools.aboutdebugging.test-local-process-debugging",
+ false
+ );
+}
+
+// Installing extensions can be disabled in enterprise policy.
+// Note: Temporary Extensions are only supported when debugging This Firefox, so checking
+// the local preference is acceptable here. If we enable Temporary extensions for remote
+// runtimes, we should retrieve the preference from the target runtime instead.
+function isTemporaryExtensionSupported() {
+ return Services.prefs.getBoolPref(PREFERENCES.XPINSTALL_ENABLED, true);
+}
+
+const ALL_DEBUG_TARGET_PANES = [
+ DEBUG_TARGET_PANE.INSTALLED_EXTENSION,
+ ...(isProcessDebuggingSupported() ? [DEBUG_TARGET_PANE.PROCESSES] : []),
+ DEBUG_TARGET_PANE.OTHER_WORKER,
+ DEBUG_TARGET_PANE.SERVICE_WORKER,
+ DEBUG_TARGET_PANE.SHARED_WORKER,
+ DEBUG_TARGET_PANE.TAB,
+ ...(isTemporaryExtensionSupported()
+ ? [DEBUG_TARGET_PANE.TEMPORARY_EXTENSION]
+ : []),
+];
+
+// All debug target panes (to filter out if any of the panels should be excluded).
+const REMOTE_DEBUG_TARGET_PANES = [...ALL_DEBUG_TARGET_PANES];
+
+const THIS_FIREFOX_DEBUG_TARGET_PANES = ALL_DEBUG_TARGET_PANES
+ // Main process debugging is not available for This Firefox.
+ // At the moment only the main process is listed under processes, so remove the category
+ // for this runtime.
+ .filter(
+ p => p !== DEBUG_TARGET_PANE.PROCESSES || isLocalProcessDebuggingSupported()
+ )
+ // Showing tab targets for This Firefox is behind a preference.
+ .filter(p => p !== DEBUG_TARGET_PANE.TAB || isLocalTabDebuggingSupported());
+
+const SUPPORTED_TARGET_PANE_BY_RUNTIME = {
+ [RUNTIMES.THIS_FIREFOX]: THIS_FIREFOX_DEBUG_TARGET_PANES,
+ [RUNTIMES.USB]: REMOTE_DEBUG_TARGET_PANES,
+ [RUNTIMES.NETWORK]: REMOTE_DEBUG_TARGET_PANES,
+};
+
+/**
+ * A debug target pane is more specialized than a debug target. For instance EXTENSION is
+ * a DEBUG_TARGET but INSTALLED_EXTENSION and TEMPORARY_EXTENSION are DEBUG_TARGET_PANES.
+ */
+function isSupportedDebugTargetPane(runtimeType, debugTargetPaneKey) {
+ return SUPPORTED_TARGET_PANE_BY_RUNTIME[runtimeType].includes(
+ debugTargetPaneKey
+ );
+}
+exports.isSupportedDebugTargetPane = isSupportedDebugTargetPane;
+
+/**
+ * Check if the given runtimeType supports temporary extension installation
+ * from about:debugging (currently disallowed on non-local runtimes).
+ */
+function supportsTemporaryExtensionInstaller(runtimeType) {
+ return runtimeType === RUNTIMES.THIS_FIREFOX;
+}
+exports.supportsTemporaryExtensionInstaller =
+ supportsTemporaryExtensionInstaller;
diff --git a/devtools/client/aboutdebugging/src/modules/extensions-helper.js b/devtools/client/aboutdebugging/src/modules/extensions-helper.js
new file mode 100644
index 0000000000..ec0d7c2661
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/extensions-helper.js
@@ -0,0 +1,92 @@
+/* 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 lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+});
+
+const {
+ PREFERENCES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+exports.parseFileUri = function (url) {
+ // Strip a leading slash from Windows drive letter URIs.
+ // file:///home/foo ~> /home/foo
+ // file:///C:/foo ~> C:/foo
+ const windowsRegex = /^file:\/\/\/([a-zA-Z]:\/.*)/;
+ if (windowsRegex.test(url)) {
+ return windowsRegex.exec(url)[1];
+ }
+ return url.slice("file://".length);
+};
+
+exports.getExtensionUuid = function (extension) {
+ const { manifestURL } = extension;
+ // Strip off the protocol and rest, leaving us with just the UUID.
+ return manifestURL ? /moz-extension:\/\/([^/]*)/.exec(manifestURL)[1] : null;
+};
+
+/**
+ * Open a file picker to allow the user to locate a temporary extension. A temporary
+ * extension can either be:
+ * - a folder
+ * - a .xpi file
+ * - a .zip file
+ *
+ * @param {Window} win
+ * The window object where the filepicker should be opened.
+ * Note: We cannot use the global window object here because it is undefined if
+ * this module is loaded from a file outside of devtools/client/aboutdebugging/.
+ * See browser-loader.js `uri.startsWith(baseURI)` for more details.
+ * @param {String} message
+ * The help message that should be displayed to the user in the filepicker.
+ * @return {Promise} returns a promise that resolves a File object corresponding to the
+ * file selected by the user.
+ */
+exports.openTemporaryExtension = function (win, message) {
+ return new Promise(resolve => {
+ const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(win, message, Ci.nsIFilePicker.modeOpen);
+
+ // Try to set the last directory used as "displayDirectory".
+ try {
+ const lastDirPath = Services.prefs.getCharPref(
+ PREFERENCES.TEMPORARY_EXTENSION_PATH,
+ ""
+ );
+ const lastDir = new lazy.FileUtils.File(lastDirPath);
+ fp.displayDirectory = lastDir;
+ } catch (e) {
+ // Empty or invalid value, nothing to handle.
+ }
+
+ fp.open(res => {
+ if (res == Ci.nsIFilePicker.returnCancel || !fp.file) {
+ return;
+ }
+ let file = fp.file;
+ // AddonManager.installTemporaryAddon accepts either
+ // addon directory or final xpi file.
+ if (
+ !file.isDirectory() &&
+ !file.leafName.endsWith(".xpi") &&
+ !file.leafName.endsWith(".zip")
+ ) {
+ file = file.parent;
+ }
+
+ // We are about to resolve, store the path to the file for the next call.
+ Services.prefs.setCharPref(
+ PREFERENCES.TEMPORARY_EXTENSION_PATH,
+ file.path
+ );
+
+ resolve(file);
+ });
+ });
+};
diff --git a/devtools/client/aboutdebugging/src/modules/l10n.js b/devtools/client/aboutdebugging/src/modules/l10n.js
new file mode 100644
index 0000000000..88c7ae8bb1
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/l10n.js
@@ -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/. */
+
+"use strict";
+
+const {
+ FluentL10n,
+} = require("resource://devtools/client/shared/fluent-l10n/fluent-l10n.js");
+
+// exports a singleton, which will be used across all aboutdebugging modules
+exports.l10n = new FluentL10n();
diff --git a/devtools/client/aboutdebugging/src/modules/moz.build b/devtools/client/aboutdebugging/src/modules/moz.build
new file mode 100644
index 0000000000..909e181714
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/moz.build
@@ -0,0 +1,16 @@
+# 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(
+ "client-wrapper.js",
+ "debug-target-collapsibilities.js",
+ "debug-target-support.js",
+ "extensions-helper.js",
+ "l10n.js",
+ "network-locations.js",
+ "runtime-client-factory.js",
+ "runtime-default-preferences.js",
+ "runtimes-state-helper.js",
+ "usb-runtimes.js",
+)
diff --git a/devtools/client/aboutdebugging/src/modules/network-locations.js b/devtools/client/aboutdebugging/src/modules/network-locations.js
new file mode 100644
index 0000000000..cbae436df7
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/network-locations.js
@@ -0,0 +1,69 @@
+/* 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 NETWORK_LOCATIONS_PREF = "devtools.aboutdebugging.network-locations";
+
+/**
+ * This module provides a collection of helper methods to read and update network
+ * locations monitored by about-debugging.
+ */
+
+function addNetworkLocationsObserver(listener) {
+ Services.prefs.addObserver(NETWORK_LOCATIONS_PREF, listener);
+}
+exports.addNetworkLocationsObserver = addNetworkLocationsObserver;
+
+function removeNetworkLocationsObserver(listener) {
+ Services.prefs.removeObserver(NETWORK_LOCATIONS_PREF, listener);
+}
+exports.removeNetworkLocationsObserver = removeNetworkLocationsObserver;
+
+/**
+ * Read the current preference value for aboutdebugging network locations.
+ * Will throw if the value cannot be parsed or is not an array.
+ */
+function _parsePreferenceAsArray() {
+ const pref = Services.prefs.getStringPref(NETWORK_LOCATIONS_PREF, "[]");
+ const parsedValue = JSON.parse(pref);
+ if (!Array.isArray(parsedValue)) {
+ throw new Error("Expected array value in " + NETWORK_LOCATIONS_PREF);
+ }
+ return parsedValue;
+}
+
+function getNetworkLocations() {
+ try {
+ return _parsePreferenceAsArray();
+ } catch (e) {
+ Services.prefs.clearUserPref(NETWORK_LOCATIONS_PREF);
+ return [];
+ }
+}
+exports.getNetworkLocations = getNetworkLocations;
+
+function addNetworkLocation(location) {
+ const locations = getNetworkLocations();
+ const locationsSet = new Set(locations);
+ locationsSet.add(location);
+
+ Services.prefs.setStringPref(
+ NETWORK_LOCATIONS_PREF,
+ JSON.stringify([...locationsSet])
+ );
+}
+exports.addNetworkLocation = addNetworkLocation;
+
+function removeNetworkLocation(location) {
+ const locations = getNetworkLocations();
+ const locationsSet = new Set(locations);
+ locationsSet.delete(location);
+
+ Services.prefs.setStringPref(
+ NETWORK_LOCATIONS_PREF,
+ JSON.stringify([...locationsSet])
+ );
+}
+exports.removeNetworkLocation = removeNetworkLocation;
diff --git a/devtools/client/aboutdebugging/src/modules/runtime-client-factory.js b/devtools/client/aboutdebugging/src/modules/runtime-client-factory.js
new file mode 100644
index 0000000000..e553416c18
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/runtime-client-factory.js
@@ -0,0 +1,68 @@
+/* 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 {
+ prepareTCPConnection,
+} = require("resource://devtools/client/shared/remote-debugging/adb/commands/index.js");
+const {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const {
+ ClientWrapper,
+} = require("resource://devtools/client/aboutdebugging/src/modules/client-wrapper.js");
+const {
+ remoteClientManager,
+} = require("resource://devtools/client/shared/remote-debugging/remote-client-manager.js");
+
+const {
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+async function createLocalClient() {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+ DevToolsServer.allowChromeProcess = true;
+
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ await client.connect();
+ return new ClientWrapper(client);
+}
+
+async function createNetworkClient(host, port) {
+ const transport = await DevToolsClient.socketConnect({ host, port });
+ const client = new DevToolsClient(transport);
+ await client.connect();
+ return new ClientWrapper(client);
+}
+
+async function createUSBClient(deviceId, socketPath) {
+ const port = await prepareTCPConnection(deviceId, socketPath);
+ return createNetworkClient("localhost", port);
+}
+
+async function createClientForRuntime(runtime) {
+ const { extra, id, type } = runtime;
+
+ if (type === RUNTIMES.THIS_FIREFOX) {
+ return createLocalClient();
+ } else if (remoteClientManager.hasClient(id, type)) {
+ const client = remoteClientManager.getClient(id, type);
+ return new ClientWrapper(client);
+ } else if (type === RUNTIMES.NETWORK) {
+ const { host, port } = extra.connectionParameters;
+ return createNetworkClient(host, port);
+ } else if (type === RUNTIMES.USB) {
+ const { deviceId, socketPath } = extra.connectionParameters;
+ return createUSBClient(deviceId, socketPath);
+ }
+
+ return null;
+}
+
+exports.createClientForRuntime = createClientForRuntime;
diff --git a/devtools/client/aboutdebugging/src/modules/runtime-default-preferences.js b/devtools/client/aboutdebugging/src/modules/runtime-default-preferences.js
new file mode 100644
index 0000000000..02c06334f7
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/runtime-default-preferences.js
@@ -0,0 +1,101 @@
+/* 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 provides a workaround for remote debugging when a preference is
+ * defined in the firefox preference file (browser/app/profile/firefox.js) but
+ * still read from the server, without any default value.
+ *
+ * This causes the server to crash and can't easily be recovered.
+ *
+ * While we work on better linting to prevent such issues (Bug 1660182), this
+ * module will be able to set default values for all missing preferences.
+ */
+
+const PREFERENCE_TYPES = {
+ BOOL: "BOOL",
+ CHAR: "CHAR",
+ INT: "INT",
+};
+exports.PREFERENCE_TYPES = PREFERENCE_TYPES;
+
+/**
+ * Expected properties for the preference descriptors:
+ * - prefName {String}: the name of the preference.
+ * - defaultValue {String|Bool|Number}: the value to set if the preference is
+ * missing.
+ * - trait {String}: the name of the trait corresponding to this pref on the
+ * PreferenceFront.
+ * - type {String}: the preference type (either BOOL, CHAR or INT).
+ */
+const DEFAULT_PREFERENCES = [];
+exports.DEFAULT_PREFERENCES = DEFAULT_PREFERENCES;
+
+const METHODS = {
+ [PREFERENCE_TYPES.BOOL]: {
+ setPref: "setBoolPref",
+ getPref: "getBoolPref",
+ },
+ [PREFERENCE_TYPES.CHAR]: {
+ setPref: "setCharPref",
+ getPref: "getCharPref",
+ },
+ [PREFERENCE_TYPES.INT]: {
+ setPref: "setIntPref",
+ getPref: "getIntPref",
+ },
+};
+
+/**
+ * Set default values for all the provided preferences on the runtime
+ * corresponding to the provided clientWrapper, if needed.
+ *
+ * Note: prefDescriptors will most likely be DEFAULT_PREFERENCES when
+ * used in production code, but can be parameterized for tests.
+ *
+ * @param {ClientWrapper} clientWrapper
+ * @param {Array} prefDescriptors
+ * Array of preference descriptors, see DEFAULT_PREFERENCES.
+ */
+async function setDefaultPreferencesIfNeeded(clientWrapper, prefDescriptors) {
+ if (!prefDescriptors || prefDescriptors.length === 0) {
+ return;
+ }
+
+ const preferenceFront = await clientWrapper.getFront("preference");
+ const preferenceTraits = await preferenceFront.getTraits();
+
+ // Note: using Promise.all here fails because the request/responses get mixed.
+ for (const prefDescriptor of prefDescriptors) {
+ // If the fix for this preference is already on this server, skip it.
+ if (preferenceTraits[prefDescriptor.trait]) {
+ continue;
+ }
+
+ await setDefaultPreference(preferenceFront, prefDescriptor);
+ }
+}
+exports.setDefaultPreferencesIfNeeded = setDefaultPreferencesIfNeeded;
+
+async function setDefaultPreference(preferenceFront, prefDescriptor) {
+ const { prefName, type, defaultValue } = prefDescriptor;
+
+ if (!Object.values(PREFERENCE_TYPES).includes(type)) {
+ throw new Error(`Unsupported type for setDefaultPreference "${type}"`);
+ }
+
+ const prefMethods = METHODS[type];
+ try {
+ // Try to read the preference only to check if the call is successful.
+ // If not, this means the preference is missing and should be initialized.
+ await preferenceFront[prefMethods.getPref](prefName);
+ } catch (e) {
+ console.warn(
+ `Preference "${prefName}"" is not set on the remote runtime. Setting default value.`
+ );
+ await preferenceFront[prefMethods.setPref](prefName, defaultValue);
+ }
+}
diff --git a/devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js b/devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js
new file mode 100644
index 0000000000..1864ef8341
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js
@@ -0,0 +1,37 @@
+/* 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 getCurrentRuntime(runtimesState) {
+ const selectedRuntimeId = runtimesState.selectedRuntimeId;
+ return findRuntimeById(selectedRuntimeId, runtimesState);
+}
+exports.getCurrentRuntime = getCurrentRuntime;
+
+function getCurrentClient(runtimesState) {
+ const runtimeDetails = getCurrentRuntimeDetails(runtimesState);
+ return runtimeDetails ? runtimeDetails.clientWrapper : null;
+}
+exports.getCurrentClient = getCurrentClient;
+
+function findRuntimeById(id, runtimesState) {
+ return getAllRuntimes(runtimesState).find(r => r.id === id);
+}
+exports.findRuntimeById = findRuntimeById;
+
+function getAllRuntimes(runtimesState) {
+ return [
+ ...runtimesState.networkRuntimes,
+ ...runtimesState.thisFirefoxRuntimes,
+ ...runtimesState.usbRuntimes,
+ ];
+}
+exports.getAllRuntimes = getAllRuntimes;
+
+function getCurrentRuntimeDetails(runtimesState) {
+ const runtime = getCurrentRuntime(runtimesState);
+ return runtime ? runtime.runtimeDetails : null;
+}
+exports.getCurrentRuntimeDetails = getCurrentRuntimeDetails;
diff --git a/devtools/client/aboutdebugging/src/modules/usb-runtimes.js b/devtools/client/aboutdebugging/src/modules/usb-runtimes.js
new file mode 100644
index 0000000000..887dc6788e
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/usb-runtimes.js
@@ -0,0 +1,122 @@
+/* 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,
+ "adb",
+ "resource://devtools/client/shared/remote-debugging/adb/adb.js",
+ true
+);
+
+/**
+ * Used to represent a regular runtime returned by ADB.
+ */
+class UsbRuntime {
+ constructor(adbRuntime) {
+ this.id = adbRuntime.id;
+ this.deviceId = adbRuntime.deviceId;
+ this.deviceName = adbRuntime.deviceName;
+ this.shortName = adbRuntime.shortName;
+ this.socketPath = adbRuntime.socketPath;
+ this.isFenix = adbRuntime.isFenix;
+ this.isUnavailable = false;
+ this.isUnplugged = false;
+ this.versionName = adbRuntime.versionName;
+ }
+}
+
+/**
+ * Used when a device was detected, meaning USB debugging is enabled on the device, but no
+ * runtime/browser is available for connection.
+ */
+class UnavailableUsbRuntime {
+ constructor(adbDevice) {
+ this.id = adbDevice.id + "|unavailable";
+ this.deviceId = adbDevice.id;
+ this.deviceName = adbDevice.name;
+ this.shortName = "Unavailable runtime";
+ this.socketPath = null;
+ this.isFenix = false;
+ this.isUnavailable = true;
+ this.isUnplugged = false;
+ this.versionName = null;
+ }
+}
+
+/**
+ * Used to represent USB devices that were previously connected but are now missing
+ * (presumably after being unplugged/disconnected from the computer).
+ */
+class UnpluggedUsbRuntime {
+ constructor(deviceId, deviceName) {
+ this.id = deviceId + "|unplugged";
+ this.deviceId = deviceId;
+ this.deviceName = deviceName;
+ this.shortName = "Unplugged runtime";
+ this.socketPath = null;
+ this.isFenix = false;
+ this.isUnavailable = true;
+ this.isUnplugged = true;
+ this.versionName = null;
+ }
+}
+
+/**
+ * Map used to keep track of discovered usb devices. Will be used to create the unplugged
+ * usb runtimes.
+ */
+const devices = new Map();
+
+/**
+ * This module provides a collection of helper methods to detect USB runtimes whom Firefox
+ * is running on.
+ */
+function addUSBRuntimesObserver(listener) {
+ adb.registerListener(listener);
+}
+exports.addUSBRuntimesObserver = addUSBRuntimesObserver;
+
+async function getUSBRuntimes() {
+ // Get the available runtimes
+ const runtimes = adb.getRuntimes().map(r => new UsbRuntime(r));
+
+ // Get devices found by ADB, but without any available runtime.
+ const runtimeDevices = runtimes.map(r => r.deviceId);
+ const unavailableRuntimes = adb
+ .getDevices()
+ .filter(d => !runtimeDevices.includes(d.id))
+ .map(d => new UnavailableUsbRuntime(d));
+
+ // Add all devices to the map detected devices.
+ const allRuntimes = runtimes.concat(unavailableRuntimes);
+ for (const runtime of allRuntimes) {
+ devices.set(runtime.deviceId, runtime.deviceName);
+ }
+
+ // Get devices previously found by ADB but no longer available.
+ const currentDevices = allRuntimes.map(r => r.deviceId);
+ const detectedDevices = [...devices.keys()];
+ const unpluggedDevices = detectedDevices.filter(
+ id => !currentDevices.includes(id)
+ );
+ const unpluggedRuntimes = unpluggedDevices.map(deviceId => {
+ const deviceName = devices.get(deviceId);
+ return new UnpluggedUsbRuntime(deviceId, deviceName);
+ });
+
+ return allRuntimes.concat(unpluggedRuntimes);
+}
+exports.getUSBRuntimes = getUSBRuntimes;
+
+function removeUSBRuntimesObserver(listener) {
+ adb.unregisterListener(listener);
+}
+exports.removeUSBRuntimesObserver = removeUSBRuntimesObserver;
+
+function refreshUSBRuntimes() {
+ return adb.updateRuntimes();
+}
+exports.refreshUSBRuntimes = refreshUSBRuntimes;
diff --git a/devtools/client/aboutdebugging/src/moz.build b/devtools/client/aboutdebugging/src/moz.build
new file mode 100644
index 0000000000..58e6f92857
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/moz.build
@@ -0,0 +1,17 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ "actions",
+ "components",
+ "middleware",
+ "modules",
+ "reducers",
+ "types",
+]
+
+DevToolsModules(
+ "constants.js",
+ "create-store.js",
+)
diff --git a/devtools/client/aboutdebugging/src/reducers/debug-targets-state.js b/devtools/client/aboutdebugging/src/reducers/debug-targets-state.js
new file mode 100644
index 0000000000..8ee0473e3c
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/reducers/debug-targets-state.js
@@ -0,0 +1,155 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ EXTENSION_BGSCRIPT_STATUS_UPDATED,
+ REQUEST_EXTENSIONS_SUCCESS,
+ REQUEST_PROCESSES_SUCCESS,
+ REQUEST_TABS_SUCCESS,
+ REQUEST_WORKERS_SUCCESS,
+ TEMPORARY_EXTENSION_RELOAD_FAILURE,
+ TEMPORARY_EXTENSION_RELOAD_START,
+ TERMINATE_EXTENSION_BGSCRIPT_FAILURE,
+ TERMINATE_EXTENSION_BGSCRIPT_START,
+ UNWATCH_RUNTIME_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+function DebugTargetsState() {
+ return {
+ installedExtensions: [],
+ otherWorkers: [],
+ processes: [],
+ serviceWorkers: [],
+ sharedWorkers: [],
+ tabs: [],
+ temporaryExtensions: [],
+ };
+}
+
+function updateExtensionDetails(extensions, id, updatedDetails) {
+ // extensions is meant to be either state.installExtensions or state.temporaryExtensions.
+ return extensions.map(extension => {
+ if (extension.id === id) {
+ extension = Object.assign({}, extension);
+
+ extension.details = Object.assign({}, extension.details, updatedDetails);
+ }
+ return extension;
+ });
+}
+
+function updateTemporaryExtension(state, id, updatedDetails) {
+ return updateExtensionDetails(state.temporaryExtensions, id, updatedDetails);
+}
+
+function updateInstalledExtension(state, id, updatedDetails) {
+ return updateExtensionDetails(state.installedExtensions, id, updatedDetails);
+}
+
+function updateExtension(state, id, updatedDetails) {
+ return {
+ installedExtensions: updateInstalledExtension(state, id, updatedDetails),
+ temporaryExtensions: updateTemporaryExtension(state, id, updatedDetails),
+ };
+}
+
+function debugTargetsReducer(state = DebugTargetsState(), action) {
+ switch (action.type) {
+ case UNWATCH_RUNTIME_SUCCESS: {
+ return DebugTargetsState();
+ }
+ case REQUEST_EXTENSIONS_SUCCESS: {
+ const { installedExtensions, temporaryExtensions } = action;
+ return Object.assign({}, state, {
+ installedExtensions,
+ temporaryExtensions,
+ });
+ }
+ case REQUEST_PROCESSES_SUCCESS: {
+ const { processes } = action;
+ return Object.assign({}, state, { processes });
+ }
+ case REQUEST_TABS_SUCCESS: {
+ const { tabs } = action;
+ return Object.assign({}, state, { tabs });
+ }
+ case REQUEST_WORKERS_SUCCESS: {
+ const { otherWorkers, serviceWorkers, sharedWorkers } = action;
+ return Object.assign({}, state, {
+ otherWorkers,
+ serviceWorkers,
+ sharedWorkers,
+ });
+ }
+ case TEMPORARY_EXTENSION_RELOAD_FAILURE: {
+ const { id, error } = action;
+ const temporaryExtensions = updateTemporaryExtension(state, id, {
+ reloadError: error.message,
+ });
+ return Object.assign({}, state, { temporaryExtensions });
+ }
+ case TEMPORARY_EXTENSION_RELOAD_START: {
+ const { id } = action;
+ const temporaryExtensions = updateTemporaryExtension(state, id, {
+ reloadError: null,
+ });
+ return Object.assign({}, state, { temporaryExtensions });
+ }
+ case TERMINATE_EXTENSION_BGSCRIPT_START: {
+ const { id } = action;
+ const { installedExtensions, temporaryExtensions } = updateExtension(
+ state,
+ id,
+ {
+ // Clear the last error if one was still set.
+ lastTerminateBackgroundScriptError: null,
+ }
+ );
+ return Object.assign({}, state, {
+ installedExtensions,
+ temporaryExtensions,
+ });
+ }
+ case TERMINATE_EXTENSION_BGSCRIPT_FAILURE: {
+ const { id, error } = action;
+ const { installedExtensions, temporaryExtensions } = updateExtension(
+ state,
+ id,
+ {
+ lastTerminateBackgroundScriptError: error.message,
+ }
+ );
+ return Object.assign({}, state, {
+ installedExtensions,
+ temporaryExtensions,
+ });
+ }
+ case EXTENSION_BGSCRIPT_STATUS_UPDATED: {
+ const { id, backgroundScriptStatus } = action;
+ const { installedExtensions, temporaryExtensions } = updateExtension(
+ state,
+ id,
+ {
+ backgroundScriptStatus,
+ // Clear the last error if one was still set.
+ lastTerminateBackgroundScriptError: null,
+ }
+ );
+ return Object.assign({}, state, {
+ installedExtensions,
+ temporaryExtensions,
+ });
+ }
+
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ DebugTargetsState,
+ debugTargetsReducer,
+};
diff --git a/devtools/client/aboutdebugging/src/reducers/index.js b/devtools/client/aboutdebugging/src/reducers/index.js
new file mode 100644
index 0000000000..8f104a4ff7
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/reducers/index.js
@@ -0,0 +1,24 @@
+/* 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 {
+ combineReducers,
+} = require("resource://devtools/client/shared/vendor/redux.js");
+const {
+ debugTargetsReducer,
+} = require("resource://devtools/client/aboutdebugging/src/reducers/debug-targets-state.js");
+const {
+ runtimesReducer,
+} = require("resource://devtools/client/aboutdebugging/src/reducers/runtimes-state.js");
+const {
+ uiReducer,
+} = require("resource://devtools/client/aboutdebugging/src/reducers/ui-state.js");
+
+module.exports = combineReducers({
+ debugTargets: debugTargetsReducer,
+ runtimes: runtimesReducer,
+ ui: uiReducer,
+});
diff --git a/devtools/client/aboutdebugging/src/reducers/moz.build b/devtools/client/aboutdebugging/src/reducers/moz.build
new file mode 100644
index 0000000000..24d3382f5b
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/reducers/moz.build
@@ -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/.
+
+DevToolsModules(
+ "debug-targets-state.js",
+ "index.js",
+ "runtimes-state.js",
+ "ui-state.js",
+)
diff --git a/devtools/client/aboutdebugging/src/reducers/runtimes-state.js b/devtools/client/aboutdebugging/src/reducers/runtimes-state.js
new file mode 100644
index 0000000000..5acf35390a
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/reducers/runtimes-state.js
@@ -0,0 +1,178 @@
+/* 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 {
+ CONNECT_RUNTIME_CANCEL,
+ CONNECT_RUNTIME_FAILURE,
+ CONNECT_RUNTIME_NOT_RESPONDING,
+ CONNECT_RUNTIME_START,
+ CONNECT_RUNTIME_SUCCESS,
+ DISCONNECT_RUNTIME_SUCCESS,
+ RUNTIMES,
+ UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS,
+ REMOTE_RUNTIMES_UPDATED,
+ SELECTED_RUNTIME_ID_UPDATED,
+ THIS_FIREFOX_RUNTIME_CREATED,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const {
+ findRuntimeById,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
+
+const {
+ remoteClientManager,
+} = require("resource://devtools/client/shared/remote-debugging/remote-client-manager.js");
+
+// Map between known runtime types and nodes in the runtimes state.
+const TYPE_TO_RUNTIMES_KEY = {
+ [RUNTIMES.THIS_FIREFOX]: "thisFirefoxRuntimes",
+ [RUNTIMES.NETWORK]: "networkRuntimes",
+ [RUNTIMES.USB]: "usbRuntimes",
+};
+
+function RuntimesState() {
+ return {
+ networkRuntimes: [],
+ selectedRuntimeId: null,
+ // "This Firefox" runtimes is an array for consistency, but it should only contain one
+ // runtime. This runtime will be added after initializing the application via
+ // THIS_FIREFOX_RUNTIME_CREATED.
+ thisFirefoxRuntimes: [],
+ usbRuntimes: [],
+ };
+}
+
+/**
+ * Update the runtime matching the provided runtimeId with the content of updatedRuntime,
+ * and return the new state.
+ *
+ * @param {String} runtimeId
+ * The id of the runtime to update
+ * @param {Object} updatedRuntime
+ * Object used to update the runtime matching the idea using Object.assign.
+ * @param {Object} state
+ * Current runtimes state.
+ * @return {Object} The updated state
+ */
+function _updateRuntimeById(runtimeId, updatedRuntime, state) {
+ // Find the array of runtimes that contains the updated runtime.
+ const runtime = findRuntimeById(runtimeId, state);
+ const key = TYPE_TO_RUNTIMES_KEY[runtime.type];
+ const runtimesToUpdate = state[key];
+
+ // Update the runtime with the provided updatedRuntime.
+ const updatedRuntimes = runtimesToUpdate.map(r => {
+ if (r.id === runtimeId) {
+ return Object.assign({}, r, updatedRuntime);
+ }
+ return r;
+ });
+ return Object.assign({}, state, { [key]: updatedRuntimes });
+}
+
+function runtimesReducer(state = RuntimesState(), action) {
+ switch (action.type) {
+ case CONNECT_RUNTIME_START: {
+ const { id } = action;
+ const updatedState = {
+ isConnecting: true,
+ isConnectionFailed: false,
+ isConnectionNotResponding: false,
+ isConnectionTimeout: false,
+ };
+ return _updateRuntimeById(id, updatedState, state);
+ }
+
+ case CONNECT_RUNTIME_NOT_RESPONDING: {
+ const { id } = action;
+ return _updateRuntimeById(id, { isConnectionNotResponding: true }, state);
+ }
+
+ case CONNECT_RUNTIME_CANCEL: {
+ const { id } = action;
+ const updatedState = {
+ isConnecting: false,
+ isConnectionFailed: false,
+ isConnectionNotResponding: false,
+ isConnectionTimeout: true,
+ };
+ return _updateRuntimeById(id, updatedState, state);
+ }
+
+ case CONNECT_RUNTIME_SUCCESS: {
+ const { id, runtimeDetails, type } = action.runtime;
+
+ // Update the remoteClientManager with the connected runtime.
+ const client = runtimeDetails.clientWrapper.client;
+ const runtimeInfo = runtimeDetails.info;
+ remoteClientManager.setClient(id, type, client, runtimeInfo);
+
+ const updatedState = {
+ isConnecting: false,
+ isConnectionFailed: false,
+ isConnectionNotResponding: false,
+ isConnectionTimeout: false,
+ runtimeDetails,
+ };
+ return _updateRuntimeById(id, updatedState, state);
+ }
+
+ case CONNECT_RUNTIME_FAILURE: {
+ const { id } = action;
+ const updatedState = {
+ isConnecting: false,
+ isConnectionFailed: true,
+ isConnectionNotResponding: false,
+ isConnectionTimeout: false,
+ };
+ return _updateRuntimeById(id, updatedState, state);
+ }
+
+ case DISCONNECT_RUNTIME_SUCCESS: {
+ const { id, type } = action.runtime;
+ remoteClientManager.removeClient(id, type);
+ return _updateRuntimeById(id, { runtimeDetails: null }, state);
+ }
+
+ case SELECTED_RUNTIME_ID_UPDATED: {
+ const selectedRuntimeId = action.runtimeId || null;
+ return Object.assign({}, state, { selectedRuntimeId });
+ }
+
+ case UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS: {
+ const { connectionPromptEnabled } = action;
+ const { id: runtimeId } = action.runtime;
+ const runtime = findRuntimeById(runtimeId, state);
+ const runtimeDetails = Object.assign({}, runtime.runtimeDetails, {
+ connectionPromptEnabled,
+ });
+ return _updateRuntimeById(runtimeId, { runtimeDetails }, state);
+ }
+
+ case REMOTE_RUNTIMES_UPDATED: {
+ const { runtimes, runtimeType } = action;
+ const key = TYPE_TO_RUNTIMES_KEY[runtimeType];
+ return Object.assign({}, state, {
+ [key]: runtimes,
+ });
+ }
+
+ case THIS_FIREFOX_RUNTIME_CREATED: {
+ const { runtime } = action;
+ return Object.assign({}, state, {
+ thisFirefoxRuntimes: [runtime],
+ });
+ }
+
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ RuntimesState,
+ runtimesReducer,
+};
diff --git a/devtools/client/aboutdebugging/src/reducers/ui-state.js b/devtools/client/aboutdebugging/src/reducers/ui-state.js
new file mode 100644
index 0000000000..771358eaac
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/reducers/ui-state.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 {
+ ADB_ADDON_STATUS_UPDATED,
+ ADB_READY_UPDATED,
+ DEBUG_TARGET_COLLAPSIBILITY_UPDATED,
+ HIDE_PROFILER_DIALOG,
+ NETWORK_LOCATIONS_UPDATE_SUCCESS,
+ PROFILER_PAGE_CONTEXT,
+ SELECT_PAGE_SUCCESS,
+ SHOW_PROFILER_DIALOG,
+ SWITCH_PROFILER_CONTEXT,
+ TEMPORARY_EXTENSION_INSTALL_FAILURE,
+ TEMPORARY_EXTENSION_INSTALL_SUCCESS,
+ USB_RUNTIMES_SCAN_START,
+ USB_RUNTIMES_SCAN_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+function UiState(
+ locations = [],
+ debugTargetCollapsibilities = {},
+ showHiddenAddons = false
+) {
+ return {
+ adbAddonStatus: null,
+ debugTargetCollapsibilities,
+ isAdbReady: false,
+ isScanningUsb: false,
+ networkLocations: locations,
+ profilerContext: PROFILER_PAGE_CONTEXT.DEVTOOLS_REMOTE,
+ selectedPage: null,
+ showProfilerDialog: false,
+ showHiddenAddons,
+ temporaryInstallError: null,
+ };
+}
+
+function uiReducer(state = UiState(), action) {
+ switch (action.type) {
+ case ADB_ADDON_STATUS_UPDATED: {
+ const { adbAddonStatus } = action;
+ return Object.assign({}, state, { adbAddonStatus });
+ }
+
+ case ADB_READY_UPDATED: {
+ const { isAdbReady } = action;
+ return Object.assign({}, state, { isAdbReady });
+ }
+
+ case DEBUG_TARGET_COLLAPSIBILITY_UPDATED: {
+ const { isCollapsed, key } = action;
+ const debugTargetCollapsibilities = new Map(
+ state.debugTargetCollapsibilities
+ );
+ debugTargetCollapsibilities.set(key, isCollapsed);
+ return Object.assign({}, state, { debugTargetCollapsibilities });
+ }
+
+ case NETWORK_LOCATIONS_UPDATE_SUCCESS: {
+ const { locations } = action;
+ return Object.assign({}, state, { networkLocations: locations });
+ }
+
+ case SELECT_PAGE_SUCCESS: {
+ const { page } = action;
+ return Object.assign({}, state, { selectedPage: page });
+ }
+
+ case SHOW_PROFILER_DIALOG: {
+ return Object.assign({}, state, {
+ showProfilerDialog: true,
+ // Always start in the devtools-remote view.
+ profilerContext: "devtools-remote",
+ });
+ }
+
+ case HIDE_PROFILER_DIALOG: {
+ return Object.assign({}, state, { showProfilerDialog: false });
+ }
+
+ case SWITCH_PROFILER_CONTEXT: {
+ const { profilerContext } = action;
+ return Object.assign({}, state, { profilerContext });
+ }
+
+ case USB_RUNTIMES_SCAN_START: {
+ return Object.assign({}, state, { isScanningUsb: true });
+ }
+
+ case USB_RUNTIMES_SCAN_SUCCESS: {
+ return Object.assign({}, state, { isScanningUsb: false });
+ }
+
+ case TEMPORARY_EXTENSION_INSTALL_SUCCESS: {
+ return Object.assign({}, state, { temporaryInstallError: null });
+ }
+
+ case TEMPORARY_EXTENSION_INSTALL_FAILURE: {
+ const { error } = action;
+ return Object.assign({}, state, { temporaryInstallError: error });
+ }
+
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ UiState,
+ uiReducer,
+};
diff --git a/devtools/client/aboutdebugging/src/types/debug-target.js b/devtools/client/aboutdebugging/src/types/debug-target.js
new file mode 100644
index 0000000000..c2f419082a
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/types/debug-target.js
@@ -0,0 +1,71 @@
+/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ DEBUG_TARGETS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const extensionTargetDetails = {
+ // actor ID for this extention.
+ actor: PropTypes.string.isRequired,
+ location: PropTypes.string.isRequired,
+ // error message forwarded from the WebExtensions internals if terminating the background script failed.
+ lastTerminateBackgroundScriptError: PropTypes.string,
+ // manifestURL points to the manifest.json file. This URL is only valid when debugging
+ // local extensions so it might be null.
+ manifestURL: PropTypes.string,
+ // error message forwarded from the addon manager during reloading temporary extension.
+ reloadError: PropTypes.string,
+ // unique extension id.
+ uuid: PropTypes.string.isRequired,
+ // warning messages forwarded from the addon manager.
+ warnings: PropTypes.arrayOf(PropTypes.string).isRequired,
+};
+
+const processTargetDetails = {
+ // Description for the process.
+ description: PropTypes.string.isRequired,
+};
+
+const tabTargetDetails = {
+ // the url of the tab.
+ url: PropTypes.string.isRequired,
+};
+
+const workerTargetDetails = {
+ // (service worker specific) one of "LISTENING", "NOT_LISTENING". undefined otherwise.
+ fetch: PropTypes.string,
+ // front for the ServiceWorkerRegistration related to this service worker.
+ registrationFront: PropTypes.object,
+ // (service worker specific) scope of the service worker registration.
+ scope: PropTypes.string,
+ // (service worker specific) one of "RUNNING", "REGISTERING", "STOPPED".
+ status: PropTypes.string,
+};
+
+const debugTarget = {
+ // details property will contain a type-specific object.
+ details: PropTypes.oneOfType([
+ PropTypes.shape(extensionTargetDetails),
+ PropTypes.shape(processTargetDetails),
+ PropTypes.shape(tabTargetDetails),
+ PropTypes.shape(workerTargetDetails),
+ ]).isRequired,
+ // icon to display for the debug target.
+ icon: PropTypes.string.isRequired,
+ // unique id for the target (unique in the scope of the application lifecycle).
+ // - extensions: {String} extension id (for instance "someextension@mozilla.org")
+ // - tabs: {Number} browserId
+ // - workers: {String} id for the WorkerTargetActor corresponding to the worker
+ id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
+ // display name for the debug target.
+ name: PropTypes.string.isRequired,
+ // one of "extension", "tab", "worker", "process".
+ type: PropTypes.oneOf(Object.values(DEBUG_TARGETS)).isRequired,
+};
+
+exports.debugTarget = PropTypes.shape(debugTarget);
diff --git a/devtools/client/aboutdebugging/src/types/index.js b/devtools/client/aboutdebugging/src/types/index.js
new file mode 100644
index 0000000000..288a063be1
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/types/index.js
@@ -0,0 +1,18 @@
+/* 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 debugTargetTypes = require("resource://devtools/client/aboutdebugging/src/types/debug-target.js");
+const runtimeTypes = require("resource://devtools/client/aboutdebugging/src/types/runtime.js");
+const uiTypes = require("resource://devtools/client/aboutdebugging/src/types/ui.js");
+
+module.exports = Object.assign(
+ {},
+ {
+ ...debugTargetTypes,
+ ...runtimeTypes,
+ ...uiTypes,
+ }
+);
diff --git a/devtools/client/aboutdebugging/src/types/moz.build b/devtools/client/aboutdebugging/src/types/moz.build
new file mode 100644
index 0000000000..a58a6e0e28
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/types/moz.build
@@ -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/.
+
+DevToolsModules(
+ "debug-target.js",
+ "index.js",
+ "runtime.js",
+ "ui.js",
+)
diff --git a/devtools/client/aboutdebugging/src/types/runtime.js b/devtools/client/aboutdebugging/src/types/runtime.js
new file mode 100644
index 0000000000..cef9ace204
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/types/runtime.js
@@ -0,0 +1,164 @@
+/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ ClientWrapper,
+} = require("resource://devtools/client/aboutdebugging/src/modules/client-wrapper.js");
+const {
+ COMPATIBILITY_STATUS,
+} = require("resource://devtools/client/shared/remote-debugging/version-checker.js");
+
+const runtimeInfo = {
+ // device name which is running the runtime,
+ // unavailable on this-firefox runtime
+ deviceName: PropTypes.string,
+
+ // icon which represents the kind of runtime
+ icon: PropTypes.string.isRequired,
+
+ // name of runtime such as "Firefox Nightly"
+ name: PropTypes.string.isRequired,
+
+ // operating system on which the runtime runs such as "Android", "Linux"
+ os: PropTypes.string.isRequired,
+
+ // runtime type, for instance "network", "usb" ...
+ type: PropTypes.string.isRequired,
+
+ // version of runtime
+ version: PropTypes.string.isRequired,
+};
+
+const compatibilityReport = {
+ // build ID for the current runtime (date formatted as yyyyMMdd eg "20193101")
+ localID: PropTypes.string.isRequired,
+
+ // "platform" version for the current runtime (eg "67.0a1")
+ localVersion: PropTypes.string.isRequired,
+
+ // minimum "platform" version supported for remote debugging by the current runtime
+ minVersion: PropTypes.string.isRequired,
+
+ // build ID for the target runtime (date formatted as yyyyMMdd eg "20193101")
+ runtimeID: PropTypes.string.isRequired,
+
+ // "platform" version for the target runtime (eg "67.0a1")
+ runtimeVersion: PropTypes.string.isRequired,
+
+ // report result, either COMPATIBLE, TOO_OLD or TOO_RECENT
+ status: PropTypes.oneOf(Object.values(COMPATIBILITY_STATUS)).isRequired,
+};
+exports.compatibilityReport = PropTypes.shape(compatibilityReport);
+
+const runtimeDetails = {
+ // True if this runtime supports debugging service workers.
+ // This might be undefined when connecting to runtimes older than Fx 66
+ canDebugServiceWorkers: PropTypes.bool,
+
+ // ClientWrapper built using a DevToolsClient for the runtime
+ clientWrapper: PropTypes.instanceOf(ClientWrapper).isRequired,
+
+ // compatibility report to check if the target runtime is in range of the backward
+ // compatibility policy for DevTools remote debugging.
+ compatibilityReport: PropTypes.shape(compatibilityReport).isRequired,
+
+ // reflect devtools.debugger.prompt-connection preference of this runtime
+ connectionPromptEnabled: PropTypes.bool.isRequired,
+
+ // runtime information
+ info: PropTypes.shape(runtimeInfo).isRequired,
+
+ // True if service workers should be available in the target runtime. Service workers
+ // can be disabled via preferences or if the runtime runs in fully private browsing
+ // mode.
+ serviceWorkersAvailable: PropTypes.bool.isRequired,
+};
+exports.runtimeDetails = PropTypes.shape(runtimeDetails);
+
+const networkRuntimeConnectionParameter = {
+ // host name of devtools server to connect
+ host: PropTypes.string.isRequired,
+
+ // port number of devtools server to connect
+ port: PropTypes.number.isRequired,
+};
+
+const usbRuntimeConnectionParameter = {
+ // device id
+ deviceId: PropTypes.string.isRequired,
+ // socket path to connect devtools server
+ socketPath: PropTypes.string.isRequired,
+};
+
+const runtimeExtra = {
+ // parameter to connect to devtools server
+ // unavailable on unavailable/unplugged runtimes
+ connectionParameters: PropTypes.oneOfType([
+ PropTypes.shape(networkRuntimeConnectionParameter),
+ PropTypes.shape(usbRuntimeConnectionParameter),
+ ]),
+
+ // device name
+ // unavailable on this-firefox and network-location runtimes
+ deviceName: PropTypes.string,
+
+ // version of the application coming from ADB, only available via USB. Useful for Fenix
+ // runtimes, because the version can't be retrieved from Service.appInfo.
+ adbPackageVersion: PropTypes.string,
+};
+
+const runtime = {
+ // unique id for the runtime
+ id: PropTypes.string.isRequired,
+
+ // object containing non standard properties that depend on the runtime type,
+ // unavailable on this-firefox runtime
+ extra: PropTypes.shape(runtimeExtra),
+
+ // this flag will be true when start to connect to the runtime, will be false after
+ // connected or has failures.
+ isConnecting: PropTypes.bool.isRequired,
+
+ // this flag will be true when the connection failed.
+ isConnectionFailed: PropTypes.bool.isRequired,
+
+ // will be true if connecting to runtime is taking time, will be false after connecting
+ // or failing.
+ isConnectionNotResponding: PropTypes.bool.isRequired,
+
+ // this flag will be true when the connection was timeout.
+ isConnectionTimeout: PropTypes.bool.isRequired,
+
+ // this flag will be true when the detected runtime is Fenix (Firefox Preview).
+ // Fenix need specific logic to get their display name, version and logos.
+ // Discussion ongoing in https://github.com/mozilla-mobile/fenix/issues/2016
+ isFenix: PropTypes.bool.isRequired,
+
+ // unavailable runtimes are placeholders for devices where the runtime has not been
+ // started yet. For instance an ADB device connected without a compatible runtime
+ // running.
+ isUnavailable: PropTypes.bool.isRequired,
+
+ // unplugged runtimes are placeholders for devices that are no longer available. For
+ // instance a USB device that was unplugged from the computer.
+ isUnplugged: PropTypes.bool.isRequired,
+
+ // display name of the runtime
+ name: PropTypes.string.isRequired,
+
+ // available after the connection to the runtime is established
+ // unavailable on disconnected runtimes
+ runtimeDetails: PropTypes.shape(runtimeDetails),
+
+ // runtime type, for instance "network", "usb" ...
+ type: PropTypes.string.isRequired,
+};
+
+/**
+ * Export type of runtime
+ */
+exports.runtime = PropTypes.shape(runtime);
diff --git a/devtools/client/aboutdebugging/src/types/ui.js b/devtools/client/aboutdebugging/src/types/ui.js
new file mode 100644
index 0000000000..68daaaa5d2
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/types/ui.js
@@ -0,0 +1,85 @@
+/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ ADB_ADDON_STATES,
+} = require("resource://devtools/client/shared/remote-debugging/adb/adb-addon.js");
+const {
+ DEBUG_TARGET_PANE,
+ PAGE_TYPES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+function makeCollapsibilitiesType(isRequired) {
+ return (props, propName, componentName, _, propFullName) => {
+ if (isRequired && props[propName] === null) {
+ return new Error(
+ `Missing prop ${propFullName} marked as required in ${componentName}`
+ );
+ }
+
+ const error = new Error(
+ `Invalid prop ${propFullName} (${props[propName]}) supplied to ` +
+ `${componentName}. Collapsibilities needs to be a Map<DEBUG_TARGET_PANE, bool>`
+ );
+
+ const map = props[propName];
+
+ // check that the prop is a Map
+ if (!(map instanceof Map)) {
+ return error;
+ }
+
+ // check that the keys refer to debug target panes
+ const areKeysValid = [...map.keys()].every(x =>
+ Object.values(DEBUG_TARGET_PANE).includes(x)
+ );
+ // check that the values are boolean
+ const areValuesValid = [...map.values()].every(x => typeof x === "boolean");
+ // error if values or keys fail their checks
+ if (!areKeysValid || !areValuesValid) {
+ return error;
+ }
+
+ return null;
+ };
+}
+
+function makeLocationType(isRequired) {
+ return (props, propName, componentName, _, propFullName) => {
+ if (isRequired && props[propName] === null) {
+ return new Error(
+ `Missing prop ${propFullName} marked as required in ${componentName}`
+ );
+ }
+
+ // check that location is a string with a semicolon in it
+ if (!/\:/.test(props[propName])) {
+ return new Error(
+ `Invalid prop ${propFullName} (${props[propName]}) supplied to ` +
+ `${componentName}. Location needs to be a string with a host:port format`
+ );
+ }
+
+ return null;
+ };
+}
+
+const collapsibilities = makeCollapsibilitiesType(false);
+collapsibilities.isRequired = makeCollapsibilitiesType(true);
+
+const location = makeLocationType(false);
+location.isRequired = makeLocationType(true);
+
+module.exports = {
+ adbAddonStatus: PropTypes.oneOf(Object.values(ADB_ADDON_STATES)),
+ // a Map<DEBUG_TARGET_PANE, bool>, to flag collapsed/expanded status of the
+ // debug target panes
+ collapsibilities,
+ // a string with "host:port" format, used for network locations
+ location,
+ page: PropTypes.oneOf(Object.values(PAGE_TYPES)),
+};
diff --git a/devtools/client/aboutdebugging/test/browser/browser.ini b/devtools/client/aboutdebugging/test/browser/browser.ini
new file mode 100644
index 0000000000..4b471537bc
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser.ini
@@ -0,0 +1,167 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+prefs =
+ # showHiddenAddons has different values depending on the build flags, ensure consistent
+ # test behavior by always setting it to false.
+ devtools.aboutdebugging.showHiddenAddons=false
+support-files =
+ empty.html
+ head.js
+ helper-adb.js
+ helper-addons.js
+ helper-collapsibilities.js
+ helper-mocks.js
+ helper-real-usb.js
+ helper-serviceworker.js
+ helper-telemetry.js
+ mocks/*
+ resources/bad-extensions/*
+ resources/packaged-extension/*
+ resources/service-workers/*
+ resources/test-adb-extension/*
+ resources/test-temporary-extension/*
+ resources/doc_aboutdebugging_devtoolstoolbox_breakpoint.html
+ resources/script_aboutdebugging_devtoolstoolbox_breakpoint.js
+ test-tab-favicons.html
+ !/devtools/client/debugger/test/mochitest/shared-head.js
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+ !/devtools/client/webconsole/test/browser/shared-head.js
+
+[browser_aboutdebugging_addons_debug_console.js]
+tags = webextensions
+[browser_aboutdebugging_addons_debug_debugger.js]
+tags = webextensions
+[browser_aboutdebugging_addons_debug_inspector.js]
+tags = webextensions
+[browser_aboutdebugging_addons_debug_nobg.js]
+tags = webextensions
+[browser_aboutdebugging_addons_debug_popup.js]
+skip-if =
+ verify && debug # verify: crashes on shutdown, timeouts
+ os == "linux" && debug # linux debug Bug 1299001
+ win10_2004 && !debug # Bug 1744778
+tags = webextensions
+[browser_aboutdebugging_addons_debug_reload.js]
+tags = webextensions
+[browser_aboutdebugging_addons_debug_storage.js]
+tags = webextensions
+[browser_aboutdebugging_addons_debug_toolbox.js]
+tags = webextensions
+[browser_aboutdebugging_addons_eventpage_actions_and_status.js]
+tags = webextensions
+[browser_aboutdebugging_addons_eventpage_terminate_on_idle.js]
+tags = webextensions
+[browser_aboutdebugging_addons_manifest_url.js]
+skip-if =
+ os == "mac" && debug # ADB start() fails on linux 32, see Bug 1499638, macosx1014 debug due to 1514751
+[browser_aboutdebugging_addons_remote_runtime.js]
+[browser_aboutdebugging_addons_temporary_addon_buttons.js]
+skip-if =
+ os == "win" # On windows the AddonManager locks the XPI file loaded as a temporary extension and we can not test the reload of the extension.
+[browser_aboutdebugging_addons_temporary_id_message.js]
+[browser_aboutdebugging_addons_temporary_install_error.js]
+[browser_aboutdebugging_addons_temporary_install_path.js]
+[browser_aboutdebugging_addons_temporary_reload_error.js]
+skip-if =
+ os == "win" # On windows the AddonManager locks the XPI file loaded as a temporary extension and we can not test the reload of the extension.
+[browser_aboutdebugging_addons_warnings.js]
+[browser_aboutdebugging_connect_networklocations.js]
+[browser_aboutdebugging_connect_toggle_usb_devices.js]
+[browser_aboutdebugging_connection_prompt_setting.js]
+[browser_aboutdebugging_debug-target-pane_collapsibilities_interaction.js]
+[browser_aboutdebugging_debug-target-pane_collapsibilities_preference.js]
+[browser_aboutdebugging_debug-target-pane_empty.js]
+[browser_aboutdebugging_debug-target-pane_usb_runtime.js]
+[browser_aboutdebugging_devtools.js]
+[browser_aboutdebugging_devtoolstoolbox_breakpoint.js]
+[browser_aboutdebugging_devtoolstoolbox_contextmenu.js]
+[browser_aboutdebugging_devtoolstoolbox_contextmenu_markupview.js]
+[browser_aboutdebugging_devtoolstoolbox_focus.js]
+[browser_aboutdebugging_devtoolstoolbox_menubar.js]
+[browser_aboutdebugging_devtoolstoolbox_navigate_back_forward.js]
+[browser_aboutdebugging_devtoolstoolbox_navigate_reload_button.js]
+[browser_aboutdebugging_devtoolstoolbox_navigate_to_url.js]
+[browser_aboutdebugging_devtoolstoolbox_reload.js]
+skip-if =
+ verify
+ ccov
+ os == "linux" && debug #bug 1544828, test loads the toolbox 2 times for each panel, might timeout or OOM
+[browser_aboutdebugging_devtoolstoolbox_shortcuts.js]
+skip-if =
+ ccov
+ os == "linux" # Bug 1521349, Bug 1548015, Bug 1544828
+[browser_aboutdebugging_devtoolstoolbox_splitconsole_key.js]
+[browser_aboutdebugging_devtoolstoolbox_target_destroyed.js]
+skip-if =
+ debug
+ asan # This test leaks. See bug 1529005
+[browser_aboutdebugging_devtoolstoolbox_tooltip_markupview.js]
+[browser_aboutdebugging_devtoolstoolbox_zoom.js]
+[browser_aboutdebugging_fenix_runtime_display.js]
+[browser_aboutdebugging_fenix_runtime_node_picker.js]
+[browser_aboutdebugging_message_close.js]
+[browser_aboutdebugging_navigate.js]
+[browser_aboutdebugging_persist_connection.js]
+[browser_aboutdebugging_addons_popup_picker.js]
+[browser_aboutdebugging_process_category.js]
+[browser_aboutdebugging_process_main.js]
+[browser_aboutdebugging_process_main_local.js]
+skip-if = debug
+[browser_aboutdebugging_profiler_dialog.js]
+support-files =
+ !/devtools/client/performance-new/test/browser/helpers.js
+[browser_aboutdebugging_real_usb_runtime_page_runtime_info.js]
+[browser_aboutdebugging_real_usb_sidebar.js]
+[browser_aboutdebugging_routes.js]
+[browser_aboutdebugging_rtl.js]
+[browser_aboutdebugging_runtime_compatibility_warning.js]
+[browser_aboutdebugging_runtime_disconnect_remote_runtime.js]
+[browser_aboutdebugging_runtime_remote_runtime_buttons.js]
+[browser_aboutdebugging_runtime_usbclient_closed.js]
+[browser_aboutdebugging_select_network_runtime.js]
+[browser_aboutdebugging_select_page_with_serviceworker.js]
+[browser_aboutdebugging_serviceworker_console.js]
+[browser_aboutdebugging_serviceworker_fetch_flag.js]
+skip-if =
+ os == "win" && debug
+ os == "mac"
+ os == "linux" #Bug 1529824
+[browser_aboutdebugging_serviceworker_not_compatible.js]
+[browser_aboutdebugging_serviceworker_push.js]
+[browser_aboutdebugging_serviceworker_pushservice_url.js]
+[browser_aboutdebugging_serviceworker_runtime-page.js]
+[browser_aboutdebugging_serviceworker_start.js]
+[browser_aboutdebugging_serviceworker_status.js]
+[browser_aboutdebugging_serviceworker_timeout.js]
+skip-if =
+ debug
+ asan # Frequent intermittent failures, Bug 1522800
+[browser_aboutdebugging_serviceworker_unregister.js]
+[browser_aboutdebugging_sidebar_connection_state.js]
+[browser_aboutdebugging_sidebar_network_runtimes.js]
+[browser_aboutdebugging_sidebar_usb_runtime.js]
+[browser_aboutdebugging_sidebar_usb_runtime_connect.js]
+[browser_aboutdebugging_sidebar_usb_runtime_refresh.js]
+[browser_aboutdebugging_sidebar_usb_runtime_select.js]
+[browser_aboutdebugging_sidebar_usb_status.js]
+[browser_aboutdebugging_sidebar_usb_unavailable_runtime.js]
+[browser_aboutdebugging_sidebar_usb_unplugged_device.js]
+[browser_aboutdebugging_hidden_addons.js]
+[browser_aboutdebugging_tab_favicons.js]
+[browser_aboutdebugging_tab_navigate.js]
+[browser_aboutdebugging_tab_zombietab.js]
+[browser_aboutdebugging_telemetry_basic.js]
+[browser_aboutdebugging_telemetry_connection_attempt.js]
+[browser_aboutdebugging_telemetry_inspect.js]
+[browser_aboutdebugging_telemetry_navigate.js]
+[browser_aboutdebugging_telemetry_runtime_actions.js]
+[browser_aboutdebugging_telemetry_runtime_connected_details.js]
+[browser_aboutdebugging_telemetry_runtime_updates.js]
+[browser_aboutdebugging_telemetry_runtime_updates_multi.js]
+[browser_aboutdebugging_telemetry_runtime_updates_network.js]
+[browser_aboutdebugging_thisfirefox.js]
+[browser_aboutdebugging_thisfirefox_runtime_info.js]
+[browser_aboutdebugging_thisfirefox_worker_inspection.js]
+[browser_aboutdebugging_workers_remote_runtime.js]
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_console.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_console.js
new file mode 100644
index 0000000000..1ed16dd1b0
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_console.js
@@ -0,0 +1,440 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
+requestLongerTimeout(2);
+
+const ADDON_ID = "test-devtools-webextension@mozilla.org";
+const ADDON_NAME = "test-devtools-webextension";
+
+const OTHER_ADDON_ID = "other-test-devtools-webextension@mozilla.org";
+const OTHER_ADDON_NAME = "other-test-devtools-webextension";
+
+const POPUPONLY_ADDON_ID = "popuponly-test-devtools-webextension@mozilla.org";
+const POPUPONLY_ADDON_NAME = "popuponly-test-devtools-webextension";
+
+const BACKGROUND_ADDON_ID = "background-test-devtools-webextension@mozilla.org";
+const BACKGROUND_ADDON_NAME = "background-test-devtools-webextension";
+
+/**
+ * This test file ensures that the webextension addon developer toolbox:
+ * - when the debug button is clicked on a webextension, the opened toolbox
+ * has a working webconsole with the background page as default target;
+ */
+add_task(async function testWebExtensionsToolboxWebConsole() {
+ await pushPref("devtools.webconsole.filter.css", true);
+ await enableExtensionDebugging();
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ await installTemporaryExtensionFromXPI(
+ {
+ background() {
+ window.myWebExtensionAddonFunction = function () {
+ console.log(
+ "Background page function called",
+ this.browser.runtime.getManifest()
+ );
+ };
+
+ const style = document.createElement("style");
+ style.textContent = "* { color: error; }";
+ document.documentElement.appendChild(style);
+
+ throw new Error("Background page exception");
+ },
+ extraProperties: {
+ browser_action: {
+ default_title: "WebExtension Popup Debugging",
+ default_popup: "popup.html",
+ default_area: "navbar",
+ },
+ },
+ files: {
+ "popup.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="popup.js"></script>
+ </head>
+ <body>
+ Popup
+ </body>
+ </html>
+ `,
+ "popup.js": function () {
+ console.log("Popup log");
+
+ const style = document.createElement("style");
+ style.textContent = "* { color: popup-error; }";
+ document.documentElement.appendChild(style);
+
+ throw new Error("Popup exception");
+ },
+ },
+ id: ADDON_ID,
+ name: ADDON_NAME,
+ },
+ document
+ );
+
+ // Install another addon in order to ensure we don't get its logs
+ await installTemporaryExtensionFromXPI(
+ {
+ background() {
+ console.log("Other addon log");
+
+ const style = document.createElement("style");
+ style.textContent = "* { background-color: error; }";
+ document.documentElement.appendChild(style);
+
+ throw new Error("Other addon exception");
+ },
+ extraProperties: {
+ browser_action: {
+ default_title: "Other addon popup",
+ default_popup: "other-popup.html",
+ default_area: "navbar",
+ },
+ },
+ files: {
+ "other-popup.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="other-popup.js"></script>
+ </head>
+ <body>
+ Other popup
+ </body>
+ </html>
+ `,
+ "other-popup.js": function () {
+ console.log("Other popup log");
+
+ const style = document.createElement("style");
+ style.textContent = "* { background-color: popup-error; }";
+ document.documentElement.appendChild(style);
+
+ throw new Error("Other popup exception");
+ },
+ },
+ id: OTHER_ADDON_ID,
+ name: OTHER_ADDON_NAME,
+ },
+ document
+ );
+
+ const { devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ ADDON_NAME
+ );
+ const toolbox = getToolbox(devtoolsWindow);
+ const webconsole = await toolbox.selectTool("webconsole");
+ const { hud } = webconsole;
+
+ info("Trigger some code in the background page logging some stuff");
+ const onMessage = waitUntil(() => {
+ return !!findMessagesByType(hud, "Background page exception", ".error")
+ .length;
+ });
+ hud.ui.wrapper.dispatchEvaluateExpression("myWebExtensionAddonFunction()");
+ await onMessage;
+
+ info("Open the two add-ons popups to cover popups messages");
+ const onPopupMessage = waitUntil(() => {
+ return !!findMessagesByType(hud, "Popup exception", ".error").length;
+ });
+ clickOnAddonWidget(OTHER_ADDON_ID);
+ clickOnAddonWidget(ADDON_ID);
+ await onPopupMessage;
+
+ info("Wait a bit to catch unexpected duplicates or mixed up messages");
+ await wait(1000);
+
+ is(
+ findMessagesByType(hud, "Background page exception", ".error").length,
+ 1,
+ "We get the background page exception"
+ );
+ is(
+ findMessagesByType(hud, "Popup exception", ".error").length,
+ 1,
+ "We get the popup exception"
+ );
+ is(
+ findMessagesByType(
+ hud,
+ "Expected color but found ‘error’. Error in parsing value for ‘color’. Declaration dropped.",
+ ".warn"
+ ).length,
+ 1,
+ "We get the addon's background page CSS error message"
+ );
+ is(
+ findMessagesByType(
+ hud,
+ "Expected color but found ‘popup-error’. Error in parsing value for ‘color’. Declaration dropped.",
+ ".warn"
+ ).length,
+ 1,
+ "We get the addon's popup CSS error message"
+ );
+
+ // Verify that we don't get the other addon log and errors
+ ok(
+ !findMessageByType(hud, "Other addon log", ".console-api"),
+ "We don't get the other addon log"
+ );
+ ok(
+ !findMessageByType(hud, "Other addon exception", ".console-api"),
+ "We don't get the other addon exception"
+ );
+ ok(
+ !findMessageByType(hud, "Other popup log", ".console-api"),
+ "We don't get the other addon popup log"
+ );
+ ok(
+ !findMessageByType(hud, "Other popup exception", ".error"),
+ "We don't get the other addon popup exception"
+ );
+ ok(
+ !findMessageByType(
+ hud,
+ "Expected color but found ‘error’. Error in parsing value for ‘background-color’. Declaration dropped.",
+ ".warn"
+ ),
+ "We don't get the other addon's background page CSS error message"
+ );
+ ok(
+ !findMessageByType(
+ hud,
+ "Expected color but found ‘popup-error’. Error in parsing value for ‘background-color’. Declaration dropped.",
+ ".warn"
+ ),
+ "We don't get the other addon's popup CSS error message"
+ );
+
+ // Verify that console evaluations still work after reloading the page
+ info("Reload the webextension document");
+ const { onDomCompleteResource } =
+ await waitForNextTopLevelDomCompleteResource(toolbox.commands);
+ hud.ui.wrapper.dispatchEvaluateExpression("location.reload()");
+ await onDomCompleteResource;
+
+ info("Try to evaluate something after reload");
+
+ const onEvaluationResultAfterReload = waitUntil(() =>
+ findMessageByType(hud, "result:2", ".result")
+ );
+ const onMessageAfterReload = waitUntil(() =>
+ findMessageByType(hud, "message after reload", ".console-api")
+ );
+ hud.ui.wrapper.dispatchEvaluateExpression(
+ "console.log('message after reload'); 'result:' + (1 + 1)"
+ );
+ // Both cover that the console.log worked
+ await onMessageAfterReload;
+ // And we received the evaluation result
+ await onEvaluationResultAfterReload;
+
+ await closeWebExtAboutDevtoolsToolbox(devtoolsWindow, window);
+
+ // Note that it seems to be important to remove the addons in the reverse order
+ // from which they were installed...
+ await removeTemporaryExtension(OTHER_ADDON_NAME, document);
+ await removeTemporaryExtension(ADDON_NAME, document);
+ await removeTab(tab);
+});
+
+add_task(async function testWebExtensionNoBgScript() {
+ await pushPref("devtools.webconsole.filter.css", true);
+ await enableExtensionDebugging();
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ await installTemporaryExtensionFromXPI(
+ {
+ extraProperties: {
+ browser_action: {
+ default_title: "WebExtension Popup Only",
+ default_popup: "popup.html",
+ default_area: "navbar",
+ },
+ },
+ files: {
+ "popup.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="popup.js"></script>
+ </head>
+ <body>
+ Popup
+ </body>
+ </html>
+ `,
+ "popup.js": function () {
+ console.log("Popup-only log");
+
+ const style = document.createElement("style");
+ style.textContent = "* { color: popup-only-error; }";
+ document.documentElement.appendChild(style);
+
+ throw new Error("Popup-only exception");
+ },
+ },
+ id: POPUPONLY_ADDON_ID,
+ name: POPUPONLY_ADDON_NAME,
+ },
+ document
+ );
+
+ const { devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ POPUPONLY_ADDON_NAME
+ );
+ const toolbox = getToolbox(devtoolsWindow);
+ const webconsole = await toolbox.selectTool("webconsole");
+ const { hud } = webconsole;
+
+ info("Open the add-on popup");
+ const onPopupMessage = waitUntil(() => {
+ return !!findMessagesByType(hud, "Popup-only exception", ".error").length;
+ });
+ clickOnAddonWidget(POPUPONLY_ADDON_ID);
+ await onPopupMessage;
+
+ info("Wait a bit to catch unexpected duplicates or mixed up messages");
+ await wait(1000);
+ is(
+ findMessagesByType(hud, "Popup-only exception", ".error").length,
+ 1,
+ "We get the popup exception"
+ );
+ is(
+ findMessagesByType(hud, "Popup-only log", ".console-api").length,
+ 1,
+ "We get the addon's popup log"
+ );
+ is(
+ findMessagesByType(
+ hud,
+ "Expected color but found ‘popup-only-error’. Error in parsing value for ‘color’. Declaration dropped.",
+ ".warn"
+ ).length,
+ 1,
+ "We get the addon's popup CSS error message"
+ );
+
+ await closeWebExtAboutDevtoolsToolbox(devtoolsWindow, window);
+ await removeTemporaryExtension(POPUPONLY_ADDON_NAME, document);
+ await removeTab(tab);
+});
+
+// Check that reloading the addon several times does not break the console,
+// see Bug 1778951.
+add_task(async function testWebExtensionTwoReloads() {
+ await enableExtensionDebugging();
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ await installTemporaryExtensionFromXPI(
+ {
+ background() {
+ console.log("Background page log");
+ },
+ extraProperties: {
+ browser_action: {
+ default_title: "WebExtension with background script",
+ default_popup: "popup.html",
+ default_area: "navbar",
+ },
+ },
+ files: {
+ "popup.html": `<!DOCTYPE html>
+ <html>
+ <body>
+ Popup
+ </body>
+ </html>
+ `,
+ },
+ id: BACKGROUND_ADDON_ID,
+ name: BACKGROUND_ADDON_NAME,
+ },
+ document
+ );
+
+ // Retrieve the addonTarget element before calling `openAboutDevtoolsToolbox`,
+ // otherwise it will pick the about:devtools-toolbox tab with the same name
+ // instead.
+ const addonTarget = findDebugTargetByText(BACKGROUND_ADDON_NAME, document);
+
+ const { devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ BACKGROUND_ADDON_NAME
+ );
+ const toolbox = getToolbox(devtoolsWindow);
+ const webconsole = await toolbox.selectTool("webconsole");
+ const { hud } = webconsole;
+
+ // Verify that console evaluations still work after reloading the addon
+ info("Reload the webextension itself");
+ let { onDomCompleteResource } = await waitForNextTopLevelDomCompleteResource(
+ toolbox.commands
+ );
+ const reloadButton = addonTarget.querySelector(
+ ".qa-temporary-extension-reload-button"
+ );
+ reloadButton.click();
+ await onDomCompleteResource;
+
+ info("Try to evaluate something after 1st addon reload");
+ // Wait before evaluating the message, otherwise they might be cleaned up by
+ // the console UI.
+ info("Wait until the background script log is visible");
+ await waitUntil(() =>
+ findMessageByType(hud, "Background page log", ".message")
+ );
+
+ hud.ui.wrapper.dispatchEvaluateExpression("40+1");
+ await waitUntil(() => findMessageByType(hud, "41", ".result"));
+
+ info("Reload the extension a second time");
+ ({ onDomCompleteResource } = await waitForNextTopLevelDomCompleteResource(
+ toolbox.commands
+ ));
+ reloadButton.click();
+ await onDomCompleteResource;
+
+ info("Wait until the background script log is visible - after reload");
+ await waitUntil(() =>
+ findMessageByType(hud, "Background page log", ".message")
+ );
+
+ info("Try to evaluate something after 2nd addon reload");
+ hud.ui.wrapper.dispatchEvaluateExpression("40+2");
+ await waitUntil(() => findMessageByType(hud, "42", ".result"));
+
+ await closeWebExtAboutDevtoolsToolbox(devtoolsWindow, window);
+ await removeTemporaryExtension(BACKGROUND_ADDON_NAME, document);
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_debugger.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_debugger.js
new file mode 100644
index 0000000000..0e100931a7
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_debugger.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/toolbox.properties"
+);
+
+add_task(async () => {
+ const EXTENSION_NAME = "temporary-web-extension";
+ const EXTENSION_ID = "test-devtools@mozilla.org";
+
+ await enableExtensionDebugging();
+
+ info(
+ "The debugger should show the source codes of extension even if " +
+ "devtools.chrome.enabled and devtools.debugger.remote-enabled are off"
+ );
+ await pushPref("devtools.chrome.enabled", false);
+ await pushPref("devtools.debugger.remote-enabled", false);
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ await installTemporaryExtensionFromXPI(
+ {
+ background() {
+ window.someRandomMethodName = () => {
+ // This will not be referred from anywhere.
+ // However this is necessary to show as the source code in the debugger.
+ };
+ },
+ id: EXTENSION_ID,
+ name: EXTENSION_NAME,
+ },
+ document
+ );
+
+ // Select the debugger right away to avoid any noise coming from the inspector.
+ await pushPref("devtools.toolbox.selectedTool", "jsdebugger");
+ const { devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ EXTENSION_NAME
+ );
+ const toolbox = getToolbox(devtoolsWindow);
+ const { panelWin } = toolbox.getCurrentPanel();
+
+ info("Check the state of redux");
+ ok(
+ panelWin.dbg.store.getState().sourcesTree.isWebExtension,
+ "isWebExtension flag in sourcesTree is true"
+ );
+
+ info("Check whether the element displays correctly");
+ let sourceList = panelWin.document.querySelector(".sources-list");
+ ok(sourceList, "Source list element displays correctly");
+ ok(
+ sourceList.textContent.includes("temporary-web-extension"),
+ "Extension name displays correctly"
+ );
+
+ const waitForLoadedPanelsReload = await watchForLoadedPanelsReload(toolbox);
+
+ info("Reload the addon using a toolbox reload shortcut");
+ toolbox.win.focus();
+ synthesizeKeyShortcut(L10N.getStr("toolbox.reload.key"), toolbox.win);
+
+ await waitForLoadedPanelsReload();
+
+ info("Wait until a new background log message is logged");
+ await waitFor(() => {
+ // As React may re-create a new sources-list element,
+ // fetch the latest instance
+ sourceList = panelWin.document.querySelector(".sources-list");
+ return sourceList?.textContent.includes("temporary-web-extension");
+ }, "Wait for the source to re-appear");
+
+ await closeWebExtAboutDevtoolsToolbox(devtoolsWindow, window);
+ await removeTemporaryExtension(EXTENSION_NAME, document);
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_inspector.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_inspector.js
new file mode 100644
index 0000000000..a652ab41a8
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_inspector.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
+requestLongerTimeout(2);
+
+const ADDON_ID = "test-devtools-webextension@mozilla.org";
+const ADDON_NAME = "test-devtools-webextension";
+
+/**
+ * This test file ensures that the webextension addon developer toolbox:
+ * - the webextension developer toolbox has a working Inspector panel, with the
+ * background page as default target;
+ */
+add_task(async function testWebExtensionsToolboxWebConsole() {
+ await enableExtensionDebugging();
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ await installTemporaryExtensionFromXPI(
+ {
+ background() {
+ document.body.innerText = "Background Page Body Test Content";
+ },
+ id: ADDON_ID,
+ name: ADDON_NAME,
+ },
+ document
+ );
+
+ info("Open a toolbox to debug the addon");
+ const { devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ ADDON_NAME
+ );
+ const toolbox = getToolbox(devtoolsWindow);
+
+ const inspector = await toolbox.selectTool("inspector");
+ const nodeActor = await inspector.walker.querySelector(
+ inspector.walker.rootNode,
+ "body"
+ );
+ ok(nodeActor, "Got a nodeActor");
+ ok(nodeActor.inlineTextChild, "Got a nodeActor with an inline text child");
+
+ const actualValue = nodeActor.inlineTextChild._form.nodeValue;
+
+ is(
+ String(actualValue).trim(),
+ "Background Page Body Test Content",
+ "nodeActor has the expected inlineTextChild value"
+ );
+
+ info("Check that the color scheme simulation buttons are hidden");
+ const lightButtonIsHidden = inspector.panelDoc
+ .querySelector("#color-scheme-simulation-light-toggle")
+ ?.hasAttribute("hidden");
+ const darkButtonIsHidded = inspector.panelDoc
+ .querySelector("#color-scheme-simulation-dark-toggle")
+ ?.hasAttribute("hidden");
+ ok(
+ lightButtonIsHidden,
+ "The light color scheme simulation button exists and is hidden"
+ );
+ ok(
+ darkButtonIsHidded,
+ "The dark color scheme simulation button exists and is hidden"
+ );
+
+ await closeWebExtAboutDevtoolsToolbox(devtoolsWindow, window);
+ await removeTemporaryExtension(ADDON_NAME, document);
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_nobg.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_nobg.js
new file mode 100644
index 0000000000..4f8f35acd2
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_nobg.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+const ADDON_NOBG_ID = "test-devtools-webextension-nobg@mozilla.org";
+const ADDON_NOBG_NAME = "test-devtools-webextension-nobg";
+
+/**
+ * This test file ensures that the webextension addon developer toolbox:
+ * - the webextension developer toolbox is connected to a fallback page when the
+ * background page is not available (and in the fallback page document body contains
+ * the expected message, which warns the user that the current page is not a real
+ * webextension context);
+ */
+add_task(async function testWebExtensionsToolboxNoBackgroundPage() {
+ await enableExtensionDebugging();
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ await installTemporaryExtensionFromXPI(
+ {
+ // Do not pass any `background` script.
+ id: ADDON_NOBG_ID,
+ name: ADDON_NOBG_NAME,
+ },
+ document
+ );
+
+ info("Open a toolbox to debug the addon");
+ const { devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ ADDON_NOBG_NAME
+ );
+ const toolbox = getToolbox(devtoolsWindow);
+
+ ok(
+ toolbox.commands.descriptorFront.isWebExtensionDescriptor,
+ "Toolbox is debugging an addon"
+ );
+ const targetName = toolbox.target.name;
+ is(targetName, ADDON_NOBG_NAME, "Toolbox has the expected target");
+
+ const inspector = await toolbox.selectTool("inspector");
+
+ let nodeActor;
+ info(`Wait the fallback window to be fully loaded`);
+ await asyncWaitUntil(async () => {
+ nodeActor = await inspector.walker.querySelector(
+ inspector.walker.rootNode,
+ "h1"
+ );
+ return nodeActor && nodeActor.inlineTextChild;
+ });
+
+ info("Got a nodeActor with an inline text child");
+ const actualValue = nodeActor.inlineTextChild._form.nodeValue;
+ is(
+ actualValue,
+ "Your addon does not have any document opened yet.",
+ "nodeActor has the expected inlineTextChild value"
+ );
+
+ await closeWebExtAboutDevtoolsToolbox(devtoolsWindow, window);
+ await removeTemporaryExtension(ADDON_NOBG_NAME, document);
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_popup.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_popup.js
new file mode 100644
index 0000000000..c205ecbfe3
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_popup.js
@@ -0,0 +1,246 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
+requestLongerTimeout(2);
+
+const ADDON_ID = "test-devtools-webextension@mozilla.org";
+const ADDON_NAME = "test-devtools-webextension";
+
+/**
+ * This test file ensures that the webextension addon developer toolbox:
+ * - has a frame list menu and the noautohide toolbar toggle button, and they
+ * can be used to switch the current target to the extension popup page.
+ */
+add_task(async function testWebExtensionsToolboxWebConsole() {
+ await enableExtensionDebugging();
+
+ // Bug 1686922: Disable the error count button to avoid intermittent failures.
+ await pushPref("devtools.command-button-errorcount.enabled", false);
+
+ is(
+ Services.prefs.getBoolPref("ui.popup.disable_autohide"),
+ false,
+ "disable_autohide should be initially false"
+ );
+
+ const {
+ document,
+ tab,
+ window: aboutDebuggingWindow,
+ } = await openAboutDebugging();
+ await selectThisFirefoxPage(
+ document,
+ aboutDebuggingWindow.AboutDebugging.store
+ );
+
+ const extension = await installTestAddon(document);
+
+ const onBackgroundFunctionCalled = waitForExtensionTestMessage(
+ extension,
+ "onBackgroundFunctionCalled"
+ );
+ const onPopupPageFunctionCalled = waitForExtensionTestMessage(
+ extension,
+ "onPopupPageFunctionCalled"
+ );
+
+ info("Open a toolbox to debug the addon");
+ const { devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ aboutDebuggingWindow,
+ ADDON_NAME
+ );
+
+ const toolbox = getToolbox(devtoolsWindow);
+ const webconsole = await toolbox.selectTool("webconsole");
+
+ info("Clicking the menu button to disable autohide");
+ await disablePopupAutohide(toolbox);
+
+ info("Check that console messages are evaluated in the background context");
+ const consoleWrapper = webconsole.hud.ui.wrapper;
+ consoleWrapper.dispatchEvaluateExpression("backgroundFunction()");
+ await onBackgroundFunctionCalled;
+
+ clickOnAddonWidget(ADDON_ID);
+
+ info("Wait until the frames list button is displayed");
+ const btn = await waitFor(() =>
+ toolbox.doc.getElementById("command-button-frames")
+ );
+
+ info("Clicking the frame list button");
+ btn.click();
+
+ const menuList = toolbox.doc.getElementById("toolbox-frame-menu");
+ const frames = Array.from(menuList.querySelectorAll(".command"));
+ is(frames.length, 2, "Has the expected number of frames");
+
+ const popupFrameBtn = frames
+ .filter(frame => {
+ return frame.querySelector(".label").textContent.endsWith("popup.html");
+ })
+ .pop();
+
+ ok(popupFrameBtn, "Extension Popup frame found in the listed frames");
+
+ info(
+ "Click on the extension popup frame and wait for a `dom-complete` resource"
+ );
+ const { onDomCompleteResource } =
+ await waitForNextTopLevelDomCompleteResource(toolbox.commands);
+ popupFrameBtn.click();
+ await onDomCompleteResource;
+
+ info("Execute `popupPageFunction()`");
+ consoleWrapper.dispatchEvaluateExpression("popupPageFunction()");
+
+ const args = await onPopupPageFunctionCalled;
+ ok(true, "Received console message from the popup page function as expected");
+ is(args[0], "onPopupPageFunctionCalled", "Got the expected console message");
+ is(
+ args[1] && args[1].name,
+ ADDON_NAME,
+ "Got the expected manifest from WebExtension API"
+ );
+
+ await closeWebExtAboutDevtoolsToolbox(devtoolsWindow, aboutDebuggingWindow);
+
+ is(
+ Services.prefs.getBoolPref("ui.popup.disable_autohide"),
+ false,
+ "disable_autohide should be reset to false when the toolbox is closed"
+ );
+
+ await removeTemporaryExtension(ADDON_NAME, document);
+ await removeTab(tab);
+});
+
+/**
+ * Helper to wait for a specific message on an Extension instance.
+ */
+function waitForExtensionTestMessage(extension, expectedMessage) {
+ return new Promise(done => {
+ extension.on("test-message", function testLogListener(evt, ...args) {
+ const [message] = args;
+
+ if (message !== expectedMessage) {
+ return;
+ }
+
+ extension.off("test-message", testLogListener);
+ done(args);
+ });
+ });
+}
+
+/**
+ * Install the addon used for this test.
+ * Returns a Promise that resolve the Extension instance that was just
+ * installed.
+ */
+async function installTestAddon(doc) {
+ // Start watching for the extension on the Extension Management before we
+ // install it.
+ const onExtensionReady = waitForExtension(ADDON_NAME);
+
+ // Install the extension.
+ await installTemporaryExtensionFromXPI(
+ {
+ background() {
+ const { browser } = this;
+ window.backgroundFunction = function () {
+ browser.test.sendMessage("onBackgroundFunctionCalled");
+ };
+ },
+ extraProperties: {
+ browser_action: {
+ default_title: "WebExtension Popup Debugging",
+ default_popup: "popup.html",
+ },
+ },
+ files: {
+ "popup.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="popup.js"></script>
+ </head>
+ <body>
+ Background Page Body Test Content
+ </body>
+ </html>
+ `,
+ "popup.js": function () {
+ const { browser } = this;
+ window.popupPageFunction = function () {
+ browser.test.sendMessage(
+ "onPopupPageFunctionCalled",
+ browser.runtime.getManifest()
+ );
+ };
+ },
+ },
+ id: ADDON_ID,
+ name: ADDON_NAME,
+ },
+ doc
+ );
+
+ // The onExtensionReady promise will resolve the extension instance.
+ return onExtensionReady;
+}
+
+/**
+ * Helper to retrieve the Extension instance.
+ */
+async function waitForExtension(addonName) {
+ const { Management } = ChromeUtils.importESModule(
+ "resource://gre/modules/Extension.sys.mjs"
+ );
+
+ return new Promise(resolve => {
+ Management.on("startup", function listener(event, extension) {
+ if (extension.name != addonName) {
+ return;
+ }
+
+ Management.off("startup", listener);
+ resolve(extension);
+ });
+ });
+}
+
+/**
+ * Disables the popup autohide feature, which is mandatory to debug webextension
+ * popups.
+ */
+function disablePopupAutohide(toolbox) {
+ return new Promise(resolve => {
+ toolbox.doc.addEventListener(
+ "popupshown",
+ () => {
+ const menuItem = toolbox.doc.getElementById(
+ "toolbox-meatball-menu-noautohide"
+ );
+ menuItem.click();
+ resolve();
+ },
+ { once: true }
+ );
+ toolbox.doc.getElementById("toolbox-meatball-menu-button").click();
+ });
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_reload.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_reload.js
new file mode 100644
index 0000000000..2293eaca0e
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_reload.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
+requestLongerTimeout(2);
+
+const ADDON_ID = "test-devtools-webextension@mozilla.org";
+const ADDON_NAME = "test-devtools-webextension";
+
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/toolbox.properties"
+);
+
+// Check that addon browsers can be reloaded via the toolbox reload shortcuts
+add_task(async function testWebExtensionToolboxReload() {
+ await enableExtensionDebugging();
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ await installTemporaryExtensionFromXPI(
+ {
+ background() {
+ console.log("background script executed " + Math.random());
+ },
+ id: ADDON_ID,
+ name: ADDON_NAME,
+ },
+ document
+ );
+
+ // Select the debugger right away to avoid any noise coming from the inspector.
+ await pushPref("devtools.toolbox.selectedTool", "webconsole");
+ const { devtoolsDocument, devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ ADDON_NAME
+ );
+ const toolbox = getToolbox(devtoolsWindow);
+
+ ok(
+ devtoolsDocument.querySelector(".qa-reload-button"),
+ "Reload button is visible"
+ );
+ ok(
+ !devtoolsDocument.querySelector(".qa-back-button"),
+ "Back button is hidden"
+ );
+ ok(
+ !devtoolsDocument.querySelector(".qa-forward-button"),
+ "Forward button is hidden"
+ );
+ ok(
+ !devtoolsDocument.querySelector(".debug-target-url-form"),
+ "URL form is hidden"
+ );
+ ok(
+ devtoolsDocument.getElementById("toolbox-meatball-menu-noautohide"),
+ "Disable popup autohide button is displayed"
+ );
+ ok(
+ !devtoolsDocument.getElementById(
+ "toolbox-meatball-menu-pseudo-locale-accented"
+ ),
+ "Accented locale is not displayed (only on browser toolbox)"
+ );
+
+ const webconsole = await toolbox.selectTool("webconsole");
+ const { hud } = webconsole;
+
+ info("Wait for the initial background message to appear in the console");
+ const initialMessage = await waitFor(() =>
+ findMessagesByType(hud, "background script executed", ".console-api")
+ );
+ ok(initialMessage, "Found the expected message from the background script");
+
+ const waitForLoadedPanelsReload = await watchForLoadedPanelsReload(toolbox);
+
+ info("Reload the addon using a toolbox reload shortcut");
+ toolbox.win.focus();
+ synthesizeKeyShortcut(L10N.getStr("toolbox.reload.key"), toolbox.win);
+
+ info("Wait until a new background log message is logged");
+ const secondMessage = await waitFor(() => {
+ const newMessage = findMessagesByType(
+ hud,
+ "background script executed",
+ ".console-api"
+ );
+ if (newMessage && newMessage !== initialMessage) {
+ return newMessage;
+ }
+ return false;
+ });
+
+ await waitForLoadedPanelsReload();
+
+ info("Reload via the debug target info bar button");
+ clickReload(devtoolsDocument);
+
+ info("Wait until yet another background log message is logged");
+ await waitFor(() => {
+ const newMessage = findMessagesByType(
+ hud,
+ "background script executed",
+ ".console-api"
+ );
+ return (
+ newMessage &&
+ newMessage !== initialMessage &&
+ newMessage !== secondMessage
+ );
+ });
+
+ await waitForLoadedPanelsReload();
+
+ await closeWebExtAboutDevtoolsToolbox(devtoolsWindow, window);
+ await removeTemporaryExtension(ADDON_NAME, document);
+ await removeTab(tab);
+});
+
+function clickReload(devtoolsDocument) {
+ devtoolsDocument.querySelector(".qa-reload-button").click();
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_storage.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_storage.js
new file mode 100644
index 0000000000..56f1b0befb
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_storage.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+add_task(async () => {
+ const EXTENSION_NAME = "temporary-web-extension";
+ const EXTENSION_ID = "test-devtools@mozilla.org";
+
+ await enableExtensionDebugging();
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ const { extension } = await installTemporaryExtensionFromXPI(
+ {
+ background() {
+ const open = indexedDB.open("TestDatabase", 1);
+
+ open.onupgradeneeded = function () {
+ const db = open.result;
+ db.createObjectStore("TestStore", { keyPath: "id" });
+ };
+
+ open.onsuccess = function () {
+ const db = open.result;
+ const tx = db.transaction("TestStore", "readwrite");
+ const store = tx.objectStore("TestStore");
+
+ store.put({ id: 1, name: "John", age: 12 });
+ store.put({ id: 2, name: "Bob", age: 24 });
+ tx.oncomplete = () => db.close();
+ };
+ },
+ id: EXTENSION_ID,
+ name: EXTENSION_NAME,
+ },
+ document
+ );
+
+ const { devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ EXTENSION_NAME
+ );
+
+ info("Select the storage panel");
+ const toolbox = getToolbox(devtoolsWindow);
+ await toolbox.selectTool("storage");
+ const storage = toolbox.getCurrentPanel();
+
+ info("Check the content of the storage panel treeview");
+ const ids = [
+ "indexedDB",
+ `moz-extension://${extension.uuid}`,
+ "TestDatabase (default)",
+ "TestStore",
+ ];
+ ok(
+ !!storage.panelWindow.document.querySelector(
+ `[data-id='${JSON.stringify(ids)}']`
+ ),
+ "The indexedDB database for the extension is visible"
+ );
+
+ info("Select the indexedDB database for the extension");
+ const updated = storage.UI.once("store-objects-updated");
+ storage.UI.tree.selectedItem = ids;
+ await updated;
+
+ info("Wait until table populated");
+ await waitUntil(() => storage.UI.table.items.size === 2);
+ const items = storage.UI.table.items;
+
+ info("Check the content of the storage panel table");
+ is(items.size, 2);
+ const user1 = JSON.parse(items.get(1).value);
+ const user2 = JSON.parse(items.get(2).value);
+ is(user1.name, "John", "user 1 has the expected name");
+ is(user1.age, 12, "user 1 has the expected age");
+ is(user2.name, "Bob", "user 2 has the expected name");
+ is(user2.age, 24, "user 2 has the expected age");
+
+ await closeWebExtAboutDevtoolsToolbox(devtoolsWindow, window);
+ await removeTemporaryExtension(EXTENSION_NAME, document);
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_toolbox.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_toolbox.js
new file mode 100644
index 0000000000..f21e7d9c4a
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_debug_toolbox.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+const ADDON_ID = "test-devtools-webextension@mozilla.org";
+const ADDON_NAME = "test-devtools-webextension";
+
+/**
+ * This test file ensures that the webextension addon developer toolbox:
+ * - always on top is enabled by default and can be toggled off
+ */
+add_task(async function testWebExtensionsToolbox() {
+ await enableExtensionDebugging();
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ await installTemporaryExtensionFromXPI(
+ {
+ background() {
+ document.body.innerText = "Background Page Body Test Content";
+ },
+ id: ADDON_ID,
+ name: ADDON_NAME,
+ },
+ document
+ );
+
+ info("Open a toolbox to debug the addon");
+ const { devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ ADDON_NAME
+ );
+
+ const toolbox = getToolbox(devtoolsWindow);
+
+ ok(
+ isWindowAlwaysOnTop(devtoolsWindow),
+ "The toolbox window is always on top"
+ );
+ const toggleButton = toolbox.doc.querySelector(".toolbox-always-on-top");
+ ok(!!toggleButton, "The always on top toggle button is visible");
+ ok(
+ toggleButton.classList.contains("checked"),
+ "The button is highlighted to report that always on top is enabled"
+ );
+
+ // When running the test, the devtools window might not be focused, while it does IRL.
+ // Force it to be focused to better reflect the default behavior.
+ info("Force focusing the devtools window");
+ devtoolsWindow.focus();
+
+ // As we update the button with a debounce, we have to wait for it to updates
+ await waitFor(
+ () => toggleButton.classList.contains("toolbox-is-focused"),
+ "Wait for the button to be highlighting that the toolbox is focused (the button isn't highlighted)"
+ );
+ ok(true, "Expected class is added when toolbox is focused");
+
+ info("Focus the browser window");
+ window.focus();
+
+ await waitFor(
+ () => !toggleButton.classList.contains("toolbox-is-focused"),
+ "Wait for the button to be highlighting that the toolbox is no longer focused (the button is highlighted)"
+ );
+ ok(true, "Focused class is removed when browser window gets focused");
+
+ info("Re-focus the DevTools window");
+ devtoolsWindow.focus();
+
+ await waitFor(
+ () => toggleButton.classList.contains("toolbox-is-focused"),
+ "Wait for the button to be re-highlighting that the toolbox is focused"
+ );
+
+ const onToolboxReady = gDevTools.once("toolbox-ready");
+ info("Click on the button");
+ toggleButton.click();
+
+ info("Wait for a new toolbox to be created");
+ const secondToolbox = await onToolboxReady;
+
+ ok(
+ !isWindowAlwaysOnTop(secondToolbox.win),
+ "The toolbox window is no longer always on top"
+ );
+ const secondToggleButton = secondToolbox.doc.querySelector(
+ ".toolbox-always-on-top"
+ );
+ ok(!!secondToggleButton, "The always on top toggle button is still visible");
+
+ ok(
+ !secondToggleButton.classList.contains("checked"),
+ "The button is no longer highlighted to report that always on top is disabled"
+ );
+
+ await closeWebExtAboutDevtoolsToolbox(secondToolbox.win, window);
+ await removeTemporaryExtension(ADDON_NAME, document);
+ await removeTab(tab);
+});
+
+// Check if the window has the "alwaysontop" chrome flag
+function isWindowAlwaysOnTop(window) {
+ return (
+ window.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow).chromeFlags &
+ Ci.nsIWebBrowserChrome.CHROME_ALWAYS_ON_TOP
+ );
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_eventpage_actions_and_status.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_eventpage_actions_and_status.js
new file mode 100644
index 0000000000..0e395704f2
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_eventpage_actions_and_status.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.eventPages.enabled", true]],
+ });
+});
+
+// Test that the terminate button is shutting down the background script as expected
+// and the background script status is updated occordingly.
+add_task(async function test_eventpage_terminate_and_status_updates() {
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ const EXTENSION_ID = "test-devtools-eventpage@mozilla.org";
+ const EXTENSION_NAME = "Temporary EventPage-based web extension";
+
+ const EXTENSION2_ID = "test-devtools-persistentbg@mozilla.org";
+ const EXTENSION2_NAME =
+ "Temporary PersistentBackgroundPage-based web extension";
+
+ const promiseBackgroundLoaded = promiseBackgroundContextLoaded(EXTENSION_ID);
+ const promiseBackgroundUnloaded =
+ promiseBackgroundContextUnloaded(EXTENSION_ID);
+
+ let waitForBGStatusUpdate = promiseBackgroundStatusUpdate(window);
+
+ // Install the extension using an event page (non persistent background page).
+ await installTemporaryExtensionFromXPI(
+ {
+ id: EXTENSION_ID,
+ name: EXTENSION_NAME,
+ // The extension is expected to have a non persistent background script.
+ extraProperties: {
+ background: {
+ scripts: ["bgpage.js"],
+ persistent: false,
+ },
+ },
+ files: {
+ "bgpage.js": function () {
+ // Emit a dump when the script is loaded to make it easier
+ // to investigate intermittents.
+ dump(`Background script loaded: ${window.location}\n`);
+ },
+ },
+ },
+ document
+ );
+
+ // Install the extension using a persistent background page.
+ await installTemporaryExtensionFromXPI(
+ {
+ id: EXTENSION2_ID,
+ name: EXTENSION2_NAME,
+ // The extension is expected to have a persistent background script.
+ extraProperties: {
+ background: {
+ page: "bppage.html",
+ persistent: true,
+ },
+ },
+ files: { "bgpage.html": "" },
+ },
+ document
+ );
+
+ const target = findDebugTargetByText(EXTENSION_NAME, document);
+ ok(
+ !!target,
+ "The EventPage-based extension is installed with the expected name"
+ );
+
+ const target2 = findDebugTargetByText(EXTENSION2_NAME, document);
+ ok(
+ !!target2,
+ "The PersistentBackgroundScript-based extension is installed with the expected name"
+ );
+
+ const terminateButton = target.querySelector(
+ ".qa-temporary-extension-terminate-bgscript-button"
+ );
+ ok(
+ !!terminateButton,
+ `${EXTENSION_NAME} is expected to have a terminate button`
+ );
+
+ const terminateButton2 = target2.querySelector(
+ ".qa-temporary-extension-terminate-bgscript-button"
+ );
+ ok(
+ !terminateButton2,
+ `${EXTENSION2_NAME} is expected to not have a terminate button`
+ );
+
+ info("Wait for the test extension background script to be loaded");
+ await promiseBackgroundLoaded;
+
+ info("Wait for the test extension background script status update");
+ await waitForBGStatusUpdate;
+
+ await assertBackgroundStatus(EXTENSION_NAME, {
+ document,
+ expectedStatus: "running",
+ });
+
+ // Verify in the card related to extensions with a persistent background page
+ // the background script status is not being rendered at all.
+ const backgroundStatus2 = target2.querySelector(
+ ".extension-backgroundscript__status"
+ );
+ ok(
+ !backgroundStatus2,
+ `${EXTENSION2_NAME} should not be showing background script status`
+ );
+
+ info(`Click on the terminate button for ${EXTENSION_NAME}`);
+ const waitForTerminateSuccess = waitForDispatch(
+ window.AboutDebugging.store,
+ "TERMINATE_EXTENSION_BGSCRIPT_SUCCESS"
+ );
+ waitForBGStatusUpdate = promiseBackgroundStatusUpdate(window);
+ terminateButton.click();
+ await waitForTerminateSuccess;
+
+ info("Wait for the extension background script to be unloaded");
+ await promiseBackgroundUnloaded;
+ await waitForBGStatusUpdate;
+ await assertBackgroundStatus(EXTENSION_NAME, {
+ document,
+ expectedStatus: "stopped",
+ });
+
+ // Uninstall the test extensions.
+ await Promise.all([
+ AddonManager.getAddonByID(EXTENSION_ID).then(addon => addon.uninstall()),
+ AddonManager.getAddonByID(EXTENSION2_ID).then(addon => addon.uninstall()),
+ ]);
+
+ info("Wait until the debug targets with test extensions disappears");
+ await waitUntil(
+ () =>
+ !findDebugTargetByText(EXTENSION_NAME, document) &&
+ !findDebugTargetByText(EXTENSION2_NAME, document)
+ );
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_eventpage_terminate_on_idle.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_eventpage_terminate_on_idle.js
new file mode 100644
index 0000000000..1b43808fa2
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_eventpage_terminate_on_idle.js
@@ -0,0 +1,200 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.eventPages.enabled", true]],
+ });
+});
+
+// Test that an extension event page isn't terminated on idle when a DevTools
+// Toolbox is attached to the extension.
+add_task(
+ async function test_eventpage_no_idle_shutdown_with_toolbox_attached() {
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ const EXTENSION_ID = "test-devtools-eventpage@mozilla.org";
+ const EXTENSION_NAME = "Temporary EventPage-based web extension";
+
+ const promiseBackgroundLoaded =
+ promiseBackgroundContextLoaded(EXTENSION_ID);
+
+ let waitForBGStatusUpdate = promiseBackgroundStatusUpdate(window);
+
+ // Install the extension using an event page (non persistent background page).
+ await installTemporaryExtensionFromXPI(
+ {
+ id: EXTENSION_ID,
+ name: EXTENSION_NAME,
+ // The extension is expected to have a non persistent background script.
+ extraProperties: {
+ background: {
+ scripts: ["bgpage.js"],
+ persistent: false,
+ },
+ },
+ files: {
+ "bgpage.js": function () {
+ // Emit a dump when the script is loaded to make it easier
+ // to investigate intermittents.
+ dump(`Background script loaded: ${window.location}\n`);
+ },
+ },
+ },
+ document
+ );
+
+ const target = findDebugTargetByText(EXTENSION_NAME, document);
+ ok(
+ !!target,
+ "The EventPage-based extension is installed with the expected name"
+ );
+
+ info("Wait for the test extension background script to be loaded");
+ await promiseBackgroundLoaded;
+
+ info("Wait for the test extension background script status update");
+ await waitForBGStatusUpdate;
+ await assertBackgroundStatus(EXTENSION_NAME, {
+ document,
+ expectedStatus: "running",
+ });
+
+ waitForBGStatusUpdate = promiseBackgroundStatusUpdate(window);
+ await triggerExtensionEventPageIdleTimeout(EXTENSION_ID);
+ await waitForBGStatusUpdate;
+ await assertBackgroundStatus(EXTENSION_NAME, {
+ document,
+ expectedStatus: "stopped",
+ });
+
+ info(
+ "Respawn the extension background script on new WebExtension API event"
+ );
+ waitForBGStatusUpdate = promiseBackgroundStatusUpdate(window);
+ await wakeupExtensionEventPage(EXTENSION_ID);
+ await waitForBGStatusUpdate;
+ await assertBackgroundStatus(EXTENSION_NAME, {
+ document,
+ expectedStatus: "running",
+ });
+
+ info("Open a DevTools toolbox on the target extension");
+ const { devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ EXTENSION_NAME
+ );
+
+ info(
+ "Verify event page terminated on terminate button clicked while the DevTools toolbox is open"
+ );
+ const terminateButton = target.querySelector(
+ ".qa-temporary-extension-terminate-bgscript-button"
+ );
+ ok(
+ !!terminateButton,
+ `${EXTENSION_NAME} is expected to have a terminate button`
+ );
+
+ info(`Click on the terminate button for ${EXTENSION_NAME}`);
+ const promiseBackgroundUnloaded =
+ promiseBackgroundContextUnloaded(EXTENSION_ID);
+ const waitForTerminateSuccess = waitForDispatch(
+ window.AboutDebugging.store,
+ "TERMINATE_EXTENSION_BGSCRIPT_SUCCESS"
+ );
+ waitForBGStatusUpdate = promiseBackgroundStatusUpdate(window);
+ terminateButton.click();
+ await waitForTerminateSuccess;
+
+ info("Wait for the extension background script to be unloaded");
+ await promiseBackgroundUnloaded;
+ await waitForBGStatusUpdate;
+ await assertBackgroundStatus(EXTENSION_NAME, {
+ document,
+ expectedStatus: "stopped",
+ targetElement: target,
+ });
+
+ info(
+ "Verify event page isn't terminated on idle while the DevTools toolbox is open"
+ );
+
+ // Make sure the event page is running again.
+ waitForBGStatusUpdate = promiseBackgroundStatusUpdate(window);
+ await wakeupExtensionEventPage(EXTENSION_ID);
+ await waitForBGStatusUpdate;
+ await assertBackgroundStatus(EXTENSION_NAME, {
+ document,
+ expectedStatus: "running",
+ targetElement: target,
+ });
+
+ const waitForBGSuspendIgnored =
+ promiseTerminateBackgroundScriptIgnored(EXTENSION_ID);
+ waitForBGStatusUpdate = promiseBackgroundStatusUpdate(window);
+ await triggerExtensionEventPageIdleTimeout(EXTENSION_ID);
+ await Promise.race([waitForBGStatusUpdate, waitForBGSuspendIgnored]);
+
+ await assertBackgroundStatus(EXTENSION_NAME, {
+ document,
+ expectedStatus: "running",
+ // After opening the toolbox there will be an additional target listed
+ // for the devtools toolbox tab, its card includes the extension name
+ // and so while the toolbox is open we should make sure to look for
+ // the background status inside the extension target card instead of
+ // the one associated to the devtools toolbox tab.
+ targetElement: target,
+ });
+
+ info(
+ "Wait for warning message expected to be logged for the event page not terminated on idle"
+ );
+ const toolbox = getToolbox(devtoolsWindow);
+ const webconsole = await toolbox.selectTool("webconsole");
+ const { hud } = webconsole;
+ const expectedWarning =
+ "Background event page was not terminated on idle because a DevTools toolbox is attached to the extension.";
+ let consoleElements;
+ await waitUntil(() => {
+ consoleElements = findMessagesByType(hud, expectedWarning, ".warn");
+ return !!consoleElements.length;
+ });
+
+ const locationElement = consoleElements[0].querySelector(
+ ".frame-link-filename"
+ );
+ ok(
+ locationElement.textContent.endsWith("_generated_background_page.html"),
+ "The warning message is associated to the event page url"
+ );
+
+ info(
+ "Verify event page is terminated on idle after closing the DevTools toolbox"
+ );
+
+ await closeWebExtAboutDevtoolsToolbox(devtoolsWindow, window);
+ await triggerExtensionEventPageIdleTimeout(EXTENSION_ID);
+ await waitForBGStatusUpdate;
+ await assertBackgroundStatus(EXTENSION_NAME, {
+ document,
+ expectedStatus: "stopped",
+ });
+
+ // Uninstall the test extensions.
+ info("Unload extension and remove about:debugging tab");
+ await AddonManager.getAddonByID(EXTENSION_ID).then(addon =>
+ addon.uninstall()
+ );
+ info("Wait until the debug targets with test extensions disappears");
+ await waitUntil(() => !findDebugTargetByText(EXTENSION_NAME, document));
+ await removeTab(tab);
+ }
+);
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_manifest_url.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_manifest_url.js
new file mode 100644
index 0000000000..6420a076b0
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_manifest_url.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ adbAddon,
+} = require("resource://devtools/client/shared/remote-debugging/adb/adb-addon.js");
+
+const ABD_ADDON_NAME = "ADB binary provider";
+
+/* import-globals-from helper-adb.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-adb.js", this);
+
+// Test that manifest URLs for addon targets show the manifest correctly in a new tab.
+// This test reuses the ADB extension to be sure to have a valid manifest URL to open.
+add_task(async function () {
+ await pushPref(
+ "devtools.remote.adb.extensionURL",
+ CHROME_URL_ROOT + "resources/test-adb-extension/adb-extension-#OS#.xpi"
+ );
+ await checkAdbNotRunning();
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+ const usbStatusElement = document.querySelector(".qa-sidebar-usb-status");
+
+ info("Install ADB");
+ adbAddon.install("internal");
+ await waitUntil(() => usbStatusElement.textContent.includes("USB enabled"));
+ await waitForAdbStart();
+
+ info("Wait until the debug target for ADB appears");
+ await waitUntil(() => findDebugTargetByText(ABD_ADDON_NAME, document));
+ const adbExtensionItem = findDebugTargetByText(ABD_ADDON_NAME, document);
+
+ const manifestUrlElement = adbExtensionItem.querySelector(".qa-manifest-url");
+ ok(manifestUrlElement, "A link to the manifest is displayed");
+
+ info("Click on the manifest URL and wait for the new tab to open");
+ const onTabOpened = once(gBrowser.tabContainer, "TabOpen");
+ manifestUrlElement.click();
+ const { target } = await onTabOpened;
+ await BrowserTestUtils.browserLoaded(target.linkedBrowser);
+
+ info("Retrieve the text content of the new tab");
+ const textContent = await SpecialPowers.spawn(
+ target.linkedBrowser,
+ [],
+ function () {
+ return content.wrappedJSObject.document.body.textContent;
+ }
+ );
+
+ const manifestObject = JSON.parse(textContent);
+ ok(manifestObject, "The displayed content is a valid JSON object");
+ is(
+ manifestObject.name,
+ ABD_ADDON_NAME,
+ "Manifest tab shows the expected content"
+ );
+
+ info("Close the manifest.json tab");
+ await removeTab(target);
+
+ info("Uninstall the adb extension and wait for the message to udpate");
+ adbAddon.uninstall();
+ await waitUntil(() => usbStatusElement.textContent.includes("USB disabled"));
+ await stopAdbProcess();
+
+ await waitForAboutDebuggingRequests(window.AboutDebugging.store);
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_popup_picker.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_popup_picker.js
new file mode 100644
index 0000000000..fc67bc7f7c
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_popup_picker.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+const ADDON_ID = "test-devtools-webextension@mozilla.org";
+const ADDON_NAME = "test-devtools-webextension";
+
+/**
+ * Check that the node picker can be used when dynamically navigating to a
+ * webextension popup.
+ */
+add_task(async function testNodePickerInExtensionPopup() {
+ await enableExtensionDebugging();
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ // Note that this extension should not define a background script in order to
+ // reproduce the issue. Otherwise opening the popup does not trigger an auto
+ // navigation from DevTools and you have to use the "Disable Popup Auto Hide"
+ // feature which works around the bug tested here.
+ await installTemporaryExtensionFromXPI(
+ {
+ extraProperties: {
+ browser_action: {
+ default_title: "WebExtension with popup",
+ default_popup: "popup.html",
+ },
+ },
+ files: {
+ "popup.html": `<!DOCTYPE html>
+ <html>
+ <body>
+ <div id="pick-me"
+ style="width:100px; height: 60px; background-color: #f5e8fc">
+ Pick me!
+ </div>
+ </body>
+ </html>
+ `,
+ },
+ id: ADDON_ID,
+ name: ADDON_NAME,
+ },
+ document
+ );
+
+ const { devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ ADDON_NAME
+ );
+ const toolbox = getToolbox(devtoolsWindow);
+ const inspector = await toolbox.getPanel("inspector");
+
+ info("Start the node picker");
+ await toolbox.nodePicker.start();
+
+ info("Open the webextension popup");
+ // Clicking on the addon popup will trigger a navigation between the DevTools
+ // fallback document and the popup document.
+ // Wait until the inspector was fully reloaded and for the node-picker to be
+ // restarted.
+ const nodePickerRestarted = toolbox.nodePicker.once(
+ "node-picker-webextension-target-restarted"
+ );
+ const reloaded = inspector.once("reloaded");
+ clickOnAddonWidget(ADDON_ID);
+ await reloaded;
+ await nodePickerRestarted;
+
+ const popup = await waitFor(() =>
+ gBrowser.ownerDocument.querySelector(".webextension-popup-browser")
+ );
+
+ info("Pick an element inside the webextension popup");
+ const onNewNodeFront = inspector.selection.once("new-node-front");
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#pick-me",
+ {},
+ popup.browsingContext
+ );
+ const nodeFront = await onNewNodeFront;
+ is(nodeFront.id, "pick-me", "The expected node front was selected");
+
+ await closeWebExtAboutDevtoolsToolbox(devtoolsWindow, window);
+ await removeTemporaryExtension(ADDON_NAME, document);
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_remote_runtime.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_remote_runtime.js
new file mode 100644
index 0000000000..607b8e15b4
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_remote_runtime.js
@@ -0,0 +1,145 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const NETWORK_RUNTIME_HOST = "localhost:6080";
+const NETWORK_RUNTIME_APP_NAME = "TestNetworkApp";
+const USB_RUNTIME_ID = "test-runtime-id";
+const USB_RUNTIME_DEVICE_NAME = "test device name";
+const USB_RUNTIME_APP_NAME = "TestUsbApp";
+
+// Test that addons are displayed and updated for USB runtimes when expected.
+add_task(async function () {
+ const mocks = new Mocks();
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ info("Prepare USB client mock");
+ const usbClient = mocks.createUSBRuntime(USB_RUNTIME_ID, {
+ deviceName: USB_RUNTIME_DEVICE_NAME,
+ name: USB_RUNTIME_APP_NAME,
+ });
+ mocks.emitUSBUpdate();
+
+ info("Test addons in runtime page for USB client");
+ await connectToRuntime(USB_RUNTIME_DEVICE_NAME, document);
+ await selectRuntime(USB_RUNTIME_DEVICE_NAME, USB_RUNTIME_APP_NAME, document);
+ await testAddonsOnMockedRemoteClient(
+ usbClient,
+ mocks.thisFirefoxClient,
+ document
+ );
+
+ info("Prepare Network client mock");
+ const networkClient = mocks.createNetworkRuntime(NETWORK_RUNTIME_HOST, {
+ name: NETWORK_RUNTIME_APP_NAME,
+ });
+
+ info("Test addons in runtime page for Network client");
+ await connectToRuntime(NETWORK_RUNTIME_HOST, document);
+ await selectRuntime(NETWORK_RUNTIME_HOST, NETWORK_RUNTIME_APP_NAME, document);
+ await testAddonsOnMockedRemoteClient(
+ networkClient,
+ mocks.thisFirefoxClient,
+ document
+ );
+
+ await removeTab(tab);
+});
+
+/**
+ * Check that addons are visible in the runtime page for a remote client (USB or network).
+ */
+async function testAddonsOnMockedRemoteClient(
+ remoteClient,
+ firefoxClient,
+ document
+) {
+ const extensionPane = getDebugTargetPane("Extensions", document);
+ info("Check an empty target pane message is displayed");
+ ok(
+ extensionPane.querySelector(".qa-debug-target-list-empty"),
+ "Extensions list is empty"
+ );
+
+ info("Add an extension to the remote client");
+ const addon = { name: "Test extension name", debuggable: true };
+ const temporaryAddon = {
+ name: "Test temporary extension",
+ debuggable: true,
+ temporarilyInstalled: true,
+ };
+ remoteClient.listAddons = () => [addon, temporaryAddon];
+ remoteClient._eventEmitter.emit("addonListChanged");
+
+ info("Wait until the extension appears");
+ await waitUntil(
+ () => !extensionPane.querySelector(".qa-debug-target-list-empty")
+ );
+
+ const extensionTarget = findDebugTargetByText(
+ "Test extension name",
+ document
+ );
+ ok(extensionTarget, "Extension target appeared for the remote runtime");
+
+ const temporaryExtensionTarget = findDebugTargetByText(
+ "Test temporary extension",
+ document
+ );
+ ok(
+ temporaryExtensionTarget,
+ "Temporary Extension target appeared for the remote runtime"
+ );
+
+ const removeButton = temporaryExtensionTarget.querySelector(
+ ".qa-temporary-extension-remove-button"
+ );
+ ok(removeButton, "Remove button expected for the temporary extension");
+
+ const reloadButton = temporaryExtensionTarget.querySelector(
+ ".qa-temporary-extension-reload-button"
+ );
+ ok(reloadButton, "Reload button expected for the temporary extension");
+
+ // The goal here is to check that runtimes addons are only updated when the remote
+ // runtime is sending addonListChanged events. The reason for this test is because the
+ // previous implementation was updating the remote runtime extensions list when the
+ // _local_ AddonManager was updated.
+ info("Remove the extension from the remote client WITHOUT sending an event");
+ remoteClient.listAddons = () => [];
+
+ info("Simulate an addon update on the ThisFirefox client");
+ firefoxClient._eventEmitter.emit("addonListChanged");
+
+ // To avoid wait for a set period of time we trigger another async update, adding a new
+ // tab. We assume that if the addon update mechanism had started, it would also be done
+ // when the new tab was processed.
+ info("Wait until the tab target for 'http://some.random/url.com' appears");
+ const testTab = {
+ retrieveFavicon: () => {},
+ outerWindowID: 0,
+ traits: {},
+ url: "http://some.random/url.com",
+ };
+ remoteClient.listTabs = () => [testTab];
+ remoteClient._eventEmitter.emit("tabListChanged");
+ await waitUntil(() =>
+ findDebugTargetByText("http://some.random/url.com", document)
+ );
+
+ ok(
+ findDebugTargetByText("Test extension name", document),
+ "The test extension is still visible"
+ );
+
+ info(
+ "Emit `addonListChanged` on remoteClient and wait for the target list to update"
+ );
+ remoteClient._eventEmitter.emit("addonListChanged");
+ await waitUntil(
+ () => !findDebugTargetByText("Test extension name", document)
+ );
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_addon_buttons.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_addon_buttons.js
new file mode 100644
index 0000000000..26b558ff37
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_addon_buttons.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+// Test that the reload button updates the addon list with the correct metadata.
+add_task(async function () {
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ const ORIGINAL_EXTENSION_NAME = "Temporary web extension (original)";
+ const UPDATED_EXTENSION_NAME = "Temporary web extension (updated)";
+ const EXTENSION_ID = "test-devtools@mozilla.org";
+
+ const { xpiFile: addonFile } = await installTemporaryExtensionFromXPI(
+ {
+ id: EXTENSION_ID,
+ name: ORIGINAL_EXTENSION_NAME,
+ },
+ document
+ );
+
+ const originalTarget = findDebugTargetByText(
+ ORIGINAL_EXTENSION_NAME,
+ document
+ );
+ ok(
+ !!originalTarget,
+ "The temporary extension isinstalled with the expected name"
+ );
+
+ info("Update the name of the temporary extension in the manifest");
+ updateTemporaryXPI(
+ { id: EXTENSION_ID, name: UPDATED_EXTENSION_NAME },
+ addonFile
+ );
+
+ info("Click on the reload button for the temporary extension");
+ const reloadButton = originalTarget.querySelector(
+ ".qa-temporary-extension-reload-button"
+ );
+ reloadButton.click();
+
+ info(
+ "Wait until the debug target with the original extension name disappears"
+ );
+ await waitUntil(
+ () => !findDebugTargetByText(ORIGINAL_EXTENSION_NAME, document)
+ );
+
+ info("Wait until the debug target with the updated extension name appears");
+ await waitUntil(() =>
+ findDebugTargetByText(UPDATED_EXTENSION_NAME, document)
+ );
+
+ const updatedTarget = findDebugTargetByText(UPDATED_EXTENSION_NAME, document);
+ ok(!!updatedTarget, "The temporary extension name has been updated");
+
+ info("Click on the remove button for the temporary extension");
+ const removeButton = updatedTarget.querySelector(
+ ".qa-temporary-extension-remove-button"
+ );
+ removeButton.click();
+
+ info(
+ "Wait until the debug target with the updated extension name disappears"
+ );
+ await waitUntil(
+ () => !findDebugTargetByText(UPDATED_EXTENSION_NAME, document)
+ );
+
+ await removeTab(tab);
+});
+
+add_task(async function () {
+ const PACKAGED_EXTENSION_ID = "packaged-extension@tests";
+ const PACKAGED_EXTENSION_NAME = "Packaged extension";
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ await installRegularExtension(
+ "resources/packaged-extension/packaged-extension.xpi"
+ );
+
+ info("Wait until extension appears in about:debugging");
+ await waitUntil(() =>
+ findDebugTargetByText(PACKAGED_EXTENSION_NAME, document)
+ );
+ const target = findDebugTargetByText(PACKAGED_EXTENSION_NAME, document);
+
+ const reloadButton = target.querySelector(
+ ".qa-temporary-extension-reload-button"
+ );
+ ok(
+ !reloadButton,
+ "No reload button displayed for a regularly installed extension"
+ );
+
+ const removeButton = target.querySelector(
+ ".qa-temporary-extension-remove-button"
+ );
+ ok(
+ !removeButton,
+ "No remove button displayed for a regularly installed extension"
+ );
+
+ await removeExtension(
+ PACKAGED_EXTENSION_ID,
+ PACKAGED_EXTENSION_NAME,
+ document
+ );
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_id_message.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_id_message.js
new file mode 100644
index 0000000000..6e0b21fb20
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_id_message.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+// Test that temporary extensions show a message about temporary ids, with a learn more
+// link.
+add_task(async function () {
+ const EXTENSION_NAME = "Temporary web extension";
+ const EXTENSION_ID = "test-devtools@mozilla.org";
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ await installTemporaryExtensionFromXPI(
+ {
+ id: EXTENSION_ID,
+ name: EXTENSION_NAME,
+ },
+ document
+ );
+
+ info("Wait until a debug target item appears");
+ await waitUntil(() => findDebugTargetByText(EXTENSION_NAME, document));
+
+ const target = findDebugTargetByText(EXTENSION_NAME, document);
+
+ const message = target.querySelector(".qa-temporary-id-message");
+ ok(!!message, "Temporary id message is displayed for temporary extensions");
+
+ const link = target.querySelector(".qa-temporary-id-link");
+ ok(!!link, "Temporary id link is displayed for temporary extensions");
+
+ await removeTemporaryExtension(EXTENSION_NAME, document);
+ await removeTab(tab);
+});
+
+// Test that the message and the link are not displayed for a regular extension.
+add_task(async function () {
+ const PACKAGED_EXTENSION_ID = "packaged-extension@tests";
+ const PACKAGED_EXTENSION_NAME = "Packaged extension";
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ await installRegularExtension(
+ "resources/packaged-extension/packaged-extension.xpi"
+ );
+
+ info("Wait until extension appears in about:debugging");
+ await waitUntil(() =>
+ findDebugTargetByText(PACKAGED_EXTENSION_NAME, document)
+ );
+ const target = findDebugTargetByText(PACKAGED_EXTENSION_NAME, document);
+
+ const tmpIdMessage = target.querySelector(".qa-temporary-id-message");
+ ok(
+ !tmpIdMessage,
+ "No temporary id message is displayed for a regular extension"
+ );
+
+ await removeExtension(
+ PACKAGED_EXTENSION_ID,
+ PACKAGED_EXTENSION_NAME,
+ document
+ );
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_install_error.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_install_error.js
new file mode 100644
index 0000000000..21a30ab3bf
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_install_error.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+/**
+ * Test that the installation error messages are displayed when installing temporary
+ * extensions.
+ */
+
+const INVALID_JSON_EXTENSION_PATH =
+ "resources/bad-extensions/invalid-json/manifest.json";
+const INVALID_PROP_EXTENSION_PATH =
+ "resources/bad-extensions/invalid-property/manifest.json";
+const EXTENSION_PATH = "resources/test-temporary-extension/manifest.json";
+const EXTENSION_NAME = "test-temporary-extension";
+
+// Extension with an invalid JSON manifest will not be parsed. We check the expected
+// error message is displayed
+add_task(async function testInvalidJsonExtension() {
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ const installError = await installBadExtension(
+ INVALID_JSON_EXTENSION_PATH,
+ document
+ );
+ ok(
+ installError.textContent.includes("JSON.parse: unexpected keyword"),
+ "The expected installation error is displayed: " + installError.textContent
+ );
+
+ info("Install a valid extension to make the message disappear");
+ await installTemporaryExtension(EXTENSION_PATH, EXTENSION_NAME, document);
+
+ info("Wait until the error message disappears");
+ await waitUntil(
+ () => !document.querySelector(".qa-tmp-extension-install-error")
+ );
+
+ info("Wait for the temporary addon to be displayed as a debug target");
+ await waitUntil(() => findDebugTargetByText(EXTENSION_NAME, document));
+
+ await removeTemporaryExtension(EXTENSION_NAME, document);
+
+ await removeTab(tab);
+});
+
+// Extension with a valid JSON manifest but containing an invalid property should display
+// a detailed error message coming from the Addon Manager.
+add_task(async function testInvalidPropertyExtension() {
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ const installError = await installBadExtension(
+ INVALID_PROP_EXTENSION_PATH,
+ document
+ );
+
+ ok(
+ installError.textContent.includes("Extension is invalid"),
+ "The basic installation error is displayed: " + installError.textContent
+ );
+ ok(
+ installError.textContent.includes(
+ "Reading manifest: Error processing content_scripts.0.matches"
+ ),
+ "The detailed installation error is also displayed: " +
+ installError.textContent
+ );
+
+ await removeTab(tab);
+});
+
+async function installBadExtension(path, document) {
+ info("Install a bad extension at path: " + path);
+ // Do not use installTemporaryAddon here since the install will fail.
+ prepareMockFilePicker(path);
+ document.querySelector(".qa-temporary-extension-install-button").click();
+
+ info("Wait until the install error message appears");
+ await waitUntil(() =>
+ document.querySelector(".qa-tmp-extension-install-error")
+ );
+ return document.querySelector(".qa-tmp-extension-install-error");
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_install_path.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_install_path.js
new file mode 100644
index 0000000000..a6f9fde0c1
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_install_path.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+/**
+ * Test the the path used to install a temporary addon is saved in a preference and reused
+ * next time the feature is used.
+ */
+
+const EXTENSION_PATH = "resources/test-temporary-extension/manifest.json";
+const EXTENSION_NAME = "test-temporary-extension";
+const LAST_DIR_PREF = "devtools.aboutdebugging.tmpExtDirPath";
+
+// Check that the preference is updated when trying to install a temporary extension.
+add_task(async function testPreferenceUpdatedAfterInstallingExtension() {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(LAST_DIR_PREF);
+ });
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ await installTemporaryExtension(EXTENSION_PATH, EXTENSION_NAME, document);
+
+ info("Check whether the selected dir sets into the pref");
+ const lastDirPath = Services.prefs.getCharPref(LAST_DIR_PREF, "");
+ const expectedPath = getTestFilePath("resources/test-temporary-extension");
+ is(lastDirPath, expectedPath, "The selected dir should set into the pref");
+
+ await waitUntil(() => findDebugTargetByText(EXTENSION_NAME, document));
+ await removeTemporaryExtension(EXTENSION_NAME, document);
+ await removeTab(tab);
+});
+
+// Check that the preference is updated when trying to install a temporary extension.
+add_task(async function testPreferenceRetrievedWhenInstallingExtension() {
+ const selectedDir = getTestFilePath("resources/packaged-extension");
+
+ await pushPref(LAST_DIR_PREF, selectedDir);
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ const MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+ const onFilePickerShown = new Promise(resolve => {
+ MockFilePicker.showCallback = fp => {
+ resolve(fp);
+ };
+ });
+ document.querySelector(".qa-temporary-extension-install-button").click();
+
+ info("Check whether the shown dir is same as the pref");
+ const fp = await onFilePickerShown;
+ is(
+ fp.displayDirectory.path,
+ selectedDir,
+ "Shown directory sets as same as the pref"
+ );
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_reload_error.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_reload_error.js
new file mode 100644
index 0000000000..da09727d9e
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_temporary_reload_error.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+// Test that the reload button updates the addon list with the correct metadata.
+add_task(async function () {
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ const EXTENSION_ID = "test-devtools@mozilla.org";
+ const EXTENSION_NAME = "Temporary web extension";
+
+ let { xpiFile: addonFile } = await installTemporaryExtensionFromXPI(
+ {
+ id: EXTENSION_ID,
+ name: EXTENSION_NAME,
+ },
+ document
+ );
+
+ const target = findDebugTargetByText(EXTENSION_NAME, document);
+ ok(!!target, "The temporary extension is installed with the expected name");
+
+ info("Update the name of the temporary extension in the manifest");
+ addonFile = updateTemporaryXPI({ id: EXTENSION_ID }, addonFile);
+
+ info("Click on the reload button for the invalid temporary extension");
+ const waitForError = waitForDispatch(
+ window.AboutDebugging.store,
+ "TEMPORARY_EXTENSION_RELOAD_FAILURE"
+ );
+ const reloadButton = target.querySelector(
+ ".qa-temporary-extension-reload-button"
+ );
+ reloadButton.click();
+ await waitForError;
+ ok(
+ target.querySelector(".qa-temporary-extension-reload-error"),
+ "The error message of reloading appears"
+ );
+
+ info("Click on the reload button for the valid temporary extension");
+ const waitForSuccess = waitForDispatch(
+ window.AboutDebugging.store,
+ "TEMPORARY_EXTENSION_RELOAD_SUCCESS"
+ );
+ updateTemporaryXPI({ id: EXTENSION_ID, name: EXTENSION_NAME }, addonFile);
+ reloadButton.click();
+ await waitForSuccess;
+ ok(
+ !target.querySelector(".qa-temporary-extension-reload-error"),
+ "The error message of reloading disappears"
+ );
+
+ info("Click on the remove button for the temporary extension");
+ const removeButton = target.querySelector(
+ ".qa-temporary-extension-remove-button"
+ );
+ removeButton.click();
+
+ info("Wait until the debug target with the extension disappears");
+ await waitUntil(() => !findDebugTargetByText(EXTENSION_NAME, document));
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_warnings.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_warnings.js
new file mode 100644
index 0000000000..db46754caa
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_addons_warnings.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+// Test that extension warnings are displayed in about:debugging.
+add_task(async function () {
+ const EXTENSION_NAME = "Temporary web extension";
+ const EXTENSION_ID = "test-devtools@mozilla.org";
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ await pushPref("extensions.webextensions.warnings-as-errors", false);
+ await installTemporaryExtensionFromXPI(
+ {
+ id: EXTENSION_ID,
+ name: EXTENSION_NAME,
+ extraProperties: {
+ // This property is not expected in the manifest and should trigger a warning!
+ wrongProperty: {},
+ },
+ },
+ document
+ );
+ await SpecialPowers.popPrefEnv();
+
+ info("Wait until a debug target item appears");
+ await waitUntil(() => findDebugTargetByText(EXTENSION_NAME, document));
+ const target = findDebugTargetByText(EXTENSION_NAME, document);
+
+ const warningMessage = target.querySelector(".qa-message");
+ ok(
+ !!warningMessage,
+ "A warning message is displayed for the installed addon"
+ );
+
+ const warningText = warningMessage.textContent;
+ ok(
+ warningText.includes("wrongProperty"),
+ "The warning message mentions wrongProperty"
+ );
+
+ await removeTemporaryExtension(EXTENSION_NAME, document);
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_connect_networklocations.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_connect_networklocations.js
new file mode 100644
index 0000000000..92096703ac
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_connect_networklocations.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test the network locations form of the Connect page.
+ * Check that a network location can be added and removed.
+ */
+
+const TEST_NETWORK_LOCATION = "localhost:1111";
+const TEST_NETWORK_LOCATION_INVALID = "testnetwork";
+
+add_task(async function () {
+ const { document, tab } = await openAboutDebugging();
+
+ await selectConnectPage(document);
+
+ let networkLocations = document.querySelectorAll(".qa-network-location");
+ is(
+ networkLocations.length,
+ 0,
+ "By default, no network locations are displayed"
+ );
+
+ info("Check whether error message should show if the input value is invalid");
+ addNetworkLocation(TEST_NETWORK_LOCATION_INVALID, document);
+ await waitUntil(() =>
+ document.querySelector(".qa-connect-page__network-form__error-message")
+ );
+
+ info("Wait until the new network location is visible in the list");
+ addNetworkLocation(TEST_NETWORK_LOCATION, document);
+ await waitUntil(
+ () => document.querySelectorAll(".qa-network-location").length === 1
+ );
+ await waitUntil(
+ () =>
+ !document.querySelector(".qa-connect-page__network-form__error-message")
+ );
+
+ networkLocations = document.querySelectorAll(".qa-network-location");
+ const networkLocationValue = networkLocations[0].querySelector(
+ ".qa-network-location-value"
+ );
+ is(
+ networkLocationValue.textContent,
+ TEST_NETWORK_LOCATION,
+ "Added network location has the expected value"
+ );
+
+ info(
+ "Check whether error message should show if the input value was duplicate"
+ );
+ addNetworkLocation(TEST_NETWORK_LOCATION, document);
+ await waitUntil(() =>
+ document.querySelector(".qa-connect-page__network-form__error-message")
+ );
+
+ info("Wait until the new network location is removed from the list");
+ removeNetworkLocation(TEST_NETWORK_LOCATION, document);
+ await waitUntil(
+ () => document.querySelectorAll(".qa-network-location").length === 0
+ );
+
+ await removeTab(tab);
+});
+
+function addNetworkLocation(location, document) {
+ info("Setting a value in the network form input");
+ const networkLocationInput = document.querySelector(".qa-network-form-input");
+ networkLocationInput.value = "";
+ networkLocationInput.focus();
+ EventUtils.sendString(location, networkLocationInput.ownerGlobal);
+
+ info("Click on network form submit button");
+ const networkLocationSubmitButton = document.querySelector(
+ ".qa-network-form-submit-button"
+ );
+ networkLocationSubmitButton.click();
+}
+
+function removeNetworkLocation(location, document) {
+ const networkLocation = getNetworkLocation(location, document);
+ ok(networkLocation, "Network location container found.");
+
+ info("Click on the remove button for the provided network location");
+ const removeButton = networkLocation.querySelector(
+ ".qa-network-location-remove-button"
+ );
+ removeButton.click();
+}
+
+function getNetworkLocation(location, document) {
+ info("Find the container for network location: " + location);
+ const networkLocations = document.querySelectorAll(".qa-network-location");
+ return [...networkLocations].find(element => {
+ return (
+ element.querySelector(".qa-network-location-value").textContent ===
+ location
+ );
+ });
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_connect_toggle_usb_devices.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_connect_toggle_usb_devices.js
new file mode 100644
index 0000000000..e0d7303669
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_connect_toggle_usb_devices.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-adb.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-adb.js", this);
+
+/**
+ * Check that USB Devices scanning can be enabled and disabled from the connect page.
+ */
+add_task(async function () {
+ await pushPref(
+ "devtools.remote.adb.extensionURL",
+ CHROME_URL_ROOT + "resources/test-adb-extension/adb-extension-#OS#.xpi"
+ );
+ await checkAdbNotRunning();
+
+ const { document, tab } = await openAboutDebugging();
+
+ await selectConnectPage(document);
+
+ info("Wait until Connect page is displayed");
+ await waitUntil(() => document.querySelector(".qa-connect-page"));
+
+ info("Check that by default USB devices are disabled");
+ const usbDisabledMessage = document.querySelector(
+ ".qa-connect-usb-disabled-message"
+ );
+ ok(usbDisabledMessage, "A message about enabling USB devices is rendered");
+
+ const usbToggleButton = document.querySelector(
+ ".qa-connect-usb-toggle-button"
+ );
+ ok(usbToggleButton, "The button to toggle USB devices debugging is rendered");
+ ok(
+ usbToggleButton.textContent.includes("Enable"),
+ "The text of the toggle USB button is correct"
+ );
+
+ info("Click on the toggle button");
+ usbToggleButton.click();
+
+ info("Wait until the toggle button text is updated");
+ await waitUntil(() => usbToggleButton.textContent.includes("Disable"));
+ ok(
+ !document.querySelector(".qa-connect-usb-disabled-message"),
+ "The message about enabling USB devices is no longer rendered"
+ );
+
+ info("Check that the addon was installed with the proper source");
+ const adbExtensionId = Services.prefs.getCharPref(
+ "devtools.remote.adb.extensionID"
+ );
+ const addon = await AddonManager.getAddonByID(adbExtensionId);
+ Assert.deepEqual(
+ addon.installTelemetryInfo,
+ { source: "about:debugging" },
+ "Got the expected addon.installTelemetryInfo"
+ );
+
+ // Right now we are resuming as soon as "USB enabled" is displayed, but ADB
+ // might still be starting up. If we move to uninstall directly, the ADB startup will
+ // fail and we will have an unhandled promise rejection.
+ // See Bug 1498469.
+ await waitForAdbStart();
+
+ info("Click on the toggle button");
+ usbToggleButton.click();
+
+ info("Wait until the toggle button text is updated");
+ await waitUntil(() => usbToggleButton.textContent.includes("Enable"));
+ ok(
+ document.querySelector(".qa-connect-usb-disabled-message"),
+ "The message about enabling USB devices is rendered again"
+ );
+
+ await stopAdbProcess();
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_connection_prompt_setting.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_connection_prompt_setting.js
new file mode 100644
index 0000000000..dfee0c1ba0
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_connection_prompt_setting.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const USB_RUNTIME_ID = "1337id";
+const USB_DEVICE_NAME = "Fancy Phone";
+const USB_APP_NAME = "Lorem ipsum";
+
+/**
+ * Check whether can toggle enable/disable connection prompt setting.
+ */
+add_task(async function () {
+ // enable USB devices mocks
+ const mocks = new Mocks();
+ const runtime = mocks.createUSBRuntime(USB_RUNTIME_ID, {
+ deviceName: USB_DEVICE_NAME,
+ name: USB_APP_NAME,
+ });
+
+ info("Set initial state for test");
+ await pushPref("devtools.debugger.prompt-connection", true);
+
+ // open a remote runtime page
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ mocks.emitUSBUpdate();
+ await connectToRuntime(USB_DEVICE_NAME, document);
+ await selectRuntime(USB_DEVICE_NAME, USB_APP_NAME, document);
+
+ info("Check whether connection prompt toggle button exists");
+ let connectionPromptToggleButton = document.querySelector(
+ ".qa-connection-prompt-toggle-button"
+ );
+ ok(connectionPromptToggleButton, "Toggle button existed");
+ ok(
+ connectionPromptToggleButton.textContent.includes("Disable"),
+ "Toggle button shows 'Disable'"
+ );
+
+ info("Click on the toggle button");
+ connectionPromptToggleButton = document.querySelector(
+ ".qa-connection-prompt-toggle-button"
+ );
+ connectionPromptToggleButton.click();
+ info("Wait until the toggle button text is updated");
+ await waitUntil(() =>
+ connectionPromptToggleButton.textContent.includes("Enable")
+ );
+ info("Check the preference");
+ const disabledPref = runtime.getPreference(
+ "devtools.debugger.prompt-connection"
+ );
+ is(disabledPref, false, "The preference should be updated");
+
+ info("Click on the toggle button again");
+ connectionPromptToggleButton.click();
+ info("Wait until the toggle button text is updated");
+ await waitUntil(() =>
+ connectionPromptToggleButton.textContent.includes("Disable")
+ );
+ info("Check the preference");
+ const enabledPref = runtime.getPreference(
+ "devtools.debugger.prompt-connection"
+ );
+ is(enabledPref, true, "The preference should be updated");
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_debug-target-pane_collapsibilities_interaction.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_debug-target-pane_collapsibilities_interaction.js
new file mode 100644
index 0000000000..7b25e02a9b
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_debug-target-pane_collapsibilities_interaction.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+/**
+ * Test that collapsibilities of DebugTargetPane on RuntimePage by mouse clicking.
+ */
+
+add_task(async function () {
+ prepareCollapsibilitiesTest();
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ for (const { title } of TARGET_PANES) {
+ info("Check whether this pane is collapsed after clicking the title");
+ await toggleCollapsibility(getDebugTargetPane(title, document));
+ assertDebugTargetCollapsed(getDebugTargetPane(title, document), title);
+
+ info("Check whether this pane is expanded after clicking the title again");
+ await toggleCollapsibility(getDebugTargetPane(title, document));
+ await assertDebugTargetExpanded(getDebugTargetPane(title, document), title);
+ }
+
+ await removeTab(tab);
+});
+
+async function assertDebugTargetCollapsed(paneEl, title) {
+ info("Check debug target is collapsed");
+
+ // check list height
+ const targetEl = paneEl.querySelector(".qa-debug-target-pane__collapsable");
+ is(targetEl.clientHeight, 0, "Height of list element is zero");
+ // check title
+ const titleEl = paneEl.querySelector(".qa-debug-target-pane-title");
+ const expectedTitle = `${title} (${
+ targetEl.querySelectorAll(".qa-debug-target-item").length
+ })`;
+ is(titleEl.textContent, expectedTitle, "Collapsed title is correct");
+}
+
+async function assertDebugTargetExpanded(paneEl, title) {
+ info("Check debug target is expanded");
+
+ // check list height
+ const targetEl = paneEl.querySelector(".qa-debug-target-pane__collapsable");
+ await waitUntil(() => targetEl.clientHeight > 0);
+ ok(true, "Height of list element is greater than zero");
+ // check title
+ const titleEl = paneEl.querySelector(".qa-debug-target-pane-title");
+ const expectedTitle = `${title} (${
+ targetEl.querySelectorAll(".qa-debug-target-item").length
+ })`;
+ is(titleEl.textContent, expectedTitle, "Expanded title is correct");
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_debug-target-pane_collapsibilities_preference.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_debug-target-pane_collapsibilities_preference.js
new file mode 100644
index 0000000000..50857ee617
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_debug-target-pane_collapsibilities_preference.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+/**
+ * Test for preference of DebugTargetPane collapsibilities.
+ */
+
+add_task(async function () {
+ prepareCollapsibilitiesTest();
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ info("Collapse all pane");
+ for (const { title } of TARGET_PANES) {
+ const debugTargetPaneEl = getDebugTargetPane(title, document);
+ await toggleCollapsibility(debugTargetPaneEl);
+ }
+
+ info("Check preference of collapsibility after closing about:debugging page");
+ await removeTab(tab);
+ // Wait until unmount.
+ await waitUntil(() => document.querySelector(".app") === null);
+
+ for (const { pref } of TARGET_PANES) {
+ is(
+ Services.prefs.getBoolPref(pref),
+ true,
+ `${pref} preference should be true`
+ );
+ }
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_debug-target-pane_empty.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_debug-target-pane_empty.js
new file mode 100644
index 0000000000..301be26a32
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_debug-target-pane_empty.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+/**
+ * Test that an "empty" message is displayed when there are no debug targets in a debug
+ * target pane.
+ */
+
+const EXTENSION_PATH = "resources/test-temporary-extension/manifest.json";
+const EXTENSION_NAME = "test-temporary-extension";
+
+add_task(async function () {
+ prepareCollapsibilitiesTest();
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ info("Check that the temporary extensions pane is empty");
+ const temporaryExtensionPane = getDebugTargetPane(
+ "Temporary Extensions",
+ document
+ );
+ ok(
+ !temporaryExtensionPane.querySelector(".qa-debug-target-item"),
+ "Temporary Extensions pane contains no debug target"
+ );
+
+ info("Check an empty target pane message is displayed");
+ ok(
+ temporaryExtensionPane.querySelector(".qa-debug-target-list-empty"),
+ "An empty target list message is displayed"
+ );
+
+ info("Install a temporary extension");
+ await installTemporaryExtension(EXTENSION_PATH, EXTENSION_NAME, document);
+
+ info("Wait until a debug target item appears");
+ await waitUntil(() =>
+ temporaryExtensionPane.querySelector(".qa-debug-target-item")
+ );
+
+ info("Check the empty target pane message is no longer displayed");
+ ok(
+ !temporaryExtensionPane.querySelector(".qa-debug-target-list-empty"),
+ "The empty target list message is no longer displayed"
+ );
+
+ const temporaryExtensionItem = temporaryExtensionPane.querySelector(
+ ".qa-debug-target-item"
+ );
+ ok(
+ temporaryExtensionItem,
+ "Temporary Extensions pane now shows debug target"
+ );
+
+ info("Remove the temporary extension");
+ temporaryExtensionItem
+ .querySelector(".qa-temporary-extension-remove-button")
+ .click();
+
+ info("Wait until the debug target item disappears");
+ await waitUntil(
+ () => !temporaryExtensionPane.querySelector(".qa-debug-target-item")
+ );
+
+ info("Check the empty target pane message is displayed again");
+ ok(
+ temporaryExtensionPane.querySelector(".qa-debug-target-list-empty"),
+ "An empty target list message is displayed again"
+ );
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_debug-target-pane_usb_runtime.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_debug-target-pane_usb_runtime.js
new file mode 100644
index 0000000000..0f50b9454b
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_debug-target-pane_usb_runtime.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+const RUNTIME_ID = "test-runtime-id";
+const RUNTIME_DEVICE_NAME = "test device name";
+const RUNTIME_APP_NAME = "TestApp";
+
+// Test that the expected supported categories are displayed for USB runtimes.
+add_task(async function () {
+ const mocks = new Mocks();
+ await checkTargetPanes({ enableLocalTabs: false }, mocks);
+
+ info(
+ "Check that enableLocalTabs has no impact on the categories displayed for remote" +
+ " runtimes."
+ );
+ await checkTargetPanes({ enableLocalTabs: true }, mocks);
+});
+
+async function checkTargetPanes({ enableLocalTabs }, mocks) {
+ const { document, tab, window } = await openAboutDebugging({
+ enableLocalTabs,
+ });
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ mocks.createUSBRuntime(RUNTIME_ID, {
+ deviceName: RUNTIME_DEVICE_NAME,
+ name: RUNTIME_APP_NAME,
+ });
+ mocks.emitUSBUpdate();
+
+ await connectToRuntime(RUNTIME_DEVICE_NAME, document);
+ await selectRuntime(RUNTIME_DEVICE_NAME, RUNTIME_APP_NAME, document);
+
+ const SUPPORTED_TARGET_PANES = [
+ "Temporary Extensions",
+ "Extensions",
+ "Other Workers",
+ "Shared Workers",
+ "Service Workers",
+ "Tabs",
+ ];
+
+ for (const { title } of TARGET_PANES) {
+ const debugTargetPaneEl = getDebugTargetPane(title, document);
+ if (SUPPORTED_TARGET_PANES.includes(title)) {
+ ok(debugTargetPaneEl, `Supported target pane [${title}] is displayed`);
+ } else {
+ ok(!debugTargetPaneEl, `Unsupported target pane [${title}] is hidden`);
+ }
+ }
+
+ const installButton = document.querySelector(
+ ".qa-temporary-extension-install-button"
+ );
+ ok(!installButton, "Temporary Extensions install button is hidden");
+
+ info("Remove USB runtime");
+ mocks.removeUSBRuntime(RUNTIME_ID);
+ mocks.emitUSBUpdate();
+ await waitUntilUsbDeviceIsUnplugged(RUNTIME_DEVICE_NAME, document);
+
+ await removeTab(tab);
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtools.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtools.js
new file mode 100644
index 0000000000..4ea3238ea3
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtools.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+/**
+ * Check that DevTools are not closed when leaving This Firefox runtime page.
+ */
+
+add_task(async function () {
+ info("Force all debug target panes to be expanded");
+ prepareCollapsibilitiesTest();
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ const connectSidebarItem = findSidebarItemByText("Setup", document);
+ const connectLink = connectSidebarItem.querySelector(".qa-sidebar-link");
+ ok(connectSidebarItem, "Found the Connect sidebar item");
+
+ info("Open devtools on the current about:debugging tab");
+ const toolbox = await openToolboxForTab(tab, "inspector");
+ const inspector = toolbox.getPanel("inspector");
+
+ info("DevTools starts workers, wait for requests to settle");
+ const store = window.AboutDebugging.store;
+ await waitForAboutDebuggingRequests(store);
+
+ info("Click on the Connect item in the sidebar");
+ connectLink.click();
+ await waitForDispatch(store, "UNWATCH_RUNTIME_SUCCESS");
+
+ info("Wait until Connect page is displayed");
+ await waitUntil(() => document.querySelector(".qa-connect-page"));
+
+ const markupViewElement = inspector.panelDoc.getElementById("markup-box");
+ ok(markupViewElement, "Inspector is still rendered");
+
+ // We explicitely destroy the toolbox in order to ensure waiting for its full destruction
+ // and avoid leak / pending requests
+ info("Destroy the opened toolbox");
+ await toolbox.destroy();
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_breakpoint.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_breakpoint.js
new file mode 100644
index 0000000000..925d40b643
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_breakpoint.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/shared-head.js",
+ this
+);
+
+const SCRIPT_FILE = "script_aboutdebugging_devtoolstoolbox_breakpoint.js";
+const TAB_URL =
+ "https://example.com/browser/devtools/client/aboutdebugging/" +
+ "test/browser/resources/doc_aboutdebugging_devtoolstoolbox_breakpoint.html";
+
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+/**
+ * Test breakpoints in about:devtools-toolbox tabs (ie non localTab tab target).
+ */
+add_task(async function () {
+ const testTab = await addTab(TAB_URL);
+
+ info("Force all debug target panes to be expanded");
+ prepareCollapsibilitiesTest();
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+ const { devtoolsTab, devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ TAB_URL
+ );
+ info("Select performance panel");
+ const toolbox = getToolbox(devtoolsWindow);
+ await toolbox.selectTool("jsdebugger");
+
+ info("Add a breakpoint at line 10 in the test script");
+ const debuggerContext = createDebuggerContext(toolbox);
+ await selectSource(debuggerContext, SCRIPT_FILE);
+ await addBreakpoint(debuggerContext, SCRIPT_FILE, 10);
+
+ info("Invoke testMethod, expect the script to pause on line 10");
+ const onContentTaskDone = ContentTask.spawn(
+ testTab.linkedBrowser,
+ {},
+ function () {
+ content.wrappedJSObject.testMethod();
+ }
+ );
+
+ info("Wait for the debugger to pause");
+ await waitForPaused(debuggerContext);
+ const script = findSource(debuggerContext, SCRIPT_FILE);
+ assertPausedAtSourceAndLine(debuggerContext, script.id, 10);
+
+ info("Resume");
+ await resume(debuggerContext);
+
+ info("Wait for the paused content task to also resolve");
+ await onContentTaskDone;
+
+ info("Remove breakpoint");
+ await removeBreakpoint(debuggerContext, script.id, 10);
+
+ await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
+ await removeTab(testTab);
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_contextmenu.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_contextmenu.js
new file mode 100644
index 0000000000..eb8f233d12
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_contextmenu.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+/**
+ * Test context menu on about:devtools-toolbox page.
+ */
+add_task(async function () {
+ info("Force all debug target panes to be expanded");
+ prepareCollapsibilitiesTest();
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+ const { devtoolsBrowser, devtoolsTab } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window
+ );
+
+ info("Check whether the menu item which opens devtools is disabled");
+ const rootDocument = devtoolsTab.ownerDocument;
+ await assertContextMenu(
+ rootDocument,
+ devtoolsBrowser,
+ ".debug-target-info",
+ false
+ );
+
+ info("Force to select about:debugging page");
+ await updateSelectedTab(gBrowser, tab, window.AboutDebugging.store);
+
+ info("Check whether the menu item which opens devtools is enabled");
+ await assertContextMenu(rootDocument, devtoolsBrowser, "#mount", true);
+
+ await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
+ await removeTab(tab);
+});
+
+async function assertContextMenu(
+ rootDocument,
+ browser,
+ targetSelector,
+ shouldBeEnabled
+) {
+ if (shouldBeEnabled) {
+ await assertContextMenuEnabled(rootDocument, browser, targetSelector);
+ } else {
+ await assertContextMenuDisabled(rootDocument, browser, targetSelector);
+ }
+}
+
+async function assertContextMenuDisabled(
+ rootDocument,
+ browser,
+ targetSelector
+) {
+ const contextMenu = rootDocument.getElementById("contentAreaContextMenu");
+ let isPopupShown = false;
+ const listener = () => {
+ isPopupShown = true;
+ };
+ contextMenu.addEventListener("popupshown", listener);
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ targetSelector,
+ { type: "contextmenu" },
+ browser
+ );
+ await wait(1000);
+ ok(!isPopupShown, `Context menu should not be shown`);
+ contextMenu.removeEventListener("popupshown", listener);
+}
+
+async function assertContextMenuEnabled(rootDocument, browser, targetSelector) {
+ // Show content context menu.
+ const contextMenu = rootDocument.getElementById("contentAreaContextMenu");
+ const popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ targetSelector,
+ { type: "contextmenu" },
+ browser
+ );
+ await popupShownPromise;
+ ok(true, `Context menu should be shown`);
+
+ // Hide content context menu.
+ const popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await popupHiddenPromise;
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_contextmenu_markupview.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_contextmenu_markupview.js
new file mode 100644
index 0000000000..bc46b828fd
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_contextmenu_markupview.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+/**
+ * Test context menu of markup view on about:devtools-toolbox page.
+ */
+add_task(async function () {
+ info("Force all debug target panes to be expanded");
+ prepareCollapsibilitiesTest();
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+ const { devtoolsTab, devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window
+ );
+
+ info("Select inspector tool");
+ const toolbox = getToolbox(devtoolsWindow);
+ await toolbox.selectTool("inspector");
+
+ info("Show context menu of markup view");
+ const markupDocument = toolbox.getPanel("inspector").markup.doc;
+ EventUtils.synthesizeMouseAtCenter(
+ markupDocument.body,
+ { type: "contextmenu" },
+ markupDocument.ownerGlobal
+ );
+
+ info("Check whether proper context menu of markup view will be shown");
+ await waitUntil(() => toolbox.topDoc.querySelector("#node-menu-edithtml"));
+ ok(true, "Context menu of markup view should be shown");
+
+ await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_focus.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_focus.js
new file mode 100644
index 0000000000..a3fa63c340
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_focus.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+/**
+ * Test whether the focus transfers to a tab which is already inspected .
+ */
+add_task(async function () {
+ info("Force all debug target panes to be expanded");
+ prepareCollapsibilitiesTest();
+
+ info(
+ "Select 'performance' panel as the initial tool since the tool does not listen " +
+ "any changes of the document without user action"
+ );
+ await pushPref("devtools.toolbox.selectedTool", "performance");
+
+ const { document, tab, window } = await openAboutDebugging();
+ const { store } = window.AboutDebugging;
+ await selectThisFirefoxPage(document, store);
+
+ const inspectionTarget = "about:debugging";
+ info(`Open ${inspectionTarget} as inspection target`);
+ await waitUntil(() => findDebugTargetByText(inspectionTarget, document));
+ info(`Inspect ${inspectionTarget} page in about:devtools-toolbox`);
+ const { devtoolsTab, devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ inspectionTarget
+ );
+
+ info(
+ "Check the tab state after clicking inspect button " +
+ "when another tab was selected"
+ );
+ await updateSelectedTab(gBrowser, tab, store);
+ clickInspectButton(inspectionTarget, document);
+ const devtoolsURL = devtoolsWindow.location.href;
+ assertDevtoolsToolboxTabState(devtoolsURL);
+
+ info(
+ "Check the tab state after clicking inspect button " +
+ "when the toolbox tab is in another window"
+ );
+ const newNavigator = gBrowser.replaceTabWithWindow(devtoolsTab);
+ await waitUntil(
+ () =>
+ newNavigator.gBrowser &&
+ newNavigator.gBrowser.selectedTab.linkedBrowser.contentWindow.location
+ .href === devtoolsURL
+ );
+
+ info(
+ "Create a tab in the window and select the tab " +
+ "so that the about:devtools-toolbox tab loses focus"
+ );
+ const newTestTab = newNavigator.gBrowser.addTab(
+ "data:text/html,<title>TEST_TAB</title>",
+ {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ }
+ );
+ await waitUntil(() => findDebugTargetByText("TEST_TAB", document));
+
+ await updateSelectedTab(newNavigator.gBrowser, newTestTab, store);
+
+ let onTabsSuccess = waitForDispatch(store, "REQUEST_TABS_SUCCESS");
+ clickInspectButton(inspectionTarget, document);
+ assertDevtoolsToolboxTabState(devtoolsURL);
+ await onTabsSuccess;
+
+ info("Close new navigator and wait until the debug target disappears");
+ onTabsSuccess = waitForDispatch(store, "REQUEST_TABS_SUCCESS");
+ const onToolboxDestroyed = gDevTools.once("toolbox-destroyed");
+ newNavigator.close();
+ await onToolboxDestroyed;
+ await onTabsSuccess;
+
+ await waitUntil(() => !findDebugTargetByText("Toolbox - ", document));
+
+ info("Remove test tab");
+ await removeTab(tab);
+});
+
+function clickInspectButton(inspectionTarget, doc) {
+ const target = findDebugTargetByText(inspectionTarget, doc);
+ const button = target.querySelector(".qa-debug-target-inspect-button");
+ button.click();
+}
+
+// Check that only one tab is currently opened for the provided URL.
+// Also check that this tab and the tab's window are focused.
+function assertDevtoolsToolboxTabState(devtoolsURL) {
+ const existingTabs = [];
+
+ for (const navigator of Services.wm.getEnumerator("navigator:browser")) {
+ for (const browser of navigator.gBrowser.browsers) {
+ if (
+ browser.contentWindow &&
+ browser.contentWindow.location.href === devtoolsURL
+ ) {
+ const tab = navigator.gBrowser.getTabForBrowser(browser);
+ existingTabs.push(tab);
+ }
+ }
+ }
+
+ is(existingTabs.length, 1, `Only one tab is opened for ${devtoolsURL}`);
+ const tab = existingTabs[0];
+ const navigator = tab.ownerGlobal;
+ is(navigator.gBrowser.selectedTab, tab, "The tab is selected");
+ const focusedNavigator = Services.wm.getMostRecentWindow("navigator:browser");
+ is(navigator, focusedNavigator, "The navigator is focused");
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_menubar.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_menubar.js
new file mode 100644
index 0000000000..c2f0c5e362
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_menubar.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+/**
+ * Test the status of menu items when open about:devtools-toolbox.
+ */
+add_task(async function () {
+ info("Force all debug target panes to be expanded");
+ prepareCollapsibilitiesTest();
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+ const { devtoolsTab, devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window
+ );
+
+ info("Check whether the menu items are disabled");
+ const rootDocument = devtoolsTab.ownerDocument;
+ await assertMenusItems(rootDocument, false);
+
+ info("Select the inspector");
+ const toolbox = getToolbox(devtoolsWindow);
+ await toolbox.selectTool("inspector");
+
+ info("Force to select about:debugging page");
+ await updateSelectedTab(gBrowser, tab, window.AboutDebugging.store);
+
+ info("Check whether the menu items are enabled");
+ await assertMenusItems(rootDocument, true);
+
+ await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
+ await removeTab(tab);
+});
+
+async function assertMenusItems(rootDocument, shouldBeEnabled) {
+ info("Wait for the Toggle Tools menu-item hidden attribute to change");
+ const menuItem = rootDocument.getElementById("menu_devToolbox");
+ await waitUntil(() => menuItem.hidden === !shouldBeEnabled);
+
+ info(
+ "Check that the state of the Toggle Tools menu-item depends on the page"
+ );
+ assertMenuItem(rootDocument, "menu_devToolbox", shouldBeEnabled);
+
+ info(
+ "Check that the tools menu-items are always enabled regardless of the page"
+ );
+ for (const toolDefinition of gDevTools.getToolDefinitionArray()) {
+ if (!toolDefinition.inMenu) {
+ continue;
+ }
+
+ assertMenuItem(rootDocument, "menuitem_" + toolDefinition.id, true);
+ }
+}
+
+function assertMenuItem(rootDocument, menuItemId, shouldBeEnabled) {
+ const menuItem = rootDocument.getElementById(menuItemId);
+ is(
+ menuItem.hidden,
+ !shouldBeEnabled,
+ `"hidden" attribute of menu item(${menuItemId}) should be correct`
+ );
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_navigate_back_forward.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_navigate_back_forward.js
new file mode 100644
index 0000000000..79428ae8da
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_navigate_back_forward.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const ORIGINAL_URL = "https://example.com/document-builder.sjs?html=page1";
+const OTHER_URL = "https://example.org/document-builder.sjs?html=page2";
+
+async function waitForUrl(url, toolbox, browserTab, win) {
+ const { onDomCompleteResource } =
+ await waitForNextTopLevelDomCompleteResource(toolbox.commands);
+
+ return Promise.all([
+ waitUntil(
+ () =>
+ toolbox.target.url === url &&
+ browserTab.linkedBrowser.currentURI.spec === url
+ ),
+ onDomCompleteResource,
+ toolbox.commands.client.waitForRequestsToSettle(),
+ waitForAboutDebuggingRequests(win.AboutDebugging.store),
+ ]);
+}
+
+// Test that ensures the remote page can go forward and back via UI buttons
+add_task(async function () {
+ const browserTab = await addTab(ORIGINAL_URL);
+
+ const { document, tab, window } = await openAboutDebugging();
+
+ // go to This Firefox and inspect the new tab
+ info("Inspecting a new tab in This Firefox");
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+ const devToolsToolbox = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ ORIGINAL_URL
+ );
+ const { devtoolsDocument, devtoolsWindow } = devToolsToolbox;
+ const toolbox = getToolbox(devtoolsWindow);
+
+ info("Navigating to another URL");
+ let onTargetSwitched = toolbox.commands.targetCommand.once("switched-target");
+ const urlInput = devtoolsDocument.querySelector(".devtools-textinput");
+ await synthesizeUrlKeyInput(devToolsToolbox, urlInput, OTHER_URL);
+ await waitForUrl(OTHER_URL, toolbox, browserTab, window);
+ await onTargetSwitched;
+
+ info("Clicking back button");
+ onTargetSwitched = toolbox.commands.targetCommand.once("switched-target");
+ devtoolsDocument.querySelector(".qa-back-button").click();
+ await waitForUrl(ORIGINAL_URL, toolbox, browserTab, window);
+ await onTargetSwitched;
+
+ info("Clicking the forward button");
+ onTargetSwitched = toolbox.commands.targetCommand.once("switched-target");
+ devtoolsDocument.querySelector(".qa-forward-button").click();
+ await waitForUrl(OTHER_URL, toolbox, browserTab, window);
+ await onTargetSwitched;
+
+ ok(true, "Clicking back and forward works!");
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_navigate_reload_button.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_navigate_reload_button.js
new file mode 100644
index 0000000000..fbd46101a2
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_navigate_reload_button.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function clickReload(devtoolsDocument) {
+ devtoolsDocument.querySelector(".qa-reload-button").click();
+}
+
+// Test that ensures the remote page is reloaded when the button is clicked
+add_task(async function () {
+ const debug_tab = await addTab("about:home");
+
+ const { document, tab, window } = await openAboutDebugging();
+
+ // go to This Firefox and inspect the new tab
+ info("Inspecting a new tab in This Firefox");
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+ const { devtoolsDocument, devtoolsTab, devtoolsWindow } =
+ await openAboutDevtoolsToolbox(document, tab, window, "about:home");
+
+ info("Clicking reload button and waiting for requests to complete");
+ const toolbox = getToolbox(devtoolsWindow);
+ const { onDomCompleteResource } =
+ await waitForNextTopLevelDomCompleteResource(toolbox.commands);
+
+ // Watch for navigation promises.
+ const refreshes = Promise.all([
+ onDomCompleteResource,
+ toolbox.commands.client.waitForRequestsToSettle(),
+ waitForAboutDebuggingRequests(window.AboutDebugging.store),
+ ]);
+
+ // We cannot include this one in the Promise.all array, as it needs to be
+ // explicitly called after navigation started.
+ const waitForLoadedPanelsReload = await watchForLoadedPanelsReload(toolbox);
+
+ clickReload(devtoolsDocument);
+ await refreshes;
+ await waitForLoadedPanelsReload();
+
+ ok(true, "Clicked refresh; both page and devtools reloaded");
+
+ await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
+
+ info("Remove the debugged tab");
+ await removeTab(debug_tab);
+ await waitUntil(() => !findDebugTargetByText("about:home", document));
+ await waitForAboutDebuggingRequests(window.AboutDebugging.store);
+
+ info("Remove the about:debugging tab.");
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_navigate_to_url.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_navigate_to_url.js
new file mode 100644
index 0000000000..055db0586c
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_navigate_to_url.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const NEW_TAB_TITLE = "PAGE 2";
+const TAB_URL = "data:text/html,<title>PAGE</title>";
+const NEW_TAB_URL = `data:text/html,<title>${NEW_TAB_TITLE}</title>`;
+
+/**
+ * This test file ensures that the URL input for DebugTargetInfo navigates the target to
+ * the specified URL.
+ */
+add_task(async function () {
+ const { document, tab, window } = await openAboutDebugging();
+
+ info("Open a new background tab.");
+ const debug_tab = await addTab(TAB_URL, { background: true });
+
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+ const devToolsToolbox = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ "PAGE"
+ );
+ const { devtoolsDocument, devtoolsTab, devtoolsWindow } = devToolsToolbox;
+ const toolbox = getToolbox(devtoolsWindow);
+
+ const urlInput = devtoolsDocument.querySelector(".devtools-textinput");
+ const waitForLoadedPanelsReload = await watchForLoadedPanelsReload(toolbox);
+
+ await synthesizeUrlKeyInput(devToolsToolbox, urlInput, NEW_TAB_URL);
+
+ await waitForLoadedPanelsReload();
+
+ info("Test that the debug target navigated to the specified URL.");
+ await waitUntil(
+ () =>
+ toolbox.target.url === NEW_TAB_URL &&
+ debug_tab.linkedBrowser.currentURI.spec === NEW_TAB_URL
+ );
+ ok(true, "Target navigated.");
+ ok(toolbox.target.title.includes(NEW_TAB_TITLE), "Target's title updated.");
+ is(urlInput.value, NEW_TAB_URL, "Input url updated.");
+
+ await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
+
+ info("Remove the background tab");
+ await removeTab(debug_tab);
+ await waitUntil(() => !findDebugTargetByText("NEW_TAB_TITLE", document));
+ await waitForAboutDebuggingRequests(window.AboutDebugging.store);
+
+ info("Remove the about:debugging tab.");
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_reload.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_reload.js
new file mode 100644
index 0000000000..3eba6bcd7e
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_reload.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test can run for a long time on debug platforms.
+requestLongerTimeout(5);
+
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+const TOOLS = [
+ "inspector",
+ "webconsole",
+ "jsdebugger",
+ "styleeditor",
+ "memory",
+ "netmonitor",
+ "storage",
+ "accessibility",
+];
+
+/**
+ * Test whether about:devtools-toolbox display correctly after reloading.
+ */
+add_task(async function () {
+ info("Force all debug target panes to be expanded");
+ prepareCollapsibilitiesTest();
+
+ for (const toolId of TOOLS) {
+ await testReloadAboutDevToolsToolbox(toolId);
+ }
+});
+
+async function testReloadAboutDevToolsToolbox(toolId) {
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+ // We set the options panel to be the default one because slower panels might lead to
+ // race conditions which create leaks in debug mode.
+ await pushPref("devtools.toolbox.selectedTool", "options");
+ const { devtoolsBrowser, devtoolsTab, devtoolsWindow } =
+ await openAboutDevtoolsToolbox(document, tab, window);
+
+ info(`Select tool: ${toolId}`);
+ const toolbox = getToolbox(devtoolsWindow);
+ await toolbox.selectTool(toolId);
+
+ info("Wait for requests to settle before reloading");
+ await toolbox.commands.client.waitForRequestsToSettle();
+
+ info("Reload about:devtools-toolbox page");
+ devtoolsBrowser.reload();
+ await gDevTools.once("toolbox-ready");
+ ok(true, "Toolbox is re-created again");
+
+ // Check that about:devtools-toolbox is still selected tab. See Bug 1570692.
+ is(
+ devtoolsBrowser,
+ gBrowser.selectedBrowser,
+ "about:devtools-toolbox is still selected"
+ );
+
+ info("Check whether about:devtools-toolbox page displays correctly");
+ ok(
+ devtoolsBrowser.contentDocument.querySelector(".debug-target-info"),
+ "about:devtools-toolbox page displays correctly"
+ );
+
+ await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
+ await removeTab(tab);
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_shortcuts.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_shortcuts.js
new file mode 100644
index 0000000000..b03a206350
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_shortcuts.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+/**
+ * Test shortcut keys on about:devtools-toolbox page.
+ */
+add_task(async function () {
+ info("Force all debug target panes to be expanded");
+ prepareCollapsibilitiesTest();
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+ const { devtoolsBrowser, devtoolsTab, devtoolsWindow } =
+ await openAboutDevtoolsToolbox(document, tab, window);
+
+ info("Check whether the shortcut keys which opens devtools is disabled");
+ await assertShortcutKeys(devtoolsBrowser, false);
+
+ info("Switch to the inspector programmatically");
+ const toolbox = getToolbox(devtoolsWindow);
+ await toolbox.selectTool("inspector");
+
+ info(
+ "Use the Webconsole keyboard shortcut and wait for the panel to be selected"
+ );
+ const onToolReady = toolbox.once("webconsole-ready");
+ EventUtils.synthesizeKey(
+ "K",
+ {
+ accelKey: true,
+ shiftKey: !navigator.userAgent.match(/Mac/),
+ altKey: navigator.userAgent.match(/Mac/),
+ },
+ devtoolsWindow
+ );
+ await onToolReady;
+
+ info("Force to select about:debugging page");
+ await updateSelectedTab(gBrowser, tab, window.AboutDebugging.store);
+
+ info("Check whether the shortcut keys which opens devtools is enabled");
+ await assertShortcutKeys(tab.linkedBrowser, true);
+
+ await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
+ await removeTab(tab);
+});
+
+async function assertShortcutKeys(browser, shouldBeEnabled) {
+ await assertShortcutKey(browser.contentWindow, "VK_F12", {}, shouldBeEnabled);
+ await assertShortcutKey(
+ browser.contentWindow,
+ "I",
+ {
+ accelKey: true,
+ shiftKey: !navigator.userAgent.match(/Mac/),
+ altKey: navigator.userAgent.match(/Mac/),
+ },
+ shouldBeEnabled
+ );
+}
+
+async function assertShortcutKey(win, key, modifiers, shouldBeEnabled) {
+ info(`Assert shortcut key [${key}]`);
+
+ if (shouldBeEnabled) {
+ await assertShortcutKeyEnabled(win, key, modifiers);
+ } else {
+ await assertShortcutKeyDisabled(win, key, modifiers);
+ }
+}
+
+async function assertShortcutKeyDisabled(win, key, modifiers) {
+ let isReadyCalled = false;
+ const toolboxListener = () => {
+ isReadyCalled = true;
+ };
+ gDevTools.on("toolbox-ready", toolboxListener);
+
+ EventUtils.synthesizeKey(key, modifiers, win);
+ await wait(1000);
+ ok(!isReadyCalled, `Devtools should not be opened by ${key}`);
+
+ gDevTools.off("toolbox-ready", toolboxListener);
+}
+
+async function assertShortcutKeyEnabled(win, key, modifiers) {
+ // Open devtools
+ const onToolboxReady = gDevTools.once("toolbox-ready");
+ EventUtils.synthesizeKey(key, modifiers, win);
+ await onToolboxReady;
+ ok(true, `Devtools should be opened by ${key}`);
+
+ // Close devtools
+ const onToolboxDestroyed = gDevTools.once("toolbox-destroyed");
+ EventUtils.synthesizeKey(key, modifiers, win);
+ await onToolboxDestroyed;
+ ok(true, `Devtopls should be closed by ${key}`);
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_splitconsole_key.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_splitconsole_key.js
new file mode 100644
index 0000000000..44e83b8c43
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_splitconsole_key.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+/**
+ * Test that the split console key shortcut works on about:devtools-toolbox.
+ */
+add_task(async function () {
+ info("Force all debug target panes to be expanded");
+ prepareCollapsibilitiesTest();
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+ const { devtoolsTab, devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window
+ );
+
+ // Select any tool that is not the Webconsole, since we will assert the split-console.
+ info("Select inspector tool");
+ const toolbox = getToolbox(devtoolsWindow);
+ await toolbox.selectTool("inspector");
+
+ info("Press Escape and wait for the split console to be opened");
+ const onSplitConsole = toolbox.once("split-console");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, devtoolsWindow);
+ await onSplitConsole;
+ await waitUntil(() => toolbox.isSplitConsoleFocused());
+ ok(true, "Split console is opened and focused");
+
+ info("Press Escape again and wait for the split console to be closed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, devtoolsWindow);
+ await waitUntil(() => !toolbox.isSplitConsoleFocused());
+ ok(true, "Split console is closed and no longer focused");
+
+ await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_target_destroyed.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_target_destroyed.js
new file mode 100644
index 0000000000..d0001d1c03
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_target_destroyed.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the expected supported categories are displayed for USB runtimes.
+add_task(async function () {
+ const targetTab = await addTab("about:home");
+
+ const { document, tab, window } = await openAboutDebugging();
+
+ // go to This Firefox and inspect the new tab
+ info("Inspecting a new tab in This Firefox");
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+ const { devtoolsDocument, devtoolsTab, devtoolsWindow } =
+ await openAboutDevtoolsToolbox(document, tab, window, "about:home");
+ const targetUrl = devtoolsDocument.querySelector(".devtools-textinput");
+ ok(
+ targetUrl.value.includes("about:home"),
+ "about:devtools-toolbox is open for the target"
+ );
+
+ // close the inspected tab and check that error page is shown
+ info("removing the inspected tab");
+ await removeTab(targetTab);
+ await waitUntil(() =>
+ devtoolsWindow.document.querySelector(".qa-error-page")
+ );
+
+ info("closing the toolbox");
+ await removeTab(devtoolsTab);
+ await waitUntil(() => !findDebugTargetByText("Toolbox -", document));
+
+ info("removing about:debugging tab");
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_tooltip_markupview.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_tooltip_markupview.js
new file mode 100644
index 0000000000..047b309740
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_tooltip_markupview.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test can run for a long time on asan or debug platforms.
+requestLongerTimeout(3);
+
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+/**
+ * Test tooltip of markup view on about:devtools-toolbox page.
+ */
+add_task(async function () {
+ info("Force all debug target panes to be expanded");
+ prepareCollapsibilitiesTest();
+
+ info("Turn on devtools.chrome.enabled to show event badges");
+ await pushPref("devtools.chrome.enabled", true);
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+ const { devtoolsDocument, devtoolsTab, devtoolsWindow } =
+ await openAboutDevtoolsToolbox(document, tab, window);
+
+ info("Select inspector tool");
+ const toolbox = getToolbox(devtoolsWindow);
+ await toolbox.selectTool("inspector");
+
+ const inspector = toolbox.getPanel("inspector");
+ const markupDocument = inspector.markup.doc;
+ const eventBadge = markupDocument.querySelector(
+ ".inspector-badge.interactive"
+ );
+
+ info(
+ "Check tooltip visibility after clicking on an element in the markup view"
+ );
+ await checkTooltipVisibility(inspector, eventBadge, markupDocument.body);
+
+ info(
+ "Check tooltip visibility after clicking on an element in the DevTools document"
+ );
+ await checkTooltipVisibility(
+ inspector,
+ eventBadge,
+ devtoolsDocument.querySelector(".debug-target-info")
+ );
+
+ info(
+ "Check tooltip visibility after clicking on an element in the root document"
+ );
+ const rootDocument = devtoolsWindow.windowRoot.ownerGlobal.document;
+ await checkTooltipVisibility(
+ inspector,
+ eventBadge,
+ rootDocument.querySelector("#titlebar")
+ );
+
+ await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
+ await removeTab(tab);
+});
+
+async function checkTooltipVisibility(
+ inspector,
+ elementForShowing,
+ elementForHiding
+) {
+ info("Show event tooltip");
+ elementForShowing.click();
+ const tooltip = inspector.markup.eventDetailsTooltip;
+ await tooltip.once("shown");
+ is(
+ tooltip.container.classList.contains("tooltip-visible"),
+ true,
+ "The tooltip should be shown"
+ );
+
+ info("Hide event tooltip");
+ EventUtils.synthesizeMouse(
+ elementForHiding,
+ 1,
+ 1,
+ {},
+ elementForHiding.ownerGlobal
+ );
+ await tooltip.once("hidden");
+ is(
+ tooltip.container.classList.contains("tooltip-visible"),
+ false,
+ "Tooltip should be hidden"
+ );
+
+ if (inspector._updateProgress) {
+ info("Need to wait for the inspector to update");
+ await inspector.once("inspector-updated");
+ }
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_zoom.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_zoom.js
new file mode 100644
index 0000000000..425efee8f1
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_devtoolstoolbox_zoom.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/toolbox.properties"
+);
+
+// Check that the about:devtools-toolbox tab can be zoomed in and that the zoom
+// persists after switching tabs.
+add_task(async function () {
+ info("Force all debug target panes to be expanded");
+ prepareCollapsibilitiesTest();
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+ const { devtoolsTab, devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window
+ );
+
+ is(getZoom(devtoolsWindow), 1, "default zoom level correct");
+
+ info("Increase the zoom level");
+
+ // Note that we read the shortcut from the toolbox properties, but that should
+ // match the default browser shortcut `full-zoom-enlarge-shortcut`.
+ synthesizeKeyShortcut(L10N.getStr("toolbox.zoomIn.key"));
+ await waitFor(() => getZoom(devtoolsWindow) > 1);
+ is(getZoom(devtoolsWindow).toFixed(2), "1.10", "zoom level increased");
+
+ info("Switch tabs between about:debugging and the toolbox tab");
+ gBrowser.selectedTab = tab;
+ gBrowser.selectedTab = devtoolsTab;
+
+ info("Wait for the browser to reapply the zoom");
+ await wait(500);
+
+ is(
+ getZoom(devtoolsWindow).toFixed(2),
+ "1.10",
+ "zoom level was restored after switching tabs"
+ );
+
+ info("Restore the default zoom level");
+ synthesizeKeyShortcut(L10N.getStr("toolbox.zoomReset.key"));
+ await waitFor(() => getZoom(devtoolsWindow) === 1);
+ is(getZoom(devtoolsWindow), 1, "default zoom level restored");
+
+ await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
+ await removeTab(tab);
+});
+
+function getZoom(win) {
+ return win.browsingContext.fullZoom;
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_fenix_runtime_display.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_fenix_runtime_display.js
new file mode 100644
index 0000000000..778b1bb967
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_fenix_runtime_display.js
@@ -0,0 +1,141 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const RUNTIME_ID = "1337id";
+const DEVICE_NAME = "Fancy Phone";
+const SERVER_RUNTIME_NAME = "Mozilla Firefox";
+const ADB_RUNTIME_NAME = "Firefox Preview";
+const SERVER_VERSION = "v7.3.31";
+const ADB_VERSION = "v1.3.37";
+
+const FENIX_RELEASE_ICON_SRC =
+ "chrome://devtools/skin/images/aboutdebugging-fenix.svg";
+const FENIX_NIGHTLY_ICON_SRC =
+ "chrome://devtools/skin/images/aboutdebugging-fenix-nightly.svg";
+
+/**
+ * Check that Fenix runtime information is correctly displayed in about:debugging.
+ */
+add_task(async function () {
+ const mocks = new Mocks();
+ mocks.createUSBRuntime(RUNTIME_ID, {
+ deviceName: DEVICE_NAME,
+ isFenix: true,
+ name: SERVER_RUNTIME_NAME,
+ shortName: ADB_RUNTIME_NAME,
+ versionName: ADB_VERSION,
+ version: SERVER_VERSION,
+ });
+
+ // open a remote runtime page
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ mocks.emitUSBUpdate();
+ await connectToRuntime(DEVICE_NAME, document);
+ await selectRuntime(DEVICE_NAME, ADB_RUNTIME_NAME, document);
+
+ info("Check that the runtime information is displayed as expected");
+ const runtimeInfo = document.querySelector(".qa-runtime-name");
+ ok(runtimeInfo, "Runtime info for the Fenix runtime is displayed");
+ const runtimeInfoText = runtimeInfo.textContent;
+
+ ok(runtimeInfoText.includes(ADB_RUNTIME_NAME), "Name is the ADB name");
+ ok(
+ !runtimeInfoText.includes(SERVER_RUNTIME_NAME),
+ "Name does not include the server name"
+ );
+
+ ok(runtimeInfoText.includes(ADB_VERSION), "Version contains the ADB version");
+ ok(
+ !runtimeInfoText.includes(SERVER_VERSION),
+ "Version does not contain the server version"
+ );
+
+ const runtimeIcon = document.querySelector(".qa-runtime-icon");
+ is(
+ runtimeIcon.src,
+ FENIX_RELEASE_ICON_SRC,
+ "The runtime icon is the Fenix icon"
+ );
+
+ info("Remove USB runtime");
+ mocks.removeUSBRuntime(RUNTIME_ID);
+ mocks.emitUSBUpdate();
+ await waitUntilUsbDeviceIsUnplugged(DEVICE_NAME, document);
+
+ await removeTab(tab);
+});
+
+/**
+ * Check that Fenix runtime information is correctly displayed in about:devtools-toolbox.
+ */
+add_task(async function () {
+ // We use a real local client combined with a mocked USB runtime to be able to open
+ // about:devtools-toolbox on a real target.
+ const clientWrapper = await createLocalClientWrapper();
+
+ const mocks = new Mocks();
+ mocks.createUSBRuntime(RUNTIME_ID, {
+ channel: "nightly",
+ clientWrapper,
+ deviceName: DEVICE_NAME,
+ isFenix: true,
+ name: SERVER_RUNTIME_NAME,
+ shortName: ADB_RUNTIME_NAME,
+ versionName: ADB_VERSION,
+ version: SERVER_VERSION,
+ });
+
+ // open a remote runtime page
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ mocks.emitUSBUpdate();
+ info("Select the runtime page for the USB runtime");
+ const onRequestSuccess = waitForRequestsSuccess(window.AboutDebugging.store);
+ await connectToRuntime(DEVICE_NAME, document);
+ await selectRuntime(DEVICE_NAME, ADB_RUNTIME_NAME, document);
+ info(
+ "Wait for requests to finish the USB runtime is backed by a real local client"
+ );
+ await onRequestSuccess;
+
+ info("Wait for the about:debugging target to be available");
+ await waitUntil(() => findDebugTargetByText("about:debugging", document));
+ const { devtoolsDocument, devtoolsTab } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window
+ );
+
+ const runtimeInfo = devtoolsDocument.querySelector(".qa-runtime-info");
+ const runtimeInfoText = runtimeInfo.textContent;
+ ok(
+ runtimeInfoText.includes(ADB_RUNTIME_NAME),
+ "Name is the ADB runtime name"
+ );
+ ok(runtimeInfoText.includes(ADB_VERSION), "Version is the ADB version");
+
+ const runtimeIcon = devtoolsDocument.querySelector(".qa-runtime-icon");
+ is(
+ runtimeIcon.src,
+ FENIX_NIGHTLY_ICON_SRC,
+ "The runtime icon is the Fenix icon"
+ );
+
+ info("Wait for all pending requests to settle on the DevToolsClient");
+ await clientWrapper.client.waitForRequestsToSettle();
+
+ await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
+
+ info("Remove USB runtime");
+ mocks.removeUSBRuntime(RUNTIME_ID);
+ mocks.emitUSBUpdate();
+ await waitUntilUsbDeviceIsUnplugged(DEVICE_NAME, document);
+
+ await removeTab(tab);
+ await clientWrapper.close();
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_fenix_runtime_node_picker.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_fenix_runtime_node_picker.js
new file mode 100644
index 0000000000..59bdbd765d
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_fenix_runtime_node_picker.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const RUNTIME_ID = "1337id";
+const DEVICE_NAME = "Fancy Phone";
+const SERVER_RUNTIME_NAME = "Mozilla Firefox";
+const ADB_RUNTIME_NAME = "Firefox Preview";
+const SERVER_VERSION = "v7.3.31";
+const ADB_VERSION = "v1.3.37";
+
+/**
+ * Check that the node picker button in about:devtools-toolbox has the expected class when
+ * connecting to an Android phone.
+ */
+add_task(async function () {
+ // We use a real local client combined with a mocked USB runtime to be able to open
+ // about:devtools-toolbox on a real target.
+ const clientWrapper = await createLocalClientWrapper();
+
+ const mocks = new Mocks();
+ mocks.createUSBRuntime(RUNTIME_ID, {
+ channel: "nightly",
+ clientWrapper,
+ deviceName: DEVICE_NAME,
+ isFenix: true,
+ name: SERVER_RUNTIME_NAME,
+ shortName: ADB_RUNTIME_NAME,
+ versionName: ADB_VERSION,
+ version: SERVER_VERSION,
+ });
+
+ // open a remote runtime page
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ mocks.emitUSBUpdate();
+ info("Select the runtime page for the USB runtime");
+ const onRequestSuccess = waitForRequestsSuccess(window.AboutDebugging.store);
+ await connectToRuntime(DEVICE_NAME, document);
+ await selectRuntime(DEVICE_NAME, ADB_RUNTIME_NAME, document);
+ info(
+ "Wait for requests to finish the USB runtime is backed by a real local client"
+ );
+ await onRequestSuccess;
+
+ info("Wait for the about:debugging target to be available");
+ await waitUntil(() => findDebugTargetByText("about:debugging", document));
+ const { devtoolsDocument, devtoolsTab } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window
+ );
+
+ const pickerButton = devtoolsDocument.querySelector("#command-button-pick");
+ ok(
+ pickerButton.classList.contains("remote-fenix"),
+ "The node picker does have the expected additional className when debugging an android phone"
+ );
+ const pickerButtonTitle = pickerButton.getAttribute("title");
+ const expectedKeyboardShortcut =
+ Services.appinfo.OS === "Darwin"
+ ? `Cmd+Shift+C or Cmd+Opt+C`
+ : `Ctrl+Shift+C`;
+ is(
+ pickerButtonTitle,
+ `Pick an element from the Android phone (${expectedKeyboardShortcut})`,
+ `The node picker does have the expected tooltip when debugging an android phone`
+ );
+
+ info("Wait for all pending requests to settle on the DevToolsClient");
+ await clientWrapper.client.waitForRequestsToSettle();
+ await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
+
+ info("Remove USB runtime");
+ mocks.removeUSBRuntime(RUNTIME_ID);
+ mocks.emitUSBUpdate();
+ await waitUntilUsbDeviceIsUnplugged(DEVICE_NAME, document);
+
+ await removeTab(tab);
+ await clientWrapper.close();
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_hidden_addons.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_hidden_addons.js
new file mode 100644
index 0000000000..69f75ef75e
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_hidden_addons.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that system and hidden addons are only displayed when the showSystemAddons
+// preferences is true.
+
+const SYSTEM_ADDON = createAddonData({
+ id: "system",
+ name: "System Addon",
+ isSystem: true,
+ hidden: true,
+});
+const HIDDEN_ADDON = createAddonData({
+ id: "hidden",
+ name: "Hidden Addon",
+ isSystem: false,
+ hidden: true,
+});
+const NORMAL_ADDON = createAddonData({
+ id: "normal",
+ name: "Normal Addon",
+ isSystem: false,
+ hidden: false,
+});
+
+add_task(async function testShowSystemAddonsTrue() {
+ info("Test with showHiddenAddons set to true");
+ await testAddonsDisplay(true);
+
+ info("Test with showHiddenAddons set to false");
+ await testAddonsDisplay(false);
+});
+
+async function testAddonsDisplay(showHidden) {
+ const thisFirefoxClient = setupThisFirefoxMock();
+ thisFirefoxClient.listAddons = () => [
+ SYSTEM_ADDON,
+ HIDDEN_ADDON,
+ NORMAL_ADDON,
+ ];
+
+ info("Set showHiddenAddons to " + showHidden);
+ await pushPref("devtools.aboutdebugging.showHiddenAddons", showHidden);
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ const hasSystemAddon = !!findDebugTargetByText("System Addon", document);
+ const hasHiddenAddon = !!findDebugTargetByText("Hidden Addon", document);
+ const hasInstalledAddon = !!findDebugTargetByText("Normal Addon", document);
+ is(
+ hasSystemAddon,
+ showHidden,
+ "System addon display is correct when showHiddenAddons is " + showHidden
+ );
+ is(
+ hasHiddenAddon,
+ showHidden,
+ "Hidden addon display is correct when showHiddenAddons is " + showHidden
+ );
+ ok(hasInstalledAddon, "Installed addon is always displayed");
+
+ await removeTab(tab);
+}
+
+// Create a basic mock for this-firefox client, and setup a runtime-client-factory mock
+// to return our mock client when needed.
+function setupThisFirefoxMock() {
+ const runtimeClientFactoryMock = createRuntimeClientFactoryMock();
+ const thisFirefoxClient = createThisFirefoxClientMock();
+ runtimeClientFactoryMock.createClientForRuntime = runtime => {
+ const {
+ RUNTIMES,
+ } = require("resource://devtools/client/aboutdebugging/src/constants.js");
+ if (runtime.id === RUNTIMES.THIS_FIREFOX) {
+ return thisFirefoxClient;
+ }
+ throw new Error("Unexpected runtime id " + runtime.id);
+ };
+
+ info("Enable mocks");
+ enableRuntimeClientFactoryMock(runtimeClientFactoryMock);
+ registerCleanupFunction(() => {
+ disableRuntimeClientFactoryMock();
+ });
+
+ return thisFirefoxClient;
+}
+
+// Create basic addon data as the DevToolsClient would return it (debuggable and non
+// temporary).
+function createAddonData({ id, name, isSystem, hidden }) {
+ return {
+ actor: `actorid-${id}`,
+ hidden,
+ iconURL: `moz-extension://${id}/icon-url.png`,
+ id,
+ manifestURL: `moz-extension://${id}/manifest-url.json`,
+ name,
+ isSystem,
+ temporarilyInstalled: false,
+ debuggable: true,
+ };
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_message_close.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_message_close.js
new file mode 100644
index 0000000000..58ae2db6a3
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_message_close.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+const EXTENSION_NAME = "Temporary web extension";
+const EXTENSION_ID = "test-devtools@mozilla.org";
+
+// Test that Message component can be closed with the X button
+add_task(async function () {
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ info("Check that the message can be closed with icon");
+ let warningMessage = await installExtensionWithWarning(document);
+ await testCloseMessageWithIcon(warningMessage, document);
+ await removeTemporaryExtension(EXTENSION_NAME, document);
+
+ info("Check that the message can be closed with the button around the icon");
+ warningMessage = await installExtensionWithWarning(document);
+ await testCloseMessageWithButton(warningMessage, document);
+ await removeTemporaryExtension(EXTENSION_NAME, document);
+
+ await removeTab(tab);
+});
+
+async function testCloseMessageWithIcon(warningMessage, doc) {
+ const closeIcon = warningMessage.querySelector(
+ ".qa-message-button-close-icon"
+ );
+ ok(!!closeIcon, "The warning message has a close icon");
+
+ info("Closing the message and waiting for it to disappear");
+ closeIcon.click();
+
+ const target = findDebugTargetByText(EXTENSION_NAME, doc);
+ await waitUntil(() => target.querySelector(".qa-message") === null);
+}
+
+async function testCloseMessageWithButton(warningMessage, doc) {
+ const closeButton = warningMessage.querySelector(
+ ".qa-message-button-close-button"
+ );
+ ok(!!closeButton, "The warning message has a close button");
+
+ info("Click on the button and wait for the message to disappear");
+ EventUtils.synthesizeMouse(closeButton, 1, 1, {}, doc.defaultView);
+
+ const target = findDebugTargetByText(EXTENSION_NAME, doc);
+ await waitUntil(() => target.querySelector(".qa-message") === null);
+}
+
+async function installExtensionWithWarning(doc) {
+ await pushPref("extensions.webextensions.warnings-as-errors", false);
+ await installTemporaryExtensionFromXPI(
+ {
+ id: EXTENSION_ID,
+ name: EXTENSION_NAME,
+ extraProperties: {
+ // This property is not expected in the manifest and should trigger a warning!
+ wrongProperty: {},
+ },
+ },
+ doc
+ );
+ await SpecialPowers.popPrefEnv();
+
+ info("Wait until a debug target item appears");
+ await waitUntil(() => findDebugTargetByText(EXTENSION_NAME, doc));
+
+ const target = findDebugTargetByText(EXTENSION_NAME, doc);
+ const warningMessage = target.querySelector(".qa-message");
+ ok(
+ !!warningMessage,
+ "A warning message is displayed for the installed addon"
+ );
+
+ return warningMessage;
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_navigate.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_navigate.js
new file mode 100644
index 0000000000..281f01dbdc
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_navigate.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+/**
+ * Check that navigating from This Firefox to Connect and back to This Firefox works and
+ * does not leak.
+ */
+
+const TAB_URL_1 = "data:text/html,<title>TAB1</title>";
+const TAB_URL_2 = "data:text/html,<title>TAB2</title>";
+
+add_task(async function () {
+ info("Force all debug target panes to be expanded");
+ prepareCollapsibilitiesTest();
+
+ const { document, tab, window } = await openAboutDebugging();
+ const AboutDebugging = window.AboutDebugging;
+ await selectThisFirefoxPage(document, AboutDebugging.store);
+
+ const connectSidebarItem = findSidebarItemByText("Setup", document);
+ const connectLink = connectSidebarItem.querySelector(".qa-sidebar-link");
+ ok(connectSidebarItem, "Found the Connect sidebar item");
+
+ const thisFirefoxString = getThisFirefoxString(window);
+ const thisFirefoxSidebarItem = findSidebarItemByText(
+ thisFirefoxString,
+ document
+ );
+ const thisFirefoxLink =
+ thisFirefoxSidebarItem.querySelector(".qa-sidebar-link");
+ ok(thisFirefoxSidebarItem, "Found the ThisFirefox sidebar item");
+ ok(
+ isSidebarItemSelected(thisFirefoxSidebarItem),
+ "ThisFirefox sidebar item is selected by default"
+ );
+
+ info("Open a new background tab TAB1");
+ const backgroundTab1 = await addTab(TAB_URL_1, { background: true });
+
+ info("Wait for the tab to appear in the debug targets with the correct name");
+ await waitUntil(() => findDebugTargetByText("TAB1", document));
+
+ await waitForAboutDebuggingRequests(AboutDebugging.store);
+ info("Click on the Connect item in the sidebar");
+ connectLink.click();
+
+ info("Wait until Connect page is displayed");
+ await waitUntil(() => document.querySelector(".qa-connect-page"));
+ // we need to wait here because the sidebar isn't updated after mounting the page
+ info("Wait until Connect sidebar item is selected");
+ await waitUntil(() => isSidebarItemSelected(connectSidebarItem));
+ ok(
+ !document.querySelector(".qa-runtime-page"),
+ "Runtime page no longer rendered"
+ );
+
+ info("Open a new tab which should be listed when we go back to This Firefox");
+ const backgroundTab2 = await addTab(TAB_URL_2, { background: true });
+
+ info("Click on the ThisFirefox item in the sidebar");
+ const requestsSuccess = waitForRequestsSuccess(AboutDebugging.store);
+ thisFirefoxLink.click();
+
+ info("Wait for all target requests to complete");
+ await requestsSuccess;
+
+ info("Wait until ThisFirefox page is displayed");
+ await waitUntil(() => document.querySelector(".qa-runtime-page"));
+ ok(
+ isSidebarItemSelected(thisFirefoxSidebarItem),
+ "ThisFirefox sidebar item is selected again"
+ );
+ ok(
+ !document.querySelector(".qa-connect-page"),
+ "Connect page no longer rendered"
+ );
+
+ info("TAB2 should already be displayed in the debug targets");
+ await waitUntil(() => findDebugTargetByText("TAB2", document));
+
+ info("Remove first background tab");
+ await removeTab(backgroundTab1);
+
+ info(
+ "Check TAB1 disappears, meaning ThisFirefox client is correctly connected"
+ );
+ await waitUntil(() => !findDebugTargetByText("TAB1", document));
+
+ info("Remove second background tab");
+ await removeTab(backgroundTab2);
+
+ info(
+ "Check TAB2 disappears, meaning ThisFirefox client is correctly connected"
+ );
+ await waitUntil(() => !findDebugTargetByText("TAB2", document));
+
+ await waitForAboutDebuggingRequests(AboutDebugging.store);
+
+ await removeTab(tab);
+});
+
+function isSidebarItemSelected(item) {
+ return item.classList.contains("qa-sidebar-item-selected");
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_persist_connection.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_persist_connection.js
new file mode 100644
index 0000000000..5ac1c2e188
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_persist_connection.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const NETWORK_RUNTIME_HOST = "localhost:6080";
+const NETWORK_RUNTIME_APP_NAME = "TestNetworkApp";
+const USB_RUNTIME_ID = "test-runtime-id";
+const USB_DEVICE_NAME = "test device name";
+const USB_APP_NAME = "TestApp";
+
+// Test that remote runtime connections are persisted across about:debugging reloads.
+add_task(async function () {
+ const mocks = new Mocks();
+
+ info("Test with a USB runtime");
+ const usbClient = mocks.createUSBRuntime(USB_RUNTIME_ID, {
+ name: USB_APP_NAME,
+ deviceName: USB_DEVICE_NAME,
+ });
+
+ await testRemoteClientPersistConnection(mocks, {
+ client: usbClient,
+ id: USB_RUNTIME_ID,
+ runtimeName: USB_APP_NAME,
+ sidebarName: USB_DEVICE_NAME,
+ type: "usb",
+ });
+
+ info("Test with a network runtime");
+ const networkClient = mocks.createNetworkRuntime(NETWORK_RUNTIME_HOST, {
+ name: NETWORK_RUNTIME_APP_NAME,
+ });
+
+ await testRemoteClientPersistConnection(mocks, {
+ client: networkClient,
+ id: NETWORK_RUNTIME_HOST,
+ runtimeName: NETWORK_RUNTIME_APP_NAME,
+ sidebarName: NETWORK_RUNTIME_HOST,
+ type: "network",
+ });
+});
+
+async function testRemoteClientPersistConnection(
+ mocks,
+ { client, id, runtimeName, sidebarName, type }
+) {
+ info("Open about:debugging and connect to the test runtime");
+ let { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ await connectToRuntime(sidebarName, document);
+ await selectRuntime(sidebarName, runtimeName, document);
+
+ info("Reload about:debugging");
+ document = await reloadAboutDebugging(tab);
+
+ info("Wait until the remote runtime appears as connected");
+ await waitUntil(() => {
+ const sidebarItem = findSidebarItemByText(sidebarName, document);
+ return sidebarItem && !sidebarItem.querySelector(".qa-connect-button");
+ });
+
+ info("Wait until the remote runtime page is selected");
+ await waitUntil(() => {
+ const runtimeInfo = document.querySelector(".qa-runtime-name");
+ return runtimeInfo && runtimeInfo.textContent.includes(runtimeName);
+ });
+
+ // Remove the runtime without emitting an update.
+ // This is what happens today when we simply close Firefox for Android.
+ info("Remove the runtime from the list of remote runtimes");
+ mocks.removeRuntime(id);
+
+ info(
+ "Emit 'closed' on the client and wait for the sidebar item to disappear"
+ );
+ client._eventEmitter.emit("closed");
+ if (type === "usb") {
+ await waitUntilUsbDeviceIsUnplugged(sidebarName, document);
+ } else {
+ await waitUntil(
+ () =>
+ !findSidebarItemByText(sidebarName, document) &&
+ !findSidebarItemByText(runtimeName, document)
+ );
+ }
+
+ info("Remove the tab");
+ await removeTab(tab);
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_process_category.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_process_category.js
new file mode 100644
index 0000000000..fd7fb9a852
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_process_category.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const CATEGORY_NAME = "Processes";
+const RUNTIME_ID = "test-runtime-id";
+const RUNTIME_DEVICE_NAME = "test device name";
+const RUNTIME_APP_NAME = "TestApp";
+
+// Test whether process category exists by the runtime type.
+add_task(async function () {
+ await pushPref("devtools.aboutdebugging.process-debugging", true);
+
+ const mocks = new Mocks();
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ const usbRuntime = mocks.createUSBRuntime(RUNTIME_ID, {
+ deviceName: RUNTIME_DEVICE_NAME,
+ name: RUNTIME_APP_NAME,
+ });
+ usbRuntime.getMainProcess = () => {
+ return {
+ getTarget: () => {
+ return { actorID: 0 };
+ },
+ };
+ };
+ mocks.emitUSBUpdate();
+
+ info("Check the process category existence for this firefox");
+ ok(
+ !getDebugTargetPane(CATEGORY_NAME, document),
+ "Process category should not display for this firefox"
+ );
+
+ info("Check the process category existence for USB runtime");
+ await connectToRuntime(RUNTIME_DEVICE_NAME, document);
+ await selectRuntime(RUNTIME_DEVICE_NAME, RUNTIME_APP_NAME, document);
+ ok(
+ getDebugTargetPane(CATEGORY_NAME, document),
+ "Process category should display for USB runtime"
+ );
+
+ info("Remove USB runtime");
+ mocks.removeUSBRuntime(RUNTIME_ID);
+ mocks.emitUSBUpdate();
+ await waitUntilUsbDeviceIsUnplugged(RUNTIME_DEVICE_NAME, document);
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_process_main.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_process_main.js
new file mode 100644
index 0000000000..bd88545d58
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_process_main.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+const MULTIPROCESS_TOOLBOX_NAME = "Multiprocess Toolbox";
+const MULTIPROCESS_TOOLBOX_DESCRIPTION =
+ "Main Process and Content Processes for the target browser";
+
+const RUNTIME_ID = "test-runtime-id";
+const RUNTIME_DEVICE_NAME = "test device name";
+const RUNTIME_APP_NAME = "TestApp";
+
+// Test for main process.
+add_task(async function () {
+ await pushPref("devtools.aboutdebugging.process-debugging", true);
+ const mocks = new Mocks();
+
+ const { document, tab, window } = await openAboutDebugging();
+
+ const usbRuntime = mocks.createUSBRuntime(RUNTIME_ID, {
+ deviceName: RUNTIME_DEVICE_NAME,
+ name: RUNTIME_APP_NAME,
+ });
+
+ // Note: about:debugging assumes that the main process has the id 0 and will
+ // rely on it to create the about:devtools-toolbox URL.
+ // Only check that about:debugging doesn't create a target unnecessarily.
+ usbRuntime.getMainProcess = () => {
+ return {
+ getTarget: () => {
+ ok(false, "about:debugging should not create the main process target");
+ },
+ };
+ };
+ mocks.emitUSBUpdate();
+
+ info("Select USB runtime");
+ await connectToRuntime(RUNTIME_DEVICE_NAME, document);
+ await selectRuntime(RUNTIME_DEVICE_NAME, RUNTIME_APP_NAME, document);
+
+ info("Check debug target item of the main process");
+ await waitUntil(() =>
+ findDebugTargetByText(MULTIPROCESS_TOOLBOX_NAME, document)
+ );
+ const mainProcessItem = findDebugTargetByText(
+ MULTIPROCESS_TOOLBOX_NAME,
+ document
+ );
+ ok(mainProcessItem, "Debug target item of the main process should display");
+ ok(
+ mainProcessItem.textContent.includes(MULTIPROCESS_TOOLBOX_DESCRIPTION),
+ "Debug target item of the main process should contains the description"
+ );
+
+ info("Inspect main process");
+ const { devtoolsTab, devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ MULTIPROCESS_TOOLBOX_NAME,
+ false
+ );
+
+ const url = new window.URL(devtoolsWindow.location.href);
+ const type = url.searchParams.get("type");
+ is(type, "process", "Correct type argument");
+ const remoteID = url.searchParams.get("remoteId");
+ is(remoteID, `${RUNTIME_ID}-usb`, "Correct remote runtime id");
+
+ info("Remove USB runtime");
+ mocks.removeUSBRuntime(RUNTIME_ID);
+ mocks.emitUSBUpdate();
+ await waitUntilUsbDeviceIsUnplugged(RUNTIME_DEVICE_NAME, document);
+
+ // Note that we can't use `closeAboutDevtoolsToolbox` because the toolbox init
+ // is expected to fail, and we are redirected to the error page.
+ await removeTab(devtoolsTab);
+ await waitUntil(() => !findDebugTargetByText("Toolbox - ", document));
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_process_main_local.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_process_main_local.js
new file mode 100644
index 0000000000..06bd29095c
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_process_main_local.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+// Test for main process against This Firefox.
+//
+// The main added value for this test is to check that listing processes
+// and opening a toolbox targeting a process works, even though debugging
+// the main process of This Firefox is not really supported.
+add_task(async function () {
+ await pushPref("devtools.aboutdebugging.process-debugging", true);
+ await pushPref("devtools.aboutdebugging.test-local-process-debugging", true);
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ info("Check debug target item of the main process");
+ const mainProcessItem = findDebugTargetByText("Multiprocess", document);
+ ok(mainProcessItem, "Debug target item of the main process should display");
+ ok(
+ mainProcessItem.textContent.includes(
+ "Main Process and Content Processes for the target browser"
+ ),
+ "Debug target item of the main process should contains the description"
+ );
+
+ info("Inspect main process and wait for DevTools to open");
+ const { devtoolsTab } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ "Main Process"
+ );
+
+ await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_profiler_dialog.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_profiler_dialog.js
new file mode 100644
index 0000000000..02995492c0
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_profiler_dialog.js
@@ -0,0 +1,238 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/performance-new/test/browser/helpers.js",
+ this
+);
+
+const BackgroundJSM = ChromeUtils.import(
+ "resource://devtools/client/performance-new/shared/background.jsm.js"
+);
+
+registerCleanupFunction(() => {
+ BackgroundJSM.revertRecordingSettings();
+});
+
+const RUNTIME_ID = "1337id";
+const DEVICE_NAME = "Fancy Phone";
+const RUNTIME_NAME = "Lorem ipsum";
+
+/**
+ * Test opening and closing the profiler dialog.
+ */
+add_task(async function test_opening_profiler_dialog() {
+ const { mocks } = await connectToLocalFirefox();
+ const { document, tab, window } = await openAboutDebugging();
+
+ mocks.emitUSBUpdate();
+ await connectToRuntime(DEVICE_NAME, document);
+ await selectRuntime(DEVICE_NAME, RUNTIME_NAME, document);
+
+ info("Open the profiler dialog");
+ await openProfilerDialogWithRealClient(document);
+ assertDialogVisible(document);
+
+ info("Click on the close button and wait until the dialog disappears");
+ const closeDialogButton = document.querySelector(".qa-profiler-dialog-close");
+ closeDialogButton.click();
+ await waitUntil(() => !document.querySelector(".qa-profiler-dialog"));
+ assertDialogHidden(document);
+
+ info("Open the profiler dialog again");
+ await openProfilerDialogWithRealClient(document);
+ assertDialogVisible(document);
+
+ info("Click on the mask element and wait until the dialog disappears");
+ const mask = document.querySelector(".qa-profiler-dialog-mask");
+ EventUtils.synthesizeMouse(mask, 5, 5, {}, window);
+ await waitUntil(() => !document.querySelector(".qa-profiler-dialog"));
+ assertDialogHidden(document);
+
+ info("Open the profiler dialog again");
+ await openProfilerDialogWithRealClient(document);
+ assertDialogVisible(document);
+
+ info("Navigate to this-firefox and wait until the dialog disappears");
+ document.location.hash = "#/runtime/this-firefox";
+ await waitUntil(() => !document.querySelector(".qa-profiler-dialog"));
+ assertDialogHidden(document);
+
+ info("Select the remote runtime again, check the dialog is still hidden");
+ await selectRuntime(DEVICE_NAME, RUNTIME_NAME, document);
+ assertDialogHidden(document);
+
+ await disconnectFromLocalFirefox({ mocks, doc: document });
+ await removeTab(tab);
+});
+
+add_task(async function test_set_profiler_settings() {
+ const { mocks } = await connectToLocalFirefox();
+ const { document, tab } = await openAboutDebugging();
+
+ mocks.emitUSBUpdate();
+ await connectToRuntime(DEVICE_NAME, document);
+ await selectRuntime(DEVICE_NAME, RUNTIME_NAME, document);
+
+ info("Open the profiler dialog");
+ await openProfilerDialogWithRealClient(document);
+ assertDialogVisible(document);
+
+ const profilerSettingsDocument = await openProfilerSettings(document);
+ const radioButtonForCustomPreset = await getNearestInputFromText(
+ profilerSettingsDocument,
+ "Custom"
+ );
+ ok(
+ radioButtonForCustomPreset.checked,
+ "The radio button for the preset 'custom' is checked."
+ );
+
+ info("Change the preset to Graphics.");
+ const radioButtonForGraphicsPreset = await getNearestInputFromText(
+ profilerSettingsDocument,
+ "Graphics"
+ );
+ radioButtonForGraphicsPreset.click();
+
+ const profilerDocument = await saveSettingsAndGoBack(document);
+ const perfPresetsSelect = await getNearestInputFromText(
+ profilerDocument,
+ "Settings"
+ );
+ is(
+ perfPresetsSelect.value,
+ "graphics",
+ "The preset has been changed in the devtools panel UI as well."
+ );
+
+ await disconnectFromLocalFirefox({ mocks, doc: document });
+ await removeTab(tab);
+});
+
+async function connectToLocalFirefox() {
+ // This is a client to the current Firefox.
+ const clientWrapper = await createLocalClientWrapper();
+
+ // enable USB devices mocks
+ const mocks = new Mocks();
+ const usbClient = mocks.createUSBRuntime(RUNTIME_ID, {
+ deviceName: DEVICE_NAME,
+ name: RUNTIME_NAME,
+ clientWrapper,
+ });
+
+ return { mocks, usbClient };
+}
+
+async function disconnectFromLocalFirefox({ doc, mocks }) {
+ info("Remove USB runtime");
+ mocks.removeUSBRuntime(RUNTIME_ID);
+ mocks.emitUSBUpdate();
+ await waitUntilUsbDeviceIsUnplugged(DEVICE_NAME, doc);
+}
+
+function assertDialogVisible(doc) {
+ ok(doc.querySelector(".qa-profiler-dialog"), "Dialog is displayed");
+ ok(doc.querySelector(".qa-profiler-dialog-mask"), "Dialog mask is displayed");
+}
+
+function assertDialogHidden(doc) {
+ ok(!doc.querySelector(".qa-profiler-dialog"), "Dialog is removed");
+ ok(!doc.querySelector(".qa-profiler-dialog-mask"), "Dialog mask is removed");
+}
+
+/**
+ * Retrieve the iframe containing the profiler UIs.
+ * Be careful as it's completely replaced when switching UIs.
+ */
+function getProfilerIframe(doc) {
+ return doc.querySelector(".profiler-dialog__frame");
+}
+
+/**
+ * This waits for the full render of the UI inside the profiler iframe, and
+ * returns the content document object.
+ */
+async function waitForProfilerUiRendering(doc, selector) {
+ // The iframe is replaced completely, so we need to retrieve a new reference
+ // each time.
+ const profilerIframe = getProfilerIframe(doc);
+ // Wait for the settings to render.
+ await TestUtils.waitForCondition(
+ () =>
+ profilerIframe.contentDocument &&
+ profilerIframe.contentDocument.querySelector(selector)
+ );
+
+ return profilerIframe.contentDocument;
+}
+
+/**
+ * Open the performance profiler dialog with a real client.
+ */
+async function openProfilerDialogWithRealClient(doc) {
+ info("Click on the Profile Runtime button");
+ const profileButton = doc.querySelector(".qa-profile-runtime-button");
+ profileButton.click();
+
+ info("Wait for the rendering of the profiler UI");
+ const contentDocument = await waitForProfilerUiRendering(
+ doc,
+ ".perf-presets"
+ );
+ await getActiveButtonFromText(contentDocument, "Start recording");
+ info("The profiler UI is rendered!");
+ return contentDocument;
+}
+
+/**
+ * Open the performance profiler settings. This assumes the profiler dialog is
+ * already open by the previous function openProfilerDialog.
+ */
+async function openProfilerSettings(doc) {
+ const profilerDocument = getProfilerIframe(doc).contentDocument;
+
+ // Select the custom preset.
+ const perfPresetsSelect = await getNearestInputFromText(
+ profilerDocument,
+ "Settings"
+ );
+ setReactFriendlyInputValue(perfPresetsSelect, "custom");
+
+ // Click on "Edit Settings".
+ const editSettingsLink = await getElementFromDocumentByText(
+ profilerDocument,
+ "Edit Settings"
+ );
+ editSettingsLink.click();
+
+ info("Wait for the rendering of the profiler settings UI");
+ const contentDocument = await waitForProfilerUiRendering(
+ doc,
+ ".perf-aboutprofiling-remote"
+ );
+ info("The profiler settings UI is rendered!");
+ return contentDocument;
+}
+
+async function saveSettingsAndGoBack(doc) {
+ const profilerDocument = getProfilerIframe(doc).contentDocument;
+
+ const saveSettingsAndGoBackButton = await getActiveButtonFromText(
+ profilerDocument,
+ "Save settings"
+ );
+ saveSettingsAndGoBackButton.click();
+
+ info("Wait for the rendering of the profiler UI");
+ const contentDocument = await waitForProfilerUiRendering(
+ doc,
+ ".perf-presets"
+ );
+ await getActiveButtonFromText(contentDocument, "Start recording");
+ info("The profiler UI is rendered!");
+ return contentDocument;
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_real_usb_runtime_page_runtime_info.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_real_usb_runtime_page_runtime_info.js
new file mode 100644
index 0000000000..16462fd2b3
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_real_usb_runtime_page_runtime_info.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-real-usb.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-real-usb.js",
+ this
+);
+
+// Test that runtime info of USB runtime appears on main pane.
+// Documentation for real usb tests in /documentation/TESTS_REAL_DEVICES.md
+add_task(async function () {
+ if (!isAvailable()) {
+ ok(true, "Real usb runtime test is not available");
+ return;
+ }
+
+ const { document, tab } = await openAboutDebuggingWithADB();
+
+ const expectedRuntime = await getExpectedRuntime();
+ const { runtimeDetails, sidebarInfo } = expectedRuntime;
+
+ info("Connect a USB runtime");
+ await Promise.race([
+ connectToRuntime(sidebarInfo.deviceName, document),
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ new Promise(resolve =>
+ setTimeout(() => {
+ ok(
+ false,
+ "Failed to connect, did you disable the connection prompt for this runtime?"
+ );
+ resolve();
+ }, 5000)
+ ),
+ /* eslint-enable mozilla/no-arbitrary-setTimeout */
+ ]);
+
+ info("Select a USB runtime");
+ await selectRuntime(
+ sidebarInfo.deviceName,
+ runtimeDetails.info.name,
+ document
+ );
+
+ info("Check that runtime info is properly displayed");
+ const runtimeInfo = document.querySelector(".qa-runtime-name");
+ ok(runtimeInfo, "Runtime info is displayed");
+ const runtimeInfoText = runtimeInfo.textContent;
+ ok(
+ runtimeInfoText.includes(runtimeDetails.info.name),
+ "Runtime info shows the correct runtime name: " + runtimeInfoText
+ );
+ ok(
+ runtimeInfoText.includes(runtimeDetails.info.version),
+ "Runtime info shows the correct version number: " + runtimeInfoText
+ );
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_real_usb_sidebar.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_real_usb_sidebar.js
new file mode 100644
index 0000000000..0b091dff87
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_real_usb_sidebar.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-real-usb.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-real-usb.js",
+ this
+);
+
+// Test that USB runtimes appear from the sidebar.
+// Documentation for real usb tests in /documentation/TESTS_REAL_DEVICES.md
+add_task(async function () {
+ if (!isAvailable()) {
+ ok(true, "Real usb runtime test is not available");
+ return;
+ }
+
+ const { document, tab } = await openAboutDebuggingWithADB();
+
+ for (const { sidebarInfo } of await getExpectedRuntimeAll()) {
+ const { deviceName, shortName } = sidebarInfo;
+ await waitUntil(() => findSidebarItemByText(deviceName, document));
+ const usbRuntimeSidebarItem = findSidebarItemByText(deviceName, document);
+ ok(
+ usbRuntimeSidebarItem.textContent.includes(shortName),
+ "The device name and short name of the usb runtime are visible in sidebar item " +
+ `[${usbRuntimeSidebarItem.textContent}]`
+ );
+ }
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_routes.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_routes.js
new file mode 100644
index 0000000000..6295d37503
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_routes.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that the initial route is /setup
+ */
+add_task(async function () {
+ info("Check root route redirects to setup page");
+ const { document, tab } = await openAboutDebugging();
+ is(document.location.hash, "#/setup");
+
+ await removeTab(tab);
+});
+
+/**
+ * Test that the routes in about:debugging show the proper views and document.title
+ */
+add_task(async function () {
+ // enable USB devices mocks
+ const mocks = new Mocks();
+
+ const { document, tab } = await openAboutDebugging();
+
+ info("Check 'This Firefox' route");
+ document.location.hash = "#/runtime/this-firefox";
+ await waitUntil(() => document.querySelector(".qa-runtime-page"));
+ const infoLabel = document.querySelector(".qa-runtime-name").textContent;
+ // NOTE: when using USB Mocks, we see only "Firefox" as the device name
+ ok(infoLabel.includes("Firefox"), "Runtime is displayed as Firefox");
+ ok(!infoLabel.includes(" on "), "Runtime is not associated to any device");
+ is(
+ document.title,
+ "Debugging - Runtime / this-firefox",
+ "Checking title for 'runtime' page"
+ );
+
+ info("Check 'Setup' page");
+ document.location.hash = "#/setup";
+ await waitUntil(() => document.querySelector(".qa-connect-page"));
+ ok(true, "Setup page has been shown");
+ is(document.title, "Debugging - Setup", "Checking title for 'setup' page");
+
+ info("Check 'USB device runtime' page");
+ // connect to a mocked USB runtime
+ mocks.createUSBRuntime("1337id", {
+ deviceName: "Fancy Phone",
+ name: "Lorem ipsum",
+ });
+ mocks.emitUSBUpdate();
+ await connectToRuntime("Fancy Phone", document);
+ // navigate to it via URL
+ document.location.hash = "#/runtime/1337id";
+ await waitUntil(() => document.querySelector(".qa-runtime-page"));
+ const runtimeLabel = document.querySelector(".qa-runtime-name").textContent;
+ is(
+ document.title,
+ "Debugging - Runtime / 1337id",
+ "Checking title for 'runtime' page with USB device"
+ );
+ ok(
+ runtimeLabel.includes("Lorem ipsum"),
+ "Runtime is displayed with the mocked name"
+ );
+
+ await removeTab(tab);
+});
+
+/**
+ * Test that an invalid route redirects to / (currently This Firefox page)
+ */
+add_task(async function () {
+ info("Check an invalid route redirects to root");
+ const { document, tab } = await openAboutDebugging();
+
+ info("Waiting for a non setup page to load");
+ document.location.hash = "#/runtime/this-firefox";
+ await waitUntil(() => document.querySelector(".qa-runtime-page"));
+
+ info("Update hash & wait for a redirect to root (connect page)");
+ document.location.hash = "#/lorem-ipsum";
+ await waitUntil(() => document.querySelector(".qa-connect-page"));
+ is(document.title, "Debugging - Setup", "Checking title for 'setup' page");
+ is(document.location.hash, "#/setup", "Redirected to root");
+
+ await removeTab(tab);
+});
+
+/**
+ * Test that routes from old about:debugging redirect to this Firefox.
+ */
+add_task(async function testOldAboutDebuggingRoutes() {
+ info("Check that routes from old about:debugging redirect to this Firefox");
+ const { document, tab } = await openAboutDebugging();
+
+ const routes = ["addons", "tabs", "workers"];
+ for (const route of routes) {
+ info("Move to setup page before testing the route");
+ document.location.hash = "#/setup";
+ await waitUntil(() => document.querySelector(".qa-connect-page"));
+
+ info(`Check that navigating to ${route} redirects to This Firefox`);
+ document.location.hash = route;
+ await waitUntil(() => document.querySelector(".qa-runtime-page"));
+ is(
+ document.location.hash,
+ "#/runtime/this-firefox",
+ `${route} was redirected to This Firefox`
+ );
+ }
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_rtl.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_rtl.js
new file mode 100644
index 0000000000..4424df6ea8
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_rtl.js
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test that the about:debugging document and the profiler dialog document
+// use the expected document direction.
+add_task(async function test_direction_is_ltr_by_default() {
+ await testAboutDebuggingDocsDirection("ltr");
+});
+
+add_task(async function test_direction_is_rtl_for_bidi_pseudolocale() {
+ await pushPref("intl.l10n.pseudo", "bidi");
+ await testAboutDebuggingDocsDirection("rtl");
+});
+
+async function testAboutDebuggingDocsDirection(expectedDir) {
+ const mocks = new Mocks();
+ const { document, usbClient } = await setupTestForMockUSBRuntime(mocks);
+
+ is(document.dir, expectedDir, "document dir is " + expectedDir);
+
+ info("Open the profiler dialog");
+ await openProfilerDialog(usbClient, document);
+
+ const profilerDialogFrame = document.querySelector(
+ ".qa-profiler-dialog iframe"
+ );
+ ok(profilerDialogFrame, "Found Profiler dialog iframe");
+
+ const profilerDoc = profilerDialogFrame.contentWindow.document;
+ is(profilerDoc.dir, expectedDir, "Profiler document dir is " + expectedDir);
+
+ await teardownTestForMockUSBRuntime(mocks, document);
+}
+
+async function setupTestForMockUSBRuntime(mocks) {
+ info("Setup mock USB runtime");
+
+ const usbClient = mocks.createUSBRuntime("runtimeId", {
+ deviceName: "deviceName",
+ name: "runtimeName",
+ });
+
+ info("Open about:debugging and select runtime page for mock USB runtime");
+ const { document } = await openAboutDebugging();
+
+ mocks.emitUSBUpdate();
+ await connectToRuntime("deviceName", document);
+ await selectRuntime("deviceName", "runtimeName", document);
+
+ return { document, usbClient };
+}
+
+async function teardownTestForMockUSBRuntime(mocks, doc) {
+ info("Remove mock USB runtime");
+
+ mocks.removeUSBRuntime("runtimeId");
+ mocks.emitUSBUpdate();
+ await waitUntilUsbDeviceIsUnplugged("deviceName", doc);
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_runtime_compatibility_warning.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_runtime_compatibility_warning.js
new file mode 100644
index 0000000000..3f45fa10d2
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_runtime_compatibility_warning.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const COMPATIBLE_RUNTIME = "Compatible Runtime";
+const COMPATIBLE_DEVICE = "Compatible Device";
+const OLD_RUNTIME = "Old Runtime";
+const OLD_DEVICE = "Old Device";
+const FENNEC_68_RUNTIME = "Bad Runtime Fennec 68";
+const FENNEC_68_DEVICE = "Bad Device Fennec 68";
+const RECENT_RUNTIME = "Recent Runtime";
+const RECENT_DEVICE = "Recent Device";
+
+add_task(async function () {
+ const {
+ COMPATIBILITY_STATUS,
+ } = require("resource://devtools/client/shared/remote-debugging/version-checker.js");
+ const { COMPATIBLE, TOO_OLD, TOO_OLD_FENNEC, TOO_RECENT } =
+ COMPATIBILITY_STATUS;
+
+ info("Create several mocked runtimes, with different compatibility reports");
+ const mocks = new Mocks();
+ createRuntimeWithReport(
+ mocks,
+ COMPATIBLE_RUNTIME,
+ COMPATIBLE_DEVICE,
+ COMPATIBLE
+ );
+ createRuntimeWithReport(mocks, OLD_RUNTIME, OLD_DEVICE, TOO_OLD);
+ createRuntimeWithReport(mocks, RECENT_RUNTIME, RECENT_DEVICE, TOO_RECENT);
+ createRuntimeWithReport(
+ mocks,
+ FENNEC_68_RUNTIME,
+ FENNEC_68_DEVICE,
+ TOO_OLD_FENNEC
+ );
+
+ const { document, tab } = await openAboutDebugging();
+ mocks.emitUSBUpdate();
+
+ info("Connect to all runtimes");
+ await connectToRuntime(COMPATIBLE_DEVICE, document);
+ await connectToRuntime(OLD_DEVICE, document);
+ await connectToRuntime(RECENT_DEVICE, document);
+ await connectToRuntime(FENNEC_68_DEVICE, document);
+
+ info("Select the compatible runtime and check that no warning is displayed");
+ await selectRuntime(COMPATIBLE_DEVICE, COMPATIBLE_RUNTIME, document);
+ ok(
+ !document.querySelector(".qa-compatibility-warning"),
+ "Compatibility warning is not displayed"
+ );
+
+ info(
+ "Select the old runtime and check that the too-old warning is displayed"
+ );
+ await selectRuntime(OLD_DEVICE, OLD_RUNTIME, document);
+ ok(
+ document.querySelector(".qa-compatibility-warning-too-old"),
+ "Expected compatibility warning is displayed (too-old)"
+ );
+
+ info(
+ "Select the recent runtime and check that the too-recent warning is displayed"
+ );
+ await selectRuntime(RECENT_DEVICE, RECENT_RUNTIME, document);
+ ok(
+ document.querySelector(".qa-compatibility-warning-too-recent"),
+ "Expected compatibility warning is displayed (too-recent)"
+ );
+
+ info(
+ "Select the Fennec 68 runtime and check that the correct warning is displayed"
+ );
+ await selectRuntime(FENNEC_68_DEVICE, FENNEC_68_RUNTIME, document);
+ ok(document.querySelector(".qa-compatibility-warning-too-old-fennec"));
+
+ await removeTab(tab);
+});
+
+function createRuntimeWithReport(mocks, name, deviceName, status) {
+ const runtimeId = [name, deviceName].join("-");
+ const compatibleUsbClient = mocks.createUSBRuntime(runtimeId, {
+ deviceName,
+ name,
+ });
+ const report = { status };
+ compatibleUsbClient.checkVersionCompatibility = () => report;
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_runtime_disconnect_remote_runtime.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_runtime_disconnect_remote_runtime.js
new file mode 100644
index 0000000000..e7cae28d29
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_runtime_disconnect_remote_runtime.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const USB_RUNTIME_ID = "1337id";
+const USB_DEVICE_NAME = "Fancy Phone";
+const USB_APP_NAME = "Lorem ipsum";
+
+const DEFAULT_PAGE = "#/runtime/this-firefox";
+
+/**
+ * Check if the disconnect button disconnects the remote runtime
+ * and redirects to the default page.
+ */
+add_task(async function () {
+ // Create a real local client and use it as the remote USB client for this
+ // test.
+ const clientWrapper = await createLocalClientWrapper();
+
+ // enable USB devices mocks
+ const mocks = new Mocks();
+ mocks.createUSBRuntime(USB_RUNTIME_ID, {
+ clientWrapper,
+ deviceName: USB_DEVICE_NAME,
+ name: USB_APP_NAME,
+ });
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ mocks.emitUSBUpdate();
+
+ const onRequestSuccess = waitForRequestsSuccess(window.AboutDebugging.store);
+ await connectToRuntime(USB_DEVICE_NAME, document);
+ await selectRuntime(USB_DEVICE_NAME, USB_APP_NAME, document);
+ await onRequestSuccess;
+
+ const disconnectRemoteRuntimeButton = document.querySelector(
+ ".qa-runtime-info__action"
+ );
+
+ info("Check whether disconnect remote runtime button exists");
+ ok(!!disconnectRemoteRuntimeButton, "Runtime contains the disconnect button");
+
+ info("Click on the disconnect button");
+ disconnectRemoteRuntimeButton.click();
+
+ info("Wait until the runtime is disconnected");
+ await waitUntil(() => document.querySelector(".qa-connect-button"));
+
+ is(
+ document.location.hash,
+ DEFAULT_PAGE,
+ "Redirection to the default page (this-firefox)"
+ );
+
+ info("Wait until the Runtime name is displayed");
+ await waitUntil(() => {
+ const runtimeInfo = document.querySelector(".qa-runtime-name");
+ return runtimeInfo && runtimeInfo.textContent.includes("Firefox");
+ });
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_runtime_remote_runtime_buttons.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_runtime_remote_runtime_buttons.js
new file mode 100644
index 0000000000..563fe659ce
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_runtime_remote_runtime_buttons.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const USB_RUNTIME_ID = "1337id";
+const USB_DEVICE_NAME = "Fancy Phone";
+const USB_APP_NAME = "Lorem ipsum";
+
+/**
+ * Test that remote runtimes show action buttons that are hidden for 'This Firefox'.
+ */
+add_task(async function () {
+ // enable USB devices mocks
+ const mocks = new Mocks();
+ mocks.createUSBRuntime(USB_RUNTIME_ID, {
+ deviceName: USB_DEVICE_NAME,
+ name: USB_APP_NAME,
+ });
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ info("Checking This Firefox");
+ ok(
+ !document.querySelector(".qa-connection-prompt-toggle-button"),
+ "This Firefox does not contain the connection prompt button"
+ );
+ ok(
+ !document.querySelector(".qa-profile-runtime-button"),
+ "This Firefox does not contain the profile runtime button"
+ );
+ ok(
+ !document.querySelector(".qa-runtime-info__action"),
+ "This Firefox does not contain the disconnect button"
+ );
+
+ info("Checking a USB runtime");
+ mocks.emitUSBUpdate();
+ await connectToRuntime(USB_DEVICE_NAME, document);
+ await selectRuntime(USB_DEVICE_NAME, USB_APP_NAME, document);
+ ok(
+ !!document.querySelector(".qa-connection-prompt-toggle-button"),
+ "Runtime contains the connection prompt button"
+ );
+ ok(
+ !!document.querySelector(".qa-profile-runtime-button"),
+ "Remote runtime contains the profile runtime button"
+ );
+ ok(
+ !!document.querySelector(".qa-runtime-info__action"),
+ "Runtime contains the disconnect button"
+ );
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_runtime_usbclient_closed.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_runtime_usbclient_closed.js
new file mode 100644
index 0000000000..9e9715a46b
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_runtime_usbclient_closed.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const NETWORK_RUNTIME_HOST = "localhost:6080";
+const NETWORK_RUNTIME_APP_NAME = "TestNetworkApp";
+const USB_RUNTIME_ID = "test-runtime-id";
+const USB_DEVICE_NAME = "test device name";
+const USB_APP_NAME = "TestApp";
+
+// Test that about:debugging navigates back to the default page when a USB device is
+// unplugged.
+add_task(async function testUsbDeviceUnplugged() {
+ const mocks = new Mocks();
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ mocks.createUSBRuntime(USB_RUNTIME_ID, {
+ deviceName: USB_DEVICE_NAME,
+ name: USB_APP_NAME,
+ });
+ mocks.emitUSBUpdate();
+
+ info("Connect to and select the USB device");
+ await connectToRuntime(USB_DEVICE_NAME, document);
+ await selectRuntime(USB_DEVICE_NAME, USB_APP_NAME, document);
+
+ info("Simulate a device unplugged");
+ mocks.removeUSBRuntime(USB_RUNTIME_ID);
+ mocks.emitUSBUpdate();
+ await waitUntilUsbDeviceIsUnplugged(USB_DEVICE_NAME, document);
+
+ is(
+ document.location.hash,
+ `#/runtime/this-firefox`,
+ "Redirection to the default page (this-firefox)"
+ );
+
+ await removeTab(tab);
+});
+
+// Test that about:debugging navigates back to the default page when the server for the
+// current USB runtime is closed.
+add_task(async function testUsbClientDisconnected() {
+ const mocks = new Mocks();
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ const usbClient = mocks.createUSBRuntime(USB_RUNTIME_ID, {
+ deviceName: USB_DEVICE_NAME,
+ name: USB_APP_NAME,
+ });
+ mocks.emitUSBUpdate();
+
+ info("Connect to and select the USB device");
+ await connectToRuntime(USB_DEVICE_NAME, document);
+ await selectRuntime(USB_DEVICE_NAME, USB_APP_NAME, document);
+
+ info("Simulate a client disconnection");
+ usbClient.isClosed = () => true;
+ usbClient._eventEmitter.emit("closed");
+
+ info("Wait until the connect button for this runtime appears");
+ await waitUntil(() => {
+ const item = findSidebarItemByText(USB_DEVICE_NAME, document);
+ return item && item.querySelector(".qa-connect-button");
+ });
+
+ is(
+ document.location.hash,
+ `#/runtime/this-firefox`,
+ "Redirection to the default page (this-firefox)"
+ );
+ await removeTab(tab);
+});
+
+// Test that about:debugging navigates back to the default page when the server for the
+// current network runtime is closed.
+add_task(async function testNetworkClientDisconnected() {
+ const mocks = new Mocks();
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ const networkClient = mocks.createNetworkRuntime(NETWORK_RUNTIME_HOST, {
+ name: NETWORK_RUNTIME_APP_NAME,
+ });
+
+ info("Connect to and select the network runtime");
+ await connectToRuntime(NETWORK_RUNTIME_HOST, document);
+ await selectRuntime(NETWORK_RUNTIME_HOST, NETWORK_RUNTIME_APP_NAME, document);
+
+ info("Simulate a client disconnection");
+ networkClient.isClosed = () => true;
+ networkClient._eventEmitter.emit("closed");
+
+ info("Wait until the connect button for this runtime appears");
+ await waitUntil(() => {
+ const item = findSidebarItemByText(NETWORK_RUNTIME_HOST, document);
+ return item && item.querySelector(".qa-connect-button");
+ });
+
+ is(
+ document.location.hash,
+ `#/runtime/this-firefox`,
+ "Redirection to the default page (this-firefox)"
+ );
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_select_network_runtime.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_select_network_runtime.js
new file mode 100644
index 0000000000..e3f770f948
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_select_network_runtime.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const NETWORK_RUNTIME_HOST = "localhost:6080";
+const NETWORK_RUNTIME_APP_NAME = "TestNetworkApp";
+const NETWORK_RUNTIME_CHANNEL = "SomeChannel";
+const NETWORK_RUNTIME_VERSION = "12.3";
+
+// Test that network runtimes can be selected.
+add_task(async function () {
+ const mocks = new Mocks();
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ info("Prepare Network client mock");
+ const networkClient = mocks.createNetworkRuntime(NETWORK_RUNTIME_HOST, {
+ name: NETWORK_RUNTIME_APP_NAME,
+ });
+ networkClient.getDeviceDescription = () => {
+ return {
+ name: NETWORK_RUNTIME_APP_NAME,
+ channel: NETWORK_RUNTIME_CHANNEL,
+ version: NETWORK_RUNTIME_VERSION,
+ };
+ };
+
+ info("Test addons in runtime page for Network client");
+ await connectToRuntime(NETWORK_RUNTIME_HOST, document);
+ await selectRuntime(NETWORK_RUNTIME_HOST, NETWORK_RUNTIME_APP_NAME, document);
+
+ info("Check that the network runtime mock is properly displayed");
+ const thisFirefoxRuntimeInfo = document.querySelector(".qa-runtime-name");
+ ok(
+ thisFirefoxRuntimeInfo,
+ "Runtime info for this-firefox runtime is displayed"
+ );
+ const runtimeInfoText = thisFirefoxRuntimeInfo.textContent;
+
+ ok(
+ runtimeInfoText.includes(NETWORK_RUNTIME_APP_NAME),
+ "network runtime info shows the correct runtime name: " + runtimeInfoText
+ );
+ ok(
+ runtimeInfoText.includes(NETWORK_RUNTIME_VERSION),
+ "network runtime info shows the correct version number: " + runtimeInfoText
+ );
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_select_page_with_serviceworker.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_select_page_with_serviceworker.js
new file mode 100644
index 0000000000..41e69a0d42
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_select_page_with_serviceworker.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const NETWORK_RUNTIME_HOST = "localhost:6080";
+const NETWORK_RUNTIME_APP_NAME = "TestNetworkApp";
+const WORKER_NAME = "testserviceworker";
+
+// Test that navigating from:
+// - a remote runtime page that contains a service worker
+// to:
+// - this firefox
+// does not crash. See Bug 1519088.
+add_task(async function () {
+ const mocks = new Mocks();
+
+ const { document, tab, window } = await openAboutDebugging({
+ enableWorkerUpdates: true,
+ });
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ info("Prepare Network client mock");
+ const networkClient = mocks.createNetworkRuntime(NETWORK_RUNTIME_HOST, {
+ name: NETWORK_RUNTIME_APP_NAME,
+ });
+
+ info("Connect and select the network runtime");
+ await connectToRuntime(NETWORK_RUNTIME_HOST, document);
+ await selectRuntime(NETWORK_RUNTIME_HOST, NETWORK_RUNTIME_APP_NAME, document);
+
+ info(`Add a service worker to the network client`);
+ const workers = {
+ otherWorkers: [],
+ serviceWorkers: [
+ {
+ name: WORKER_NAME,
+ workerDescriptorFront: { actorID: WORKER_NAME },
+ },
+ ],
+ sharedWorkers: [],
+ };
+ networkClient.listWorkers = () => workers;
+ networkClient._eventEmitter.emit("workersUpdated");
+
+ info("Wait until the service worker is displayed");
+ await waitUntil(() => findDebugTargetByText(WORKER_NAME, document));
+
+ info("Go to This Firefox again");
+ const thisFirefoxString = getThisFirefoxString(window);
+ const thisFirefoxSidebarItem = findSidebarItemByText(
+ thisFirefoxString,
+ document
+ );
+ const thisFirefoxLink =
+ thisFirefoxSidebarItem.querySelector(".qa-sidebar-link");
+ info("Click on the ThisFirefox item in the sidebar");
+ const requestsSuccess = waitForRequestsSuccess(window.AboutDebugging.store);
+ thisFirefoxLink.click();
+
+ info("Wait for all target requests to complete");
+ await requestsSuccess;
+
+ info("Check that the runtime info is rendered for This Firefox");
+ const thisFirefoxRuntimeInfo = document.querySelector(".qa-runtime-name");
+ ok(
+ thisFirefoxRuntimeInfo,
+ "Runtime info for this-firefox runtime is displayed"
+ );
+
+ const text = thisFirefoxRuntimeInfo.textContent;
+ ok(
+ text.includes("Firefox") && text.includes("63.0"),
+ "this-firefox runtime info shows the correct values"
+ );
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_console.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_console.js
new file mode 100644
index 0000000000..742791668d
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_console.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-serviceworker.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-serviceworker.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/shared-head.js",
+ this
+);
+
+const SW_TAB_URL =
+ URL_ROOT_SSL + "resources/service-workers/controlled-sw.html";
+const SW_URL = URL_ROOT_SSL + "resources/service-workers/controlled-sw.js";
+
+/**
+ * Test various simple debugging operation against service workers debugged through about:debugging.
+ */
+add_task(async function () {
+ await enableServiceWorkerDebugging();
+
+ const { document, tab, window } = await openAboutDebugging({
+ enableWorkerUpdates: true,
+ });
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ // Open a tab that registers a basic service worker.
+ const swTab = await addTab(SW_TAB_URL);
+
+ // Wait for the registration to make sure service worker has been started, and that we
+ // are not just reading STOPPED as the initial state.
+ await waitForRegistration(swTab);
+
+ info("Open a toolbox to debug the worker");
+ const { devtoolsTab, devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ SW_URL
+ );
+
+ const toolbox = getToolbox(devtoolsWindow);
+
+ info("Assert the default tools displayed in worker toolboxes");
+ const toolTabs = toolbox.doc.querySelectorAll(".devtools-tab");
+ const activeTools = [...toolTabs].map(toolTab =>
+ toolTab.getAttribute("data-id")
+ );
+
+ is(
+ activeTools.join(","),
+ "webconsole,jsdebugger",
+ "Correct set of tools supported by worker"
+ );
+
+ const webconsole = await toolbox.selectTool("webconsole");
+ const { hud } = webconsole;
+
+ info("Evaluate location in the console");
+ await executeAndWaitForMessage(hud, "this.location.toString()", SW_URL);
+ ok(true, "Got the location logged in the console");
+
+ info(
+ "Evaluate Date and RegExp to ensure their formater also work from worker threads"
+ );
+ await executeAndWaitForMessage(
+ hud,
+ "new Date(2013, 3, 1)",
+ "Mon Apr 01 2013 00:00:00"
+ );
+ ok(true, "Date object has expected text content");
+ await executeAndWaitForMessage(hud, "new RegExp('.*')", "/.*/");
+ ok(true, "RegExp has expected text content");
+
+ await toolbox.selectTool("jsdebugger");
+ const dbg = createDebuggerContext(toolbox);
+ const {
+ selectors: { getIsWaitingOnBreak, getCurrentThread },
+ } = dbg;
+
+ info("Wait for next interupt in the worker thread");
+ await clickElement(dbg, "pause");
+ await waitForState(dbg, state => getIsWaitingOnBreak(getCurrentThread()));
+
+ info("Trigger some code in the worker and wait for pause");
+ await SpecialPowers.spawn(swTab.linkedBrowser, [], async function () {
+ content.wrappedJSObject.installServiceWorker();
+ });
+ await waitForPaused(dbg);
+ ok(true, "successfully paused");
+
+ info(
+ "Evaluate some variable only visible if we execute in the breakpoint frame"
+ );
+ await executeAndWaitForMessage(hud, "event.data", "install-service-worker");
+
+ info("Resume execution");
+ await resume(dbg);
+
+ info("Test pausing from console evaluation");
+ hud.ui.wrapper.dispatchEvaluateExpression("debugger; 42");
+ await waitForPaused(dbg);
+ ok(true, "successfully paused");
+ info("Immediately resume");
+ await resume(dbg);
+ await waitFor(() => findMessagesByType(hud, "42", ".result"));
+ ok("The paused console evaluation resumed and logged its magic number");
+
+ info("Destroy the toolbox");
+ await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
+
+ info("Unregister service worker");
+ await unregisterServiceWorker(swTab);
+
+ info("Wait until the service worker disappears from about:debugging");
+ await waitUntil(() => !findDebugTargetByText(SW_URL, document));
+
+ info("Remove tabs");
+ await removeTab(swTab);
+ await removeTab(tab);
+});
+
+async function executeAndWaitForMessage(hud, evaluationString, expectedResult) {
+ hud.ui.wrapper.dispatchEvaluateExpression();
+ await waitFor(() => findMessagesByType(hud, expectedResult, ".result"));
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_fetch_flag.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_fetch_flag.js
new file mode 100644
index 0000000000..960635a56d
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_fetch_flag.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-serviceworker.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-serviceworker.js",
+ this
+);
+
+const FETCH_SW_JS = URL_ROOT_SSL + "resources/service-workers/fetch-sw.js";
+const FETCH_SW_HTML = URL_ROOT_SSL + "resources/service-workers/fetch-sw.html";
+
+const EMPTY_SW_JS = URL_ROOT_SSL + "resources/service-workers/empty-sw.js";
+const EMPTY_SW_HTML = URL_ROOT_SSL + "resources/service-workers/empty-sw.html";
+
+/**
+ * Test that the appropriate fetch flag is displayed for service workers.
+ */
+add_task(async function () {
+ await enableServiceWorkerDebugging();
+ const { document, tab, window } = await openAboutDebugging({
+ enableWorkerUpdates: true,
+ });
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ info("Test fetch status for a service worker listening to fetch events");
+ await testServiceWorkerFetchStatus(
+ document,
+ FETCH_SW_HTML,
+ FETCH_SW_JS,
+ true
+ );
+
+ info("Test fetch status for a service worker not listening to fetch events");
+ await testServiceWorkerFetchStatus(
+ document,
+ EMPTY_SW_HTML,
+ EMPTY_SW_JS,
+ false
+ );
+
+ await removeTab(tab);
+});
+
+async function testServiceWorkerFetchStatus(doc, url, workerUrl, isListening) {
+ // Open a tab that registers a fetch service worker.
+ const swTab = await addTab(url);
+
+ info("Wait until the service worker appears and is running");
+ const targetElement = await waitForServiceWorkerRunning(workerUrl, doc);
+
+ const expectedClassName = isListening
+ ? ".qa-worker-fetch-listening"
+ : ".qa-worker-fetch-not-listening";
+ const fetchStatus = targetElement.querySelector(expectedClassName);
+ ok(!!fetchStatus, "Found the expected fetch status: " + expectedClassName);
+
+ info("Unregister the service worker");
+ await unregisterServiceWorker(swTab);
+
+ info("Wait until the service worker disappears from about:debugging");
+ await waitUntil(() => !findDebugTargetByText(workerUrl, doc));
+
+ info("Remove the service worker tab");
+ await removeTab(swTab);
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_not_compatible.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_not_compatible.js
new file mode 100644
index 0000000000..d1a6782c09
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_not_compatible.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test started running during a GC and timing out on coverage platforms.
+// See Bug 1526153.
+requestLongerTimeout(2);
+
+const TEST_DATA = [
+ {
+ serviceWorkersEnabled: true,
+ privateBrowsingEnabled: false,
+ expectedMessage: false,
+ },
+ {
+ serviceWorkersEnabled: false,
+ privateBrowsingEnabled: false,
+ expectedMessage: true,
+ },
+ {
+ serviceWorkersEnabled: true,
+ privateBrowsingEnabled: true,
+ expectedMessage: true,
+ },
+ {
+ serviceWorkersEnabled: false,
+ privateBrowsingEnabled: true,
+ expectedMessage: true,
+ },
+];
+
+/**
+ * Check that the warning message for service workers is displayed if permanent private
+ * browsing is enabled or/and if service workers are disabled.
+ */
+add_task(async function testLocalRuntime() {
+ for (const testData of TEST_DATA) {
+ const { serviceWorkersEnabled, privateBrowsingEnabled, expectedMessage } =
+ testData;
+
+ info(
+ `Test warning message on this-firefox ` +
+ `with serviceWorkersEnabled: ${serviceWorkersEnabled} ` +
+ `and with privateBrowsingEnabled: ${privateBrowsingEnabled}`
+ );
+
+ await pushPref("dom.serviceWorkers.enabled", serviceWorkersEnabled);
+ await pushPref("browser.privatebrowsing.autostart", privateBrowsingEnabled);
+
+ const { document, tab, window } = await openAboutDebugging({
+ // Even though this is a service worker test, we are not adding/removing
+ // workers here. Since the test is really fast it can create intermittent
+ // failures due to pending requests to update the worker list
+ // We are updating the worker list whenever the list of processes changes
+ // and this can happen very frequently, and it's hard to control from
+ // DevTools.
+ // Set enableWorkerUpdates to false to avoid intermittent failures.
+ enableWorkerUpdates: false,
+ });
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+ assertWarningMessage(document, expectedMessage);
+ await removeTab(tab);
+ }
+});
+
+add_task(async function testRemoteRuntime() {
+ const {
+ remoteClientManager,
+ } = require("resource://devtools/client/shared/remote-debugging/remote-client-manager.js");
+
+ // enable USB devices mocks
+ const mocks = new Mocks();
+ const client = mocks.createUSBRuntime("1337id", {
+ deviceName: "Fancy Phone",
+ name: "Lorem ipsum",
+ });
+
+ for (const testData of TEST_DATA) {
+ const { serviceWorkersEnabled, privateBrowsingEnabled, expectedMessage } =
+ testData;
+
+ info(
+ `Test warning message on mocked USB runtime ` +
+ `with serviceWorkersEnabled: ${serviceWorkersEnabled} ` +
+ `and with privateBrowsingEnabled: ${privateBrowsingEnabled}`
+ );
+
+ client.setPreference("dom.serviceWorkers.enabled", serviceWorkersEnabled);
+ client.setPreference(
+ "browser.privatebrowsing.autostart",
+ privateBrowsingEnabled
+ );
+
+ const { document, tab, window } = await openAboutDebugging({
+ enableWorkerUpdates: false,
+ });
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ info("Checking a USB runtime");
+ mocks.emitUSBUpdate();
+ await connectToRuntime("Fancy Phone", document);
+ await selectRuntime("Fancy Phone", "Lorem ipsum", document);
+
+ assertWarningMessage(document, expectedMessage);
+
+ // We remove all clients in order to be able to simply connect to the runtime at
+ // every iteration of the loop without checking of the runtime is already connected.
+ info("Remove all remote clients");
+ await remoteClientManager.removeAllClients();
+
+ await removeTab(tab);
+ }
+});
+
+function assertWarningMessage(doc, expectedMessage) {
+ const hasMessage = !!doc.querySelector(".qa-service-workers-warning");
+ ok(
+ hasMessage === expectedMessage,
+ expectedMessage
+ ? "Warning message is displayed"
+ : "Warning message is not displayed"
+ );
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_push.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_push.js
new file mode 100644
index 0000000000..740f066903
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_push.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-serviceworker.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-serviceworker.js",
+ this
+);
+
+const SERVICE_WORKER = URL_ROOT_SSL + "resources/service-workers/push-sw.js";
+const TAB_URL = URL_ROOT_SSL + "resources/service-workers/push-sw.html";
+
+// Test that clicking on the Push button next to a Service Worker works as intended.
+// It should trigger a "push" notification in the worker.
+add_task(async function () {
+ await enableServiceWorkerDebugging();
+ const { document, tab, window } = await openAboutDebugging({
+ enableWorkerUpdates: true,
+ });
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ // Open a tab that registers a push service worker.
+ const swTab = await addTab(TAB_URL);
+
+ info(
+ "Wait for the service worker to claim the test window before proceeding."
+ );
+ await SpecialPowers.spawn(
+ swTab.linkedBrowser,
+ [],
+ () => content.wrappedJSObject.onSwClaimed
+ );
+
+ info("Wait until the service worker appears and is running");
+ const targetElement = await waitForServiceWorkerRunning(
+ SERVICE_WORKER,
+ document
+ );
+
+ // Retrieve the Push button for the worker.
+ const pushButton = targetElement.querySelector(".qa-push-button");
+ ok(pushButton, "Found its push button");
+
+ info("Click on the Push button and wait for the push notification");
+ const onPushNotification = onServiceWorkerMessage(swTab, "sw-pushed");
+ pushButton.click();
+ await onPushNotification;
+
+ info("Unregister the service worker");
+ await unregisterServiceWorker(swTab);
+
+ info("Wait until the service worker disappears from about:debugging");
+ await waitUntil(() => !findDebugTargetByText(SERVICE_WORKER, document));
+
+ info("Remove the service worker tab");
+ await removeTab(swTab);
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_pushservice_url.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_pushservice_url.js
new file mode 100644
index 0000000000..9581a493d9
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_pushservice_url.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-serviceworker.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-serviceworker.js",
+ this
+);
+
+const SERVICE_WORKER = URL_ROOT_SSL + "resources/service-workers/push-sw.js";
+const TAB_URL = URL_ROOT_SSL + "resources/service-workers/push-sw.html";
+
+const FAKE_ENDPOINT = "https://fake/endpoint";
+
+// Test that the push service url is displayed for service workers subscribed to a push
+// service.
+add_task(async function () {
+ await enableServiceWorkerDebugging();
+
+ info("Mock the push service");
+ mockPushService(FAKE_ENDPOINT);
+
+ const { document, tab, window } = await openAboutDebugging({
+ enableWorkerUpdates: true,
+ });
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ // Open a tab that registers a push service worker.
+ const swTab = await addTab(TAB_URL);
+
+ info(
+ "Wait for the service worker to claim the test window before proceeding."
+ );
+ await SpecialPowers.spawn(
+ swTab.linkedBrowser,
+ [],
+ () => content.wrappedJSObject.onSwClaimed
+ );
+
+ info("Wait until the service worker appears and is running");
+ const targetElement = await waitForServiceWorkerRunning(
+ SERVICE_WORKER,
+ document
+ );
+
+ info("Subscribe from the push service");
+ SpecialPowers.spawn(swTab.linkedBrowser, [], () => {
+ content.wrappedJSObject.subscribeToPush();
+ });
+
+ info("Wait until the push service appears");
+ await waitUntil(() =>
+ targetElement.querySelector(".qa-worker-push-service-value")
+ );
+ const pushUrl = targetElement.querySelector(".qa-worker-push-service-value");
+
+ ok(!!pushUrl, "Push URL is displayed for the serviceworker");
+ is(pushUrl.textContent, FAKE_ENDPOINT, "Push URL shows the expected content");
+
+ info("Unsubscribe from the push service");
+ SpecialPowers.spawn(swTab.linkedBrowser, [], () => {
+ content.wrappedJSObject.unsubscribeToPush();
+ });
+
+ info("Wait until the push service disappears");
+ await waitUntil(
+ () => !targetElement.querySelector(".qa-worker-push-service-value")
+ );
+
+ info("Unregister the service worker");
+ await unregisterServiceWorker(swTab);
+
+ info("Wait until the service worker disappears from about:debugging");
+ await waitUntil(() => !findDebugTargetByText(SERVICE_WORKER, document));
+
+ info("Remove the service worker tab");
+ await removeTab(swTab);
+
+ await removeTab(tab);
+});
+
+function mockPushService(endpoint) {
+ const PushService = Cc["@mozilla.org/push/Service;1"].getService(
+ Ci.nsIPushService
+ ).wrappedJSObject;
+
+ PushService.service = {
+ _registrations: new Map(),
+ _notify(scope) {
+ Services.obs.notifyObservers(
+ null,
+ PushService.subscriptionModifiedTopic,
+ scope
+ );
+ },
+ init() {},
+ register(pageRecord) {
+ const registration = {
+ endpoint,
+ };
+ this._registrations.set(pageRecord.scope, registration);
+ this._notify(pageRecord.scope);
+ return Promise.resolve(registration);
+ },
+ registration(pageRecord) {
+ return Promise.resolve(this._registrations.get(pageRecord.scope));
+ },
+ unregister(pageRecord) {
+ const deleted = this._registrations.delete(pageRecord.scope);
+ if (deleted) {
+ this._notify(pageRecord.scope);
+ }
+ return Promise.resolve(deleted);
+ },
+ };
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_runtime-page.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_runtime-page.js
new file mode 100644
index 0000000000..410cbadefd
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_runtime-page.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-serviceworker.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-serviceworker.js",
+ this
+);
+/* import-globals-from helper-collapsibilities.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-collapsibilities.js",
+ this
+);
+
+const SW_TAB_URL = URL_ROOT_SSL + "resources/service-workers/push-sw.html";
+const SW_URL = URL_ROOT_SSL + "resources/service-workers/push-sw.js";
+
+/**
+ * Test that service workers appear and dissapear from the runtime page when they
+ * are registered / unregistered.
+ */
+add_task(async function () {
+ prepareCollapsibilitiesTest();
+ await enableServiceWorkerDebugging();
+ const { document, tab, window } = await openAboutDebugging({
+ enableWorkerUpdates: true,
+ });
+ const store = window.AboutDebugging.store;
+
+ await selectThisFirefoxPage(document, store);
+
+ // check that SW list is empty
+ info("Check that the SW pane is empty");
+ let swPane = getDebugTargetPane("Service Workers", document);
+ ok(!swPane.querySelector(".qa-debug-target-item"), "SW list is empty");
+
+ // open a tab and register service worker
+ info("Register a service worker");
+ const swTab = await addTab(SW_TAB_URL);
+
+ // check that service worker is rendered
+ info("Wait until the service worker appears and is running");
+ await waitForServiceWorkerRunning(SW_URL, document);
+
+ swPane = getDebugTargetPane("Service Workers", document);
+ ok(
+ swPane.querySelectorAll(".qa-debug-target-item").length === 1,
+ "Service worker list has one element"
+ );
+ ok(
+ swPane.querySelector(".qa-debug-target-item").textContent.includes(SW_URL),
+ "Service worker list is the one we registered"
+ );
+
+ // unregister the service worker
+ info("Unregister service worker");
+ await unregisterServiceWorker(swTab);
+ // check that service worker is not rendered anymore
+ info("Wait for service worker to disappear");
+ await waitUntil(() => {
+ swPane = getDebugTargetPane("Service Workers", document);
+ return swPane.querySelectorAll(".qa-debug-target-item").length === 0;
+ });
+
+ info("Remove tabs");
+ await removeTab(swTab);
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_start.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_start.js
new file mode 100644
index 0000000000..483a1cad43
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_start.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-serviceworker.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-serviceworker.js",
+ this
+);
+
+const SW_TAB_URL = URL_ROOT_SSL + "resources/service-workers/empty-sw.html";
+const SW_URL = URL_ROOT_SSL + "resources/service-workers/empty-sw.js";
+
+/**
+ * Test that service workers can be started using about:debugging.
+ */
+add_task(async function () {
+ await enableServiceWorkerDebugging();
+
+ // Setting a low idle_timeout and idle_extended_timeout will allow the service worker
+ // to reach the STOPPED state quickly, which will allow us to test the start button.
+ // The default value is 30000 milliseconds.
+ info("Set a low service worker idle timeout");
+ await pushPref("dom.serviceWorkers.idle_timeout", 1000);
+ await pushPref("dom.serviceWorkers.idle_extended_timeout", 1000);
+
+ const { document, tab, window } = await openAboutDebugging({
+ enableWorkerUpdates: true,
+ });
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ // Open a tab that registers a basic service worker.
+ const swTab = await addTab(SW_TAB_URL);
+
+ // Wait for the registration to make sure service worker has been started, and that we
+ // are not just reading STOPPED as the initial state.
+ await waitForRegistration(swTab);
+
+ info("Wait until the service worker stops");
+ const targetElement = await waitForServiceWorkerStopped(SW_URL, document);
+
+ // Retrieve the Start button for the worker.
+ const startButton = targetElement.querySelector(".qa-start-button");
+ ok(startButton, "Found its start button");
+
+ info(
+ "Click on the start button and wait for the service worker to be running"
+ );
+ const onServiceWorkerRunning = waitForServiceWorkerRunning(SW_URL, document);
+ startButton.click();
+ const updatedTarget = await onServiceWorkerRunning;
+
+ // Check that the buttons are displayed as expected.
+ const hasInspectButton = updatedTarget.querySelector(
+ ".qa-debug-target-inspect-button"
+ );
+ const hasStartButton = updatedTarget.querySelector(".qa-start-button");
+ ok(hasInspectButton, "Service worker has an inspect button");
+ ok(!hasStartButton, "Service worker does not have a start button");
+
+ info("Unregister service worker");
+ await unregisterServiceWorker(swTab);
+
+ info("Wait until the service worker disappears from about:debugging");
+ await waitUntil(() => !findDebugTargetByText(SW_URL, document));
+
+ info("Remove tabs");
+ await removeTab(swTab);
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_status.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_status.js
new file mode 100644
index 0000000000..50426dcb47
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_status.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-serviceworker.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-serviceworker.js",
+ this
+);
+
+const SW_TAB_URL =
+ URL_ROOT_SSL + "resources/service-workers/controlled-sw.html";
+const SW_URL = URL_ROOT_SSL + "resources/service-workers/controlled-sw.js";
+
+/**
+ * Test that the service worker has the status "registering" when the service worker is
+ * not installed yet. Other states (stopped, running) are covered by the existing tests.
+ */
+add_task(async function () {
+ await enableServiceWorkerDebugging();
+
+ const { document, tab, window } = await openAboutDebugging({
+ enableWorkerUpdates: true,
+ });
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ info("Open tab with a service worker that never leaves `registering` status");
+ const swTab = await addTab(SW_TAB_URL);
+
+ // Wait for the registration to make sure service worker has been started, and that we
+ // are not just reading STOPPED as the initial state.
+ await waitForRegistration(swTab);
+
+ info("Wait until the service worker is in registering status");
+ await waitForServiceWorkerRegistering(SW_URL, document);
+
+ // Check that the buttons are displayed as expected.
+ checkButtons(
+ { inspect: true, push: false, start: false, unregister: false },
+ SW_URL,
+ document
+ );
+
+ info("Install the service worker");
+ SpecialPowers.spawn(swTab.linkedBrowser, [], () =>
+ content.wrappedJSObject.installServiceWorker()
+ );
+
+ info("Wait until the service worker is running");
+ await waitForServiceWorkerRunning(SW_URL, document);
+
+ checkButtons(
+ { inspect: true, push: true, start: false, unregister: true },
+ SW_URL,
+ document
+ );
+
+ info("Unregister service worker");
+ await unregisterServiceWorker(swTab);
+
+ info("Wait until the service worker disappears from about:debugging");
+ await waitUntil(() => !findDebugTargetByText(SW_URL, document));
+
+ info("Remove tabs");
+ await removeTab(swTab);
+ await removeTab(tab);
+});
+
+function checkButtons(
+ { inspect, push, start, unregister },
+ workerText,
+ document
+) {
+ const targetElement = findDebugTargetByText(SW_URL, document);
+
+ const inspectButton = targetElement.querySelector(
+ ".qa-debug-target-inspect-button"
+ );
+ const pushButton = targetElement.querySelector(".qa-push-button");
+ const startButton = targetElement.querySelector(".qa-start-button");
+ const unregisterButton = targetElement.querySelector(".qa-unregister-button");
+
+ is(
+ !!inspectButton,
+ inspect,
+ "Inspect button should be " + (inspect ? "visible" : "hidden")
+ );
+ is(
+ !!pushButton,
+ push,
+ "Push button should be " + (push ? "visible" : "hidden")
+ );
+ is(
+ !!startButton,
+ start,
+ "Start button should be " + (start ? "visible" : "hidden")
+ );
+ is(
+ !!unregisterButton,
+ unregister,
+ "Unregister button should be " + (unregister ? "visible" : "hidden")
+ );
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_timeout.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_timeout.js
new file mode 100644
index 0000000000..f4fc04d229
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_timeout.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test will be idle for a long period to give a chance to the service worker to
+// timeout.
+requestLongerTimeout(3);
+
+/* import-globals-from helper-serviceworker.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-serviceworker.js",
+ this
+);
+
+const SW_TAB_URL = URL_ROOT_SSL + "resources/service-workers/empty-sw.html";
+const SW_URL = URL_ROOT_SSL + "resources/service-workers/empty-sw.js";
+const SW_TIMEOUT = 4000;
+
+/**
+ * Test that service workers will _not_ timeout and be stopped when a toolbox is attached
+ * to them. Feature implemented in Bug 1228382.
+ */
+add_task(async function () {
+ await enableServiceWorkerDebugging();
+
+ // Setting a low idle_timeout and idle_extended_timeout will allow the service worker
+ // to reach the STOPPED state quickly, which will allow us to test the start button.
+ // The default value is 30000 milliseconds.
+ info("Set a low service worker idle timeout");
+ await pushPref("dom.serviceWorkers.idle_timeout", SW_TIMEOUT);
+ await pushPref("dom.serviceWorkers.idle_extended_timeout", SW_TIMEOUT);
+
+ const { document, tab, window } = await openAboutDebugging({
+ enableWorkerUpdates: true,
+ });
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ // Open a tab that registers a basic service worker.
+ const swTab = await addTab(SW_TAB_URL);
+
+ // Wait for the registration to make sure service worker has been started, and that we
+ // are not just reading STOPPED as the initial state.
+ await waitForRegistration(swTab);
+
+ info("Wait until the service worker stops");
+ await waitForServiceWorkerStopped(SW_URL, document);
+
+ info(
+ "Click on the start button and wait for the service worker to be running"
+ );
+ const onServiceWorkerRunning = waitForServiceWorkerRunning(SW_URL, document);
+ const startButton = getStartButton(SW_URL, document);
+ startButton.click();
+ await onServiceWorkerRunning;
+
+ const inspectButton = getInspectButton(SW_URL, document);
+ ok(!!inspectButton, "Service worker target has an inspect button");
+
+ info("Click on inspect and wait for the toolbox to open");
+ const onToolboxReady = gDevTools.once("toolbox-ready");
+ inspectButton.click();
+ await onToolboxReady;
+
+ // Wait for more 5 times the service worker timeout to check that the toolbox prevents
+ // the worker from being destroyed.
+ await wait(SW_TIMEOUT * 5);
+
+ // Check that the service worker is still running, even after waiting 5 times the
+ // service worker timeout.
+ const hasInspectButton = !!getInspectButton(SW_URL, document);
+ ok(hasInspectButton, "Service worker target still has an inspect button");
+
+ info("Destroy the toolbox");
+ const devtoolsTab = gBrowser.selectedTab;
+ await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
+
+ // After stopping the toolbox, the service worker instance should be released and the
+ // service worker registration should be displayed as stopped again.
+ info("Wait until the service worker stops after closing the toolbox");
+ await waitForServiceWorkerStopped(SW_URL, document);
+
+ info("Unregister service worker");
+ await unregisterServiceWorker(swTab);
+
+ info("Wait until the service worker disappears from about:debugging");
+ await waitUntil(() => !findDebugTargetByText(SW_URL, document));
+
+ info("Remove tabs");
+ await removeTab(swTab);
+ await removeTab(tab);
+});
+
+function getStartButton(workerText, doc) {
+ const target = findDebugTargetByText(workerText, doc);
+ return target ? target.querySelector(".qa-start-button") : null;
+}
+
+function getInspectButton(workerText, doc) {
+ const target = findDebugTargetByText(workerText, doc);
+ return target
+ ? target.querySelector(".qa-debug-target-inspect-button")
+ : null;
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_unregister.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_unregister.js
new file mode 100644
index 0000000000..f9237e1795
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_serviceworker_unregister.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-serviceworker.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-serviceworker.js",
+ this
+);
+
+const SW_TAB_URL = URL_ROOT_SSL + "resources/service-workers/empty-sw.html";
+const SW_URL = URL_ROOT_SSL + "resources/service-workers/empty-sw.js";
+
+/**
+ * Test that service workers can be started using about:debugging.
+ */
+add_task(async function () {
+ await enableServiceWorkerDebugging();
+
+ const { document, tab, window } = await openAboutDebugging({
+ enableWorkerUpdates: true,
+ });
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ // Open a tab that registers a basic service worker.
+ const swTab = await addTab(SW_TAB_URL);
+
+ info("Wait until the service worker appears and is running");
+ const targetElement = await waitForServiceWorkerRunning(SW_URL, document);
+
+ // Retrieve the Start button for the worker.
+ const unregisterButton = targetElement.querySelector(".qa-unregister-button");
+ ok(unregisterButton, "Found its unregister button");
+
+ info(
+ "Click on the unregister button and wait for the service worker to disappear"
+ );
+ unregisterButton.click();
+ await waitUntil(() => !findDebugTargetByText(SW_URL, document));
+
+ const hasServiceWorkerTarget = !!findDebugTargetByText(SW_URL, document);
+ ok(!hasServiceWorkerTarget, "Service worker was successfully unregistered");
+
+ info("Remove tabs");
+ await removeTab(swTab);
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_connection_state.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_connection_state.js
new file mode 100644
index 0000000000..d4d31a7522
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_connection_state.js
@@ -0,0 +1,192 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const RUNTIME_ID = "test-runtime-id";
+const RUNTIME_NAME = "test runtime name";
+const RUNTIME_DEVICE_NAME = "test device name";
+const RUNTIME_SHORT_NAME = "test short name";
+
+const CONNECTION_TIMING_OUT_DELAY = 1000;
+const CONNECTION_CANCEL_DELAY = 2000;
+
+// Test following connection state tests.
+// * Connect button label and state will change during connecting.
+// * Show error message if connection failed.
+// * Show warninng if connection has been taken time.
+add_task(async function () {
+ await setupPreferences();
+
+ const mocks = new Mocks();
+
+ const { document, tab } = await openAboutDebugging();
+
+ mocks.createUSBRuntime(RUNTIME_ID, {
+ name: RUNTIME_NAME,
+ deviceName: RUNTIME_DEVICE_NAME,
+ shortName: RUNTIME_SHORT_NAME,
+ });
+ mocks.emitUSBUpdate();
+
+ info("Wait until the USB sidebar item appears");
+ await waitUntil(() => findSidebarItemByText(RUNTIME_DEVICE_NAME, document));
+ const usbRuntimeSidebarItem = findSidebarItemByText(
+ RUNTIME_DEVICE_NAME,
+ document
+ );
+ const connectButton =
+ usbRuntimeSidebarItem.querySelector(".qa-connect-button");
+
+ info("Simulate to happen connection error");
+ mocks.runtimeClientFactoryMock.createClientForRuntime = async runtime => {
+ throw new Error("Dummy connection error");
+ };
+
+ info(
+ "Check whether the error message displayed after clicking connect button"
+ );
+ connectButton.click();
+ await waitUntil(() => document.querySelector(".qa-connection-error"));
+ ok(true, "Error message displays when connection failed");
+
+ info("Simulate to wait for the connection prompt on remote runtime");
+ let resumeConnection;
+ const resumeConnectionPromise = new Promise(r => {
+ resumeConnection = r;
+ });
+ mocks.runtimeClientFactoryMock.createClientForRuntime = async runtime => {
+ await resumeConnectionPromise;
+ return mocks._clients[runtime.type][runtime.id];
+ };
+
+ info("Click on the connect button and wait until it disappears");
+ connectButton.click();
+ info("Check whether a warning of connection not responding displays");
+ await waitUntil(() =>
+ document.querySelector(".qa-connection-not-responding")
+ );
+ ok(
+ document.querySelector(".qa-connection-not-responding"),
+ "A warning of connection not responding displays"
+ );
+ ok(connectButton.disabled, "Connect button is disabled");
+ ok(
+ connectButton.textContent.startsWith("Connecting"),
+ "Label of the connect button changes"
+ );
+ ok(
+ !document.querySelector(".qa-connection-error"),
+ "Error message disappears"
+ );
+
+ info(
+ "Unblock the connection and check the message and connect button disappear"
+ );
+ resumeConnection();
+ await waitUntil(
+ () => !usbRuntimeSidebarItem.querySelector(".qa-connect-button")
+ );
+ ok(!document.querySelector(".qa-connection-error"), "Error disappears");
+ ok(
+ !document.querySelector(".qa-connection-not-responding"),
+ "Warning disappears"
+ );
+
+ info("Remove a USB runtime");
+ mocks.removeUSBRuntime(RUNTIME_ID);
+ mocks.emitUSBUpdate();
+ await waitUntilUsbDeviceIsUnplugged(RUNTIME_DEVICE_NAME, document);
+
+ await removeTab(tab);
+});
+
+// Test whether the status of all will be reverted after a certain period of time during
+// waiting connection.
+add_task(async function () {
+ await setupPreferences();
+
+ const mocks = new Mocks();
+
+ const { document, tab } = await openAboutDebugging();
+
+ mocks.createUSBRuntime(RUNTIME_ID, {
+ name: RUNTIME_NAME,
+ deviceName: RUNTIME_DEVICE_NAME,
+ shortName: RUNTIME_SHORT_NAME,
+ });
+ mocks.emitUSBUpdate();
+
+ info("Wait until the USB sidebar item appears");
+ await waitUntil(() => findSidebarItemByText(RUNTIME_DEVICE_NAME, document));
+ const usbRuntimeSidebarItem = findSidebarItemByText(
+ RUNTIME_DEVICE_NAME,
+ document
+ );
+ const connectButton =
+ usbRuntimeSidebarItem.querySelector(".qa-connect-button");
+
+ let resumeConnection;
+ const resumeConnectionPromise = new Promise(r => {
+ resumeConnection = r;
+ });
+ mocks.runtimeClientFactoryMock.createClientForRuntime = async runtime => {
+ await resumeConnectionPromise;
+ return mocks._clients[runtime.type][runtime.id];
+ };
+
+ info("Click on the connect button and wait until it disappears");
+ connectButton.click();
+ await waitUntil(() =>
+ document.querySelector(".qa-connection-not-responding")
+ );
+ info("Check whether the all status will be reverted");
+ await waitUntil(
+ () => !document.querySelector(".qa-connection-not-responding")
+ );
+ ok(
+ document.querySelector(".qa-connection-timeout"),
+ "Connection timeout message displays"
+ );
+ ok(!connectButton.disabled, "Connect button is enabled");
+ is(
+ connectButton.textContent,
+ "Connect",
+ "Label of the connect button reverted"
+ );
+ ok(
+ !document.querySelector(".qa-connection-error"),
+ "Error message disappears"
+ );
+
+ info("Check whether the timeout message disappears");
+ resumeConnection();
+ await waitUntil(() => !document.querySelector(".qa-connection-timeout"));
+
+ info("Remove a USB runtime");
+ mocks.removeUSBRuntime(RUNTIME_ID);
+ mocks.emitUSBUpdate();
+
+ info("Wait until the USB sidebar item disappears");
+ await waitUntilUsbDeviceIsUnplugged(RUNTIME_DEVICE_NAME, document);
+
+ await removeTab(tab);
+});
+
+async function setupPreferences() {
+ if (SpecialPowers.isDebugBuild) {
+ // On debug builds, reducing the timings might lead to skip the "warning"
+ // state and will block the test execution.
+ // Do not change the timings in debug builds.
+ return;
+ }
+
+ await pushPref(
+ "devtools.aboutdebugging.test-connection-timing-out-delay",
+ CONNECTION_TIMING_OUT_DELAY
+ );
+ await pushPref(
+ "devtools.aboutdebugging.test-connection-cancel-delay",
+ CONNECTION_CANCEL_DELAY
+ );
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_network_runtimes.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_network_runtimes.js
new file mode 100644
index 0000000000..0482aa70c2
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_network_runtimes.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const networkLocationsModule = require("resource://devtools/client/aboutdebugging/src/modules/network-locations.js");
+
+/**
+ * Test the sidebar is updated correctly when network runtimes are added/removed.
+ */
+
+add_task(async function () {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.aboutdebugging.network-locations");
+ });
+
+ const { document, tab } = await openAboutDebugging();
+
+ const noDevicesElement = document.querySelector(".qa-sidebar-no-devices");
+ ok(noDevicesElement, "Sidebar shows the 'no devices' element");
+
+ info("Add a network location");
+ networkLocationsModule.addNetworkLocation("localhost:6080");
+
+ info("Wait for 'no devices' element to disappear");
+ waitUntil(() => !document.querySelector(".qa-sidebar-no-devices"));
+ ok(
+ findSidebarItemByText("localhost:6080", document),
+ "Found a sidebar item for localhost:6080"
+ );
+
+ info("Remove the network location");
+ networkLocationsModule.removeNetworkLocation("localhost:6080");
+
+ info("Wait for 'no devices' element to reappear");
+ waitUntil(() => document.querySelector(".qa-sidebar-no-devices"));
+ ok(
+ !findSidebarItemByText("localhost:6080", document),
+ "Sidebar item for localhost:6080 removed"
+ );
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_runtime.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_runtime.js
new file mode 100644
index 0000000000..b63b7ae08e
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_runtime.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const RUNTIME_ID = "RUNTIME_ID";
+const RUNTIME_DEVICE_NAME = "RUNTIME_DEVICE_NAME";
+const RUNTIME_SHORT_NAME = "testshort";
+
+// Test that USB runtimes appear and disappear from the sidebar.
+add_task(async function () {
+ const mocks = new Mocks();
+
+ const { document, tab } = await openAboutDebugging();
+
+ mocks.createUSBRuntime(RUNTIME_ID, {
+ deviceName: RUNTIME_DEVICE_NAME,
+ shortName: RUNTIME_SHORT_NAME,
+ });
+ mocks.emitUSBUpdate();
+
+ info("Wait until the USB sidebar item appears");
+ await waitUntil(() => findSidebarItemByText(RUNTIME_DEVICE_NAME, document));
+ const usbRuntimeSidebarItem = findSidebarItemByText(
+ RUNTIME_DEVICE_NAME,
+ document
+ );
+ ok(
+ usbRuntimeSidebarItem.textContent.includes(RUNTIME_SHORT_NAME),
+ "The short name of the usb runtime is visible"
+ );
+
+ mocks.removeUSBRuntime(RUNTIME_ID);
+ mocks.emitUSBUpdate();
+ await waitUntilUsbDeviceIsUnplugged(RUNTIME_DEVICE_NAME, document);
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_runtime_connect.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_runtime_connect.js
new file mode 100644
index 0000000000..f4978b0495
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_runtime_connect.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const RUNTIME_ID = "test-runtime-id";
+const RUNTIME_NAME = "test runtime name";
+const RUNTIME_DEVICE_NAME = "test device name";
+const RUNTIME_SHORT_NAME = "test short name";
+
+// Test that USB runtimes appear and disappear from the sidebar,
+// as well as their connect button.
+// Also checks whether the label of item is updated after connecting.
+add_task(async function () {
+ const mocks = new Mocks();
+
+ const { document, tab } = await openAboutDebugging();
+
+ mocks.createUSBRuntime(RUNTIME_ID, {
+ name: RUNTIME_NAME,
+ deviceName: RUNTIME_DEVICE_NAME,
+ shortName: RUNTIME_SHORT_NAME,
+ });
+ mocks.emitUSBUpdate();
+
+ info("Wait until the USB sidebar item appears");
+ await waitUntil(() => findSidebarItemByText(RUNTIME_DEVICE_NAME, document));
+ const usbRuntimeSidebarItem = findSidebarItemByText(
+ RUNTIME_DEVICE_NAME,
+ document
+ );
+ const connectButton =
+ usbRuntimeSidebarItem.querySelector(".qa-connect-button");
+ ok(connectButton, "Connect button is displayed for the USB runtime");
+
+ info("Click on the connect button and wait until it disappears");
+ connectButton.click();
+ await waitUntil(
+ () => !usbRuntimeSidebarItem.querySelector(".qa-connect-button")
+ );
+
+ info("Check whether the label of item is updated after connecting");
+ ok(
+ usbRuntimeSidebarItem.textContent.includes(RUNTIME_NAME),
+ "Label of item updated"
+ );
+
+ info("Remove all USB runtimes");
+ mocks.removeUSBRuntime(RUNTIME_ID);
+ mocks.emitUSBUpdate();
+ await waitUntilUsbDeviceIsUnplugged(RUNTIME_DEVICE_NAME, document);
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_runtime_refresh.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_runtime_refresh.js
new file mode 100644
index 0000000000..47ecc03239
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_runtime_refresh.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const RUNTIME_ID = "test-runtime-id";
+const RUNTIME_DEVICE_NAME = "test device name";
+const RUNTIME_APP_NAME = "TestApp";
+
+const OTHER_RUNTIME_ID = "other-runtime-id";
+const OTHER_RUNTIME_APP_NAME = "OtherApp";
+
+// Test that USB runtimes are not disconnected on refresh.
+add_task(async function () {
+ const mocks = new Mocks();
+
+ const { document, tab } = await openAboutDebugging();
+
+ info("Create a first runtime and connect to it");
+ mocks.createUSBRuntime(RUNTIME_ID, {
+ deviceName: RUNTIME_DEVICE_NAME,
+ name: RUNTIME_APP_NAME,
+ });
+ mocks.emitUSBUpdate();
+
+ await connectToRuntime(RUNTIME_DEVICE_NAME, document);
+ await selectRuntime(RUNTIME_DEVICE_NAME, RUNTIME_APP_NAME, document);
+
+ info("Create a second runtime and click on Refresh Devices");
+ mocks.createUSBRuntime(OTHER_RUNTIME_ID, {
+ deviceName: OTHER_RUNTIME_APP_NAME,
+ });
+
+ // adb.updateRuntimes should ultimately fire the "runtime-list-updated" event.
+ mocks.adbMock.adb.updateRuntimes = () => mocks.emitUSBUpdate();
+ document.querySelector(".qa-refresh-devices-button").click();
+
+ info(`Wait until the sidebar item for ${OTHER_RUNTIME_APP_NAME} appears`);
+ await waitUntil(() =>
+ findSidebarItemByText(OTHER_RUNTIME_APP_NAME, document)
+ );
+
+ const sidebarItem = findSidebarItemByText(RUNTIME_DEVICE_NAME, document);
+ ok(
+ !sidebarItem.querySelector(".qa-connect-button"),
+ "Original USB runtime is still connected"
+ );
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_runtime_select.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_runtime_select.js
new file mode 100644
index 0000000000..66ca219bf3
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_runtime_select.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const RUNTIME_DEVICE_ID = "1234";
+const RUNTIME_DEVICE_NAME = "A device";
+
+// Test that we can select a runtime in the sidebar
+add_task(async function () {
+ const mocks = new Mocks();
+
+ const { document, tab } = await openAboutDebugging();
+
+ mocks.createUSBRuntime(RUNTIME_DEVICE_ID, {
+ deviceName: RUNTIME_DEVICE_NAME,
+ });
+ mocks.emitUSBUpdate();
+
+ info("Wait until the USB sidebar item appears");
+ await waitUntil(() => findSidebarItemByText(RUNTIME_DEVICE_NAME, document));
+ const sidebarItem = findSidebarItemByText(RUNTIME_DEVICE_NAME, document);
+ const connectButton = sidebarItem.querySelector(".qa-connect-button");
+ ok(connectButton, "Connect button is displayed for the USB runtime");
+
+ info(
+ "Click on the connect button and wait until the sidebar displays a link"
+ );
+ connectButton.click();
+ await waitUntil(() =>
+ findSidebarItemLinkByText(RUNTIME_DEVICE_NAME, document)
+ );
+
+ info("Click on the runtime link");
+ const link = findSidebarItemLinkByText(RUNTIME_DEVICE_NAME, document);
+ link.click();
+ is(
+ document.location.hash,
+ `#/runtime/${RUNTIME_DEVICE_ID}`,
+ "Redirection to runtime page"
+ );
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_status.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_status.js
new file mode 100644
index 0000000000..e67cde31bb
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_status.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ adbAddon,
+} = require("resource://devtools/client/shared/remote-debugging/adb/adb-addon.js");
+
+/**
+ * This test asserts that the sidebar shows a message describing the status of the USB
+ * devices scanning.
+ */
+add_task(async function () {
+ const mocks = new Mocks();
+
+ await pushPref(
+ "devtools.remote.adb.extensionURL",
+ CHROME_URL_ROOT + "resources/test-adb-extension/adb-extension-#OS#.xpi"
+ );
+ const { document, tab } = await openAboutDebugging();
+
+ const usbStatusElement = document.querySelector(".qa-sidebar-usb-status");
+ ok(usbStatusElement, "Sidebar shows the USB status element");
+ ok(
+ usbStatusElement.textContent.includes("USB disabled"),
+ "USB status element has 'disabled' content"
+ );
+
+ info("Install the adb extension and wait for the message to udpate");
+ // Use "internal" as the install source to avoid triggering telemetry.
+ adbAddon.install("internal");
+ // When using mocks, we manually control the .start() call
+ await mocks.adbProcessMock.adbProcess.start();
+
+ info("Wait till the USB status element has 'enabled' content");
+ await waitUntil(() => {
+ const el = document.querySelector(".qa-sidebar-usb-status");
+ return el.textContent.includes("USB enabled");
+ });
+
+ info("Uninstall the adb extension and wait for USB status element to update");
+ adbAddon.uninstall();
+ await waitUntil(() => {
+ const el = document.querySelector(".qa-sidebar-usb-status");
+ return el.textContent.includes("USB disabled");
+ });
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_unavailable_runtime.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_unavailable_runtime.js
new file mode 100644
index 0000000000..e073939b6d
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_unavailable_runtime.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const RUNTIME_NAME = "Firefox 123";
+const DEVICE_NAME = "DEVICE_NAME";
+const DEVICE_ID = "DEVICE_ID";
+const RUNTIME_ID = "RUNTIME_ID";
+
+// Test that unavailable runtimes:
+// - are displayed without a connect button.
+// - cannot be selected
+// - display a specific text ("Waiting for runtime") instead of the runtime name
+add_task(async function () {
+ const mocks = new Mocks();
+ const { document, tab } = await openAboutDebugging();
+
+ info("Create a device without a corresponding runtime");
+ mocks.addDevice(DEVICE_ID, DEVICE_NAME);
+ mocks.emitUSBUpdate();
+
+ info("Wait until the USB sidebar item appears");
+ await waitUntil(() => findSidebarItemByText(DEVICE_NAME, document));
+
+ const usbRuntimeSidebarItem = findSidebarItemByText(DEVICE_NAME, document);
+
+ ok(
+ usbRuntimeSidebarItem.querySelector(".qa-runtime-item-waiting-for-browser"),
+ "Sidebar item shows as `Waiting for browser`"
+ );
+
+ const hasConnectButton =
+ usbRuntimeSidebarItem.querySelector(".qa-connect-button");
+ ok(!hasConnectButton, "Connect button is not displayed");
+
+ const hasLink = usbRuntimeSidebarItem.querySelector(".qa-sidebar-link");
+ ok(!hasLink, "Unavailable runtime is not selectable");
+
+ info("Add a valid runtime for the same device id and emit update event");
+ mocks.createUSBRuntime(RUNTIME_ID, {
+ deviceId: DEVICE_ID,
+ deviceName: DEVICE_NAME,
+ shortName: RUNTIME_NAME,
+ });
+ mocks.removeDevice(DEVICE_ID);
+ mocks.emitUSBUpdate();
+
+ info("Wait until connect button appears for the USB runtime");
+ let updatedSidebarItem = null;
+ await waitUntil(() => {
+ updatedSidebarItem = findSidebarItemByText(DEVICE_NAME, document);
+ return (
+ updatedSidebarItem &&
+ updatedSidebarItem.querySelector(".qa-connect-button")
+ );
+ });
+
+ ok(
+ updatedSidebarItem.querySelector(".qa-runtime-item-standard"),
+ "Sidebar item for the USB runtime is now a standard sidebar item"
+ );
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_unplugged_device.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_unplugged_device.js
new file mode 100644
index 0000000000..5074538a94
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_sidebar_usb_unplugged_device.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const RUNTIME_NAME = "RUNTIME_NAME_1";
+const DEVICE_NAME = "DEVICE_NAME_1";
+const DEVICE_ID = "DEVICE_ID_1";
+const RUNTIME_ID = "RUNTIME_ID_1";
+
+const RUNTIME_NAME_2 = "RUNTIME_NAME_2";
+const DEVICE_NAME_2 = "DEVICE_NAME_2";
+const DEVICE_ID_2 = "DEVICE_ID_2";
+const RUNTIME_ID_2 = "RUNTIME_ID_2";
+
+// Test that removed USB devices are still visible as "Unplugged devices", until
+// about:debugging is reloaded.
+add_task(async function () {
+ const mocks = new Mocks();
+ let { document, tab } = await openAboutDebugging();
+
+ info("Create a mocked USB runtime");
+ mocks.createUSBRuntime(RUNTIME_ID, {
+ deviceId: DEVICE_ID,
+ deviceName: DEVICE_NAME,
+ shortName: RUNTIME_NAME,
+ });
+ mocks.emitUSBUpdate();
+
+ info("Wait until the USB sidebar item appears");
+ await waitUntil(() => findSidebarItemByText(DEVICE_NAME, document));
+ const sidebarItem = findSidebarItemByText(DEVICE_NAME, document);
+ ok(
+ sidebarItem.textContent.includes(RUNTIME_NAME),
+ "Sidebar item shows the runtime name"
+ );
+
+ mocks.removeUSBRuntime(RUNTIME_ID);
+ mocks.emitUSBUpdate();
+ await waitUntilUsbDeviceIsUnplugged(DEVICE_NAME, document);
+
+ const unpluggedItem = findSidebarItemByText(DEVICE_NAME, document);
+ ok(
+ unpluggedItem.querySelector(".qa-runtime-item-unplugged"),
+ "Sidebar item is shown as `Unplugged…`"
+ );
+
+ info("Reload about:debugging");
+ document = await reloadAboutDebugging(tab);
+
+ info(
+ "Add another mocked USB runtime, to make sure the sidebar items are rendered."
+ );
+ mocks.createUSBRuntime(RUNTIME_ID_2, {
+ deviceId: DEVICE_ID_2,
+ deviceName: DEVICE_NAME_2,
+ shortName: RUNTIME_NAME_2,
+ });
+ mocks.emitUSBUpdate();
+
+ info("Wait until the other USB sidebar item appears");
+ await waitUntil(() => findSidebarItemByText(DEVICE_NAME_2, document));
+ ok(
+ !findSidebarItemByText(DEVICE_NAME, document),
+ "Unplugged device is no longer displayed after reloading aboutdebugging"
+ );
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_tab_favicons.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_tab_favicons.js
new file mode 100644
index 0000000000..e4acd1727d
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_tab_favicons.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that about:debugging uses the favicon of tab targets as the icon of their debug
+ * target item, and doesn't always use the default globe icon.
+ */
+
+// PlaceUtils will not store any favicon for data: uris so we need to use a dedicated page
+// here.
+const TAB_URL =
+ "https://example.com/browser/devtools/client/aboutdebugging/" +
+ "test/browser/test-tab-favicons.html";
+
+// This is the same png data-url as the one used in test-tab-favicons.html.
+const EXPECTED_FAVICON =
+ "data:image/png;base64," +
+ "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATklEQVRYhe3SIQ4AI" +
+ "BADwf7/04elBAtrVlSduGnSTDJ7cuT1PQJwwO+Hl7sAGAA07gjAAfgIBeAAoH" +
+ "FHAA7ARygABwCNOwJwAD5CATRgAYXh+kypw86nAAAAAElFTkSuQmCC";
+
+add_task(async function () {
+ const faviconTab = await addTab(TAB_URL, { background: true });
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ await waitUntil(() => {
+ const target = findDebugTargetByText("Favicon tab", document);
+ if (!target) {
+ return false;
+ }
+ // We may get a default globe.svg icon for a short period of time while
+ // the target tab is still loading.
+ return target
+ .querySelector(".qa-debug-target-item-icon")
+ .src.includes("data:");
+ });
+ const faviconTabTarget = findDebugTargetByText("Favicon tab", document);
+ const faviconTabIcon = faviconTabTarget.querySelector(
+ ".qa-debug-target-item-icon"
+ );
+
+ // Note this relies on PlaceUtils.promiseFaviconData returning the same data-url as the
+ // one provided in the test page. If the implementation changes and PlaceUtils returns a
+ // different base64 from the one we defined, we can instead load the image and check a
+ // few pixels to verify it matches the expected icon.
+ is(
+ faviconTabIcon.src,
+ EXPECTED_FAVICON,
+ "The debug target item for the tab shows the favicon of the tab"
+ );
+
+ await removeTab(tab);
+ await removeTab(faviconTab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_tab_navigate.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_tab_navigate.js
new file mode 100644
index 0000000000..45355203e3
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_tab_navigate.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TAB_1_URL =
+ "http://example.org/document-builder.sjs?html=<title>TITLE1</title>";
+const TAB_2_URL =
+ "http://example.org/document-builder.sjs?html=<title>TITLE2</title>";
+
+// Check that the list of tabs in about:debugging is updated when a page
+// navigates. This indirectly checks that the tabListChanged event is correctly
+// fired from the root actor.
+add_task(async function () {
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ const testTab = await addTab(TAB_1_URL, { background: true });
+ await waitFor(() => findDebugTargetByText("TITLE1", document));
+
+ navigateTo(TAB_2_URL, { browser: testTab.linkedBrowser });
+ await waitFor(() => findDebugTargetByText("TITLE2", document));
+
+ ok(
+ !findDebugTargetByText("TITLE1", document),
+ "TITLE2 target replaced TITLE1"
+ );
+
+ await removeTab(tab);
+ await removeTab(testTab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_tab_zombietab.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_tab_zombietab.js
new file mode 100644
index 0000000000..1243d3374d
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_tab_zombietab.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let gUniqueCounter = 0;
+
+const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
+
+const BROWSER_STATE_TABS = [
+ "about:debugging",
+ "data:text/html,<title>TEST_TAB_1</title>",
+ "data:text/html,<title>TEST_TAB_2</title>",
+ "data:text/html,<title>TEST_TAB_3</title>",
+];
+const BROWSER_STATE = {
+ windows: [
+ {
+ tabs: BROWSER_STATE_TABS.map(url => {
+ return {
+ entries: [{ url, triggeringPrincipal_base64 }],
+ extData: { uniq: Date.now() + "-" + ++gUniqueCounter },
+ };
+ }),
+ selected: 1,
+ },
+ ],
+};
+
+// Check that the inspect action is disabled for lazy/zombie tabs, such as the
+// ones created after a session restore.
+add_task(async function () {
+ // This setup is normally handed by the openAboutDebugging helper, but here we
+ // open about:debugging via session restore.
+ silenceWorkerUpdates();
+ await pushPref("devtools.aboutdebugging.local-tab-debugging", true);
+
+ info("Restore 4 tabs including a selected about:debugging tab");
+ const onBrowserSessionRestored = Promise.all([
+ TestUtils.topicObserved("sessionstore-browser-state-restored"),
+ BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "SSTabRestored"),
+ ]);
+ SessionStore.setBrowserState(JSON.stringify(BROWSER_STATE));
+ await onBrowserSessionRestored;
+
+ const tab = gBrowser.selectedTab;
+ const browser = tab.linkedBrowser;
+ const doc = browser.contentDocument;
+ const win = browser.contentWindow;
+ const store = win.AboutDebugging.store;
+
+ info("Wait until Connect page is displayed");
+ await waitUntil(() => doc.querySelector(".qa-connect-page"));
+
+ await selectThisFirefoxPage(doc, store);
+
+ // Check that all inspect butttons are disabled.
+ checkInspectButton("TEST_TAB_1", doc, { expectDisabled: true });
+ checkInspectButton("TEST_TAB_2", doc, { expectDisabled: true });
+ checkInspectButton("TEST_TAB_3", doc, { expectDisabled: true });
+
+ info("Select the TEST_TAB_2 tab top restore it completely");
+ const onTabRestored = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "SSTabRestored"
+ );
+ gBrowser.selectedTab = gBrowser.tabs[2];
+ await onTabRestored;
+
+ const onTabsSuccess = waitForDispatch(store, "REQUEST_TABS_SUCCESS");
+
+ info("Select the about:debugging tab again");
+ gBrowser.selectedTab = tab;
+
+ info("Wait until the tabs update is finished");
+ await onTabsSuccess;
+
+ info("Wait until the inspect button for TEST_TAB_2 is enabled");
+ await waitUntil(() => {
+ const target = findDebugTargetByText("TEST_TAB_2", doc);
+ if (!target) {
+ // TEST_TAB_2 target might be missing while the tab target list updates.
+ return false;
+ }
+
+ const button = target.querySelector(".qa-debug-target-inspect-button");
+ return !button.disabled;
+ });
+
+ // Check that all inspect butttons are disabled, except for #2.
+ checkInspectButton("TEST_TAB_1", doc, { expectDisabled: true });
+ checkInspectButton("TEST_TAB_2", doc, { expectDisabled: false });
+ checkInspectButton("TEST_TAB_3", doc, { expectDisabled: true });
+});
+
+function checkInspectButton(targetText, doc, { expectDisabled }) {
+ const inspectButton = getInspectButton(targetText, doc);
+ if (expectDisabled) {
+ ok(inspectButton.disabled, `Inspect button is disabled for ${targetText}`);
+ } else {
+ ok(!inspectButton.disabled, `Inspect button is enabled for ${targetText}`);
+ }
+}
+
+function getInspectButton(targetText, doc) {
+ const targetElement = findDebugTargetByText(targetText, doc);
+ return targetElement.querySelector(".qa-debug-target-inspect-button");
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_basic.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_basic.js
new file mode 100644
index 0000000000..5970d6a70d
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_basic.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-telemetry.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-telemetry.js",
+ this
+);
+
+/**
+ * Check that telemetry events are recorded when opening and closing about debugging.
+ */
+add_task(async function () {
+ setupTelemetryTest();
+
+ const { tab } = await openAboutDebugging();
+
+ const openEvents = readAboutDebuggingEvents().filter(
+ e => e.method === "open_adbg"
+ );
+ is(
+ openEvents.length,
+ 1,
+ "Exactly one open event was logged for about:debugging"
+ );
+ const sessionId = openEvents[0].extras.session_id;
+ ok(!isNaN(sessionId), "Open event has a valid session id");
+
+ await removeTab(tab);
+
+ const closeEvents = readAboutDebuggingEvents().filter(
+ e => e.method === "close_adbg"
+ );
+ is(
+ closeEvents.length,
+ 1,
+ "Exactly one close event was logged for about:debugging"
+ );
+ is(
+ closeEvents[0].extras.session_id,
+ sessionId,
+ "Close event has the same session id as the open event"
+ );
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_connection_attempt.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_connection_attempt.js
new file mode 100644
index 0000000000..a5836ad50a
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_connection_attempt.js
@@ -0,0 +1,274 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-telemetry.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-telemetry.js",
+ this
+);
+
+const USB_RUNTIME = {
+ id: "runtime-id-1",
+ deviceName: "Device A",
+ name: "Runtime 1",
+ shortName: "R1",
+};
+
+/**
+ * Check that telemetry events for connection attempts are correctly recorded in various
+ * scenarios:
+ * - successful connection
+ * - successful connection after showing the timeout warning
+ * - failed connection
+ * - connection timeout
+ */
+add_task(async function testSuccessfulConnectionAttempt() {
+ const { doc, mocks, runtimeId, sessionId, tab } =
+ await setupConnectionAttemptTest();
+
+ await connectToRuntime(USB_RUNTIME.deviceName, doc);
+
+ const connectionEvents = checkTelemetryEvents(
+ [
+ { method: "runtime_connected", extras: { runtime_id: runtimeId } },
+ {
+ method: "connection_attempt",
+ extras: getEventExtras("start", runtimeId),
+ },
+ {
+ method: "connection_attempt",
+ extras: getEventExtras("success", runtimeId),
+ },
+ ],
+ sessionId
+ ).filter(({ method }) => method === "connection_attempt");
+
+ checkConnectionId(connectionEvents);
+
+ await removeUsbRuntime(USB_RUNTIME, mocks, doc);
+ await removeTab(tab);
+});
+
+add_task(async function testFailedConnectionAttempt() {
+ const { doc, mocks, runtimeId, sessionId, tab } =
+ await setupConnectionAttemptTest();
+ mocks.runtimeClientFactoryMock.createClientForRuntime = async runtime => {
+ throw new Error("failed");
+ };
+
+ info(
+ "Try to connect to the runtime and wait for the connection error message"
+ );
+ const usbRuntimeSidebarItem = findSidebarItemByText(
+ USB_RUNTIME.deviceName,
+ doc
+ );
+ const connectButton =
+ usbRuntimeSidebarItem.querySelector(".qa-connect-button");
+ connectButton.click();
+ await waitUntil(() =>
+ usbRuntimeSidebarItem.querySelector(".qa-connection-error")
+ );
+
+ const connectionEvents = checkTelemetryEvents(
+ [
+ {
+ method: "connection_attempt",
+ extras: getEventExtras("start", runtimeId),
+ },
+ {
+ method: "connection_attempt",
+ extras: getEventExtras("failed", runtimeId),
+ },
+ ],
+ sessionId
+ ).filter(({ method }) => method === "connection_attempt");
+
+ checkConnectionId(connectionEvents);
+
+ await removeUsbRuntime(USB_RUNTIME, mocks, doc);
+ await removeTab(tab);
+});
+
+add_task(async function testPendingConnectionAttempt() {
+ info("Set timeout preferences to avoid cancelling the connection");
+ await pushPref(
+ "devtools.aboutdebugging.test-connection-timing-out-delay",
+ 100
+ );
+ await pushPref(
+ "devtools.aboutdebugging.test-connection-cancel-delay",
+ 100000
+ );
+
+ const { doc, mocks, runtimeId, sessionId, tab } =
+ await setupConnectionAttemptTest();
+
+ info("Simulate a pending connection");
+ let resumeConnection;
+ const resumeConnectionPromise = new Promise(r => {
+ resumeConnection = r;
+ });
+ mocks.runtimeClientFactoryMock.createClientForRuntime = async runtime => {
+ await resumeConnectionPromise;
+ return mocks._clients[runtime.type][runtime.id];
+ };
+
+ info("Click on the connect button and wait for the warning message");
+ const usbRuntimeSidebarItem = findSidebarItemByText(
+ USB_RUNTIME.deviceName,
+ doc
+ );
+ const connectButton =
+ usbRuntimeSidebarItem.querySelector(".qa-connect-button");
+ connectButton.click();
+ await waitUntil(() => doc.querySelector(".qa-connection-not-responding"));
+
+ info("Resume the connection and wait for the connection to succeed");
+ resumeConnection();
+ await waitUntil(
+ () => !usbRuntimeSidebarItem.querySelector(".qa-connect-button")
+ );
+
+ const connectionEvents = checkTelemetryEvents(
+ [
+ { method: "runtime_connected", extras: { runtime_id: runtimeId } },
+ {
+ method: "connection_attempt",
+ extras: getEventExtras("start", runtimeId),
+ },
+ {
+ method: "connection_attempt",
+ extras: getEventExtras("not responding", runtimeId),
+ },
+ {
+ method: "connection_attempt",
+ extras: getEventExtras("success", runtimeId),
+ },
+ ],
+ sessionId
+ ).filter(({ method }) => method === "connection_attempt");
+ checkConnectionId(connectionEvents);
+
+ await removeUsbRuntime(USB_RUNTIME, mocks, doc);
+ await removeTab(tab);
+});
+
+add_task(async function testCancelledConnectionAttempt() {
+ info("Set timeout preferences to quickly cancel the connection");
+ await pushPref(
+ "devtools.aboutdebugging.test-connection-timing-out-delay",
+ 100
+ );
+ await pushPref("devtools.aboutdebugging.test-connection-cancel-delay", 1000);
+
+ const { doc, mocks, runtimeId, sessionId, tab } =
+ await setupConnectionAttemptTest();
+
+ info("Simulate a connection timeout");
+ mocks.runtimeClientFactoryMock.createClientForRuntime = async runtime => {
+ await new Promise(r => {});
+ };
+
+ info("Click on the connect button and wait for the error message");
+ const usbRuntimeSidebarItem = findSidebarItemByText(
+ USB_RUNTIME.deviceName,
+ doc
+ );
+ const connectButton =
+ usbRuntimeSidebarItem.querySelector(".qa-connect-button");
+ connectButton.click();
+ await waitUntil(() =>
+ usbRuntimeSidebarItem.querySelector(".qa-connection-timeout")
+ );
+
+ const connectionEvents = checkTelemetryEvents(
+ [
+ {
+ method: "connection_attempt",
+ extras: getEventExtras("start", runtimeId),
+ },
+ {
+ method: "connection_attempt",
+ extras: getEventExtras("not responding", runtimeId),
+ },
+ {
+ method: "connection_attempt",
+ extras: getEventExtras("cancelled", runtimeId),
+ },
+ ],
+ sessionId
+ ).filter(({ method }) => method === "connection_attempt");
+ checkConnectionId(connectionEvents);
+
+ await removeUsbRuntime(USB_RUNTIME, mocks, doc);
+ await removeTab(tab);
+});
+
+function checkConnectionId(connectionEvents) {
+ const connectionId = connectionEvents[0].extras.connection_id;
+ ok(
+ !!connectionId,
+ "Found a valid connection id in the first connection_attempt event"
+ );
+ for (const evt of connectionEvents) {
+ is(
+ evt.extras.connection_id,
+ connectionId,
+ "All connection_attempt events share the same connection id"
+ );
+ }
+}
+
+// Small helper to create the expected event extras object for connection_attempt events
+function getEventExtras(status, runtimeId) {
+ return {
+ connection_type: "usb",
+ runtime_id: runtimeId,
+ status,
+ };
+}
+
+// Open about:debugging, setup telemetry, mocks and create a mocked USB runtime.
+async function setupConnectionAttemptTest() {
+ const mocks = new Mocks();
+ setupTelemetryTest();
+
+ const { tab, document } = await openAboutDebugging();
+
+ const sessionId = getOpenEventSessionId();
+ ok(!isNaN(sessionId), "Open event has a valid session id");
+
+ mocks.createUSBRuntime(USB_RUNTIME.id, {
+ deviceName: USB_RUNTIME.deviceName,
+ name: USB_RUNTIME.name,
+ shortName: USB_RUNTIME.shortName,
+ });
+ mocks.emitUSBUpdate();
+
+ info("Wait for the runtime to appear in the sidebar");
+ await waitUntil(() => findSidebarItemByText(USB_RUNTIME.shortName, document));
+ const evts = checkTelemetryEvents(
+ [
+ { method: "device_added", extras: {} },
+ { method: "runtime_added", extras: {} },
+ ],
+ sessionId
+ );
+
+ const runtimeId = evts.filter(e => e.method === "runtime_added")[0].extras
+ .runtime_id;
+ return { doc: document, mocks, runtimeId, sessionId, tab };
+}
+
+async function removeUsbRuntime(runtime, mocks, doc) {
+ mocks.removeRuntime(runtime.id);
+ mocks.emitUSBUpdate();
+ await waitUntil(
+ () =>
+ !findSidebarItemByText(runtime.name, doc) &&
+ !findSidebarItemByText(runtime.shortName, doc)
+ );
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_inspect.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_inspect.js
new file mode 100644
index 0000000000..60176258b1
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_inspect.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-telemetry.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-telemetry.js",
+ this
+);
+
+const TAB_URL = "data:text/html,<title>TEST_TAB</title>";
+
+/**
+ * Check that telemetry events are recorded when inspecting a target.
+ */
+add_task(async function () {
+ setupTelemetryTest();
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ const sessionId = getOpenEventSessionId();
+ ok(!isNaN(sessionId), "Open event has a valid session id");
+
+ info("Open a new background tab TEST_TAB");
+ const backgroundTab1 = await addTab(TAB_URL, { background: true });
+
+ info("Wait for the tab to appear in the debug targets with the correct name");
+ await waitUntil(() => findDebugTargetByText("TEST_TAB", document));
+
+ const { devtoolsTab } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ "TEST_TAB"
+ );
+
+ const evts = readAboutDebuggingEvents().filter(e => e.method === "inspect");
+ is(evts.length, 1, "Exactly one Inspect event found");
+ is(
+ evts[0].extras.target_type,
+ "TAB",
+ "Inspect event has the expected target type"
+ );
+ is(
+ evts[0].extras.runtime_type,
+ "this-firefox",
+ "Inspect event has the expected runtime type"
+ );
+ is(
+ evts[0].extras.session_id,
+ sessionId,
+ "Inspect event has the expected session"
+ );
+
+ info("Close the about:devtools-toolbox tab");
+ await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
+ await waitForAboutDebuggingRequests(window.AboutDebugging.store);
+
+ info("Remove first background tab");
+ await removeTab(backgroundTab1);
+ await waitUntil(() => !findDebugTargetByText("TEST_TAB", document));
+ await waitForAboutDebuggingRequests(window.AboutDebugging.store);
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_navigate.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_navigate.js
new file mode 100644
index 0000000000..946f5e9e43
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_navigate.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+/* import-globals-from helper-telemetry.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-telemetry.js",
+ this
+);
+
+/**
+ * Check that telemetry events are recorded when navigating between different
+ * about:debugging pages.
+ */
+add_task(async function () {
+ // enable USB devices mocks
+ const mocks = new Mocks();
+
+ setupTelemetryTest();
+
+ const { tab, document, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ const sessionId = getOpenEventSessionId();
+ ok(!isNaN(sessionId), "Open event has a valid session id");
+
+ info("Navigate to 'Connect' page");
+ document.location.hash = "#/connect";
+ await waitUntil(() => document.querySelector(".qa-connect-page"));
+ checkSelectPageEvent("connect", sessionId);
+
+ info("Navigate to 'USB device runtime' page");
+ await navigateToUSBRuntime(mocks, document);
+ checkSelectPageEvent("runtime", sessionId);
+ await waitForAboutDebuggingRequests(window.AboutDebugging.store);
+
+ await removeTab(tab);
+});
+
+function checkSelectPageEvent(expectedType, expectedSessionId) {
+ const evts = readAboutDebuggingEvents().filter(
+ e => e.method === "select_page"
+ );
+ is(evts.length, 1, "Exactly one select_page event recorded");
+ is(
+ evts[0].extras.page_type,
+ expectedType,
+ "Select page event has the expected type"
+ );
+ is(
+ evts[0].extras.session_id,
+ expectedSessionId,
+ "Select page event has the expected session"
+ );
+}
+
+async function navigateToUSBRuntime(mocks, doc) {
+ mocks.createUSBRuntime("1337id", {
+ deviceName: "Fancy Phone",
+ name: "Lorem ipsum",
+ });
+ mocks.emitUSBUpdate();
+ await connectToRuntime("Fancy Phone", doc);
+ // navigate to it via URL
+ doc.location.hash = "#/runtime/1337id";
+ await waitUntil(() => doc.querySelector(".qa-runtime-page"));
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_actions.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_actions.js
new file mode 100644
index 0000000000..92d07a3d9c
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_actions.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-telemetry.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-telemetry.js",
+ this
+);
+
+const RUNTIME_ID = "test-runtime-id";
+const RUNTIME_NAME = "Test Runtime";
+const RUNTIME_DEVICE_NAME = "Test Device";
+
+/**
+ * Test that runtime specific actions are logged as telemetry events with the expected
+ * runtime id and action type.
+ */
+add_task(async function testUsbRuntimeUpdates() {
+ // enable USB devices mocks
+ const mocks = new Mocks();
+ setupTelemetryTest();
+
+ const { tab, document } = await openAboutDebugging();
+
+ const sessionId = getOpenEventSessionId();
+ ok(!isNaN(sessionId), "Open event has a valid session id");
+
+ const usbClient = mocks.createUSBRuntime(RUNTIME_ID, {
+ deviceName: RUNTIME_DEVICE_NAME,
+ name: RUNTIME_NAME,
+ shortName: RUNTIME_NAME,
+ });
+ mocks.emitUSBUpdate();
+
+ info("Wait for the runtime to appear in the sidebar");
+ await waitUntil(() => findSidebarItemByText(RUNTIME_NAME, document));
+ await connectToRuntime(RUNTIME_DEVICE_NAME, document);
+ await selectRuntime(RUNTIME_DEVICE_NAME, RUNTIME_NAME, document);
+
+ info("Read telemetry events to flush unrelated events");
+ const evts = readAboutDebuggingEvents();
+ const runtimeAddedEvent = evts.filter(e => e.method === "runtime_added")[0];
+ const telemetryRuntimeId = runtimeAddedEvent.extras.runtime_id;
+
+ info("Click on the toggle button and wait until the text is updated");
+ const promptButton = document.querySelector(
+ ".qa-connection-prompt-toggle-button"
+ );
+ promptButton.click();
+ await waitUntil(() => promptButton.textContent.includes("Enable"));
+
+ checkTelemetryEvents(
+ [
+ {
+ method: "update_conn_prompt",
+ extras: { prompt_enabled: "false", runtime_id: telemetryRuntimeId },
+ },
+ ],
+ sessionId
+ );
+
+ info("Click on the toggle button again and check we log the correct value");
+ promptButton.click();
+ await waitUntil(() => promptButton.textContent.includes("Disable"));
+
+ checkTelemetryEvents(
+ [
+ {
+ method: "update_conn_prompt",
+ extras: { prompt_enabled: "true", runtime_id: telemetryRuntimeId },
+ },
+ ],
+ sessionId
+ );
+
+ info("Open the profiler dialog");
+ await openProfilerDialog(usbClient, document);
+
+ checkTelemetryEvents(
+ [
+ {
+ method: "show_profiler",
+ extras: { runtime_id: telemetryRuntimeId },
+ },
+ ],
+ sessionId
+ );
+
+ info("Remove runtime");
+ mocks.removeRuntime(RUNTIME_ID);
+ mocks.emitUSBUpdate();
+ await waitUntil(() => !findSidebarItemByText(RUNTIME_NAME, document));
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_connected_details.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_connected_details.js
new file mode 100644
index 0000000000..808cfdcae8
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_connected_details.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-telemetry.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-telemetry.js",
+ this
+);
+
+const REMOTE_RUNTIME_ID = "remote-runtime";
+const REMOTE_RUNTIME = "Remote Runtime";
+const REMOTE_DEVICE = "Remote Device";
+
+const REMOTE_VERSION = "12.0a1";
+const REMOTE_OS = "SOME_OS";
+
+/**
+ * Runtime connected events will log additional extras about the runtime connection that
+ * was established.
+ */
+add_task(async function () {
+ const mocks = new Mocks();
+
+ const usbClient = mocks.createUSBRuntime(REMOTE_RUNTIME_ID, {
+ deviceName: REMOTE_DEVICE,
+ name: REMOTE_RUNTIME,
+ shortName: REMOTE_RUNTIME,
+ });
+ usbClient.getDeviceDescription = () => {
+ return {
+ os: REMOTE_OS,
+ version: REMOTE_VERSION,
+ };
+ };
+
+ const { document, tab } = await openAboutDebugging();
+
+ mocks.emitUSBUpdate();
+ await connectToRuntime(REMOTE_DEVICE, document);
+ const evts = readAboutDebuggingEvents().filter(
+ e => e.method === "runtime_connected"
+ );
+
+ is(
+ evts.length,
+ 1,
+ "runtime_connected event logged when connecting to remote runtime"
+ );
+ const {
+ connection_type,
+ device_name,
+ runtime_name,
+ runtime_os,
+ runtime_version,
+ } = evts[0].extras;
+ is(connection_type, "usb", "Expected value for `connection_type` extra");
+ is(device_name, REMOTE_DEVICE, "Expected value for `device_name` extra");
+ is(runtime_name, REMOTE_RUNTIME, "Expected value for `runtime_name` extra");
+ is(runtime_os, REMOTE_OS, "Expected value for `runtime_os` extra");
+ is(
+ runtime_version,
+ REMOTE_VERSION,
+ "Expected value for `runtime_version` extra"
+ );
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_updates.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_updates.js
new file mode 100644
index 0000000000..51934550a8
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_updates.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-telemetry.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-telemetry.js",
+ this
+);
+
+const DEVICE_A = "Device A";
+const USB_RUNTIME_1 = {
+ id: "runtime-id-1",
+ deviceName: DEVICE_A,
+ name: "Runtime 1",
+ shortName: "R1",
+};
+
+const USB_RUNTIME_2 = {
+ id: "runtime-id-2",
+ deviceName: DEVICE_A,
+ name: "Runtime 2",
+ shortName: "R2",
+};
+
+const DEVICE_A_EXTRAS = {
+ connection_type: "usb",
+ device_name: DEVICE_A,
+};
+
+const RUNTIME_1_EXTRAS = {
+ connection_type: "usb",
+ device_name: USB_RUNTIME_1.deviceName,
+ runtime_name: USB_RUNTIME_1.shortName,
+};
+
+const RUNTIME_2_EXTRAS = {
+ connection_type: "usb",
+ device_name: USB_RUNTIME_2.deviceName,
+ runtime_name: USB_RUNTIME_2.shortName,
+};
+
+/**
+ * Check that telemetry events are recorded for USB runtimes when:
+ * - adding a device/runtime
+ * - removing a device/runtime
+ * - connecting to a runtime
+ */
+add_task(async function testUsbRuntimeUpdates() {
+ // enable USB devices mocks
+ const mocks = new Mocks();
+ setupTelemetryTest();
+
+ const { tab, document } = await openAboutDebugging();
+
+ const sessionId = getOpenEventSessionId();
+ ok(!isNaN(sessionId), "Open event has a valid session id");
+
+ await addUsbRuntime(USB_RUNTIME_1, mocks, document);
+
+ let evts = checkTelemetryEvents(
+ [
+ { method: "device_added", extras: DEVICE_A_EXTRAS },
+ { method: "runtime_added", extras: RUNTIME_1_EXTRAS },
+ ],
+ sessionId
+ );
+
+ // Now that a first telemetry event has been logged for RUNTIME_1, retrieve the id
+ // generated for telemetry, and check that we keep logging the same id for all events
+ // related to runtime 1.
+ const runtime1Id = evts.filter(e => e.method === "runtime_added")[0].extras
+ .runtime_id;
+ const runtime1Extras = Object.assign({}, RUNTIME_1_EXTRAS, {
+ runtime_id: runtime1Id,
+ });
+ // Same as runtime1Extras, but the runtime name should be the complete one.
+ const runtime1ConnectedExtras = Object.assign({}, runtime1Extras, {
+ runtime_name: USB_RUNTIME_1.name,
+ });
+
+ await connectToRuntime(USB_RUNTIME_1.deviceName, document);
+
+ checkTelemetryEvents(
+ [
+ { method: "runtime_connected", extras: runtime1ConnectedExtras },
+ { method: "connection_attempt", extras: { status: "start" } },
+ { method: "connection_attempt", extras: { status: "success" } },
+ ],
+ sessionId
+ );
+
+ info("Add a second runtime");
+ await addUsbRuntime(USB_RUNTIME_2, mocks, document);
+ evts = checkTelemetryEvents(
+ [{ method: "runtime_added", extras: RUNTIME_2_EXTRAS }],
+ sessionId
+ );
+
+ // Similar to what we did for RUNTIME_1,w e want to check we reuse the same telemetry id
+ // for all the events related to RUNTIME_2.
+ const runtime2Id = evts.filter(e => e.method === "runtime_added")[0].extras
+ .runtime_id;
+ const runtime2Extras = Object.assign({}, RUNTIME_2_EXTRAS, {
+ runtime_id: runtime2Id,
+ });
+
+ info("Remove runtime 1");
+ await removeUsbRuntime(USB_RUNTIME_1, mocks, document);
+
+ checkTelemetryEvents(
+ [
+ { method: "runtime_disconnected", extras: runtime1ConnectedExtras },
+ { method: "runtime_removed", extras: runtime1Extras },
+ ],
+ sessionId
+ );
+
+ info("Remove runtime 2");
+ await removeUsbRuntime(USB_RUNTIME_2, mocks, document);
+
+ checkTelemetryEvents(
+ [
+ { method: "runtime_removed", extras: runtime2Extras },
+ { method: "device_removed", extras: DEVICE_A_EXTRAS },
+ ],
+ sessionId
+ );
+
+ await removeTab(tab);
+});
+
+async function addUsbRuntime(runtime, mocks, doc) {
+ mocks.createUSBRuntime(runtime.id, {
+ deviceName: runtime.deviceName,
+ name: runtime.name,
+ shortName: runtime.shortName,
+ });
+ mocks.emitUSBUpdate();
+
+ info("Wait for the runtime to appear in the sidebar");
+ await waitUntil(() => findSidebarItemByText(runtime.shortName, doc));
+}
+
+async function removeUsbRuntime(runtime, mocks, doc) {
+ mocks.removeRuntime(runtime.id);
+ mocks.emitUSBUpdate();
+ await waitUntil(
+ () =>
+ !findSidebarItemByText(runtime.name, doc) &&
+ !findSidebarItemByText(runtime.shortName, doc)
+ );
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_updates_multi.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_updates_multi.js
new file mode 100644
index 0000000000..707ddf4621
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_updates_multi.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-telemetry.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-telemetry.js",
+ this
+);
+
+const DEVICE_A = "Device A";
+const USB_RUNTIME_1 = {
+ id: "runtime-id-1",
+ deviceName: DEVICE_A,
+ name: "Runtime 1",
+ shortName: "R1",
+};
+
+const USB_RUNTIME_2 = {
+ id: "runtime-id-2",
+ deviceName: DEVICE_A,
+ name: "Runtime 2",
+ shortName: "R2",
+};
+
+const DEVICE_A_EXTRAS = {
+ connection_type: "usb",
+ device_name: DEVICE_A,
+};
+
+const RUNTIME_1_EXTRAS = {
+ connection_type: "usb",
+ device_name: USB_RUNTIME_1.deviceName,
+ runtime_name: USB_RUNTIME_1.shortName,
+};
+
+const RUNTIME_2_EXTRAS = {
+ connection_type: "usb",
+ device_name: USB_RUNTIME_2.deviceName,
+ runtime_name: USB_RUNTIME_2.shortName,
+};
+
+/**
+ * Test runtime update events when a device is connected/disconnected with multiple
+ * runtimes available on the same device.
+ */
+add_task(async function () {
+ // enable USB devices mocks
+ const mocks = new Mocks();
+ setupTelemetryTest();
+
+ const { tab, document } = await openAboutDebugging();
+
+ const sessionId = getOpenEventSessionId();
+ ok(!isNaN(sessionId), "Open event has a valid session id");
+
+ info("Add two runtimes on the same device at the same time");
+ mocks.createUSBRuntime(USB_RUNTIME_1.id, {
+ deviceName: USB_RUNTIME_1.deviceName,
+ name: USB_RUNTIME_1.name,
+ shortName: USB_RUNTIME_1.shortName,
+ });
+ mocks.createUSBRuntime(USB_RUNTIME_2.id, {
+ deviceName: USB_RUNTIME_2.deviceName,
+ name: USB_RUNTIME_2.name,
+ shortName: USB_RUNTIME_2.shortName,
+ });
+ mocks.emitUSBUpdate();
+ await waitUntil(() =>
+ findSidebarItemByText(USB_RUNTIME_1.shortName, document)
+ );
+ await waitUntil(() =>
+ findSidebarItemByText(USB_RUNTIME_2.shortName, document)
+ );
+
+ checkTelemetryEvents(
+ [
+ { method: "device_added", extras: DEVICE_A_EXTRAS },
+ { method: "runtime_added", extras: RUNTIME_1_EXTRAS },
+ { method: "runtime_added", extras: RUNTIME_2_EXTRAS },
+ ],
+ sessionId
+ );
+
+ info("Remove both runtimes at once to simulate a device disconnection");
+ mocks.removeRuntime(USB_RUNTIME_1.id);
+ mocks.removeRuntime(USB_RUNTIME_2.id);
+ mocks.emitUSBUpdate();
+ await waitUntil(
+ () =>
+ !findSidebarItemByText(USB_RUNTIME_1.name, document) &&
+ !findSidebarItemByText(USB_RUNTIME_1.shortName, document)
+ );
+ await waitUntil(
+ () =>
+ !findSidebarItemByText(USB_RUNTIME_2.name, document) &&
+ !findSidebarItemByText(USB_RUNTIME_2.shortName, document)
+ );
+
+ checkTelemetryEvents(
+ [
+ { method: "runtime_removed", extras: RUNTIME_1_EXTRAS },
+ { method: "runtime_removed", extras: RUNTIME_2_EXTRAS },
+ { method: "device_removed", extras: DEVICE_A_EXTRAS },
+ ],
+ sessionId
+ );
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_updates_network.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_updates_network.js
new file mode 100644
index 0000000000..d33810dc59
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_telemetry_runtime_updates_network.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-telemetry.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "helper-telemetry.js",
+ this
+);
+
+const NETWORK_RUNTIME = {
+ host: "localhost:1234",
+ // No device name for network runtimes.
+ name: "Local Network Runtime",
+};
+
+/**
+ * Test runtime update events for network runtimes.
+ */
+add_task(async function testNetworkRuntimeUpdates() {
+ // enable USB devices mocks
+ const mocks = new Mocks();
+ setupTelemetryTest();
+
+ const { tab, document } = await openAboutDebugging();
+
+ const sessionId = getOpenEventSessionId();
+ ok(!isNaN(sessionId), "Open event has a valid session id");
+
+ info("Add a network runtime");
+ await addNetworkRuntime(NETWORK_RUNTIME, mocks, document);
+
+ // Before the connection, we don't have any information about the runtime.
+ // Device information is also not available to network runtimes.
+ const networkRuntimeExtras = {
+ connection_type: "network",
+ device_name: "",
+ runtime_name: "",
+ };
+
+ // Once connected we should be able to log a valid runtime name.
+ const connectedNetworkRuntimeExtras = Object.assign(
+ {},
+ networkRuntimeExtras,
+ {
+ runtime_name: NETWORK_RUNTIME.name,
+ }
+ );
+
+ // For network runtimes, we don't have any device information, so we shouldn't have any
+ // device_added event.
+ checkTelemetryEvents(
+ [{ method: "runtime_added", extras: networkRuntimeExtras }],
+ sessionId
+ );
+
+ await connectToRuntime(NETWORK_RUNTIME.host, document);
+ checkTelemetryEvents(
+ [
+ { method: "runtime_connected", extras: connectedNetworkRuntimeExtras },
+ { method: "connection_attempt", extras: { status: "start" } },
+ { method: "connection_attempt", extras: { status: "success" } },
+ ],
+ sessionId
+ );
+
+ info("Remove network runtime");
+ mocks.removeRuntime(NETWORK_RUNTIME.host);
+ await waitUntil(() => !findSidebarItemByText(NETWORK_RUNTIME.host, document));
+ // Similarly we should not have any device removed event.
+ checkTelemetryEvents(
+ [
+ { method: "runtime_disconnected", extras: connectedNetworkRuntimeExtras },
+ { method: "runtime_removed", extras: networkRuntimeExtras },
+ ],
+ sessionId
+ );
+
+ await removeTab(tab);
+});
+
+async function addNetworkRuntime(runtime, mocks, doc) {
+ mocks.createNetworkRuntime(runtime.host, {
+ name: runtime.name,
+ });
+
+ info("Wait for the Network Runtime to appear in the sidebar");
+ await waitUntil(() => findSidebarItemByText(runtime.host, doc));
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_thisfirefox.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_thisfirefox.js
new file mode 100644
index 0000000000..3f673df758
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_thisfirefox.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const EXPECTED_TARGET_PANES = [
+ "Tabs",
+ "Temporary Extensions",
+ "Extensions",
+ "Service Workers",
+ "Shared Workers",
+ "Other Workers",
+];
+
+/**
+ * Check that the This Firefox runtime page contains the expected categories if
+ * the preference to enable local tab debugging is true.
+ */
+add_task(async function testThisFirefoxWithLocalTab() {
+ const { document, tab, window } = await openAboutDebugging({
+ enableLocalTabs: true,
+ });
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ // Expect all target panes to be displayed including tabs.
+ await checkThisFirefoxTargetPanes(document, EXPECTED_TARGET_PANES);
+
+ await removeTab(tab);
+});
+
+/**
+ * Check that the This Firefox runtime page contains the expected categories if
+ * the preference to enable local tab debugging is false.
+ */
+add_task(async function testThisFirefoxWithoutLocalTab() {
+ const { document, tab, window } = await openAboutDebugging({
+ enableLocalTabs: false,
+ });
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ // Expect all target panes but tabs to be displayed.
+ const expectedTargetPanesWithoutTabs = EXPECTED_TARGET_PANES.filter(
+ p => p !== "Tabs"
+ );
+ await checkThisFirefoxTargetPanes(document, expectedTargetPanesWithoutTabs);
+
+ await removeTab(tab);
+});
+
+/**
+ * Check that the tab which is discarded keeps the state after open the aboutdebugging.
+ */
+add_task(async function testThisFirefoxKeepDiscardedTab() {
+ const targetTab = await addTab("https://example.com/");
+ const blankTab = await addTab("about:blank");
+ targetTab.ownerGlobal.gBrowser.discardBrowser(targetTab);
+
+ const { document, tab, window } = await openAboutDebugging({
+ enableLocalTabs: false,
+ });
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ ok(!targetTab.linkedPanel, "The target tab is still discarded");
+
+ await removeTab(blankTab);
+ await removeTab(targetTab);
+ await removeTab(tab);
+});
+
+/**
+ * Check that the Temporary Extensions is hidden if "xpinstall.enabled" is set to false.
+ */
+add_task(async function testThisFirefoxWithXpinstallDisabled() {
+ await pushPref("xpinstall.enabled", false);
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ // Expect all target panes but temporary extensions to be displayed.
+ const expectedTargetPanesWithXpinstallDisabled = EXPECTED_TARGET_PANES.filter(
+ p => p !== "Temporary Extensions"
+ );
+ await checkThisFirefoxTargetPanes(
+ document,
+ expectedTargetPanesWithXpinstallDisabled
+ );
+
+ await removeTab(tab);
+});
+
+async function checkThisFirefoxTargetPanes(doc, expectedTargetPanes) {
+ const win = doc.ownerGlobal;
+ // Check that the selected sidebar item is "This Firefox"/"This Nightly"/...
+ const selectedSidebarItem = doc.querySelector(".qa-sidebar-item-selected");
+ ok(selectedSidebarItem, "An item is selected in the sidebar");
+
+ const thisFirefoxString = getThisFirefoxString(win);
+ is(
+ selectedSidebarItem.textContent,
+ thisFirefoxString,
+ "The selected sidebar item is " + thisFirefoxString
+ );
+
+ const paneTitlesEls = doc.querySelectorAll(".qa-debug-target-pane-title");
+ is(
+ paneTitlesEls.length,
+ expectedTargetPanes.length,
+ "This Firefox has the expected number of debug target categories"
+ );
+
+ const paneTitles = [...paneTitlesEls].map(el => el.textContent);
+
+ for (let i = 0; i < expectedTargetPanes.length; i++) {
+ const expectedPaneTitle = expectedTargetPanes[i];
+ const actualPaneTitle = paneTitles[i];
+ ok(
+ actualPaneTitle.startsWith(expectedPaneTitle),
+ `Expected debug target category found: ${expectedPaneTitle}`
+ );
+ }
+}
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_thisfirefox_runtime_info.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_thisfirefox_runtime_info.js
new file mode 100644
index 0000000000..2475994fed
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_thisfirefox_runtime_info.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that the runtime info is correctly displayed for ThisFirefox.
+ * Also acts as basic sanity check for the default mock of the this-firefox client.
+ */
+
+add_task(async function () {
+ // Setup a mock for our runtime client factory to return the default THIS_FIREFOX client
+ // when the client for the this-firefox runtime is requested.
+ const runtimeClientFactoryMock = createRuntimeClientFactoryMock();
+ const thisFirefoxClient = createThisFirefoxClientMock();
+ runtimeClientFactoryMock.createClientForRuntime = runtime => {
+ const {
+ RUNTIMES,
+ } = require("resource://devtools/client/aboutdebugging/src/constants.js");
+ if (runtime.id === RUNTIMES.THIS_FIREFOX) {
+ return thisFirefoxClient;
+ }
+ throw new Error("Unexpected runtime id " + runtime.id);
+ };
+
+ info("Enable mocks");
+ enableRuntimeClientFactoryMock(runtimeClientFactoryMock);
+ registerCleanupFunction(() => {
+ disableRuntimeClientFactoryMock();
+ });
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ info("Check that the 'This Firefox' mock is properly displayed");
+ const thisFirefoxRuntimeInfo = document.querySelector(".qa-runtime-name");
+ ok(
+ thisFirefoxRuntimeInfo,
+ "Runtime info for this-firefox runtime is displayed"
+ );
+ const runtimeInfoText = thisFirefoxRuntimeInfo.textContent;
+ ok(
+ runtimeInfoText.includes("Firefox"),
+ "this-firefox runtime info shows the correct runtime name: " +
+ runtimeInfoText
+ );
+ ok(
+ runtimeInfoText.includes("63.0"),
+ "this-firefox runtime info shows the correct version number: " +
+ runtimeInfoText
+ );
+
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_thisfirefox_worker_inspection.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_thisfirefox_worker_inspection.js
new file mode 100644
index 0000000000..4d02c73b1b
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_thisfirefox_worker_inspection.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ const thisFirefoxClient = createThisFirefoxClientMock();
+ // Prepare a worker mock.
+ const testWorker = {
+ id: "test-worker-id",
+ name: "Test Worker",
+ };
+ // Add a worker mock as other worker.
+ thisFirefoxClient.listWorkers = () => ({
+ otherWorkers: [testWorker],
+ serviceWorkers: [],
+ sharedWorkers: [],
+ });
+ thisFirefoxClient.client.mainRoot = {
+ getWorker: id => {
+ return id === testWorker.id ? testWorker : null;
+ },
+ };
+
+ const runtimeClientFactoryMock = createRuntimeClientFactoryMock();
+ runtimeClientFactoryMock.createClientForRuntime = runtime => {
+ const {
+ RUNTIMES,
+ } = require("resource://devtools/client/aboutdebugging/src/constants.js");
+ if (runtime.id === RUNTIMES.THIS_FIREFOX) {
+ return thisFirefoxClient;
+ }
+ throw new Error("Unexpected runtime id " + runtime.id);
+ };
+
+ info("Enable mocks");
+ enableRuntimeClientFactoryMock(runtimeClientFactoryMock);
+ registerCleanupFunction(() => {
+ disableRuntimeClientFactoryMock();
+ });
+
+ const { document, tab, window } = await openAboutDebugging();
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ info("Open a toolbox to debug the worker");
+ const { devtoolsTab, devtoolsWindow } = await openAboutDevtoolsToolbox(
+ document,
+ tab,
+ window,
+ testWorker.name,
+ false
+ );
+
+ info(
+ "Check whether the correct actor front will be opened in worker toolbox"
+ );
+ const url = new window.URL(devtoolsWindow.location.href);
+ const workerID = url.searchParams.get("id");
+ is(
+ workerID,
+ testWorker.id,
+ "Correct actor front will be opened in worker toolbox"
+ );
+
+ await removeTab(devtoolsTab);
+ await waitUntil(() => !findDebugTargetByText("Toolbox - ", document));
+ await removeTab(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_workers_remote_runtime.js b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_workers_remote_runtime.js
new file mode 100644
index 0000000000..3e0ccf1532
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/browser_aboutdebugging_workers_remote_runtime.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const NETWORK_RUNTIME_HOST = "localhost:6080";
+const NETWORK_RUNTIME_APP_NAME = "TestNetworkApp";
+const USB_RUNTIME_ID = "test-runtime-id";
+const USB_RUNTIME_DEVICE_NAME = "test device name";
+const USB_RUNTIME_APP_NAME = "TestUsbApp";
+
+const TESTS = [
+ {
+ category: "Other Workers",
+ propertyName: "otherWorkers",
+ workerName: "other/worker/script.js",
+ },
+ {
+ category: "Service Workers",
+ propertyName: "serviceWorkers",
+ workerName: "service/worker/script.js",
+ },
+ {
+ category: "Shared Workers",
+ propertyName: "sharedWorkers",
+ workerName: "shared/worker/script.js",
+ },
+];
+
+const EMPTY_WORKERS_RESPONSE = {
+ otherWorkers: [],
+ serviceWorkers: [],
+ sharedWorkers: [],
+};
+
+// Test that workers are displayed and updated for remote runtimes when expected.
+add_task(async function () {
+ const mocks = new Mocks();
+
+ const { document, tab, window } = await openAboutDebugging({
+ enableWorkerUpdates: true,
+ });
+ await selectThisFirefoxPage(document, window.AboutDebugging.store);
+
+ info("Prepare USB client mock");
+ const usbClient = mocks.createUSBRuntime(USB_RUNTIME_ID, {
+ deviceName: USB_RUNTIME_DEVICE_NAME,
+ name: USB_RUNTIME_APP_NAME,
+ });
+ mocks.emitUSBUpdate();
+
+ info("Test addons in runtime page for USB client");
+ await connectToRuntime(USB_RUNTIME_DEVICE_NAME, document);
+ await selectRuntime(USB_RUNTIME_DEVICE_NAME, USB_RUNTIME_APP_NAME, document);
+ for (const testData of TESTS) {
+ await testWorkerOnMockedRemoteClient(
+ testData,
+ usbClient,
+ mocks.thisFirefoxClient,
+ document
+ );
+ }
+
+ info("Prepare Network client mock");
+ const networkClient = mocks.createNetworkRuntime(NETWORK_RUNTIME_HOST, {
+ name: NETWORK_RUNTIME_APP_NAME,
+ });
+
+ info("Test workers in runtime page for Network client");
+ await connectToRuntime(NETWORK_RUNTIME_HOST, document);
+ await selectRuntime(NETWORK_RUNTIME_HOST, NETWORK_RUNTIME_APP_NAME, document);
+
+ for (const testData of TESTS) {
+ await testWorkerOnMockedRemoteClient(
+ testData,
+ networkClient,
+ mocks.thisFirefoxClient,
+ document
+ );
+ }
+
+ await removeTab(tab);
+});
+
+/**
+ * Check that workers are visible in the runtime page for a remote client.
+ */
+async function testWorkerOnMockedRemoteClient(
+ testData,
+ remoteClient,
+ firefoxClient,
+ document
+) {
+ const { category, propertyName, workerName } = testData;
+ info(`Test workers for category [${category}] in remote runtime`);
+
+ const workersPane = getDebugTargetPane(category, document);
+ info("Check an empty target pane message is displayed");
+ ok(
+ workersPane.querySelector(".qa-debug-target-list-empty"),
+ "Workers list is empty"
+ );
+
+ info(`Add a worker of type [${propertyName}] to the remote client`);
+ const workers = Object.assign({}, EMPTY_WORKERS_RESPONSE, {
+ [propertyName]: [
+ {
+ name: workerName,
+ workerDescriptorFront: {
+ actorID: workerName,
+ },
+ },
+ ],
+ });
+ remoteClient.listWorkers = () => workers;
+ remoteClient._eventEmitter.emit("workersUpdated");
+
+ info("Wait until the worker appears");
+ await waitUntil(
+ () => !workersPane.querySelector(".qa-debug-target-list-empty")
+ );
+
+ const workerTarget = findDebugTargetByText(workerName, document);
+ ok(workerTarget, "Worker target appeared for the remote runtime");
+
+ // Check that the list of REMOTE workers are NOT updated when the local this-firefox
+ // emits a workersUpdated event.
+ info("Remove the worker from the remote client WITHOUT sending an event");
+ remoteClient.listWorkers = () => EMPTY_WORKERS_RESPONSE;
+
+ info("Simulate a worker update on the ThisFirefox client");
+ firefoxClient._eventEmitter.emit("workersUpdated");
+
+ // To avoid wait for a set period of time we trigger another async update, adding a new
+ // tab. We assume that if the worker update mechanism had started, it would also be done
+ // when the new tab was processed.
+ info("Wait until the tab target for 'http://some.random/url.com' appears");
+ const testTab = {
+ retrieveFavicon: () => {},
+ outerWindowID: 0,
+ traits: {},
+ url: "http://some.random/url.com",
+ };
+ remoteClient.listTabs = () => [testTab];
+ remoteClient._eventEmitter.emit("tabListChanged");
+ await waitUntil(() =>
+ findDebugTargetByText("http://some.random/url.com", document)
+ );
+
+ ok(
+ findDebugTargetByText(workerName, document),
+ "The test worker is still visible"
+ );
+
+ info(
+ "Emit `workersUpdated` on remoteClient and wait for the target list to update"
+ );
+ remoteClient._eventEmitter.emit("workersUpdated");
+ await waitUntil(() => !findDebugTargetByText(workerName, document));
+}
diff --git a/devtools/client/aboutdebugging/test/browser/empty.html b/devtools/client/aboutdebugging/test/browser/empty.html
new file mode 100644
index 0000000000..d0fd95ab08
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/empty.html
@@ -0,0 +1 @@
+<body>fake_profiler_page</body>
diff --git a/devtools/client/aboutdebugging/test/browser/head.js b/devtools/client/aboutdebugging/test/browser/head.js
new file mode 100644
index 0000000000..3c32e0b87b
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/head.js
@@ -0,0 +1,505 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint-env browser */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+/* import-globals-from helper-mocks.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-mocks.js", this);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/shared-head.js",
+ this
+);
+
+// Make sure the ADB addon is removed and ADB is stopped when the test ends.
+registerCleanupFunction(async function () {
+ // Reset the selected tool in case we opened about:devtools-toolbox to
+ // avoid side effects between tests.
+ Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
+
+ try {
+ const {
+ adbAddon,
+ } = require("resource://devtools/client/shared/remote-debugging/adb/adb-addon.js");
+ await adbAddon.uninstall();
+ } catch (e) {
+ // Will throw if the addon is already uninstalled, ignore exceptions here.
+ }
+ const {
+ adbProcess,
+ } = require("resource://devtools/client/shared/remote-debugging/adb/adb-process.js");
+ await adbProcess.kill();
+
+ const {
+ remoteClientManager,
+ } = require("resource://devtools/client/shared/remote-debugging/remote-client-manager.js");
+ await remoteClientManager.removeAllClients();
+});
+
+async function openAboutDebugging({
+ enableWorkerUpdates,
+ enableLocalTabs = true,
+} = {}) {
+ if (!enableWorkerUpdates) {
+ silenceWorkerUpdates();
+ }
+
+ // This preference changes value depending on the build type, tests need to use a
+ // consistent value regarless of the build used.
+ await pushPref(
+ "devtools.aboutdebugging.local-tab-debugging",
+ enableLocalTabs
+ );
+
+ info("opening about:debugging");
+
+ const tab = await addTab("about:debugging");
+ const browser = tab.linkedBrowser;
+ const document = browser.contentDocument;
+ const window = browser.contentWindow;
+
+ info("Wait until Connect page is displayed");
+ await waitUntil(() => document.querySelector(".qa-connect-page"));
+
+ return { tab, document, window };
+}
+
+async function openAboutDevtoolsToolbox(
+ doc,
+ tab,
+ win,
+ targetText = "about:debugging",
+ shouldWaitToolboxReady = true
+) {
+ info("Open about:devtools-toolbox page");
+
+ info("Wait for the target to appear: " + targetText);
+ await waitUntil(() => findDebugTargetByText(targetText, doc));
+
+ const target = findDebugTargetByText(targetText, doc);
+ ok(target, `${targetText} target appeared`);
+
+ const {
+ DEBUG_TARGETS,
+ } = require("resource://devtools/client/aboutdebugging/src/constants.js");
+ const isWebExtension = target.dataset.qaTargetType == DEBUG_TARGETS.EXTENSION;
+
+ const inspectButton = target.querySelector(".qa-debug-target-inspect-button");
+ ok(inspectButton, `Inspect button for ${targetText} appeared`);
+ inspectButton.click();
+ const onToolboxReady = gDevTools.once("toolbox-ready");
+ await Promise.all([
+ waitForAboutDebuggingRequests(win.AboutDebugging.store),
+ shouldWaitToolboxReady ? onToolboxReady : Promise.resolve(),
+ ]);
+
+ // WebExtension open a toolbox in a dedicated window
+ if (isWebExtension) {
+ const toolbox = await onToolboxReady;
+ // For some reason the test helpers prevents the toolbox from being automatically focused on opening,
+ // whereas it is IRL.
+ const focusedWin = Services.focus.focusedWindow;
+ if (focusedWin?.top != toolbox.win) {
+ info("Wait for the toolbox window to be focused");
+ await new Promise(r => {
+ // focus event only fired on the chrome event handler and in capture phase
+ toolbox.win.docShell.chromeEventHandler.addEventListener("focus", r, {
+ once: true,
+ capture: true,
+ });
+ toolbox.win.focus();
+ });
+ info("The toolbox is focused");
+ }
+ return {
+ devtoolsBrowser: null,
+ devtoolsDocument: toolbox.doc,
+ devtoolsTab: null,
+ devtoolsWindow: toolbox.win,
+ };
+ }
+
+ await waitUntil(() => tab.nextElementSibling);
+
+ info("Wait for about:devtools-toolbox tab will be selected");
+ const devtoolsTab = tab.nextElementSibling;
+ await waitUntil(() => gBrowser.selectedTab === devtoolsTab);
+ const devtoolsBrowser = gBrowser.selectedBrowser;
+ await waitUntil(() =>
+ devtoolsBrowser.contentWindow.location.href.startsWith(
+ "about:devtools-toolbox?"
+ )
+ );
+
+ if (!shouldWaitToolboxReady) {
+ // Wait for show error page.
+ await waitUntil(() =>
+ devtoolsBrowser.contentDocument.querySelector(".qa-error-page")
+ );
+ }
+
+ return {
+ devtoolsBrowser,
+ devtoolsDocument: devtoolsBrowser.contentDocument,
+ devtoolsTab,
+ devtoolsWindow: devtoolsBrowser.contentWindow,
+ };
+}
+
+async function closeAboutDevtoolsToolbox(
+ aboutDebuggingDocument,
+ devtoolsTab,
+ win
+) {
+ // Wait for all requests to settle on the opened about:devtools toolbox.
+ const devtoolsBrowser = devtoolsTab.linkedBrowser;
+ const devtoolsWindow = devtoolsBrowser.contentWindow;
+ const toolbox = getToolbox(devtoolsWindow);
+ await toolbox.commands.client.waitForRequestsToSettle();
+
+ info("Close about:devtools-toolbox page");
+ const onToolboxDestroyed = gDevTools.once("toolbox-destroyed");
+
+ info("Wait for removeTab");
+ await removeTab(devtoolsTab);
+
+ info("Wait for toolbox destroyed");
+ await onToolboxDestroyed;
+
+ // Changing the tab will also trigger a request to list tabs, so wait until the selected
+ // tab has changed to wait for requests to settle.
+ info("Wait until aboutdebugging is selected");
+ await waitUntil(() => gBrowser.selectedTab !== devtoolsTab);
+
+ // Wait for removing about:devtools-toolbox tab info from about:debugging.
+ info("Wait until about:devtools-toolbox is removed from debug targets");
+ await waitUntil(
+ () => !findDebugTargetByText("Toolbox - ", aboutDebuggingDocument)
+ );
+
+ await waitForAboutDebuggingRequests(win.AboutDebugging.store);
+}
+
+async function closeWebExtAboutDevtoolsToolbox(devtoolsWindow, win) {
+ // Wait for all requests to settle on the opened about:devtools toolbox.
+ const toolbox = getToolbox(devtoolsWindow);
+ await toolbox.commands.client.waitForRequestsToSettle();
+
+ info("Close the toolbox and wait for its destruction");
+ await toolbox.destroy();
+
+ await waitForAboutDebuggingRequests(win.AboutDebugging.store);
+}
+
+async function reloadAboutDebugging(tab) {
+ info("reload about:debugging");
+
+ await reloadBrowser(tab.linkedBrowser);
+ const browser = tab.linkedBrowser;
+ const document = browser.contentDocument;
+ const window = browser.contentWindow;
+ info("wait for the initial about:debugging requests to settle");
+ await waitForAboutDebuggingRequests(window.AboutDebugging.store);
+
+ return document;
+}
+
+// Wait for all about:debugging target request actions to succeed.
+// They will typically be triggered after watching a new runtime or loading
+// about:debugging.
+function waitForRequestsSuccess(store) {
+ return Promise.all([
+ waitForDispatch(store, "REQUEST_EXTENSIONS_SUCCESS"),
+ waitForDispatch(store, "REQUEST_TABS_SUCCESS"),
+ waitForDispatch(store, "REQUEST_WORKERS_SUCCESS"),
+ ]);
+}
+
+/**
+ * Wait for all aboutdebugging REQUEST_*_SUCCESS actions to settle, meaning here
+ * that no new request has been dispatched after the provided delay.
+ */
+async function waitForAboutDebuggingRequests(store, delay = 500) {
+ let hasSettled = false;
+
+ // After each iteration of this while loop, we check is the timerPromise had the time
+ // to resolve or if we captured a REQUEST_*_SUCCESS action before.
+ while (!hasSettled) {
+ let timer;
+
+ // This timer will be executed only if no REQUEST_*_SUCCESS action is dispatched
+ // during the delay. We consider that when no request are received for some time, it
+ // means there are no ongoing requests anymore.
+ const timerPromise = new Promise(resolve => {
+ timer = setTimeout(() => {
+ hasSettled = true;
+ resolve();
+ }, delay);
+ });
+
+ // Wait either for a REQUEST_*_SUCCESS to be dispatched, or for the timer to resolve.
+ await Promise.race([
+ waitForDispatch(store, "REQUEST_EXTENSIONS_SUCCESS"),
+ waitForDispatch(store, "REQUEST_TABS_SUCCESS"),
+ waitForDispatch(store, "REQUEST_WORKERS_SUCCESS"),
+ timerPromise,
+ ]);
+
+ // Clear the timer to avoid setting hasSettled to true accidently unless timerPromise
+ // was the first to resolve.
+ clearTimeout(timer);
+ }
+}
+
+/**
+ * Navigate to "This Firefox"
+ */
+async function selectThisFirefoxPage(doc, store) {
+ info("Select This Firefox page");
+
+ const onRequestSuccess = waitForRequestsSuccess(store);
+ doc.location.hash = "#/runtime/this-firefox";
+ info("Wait for requests to be complete");
+ await onRequestSuccess;
+
+ info("Wait for runtime page to be rendered");
+ await waitUntil(() => doc.querySelector(".qa-runtime-page"));
+
+ // Navigating to this-firefox will trigger a title change for the
+ // about:debugging tab. This title change _might_ trigger a tablist update.
+ // If it does, we should make sure to wait for pending tab requests.
+ await waitForAboutDebuggingRequests(store);
+}
+
+/**
+ * Navigate to the Connect page. Resolves when the Connect page is rendered.
+ */
+async function selectConnectPage(doc) {
+ const sidebarItems = doc.querySelectorAll(".qa-sidebar-item");
+ const connectSidebarItem = [...sidebarItems].find(element => {
+ return element.textContent === "Setup";
+ });
+ ok(connectSidebarItem, "Sidebar contains a Connect item");
+ const connectLink = connectSidebarItem.querySelector(".qa-sidebar-link");
+ ok(connectLink, "Sidebar contains a Connect link");
+
+ info("Click on the Connect link in the sidebar");
+ connectLink.click();
+
+ info("Wait until Connect page is displayed");
+ await waitUntil(() => doc.querySelector(".qa-connect-page"));
+}
+
+function getDebugTargetPane(title, document) {
+ // removes the suffix "(<NUMBER>)" in debug target pane's title, if needed
+ const sanitizeTitle = x => {
+ return x.replace(/\s+\(\d+\)$/, "");
+ };
+
+ const targetTitle = sanitizeTitle(title);
+ for (const titleEl of document.querySelectorAll(
+ ".qa-debug-target-pane-title"
+ )) {
+ if (sanitizeTitle(titleEl.textContent) !== targetTitle) {
+ continue;
+ }
+
+ return titleEl.closest(".qa-debug-target-pane");
+ }
+
+ return null;
+}
+
+function findDebugTargetByText(text, document) {
+ const targets = [...document.querySelectorAll(".qa-debug-target-item")];
+ return targets.find(target => target.textContent.includes(text));
+}
+
+function findSidebarItemByText(text, document) {
+ const sidebarItems = document.querySelectorAll(".qa-sidebar-item");
+ return [...sidebarItems].find(element => {
+ return element.textContent.includes(text);
+ });
+}
+
+function findSidebarItemLinkByText(text, document) {
+ const links = document.querySelectorAll(".qa-sidebar-link");
+ return [...links].find(element => {
+ return element.textContent.includes(text);
+ });
+}
+
+async function connectToRuntime(deviceName, document) {
+ info(`Wait until the sidebar item for ${deviceName} appears`);
+ await waitUntil(() => findSidebarItemByText(deviceName, document));
+ const sidebarItem = findSidebarItemByText(deviceName, document);
+ const connectButton = sidebarItem.querySelector(".qa-connect-button");
+ ok(
+ connectButton,
+ `Connect button is displayed for the runtime ${deviceName}`
+ );
+
+ info("Click on the connect button and wait until it disappears");
+ connectButton.click();
+ await waitUntil(() => !sidebarItem.querySelector(".qa-connect-button"));
+}
+
+async function selectRuntime(deviceName, name, document) {
+ const sidebarItem = findSidebarItemByText(deviceName, document);
+ const store = document.defaultView.AboutDebugging.store;
+ const onSelectPageSuccess = waitForDispatch(store, "SELECT_PAGE_SUCCESS");
+
+ sidebarItem.querySelector(".qa-sidebar-link").click();
+
+ await waitUntil(() => {
+ const runtimeInfo = document.querySelector(".qa-runtime-name");
+ return runtimeInfo && runtimeInfo.textContent.includes(name);
+ });
+
+ info("Wait for SELECT_PAGE_SUCCESS to be dispatched");
+ await onSelectPageSuccess;
+}
+
+function getToolbox(win) {
+ return gDevTools.getToolboxes().find(toolbox => toolbox.win === win);
+}
+
+/**
+ * Open the performance profiler dialog. Assumes the client is a mocked remote runtime
+ * client.
+ */
+async function openProfilerDialog(client, doc) {
+ const onProfilerLoaded = new Promise(r => {
+ client.loadPerformanceProfiler = r;
+ });
+
+ info("Click on the Profile Runtime button");
+ const profileButton = doc.querySelector(".qa-profile-runtime-button");
+ profileButton.click();
+
+ info(
+ "Wait for the loadPerformanceProfiler callback to be executed on client-wrapper"
+ );
+ return onProfilerLoaded;
+}
+
+/**
+ * The "This Firefox" string depends on the brandShortName, which will be different
+ * depending on the channel where tests are running.
+ */
+function getThisFirefoxString(aboutDebuggingWindow) {
+ const loader = aboutDebuggingWindow.getBrowserLoaderForWindow();
+ const { l10n } = loader.require(
+ "resource://devtools/client/aboutdebugging/src/modules/l10n.js"
+ );
+ return l10n.getString("about-debugging-this-firefox-runtime-name");
+}
+
+function waitUntilUsbDeviceIsUnplugged(deviceName, aboutDebuggingDocument) {
+ info("Wait until the USB sidebar item appears as unplugged");
+ return waitUntil(() => {
+ const sidebarItem = findSidebarItemByText(
+ deviceName,
+ aboutDebuggingDocument
+ );
+ return !!sidebarItem.querySelector(".qa-runtime-item-unplugged");
+ });
+}
+
+/**
+ * Changing the selected tab in the current browser will trigger a tablist
+ * update.
+ * If the currently selected page is "this-firefox", we should wait for the
+ * the corresponding REQUEST_TABS_SUCCESS that will be triggered by the change.
+ *
+ * @param {Browser} browser
+ * The browser instance to update.
+ * @param {XULTab} tab
+ * The tab to select.
+ * @param {Object} store
+ * The about:debugging redux store.
+ */
+async function updateSelectedTab(browser, tab, store) {
+ info("Update the selected tab");
+
+ const { runtimes, ui } = store.getState();
+ const isOnThisFirefox =
+ runtimes.selectedRuntimeId === "this-firefox" &&
+ ui.selectedPage === "runtime";
+
+ // A tabs request will only be issued if we are on this-firefox.
+ const onTabsSuccess = isOnThisFirefox
+ ? waitForDispatch(store, "REQUEST_TABS_SUCCESS")
+ : null;
+
+ // Update the selected tab.
+ browser.selectedTab = tab;
+
+ if (onTabsSuccess) {
+ info("Wait for the tablist update after updating the selected tab");
+ await onTabsSuccess;
+ }
+}
+
+/**
+ * Synthesizes key input inside the DebugTargetInfo's URL component.
+ *
+ * @param {DevToolsToolbox} toolbox
+ * The DevToolsToolbox debugging the target.
+ * @param {HTMLElement} inputEl
+ * The <input> element to submit the URL with.
+ * @param {String} url
+ * The URL to navigate to.
+ */
+async function synthesizeUrlKeyInput(toolbox, inputEl, url) {
+ const { devtoolsDocument, devtoolsWindow } = toolbox;
+ info("Wait for URL input to be focused.");
+ const onInputFocused = waitUntil(
+ () => devtoolsDocument.activeElement === inputEl
+ );
+ inputEl.focus();
+ await onInputFocused;
+
+ info("Synthesize entering URL into text field");
+ const onInputChange = waitUntil(() => inputEl.value === url);
+ for (const key of url.split("")) {
+ EventUtils.synthesizeKey(key, {}, devtoolsWindow);
+ }
+ await onInputChange;
+
+ info("Submit URL to navigate to");
+ EventUtils.synthesizeKey("KEY_Enter");
+}
+
+/**
+ * Click on a given add-on widget button so that its browser actor is fired.
+ * Typically a popup would open, or a listener would be called in the background page.
+ *
+ * @param {String} addonId
+ * The ID of the add-on to click on.
+ */
+function clickOnAddonWidget(addonId) {
+ // Devtools are in another window and may have the focus.
+ // Ensure focusing the browser window when clicking on the widget.
+ const focusedWin = Services.focus.focusedWindow;
+ if (focusedWin != window) {
+ window.focus();
+ }
+ // Find the browserAction button that will show the webextension popup.
+ const widgetId = addonId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
+ const browserActionId = widgetId + "-browser-action";
+ const browserActionEl = window.document.getElementById(browserActionId);
+ ok(browserActionEl, "Got the browserAction button from the browser UI");
+
+ info("Show the web extension popup");
+ browserActionEl.firstElementChild.click();
+}
diff --git a/devtools/client/aboutdebugging/test/browser/helper-adb.js b/devtools/client/aboutdebugging/test/browser/helper-adb.js
new file mode 100644
index 0000000000..f5e6adfc6b
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/helper-adb.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head.js */
+
+async function checkAdbNotRunning() {
+ info("Check if ADB is already running before the test starts");
+ const {
+ check,
+ } = require("resource://devtools/client/shared/remote-debugging/adb/adb-running-checker.js");
+ const isAdbAlreadyRunning = await check();
+ if (isAdbAlreadyRunning) {
+ throw new Error(
+ "The ADB process is already running on this machine, it should be " +
+ "stopped before running this test"
+ );
+ }
+}
+/* exported checkAdbNotRunning */
+
+// Returns a promise that resolves when the adb process exists and is running.
+async function waitForAdbStart() {
+ info("Wait for ADB to start");
+ const {
+ adbProcess,
+ } = require("resource://devtools/client/shared/remote-debugging/adb/adb-process.js");
+ const {
+ check,
+ } = require("resource://devtools/client/shared/remote-debugging/adb/adb-running-checker.js");
+ return asyncWaitUntil(async () => {
+ const isProcessReady = adbProcess.ready;
+ const isRunning = await check();
+ return isProcessReady && isRunning;
+ });
+}
+/* exported waitForAdbStart */
+
+// Attempt to stop ADB. Will only work if ADB was started by the current Firefox instance.
+// Returns a promise that resolves when the adb process is no longer running.
+async function stopAdbProcess() {
+ info("Attempt to stop ADB");
+ const {
+ adbProcess,
+ } = require("resource://devtools/client/shared/remote-debugging/adb/adb-process.js");
+ await adbProcess.stop();
+
+ info("Wait for ADB to stop");
+ const {
+ check,
+ } = require("resource://devtools/client/shared/remote-debugging/adb/adb-running-checker.js");
+ return asyncWaitUntil(async () => {
+ const isProcessReady = adbProcess.ready;
+ const isRunning = await check();
+ return !isProcessReady && !isRunning;
+ });
+}
+/* exported stopAdbProcess */
diff --git a/devtools/client/aboutdebugging/test/browser/helper-addons.js b/devtools/client/aboutdebugging/test/browser/helper-addons.js
new file mode 100644
index 0000000000..e3a8be3761
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/helper-addons.js
@@ -0,0 +1,262 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head.js */
+
+function _getSupportsFile(path) {
+ const cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+ Ci.nsIChromeRegistry
+ );
+ const uri = Services.io.newURI(CHROME_URL_ROOT + path);
+ const fileurl = cr.convertChromeURL(uri);
+ return fileurl.QueryInterface(Ci.nsIFileURL);
+}
+
+async function enableExtensionDebugging() {
+ // Disable security prompt
+ await pushPref("devtools.debugger.prompt-connection", false);
+}
+/* exported enableExtensionDebugging */
+
+/**
+ * Install an extension using the AddonManager so it does not show up as temporary.
+ */
+async function installRegularExtension(pathOrFile) {
+ const isFile = typeof pathOrFile.isFile === "function" && pathOrFile.isFile();
+ const file = isFile ? pathOrFile : _getSupportsFile(pathOrFile).file;
+ const install = await AddonManager.getInstallForFile(file);
+ return new Promise((resolve, reject) => {
+ if (!install) {
+ throw new Error(`An install was not created for ${file.path}`);
+ }
+ install.addListener({
+ onDownloadFailed: reject,
+ onDownloadCancelled: reject,
+ onInstallFailed: reject,
+ onInstallCancelled: reject,
+ onInstallEnded: resolve,
+ });
+ install.install();
+ });
+}
+/* exported installRegularExtension */
+
+/**
+ * Install a temporary extension at the provided path, with the provided name.
+ * Will use a mock file picker to select the file.
+ */
+async function installTemporaryExtension(pathOrFile, name, document) {
+ const { Management } = ChromeUtils.importESModule(
+ "resource://gre/modules/Extension.sys.mjs"
+ );
+
+ info("Install temporary extension named " + name);
+ // Mock the file picker to select a test addon
+ prepareMockFilePicker(pathOrFile);
+
+ const onAddonInstalled = new Promise(done => {
+ Management.on("startup", function listener(event, extension) {
+ if (extension.name != name) {
+ return;
+ }
+
+ Management.off("startup", listener);
+ done(extension);
+ });
+ });
+
+ // Trigger the file picker by clicking on the button
+ document.querySelector(".qa-temporary-extension-install-button").click();
+
+ info("Wait for addon to be installed");
+ return onAddonInstalled;
+}
+/* exported installTemporaryExtension */
+
+function createTemporaryXPI(xpiData) {
+ const { ExtensionTestCommon } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionTestCommon.sys.mjs"
+ );
+
+ const { background, files, id, name, extraProperties } = xpiData;
+ info("Generate XPI file for " + id);
+
+ const manifest = Object.assign(
+ {},
+ {
+ browser_specific_settings: { gecko: { id } },
+ manifest_version: 2,
+ name,
+ version: "1.0",
+ },
+ extraProperties
+ );
+
+ const xpiFile = ExtensionTestCommon.generateXPI({
+ background,
+ files,
+ manifest,
+ });
+ registerCleanupFunction(() => xpiFile.exists() && xpiFile.remove(false));
+ return xpiFile;
+}
+/* exported createTemporaryXPI */
+
+/**
+ * Remove the existing temporary XPI file generated by ExtensionTestCommon and create a
+ * new one at the same location.
+ * @return {File} the temporary extension XPI file created
+ */
+function updateTemporaryXPI(xpiData, existingXPI) {
+ info("Delete and regenerate XPI for " + xpiData.id);
+
+ // Store the current name to check the xpi is correctly replaced.
+ const existingName = existingXPI.leafName;
+ info("Delete existing XPI named: " + existingName);
+ existingXPI.exists() && existingXPI.remove(false);
+
+ const xpiFile = createTemporaryXPI(xpiData);
+ // Check that the name of the new file is correct
+ if (xpiFile.leafName !== existingName) {
+ throw new Error(
+ "New XPI created with unexpected name: " + xpiFile.leafName
+ );
+ }
+ return xpiFile;
+}
+/* exported updateTemporaryXPI */
+
+/**
+ * Install a fake temporary extension by creating a temporary in-memory XPI file.
+ * @return {File} the temporary extension XPI file created
+ */
+async function installTemporaryExtensionFromXPI(xpiData, document) {
+ const xpiFile = createTemporaryXPI(xpiData);
+ const extension = await installTemporaryExtension(
+ xpiFile,
+ xpiData.name,
+ document
+ );
+
+ info("Wait until the addon debug target appears");
+ await waitUntil(() => findDebugTargetByText(xpiData.name, document));
+ return { extension, xpiFile };
+}
+/* exported installTemporaryExtensionFromXPI */
+
+async function removeTemporaryExtension(name, document) {
+ info(`Wait for removable extension with name: '${name}'`);
+ const buttonName = ".qa-temporary-extension-remove-button";
+ await waitUntil(() => {
+ const extension = findDebugTargetByText(name, document);
+ return extension && extension.querySelector(buttonName);
+ });
+ info(`Remove the temporary extension with name: '${name}'`);
+ const temporaryExtensionItem = findDebugTargetByText(name, document);
+ temporaryExtensionItem.querySelector(buttonName).click();
+
+ info("Wait until the debug target item disappears");
+ await waitUntil(() => !findDebugTargetByText(name, document));
+}
+/* exported removeTemporaryExtension */
+
+async function removeExtension(id, name, document) {
+ info(
+ "Retrieve the extension instance from the addon manager, and uninstall it"
+ );
+ const extension = await AddonManager.getAddonByID(id);
+ extension.uninstall();
+
+ info("Wait until the addon disappears from about:debugging");
+ await waitUntil(() => !findDebugTargetByText(name, document));
+}
+/* exported removeExtension */
+
+function prepareMockFilePicker(pathOrFile) {
+ const isFile = typeof pathOrFile.isFile === "function" && pathOrFile.isFile();
+ const file = isFile ? pathOrFile : _getSupportsFile(pathOrFile).file;
+
+ // Mock the file picker to select a test addon
+ const MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+ MockFilePicker.setFiles([file]);
+}
+/* exported prepareMockFilePicker */
+
+function promiseBackgroundContextEvent(extensionId, eventName) {
+ const { Management } = ChromeUtils.importESModule(
+ "resource://gre/modules/Extension.sys.mjs"
+ );
+
+ return new Promise(resolve => {
+ Management.on(eventName, function listener(_evtName, context) {
+ if (context.extension.id === extensionId) {
+ Management.off(eventName, listener);
+ resolve();
+ }
+ });
+ });
+}
+
+function promiseBackgroundContextLoaded(extensionId) {
+ return promiseBackgroundContextEvent(extensionId, "proxy-context-load");
+}
+/* exported promiseBackgroundContextLoaded */
+
+function promiseBackgroundContextUnloaded(extensionId) {
+ return promiseBackgroundContextEvent(extensionId, "proxy-context-unload");
+}
+/* exported promiseBackgroundContextUnloaded */
+
+async function assertBackgroundStatus(
+ extName,
+ { document, expectedStatus, targetElement }
+) {
+ const target = targetElement || findDebugTargetByText(extName, document);
+ const getBackgroundStatusElement = () =>
+ target.querySelector(".extension-backgroundscript__status");
+ await waitFor(
+ () =>
+ getBackgroundStatusElement()?.classList.contains(
+ `extension-backgroundscript__status--${expectedStatus}`
+ ),
+ `Wait ${extName} Background script status "${expectedStatus}" to be rendered`
+ );
+}
+/* exported assertBackgroundStatus */
+
+function getExtensionInstance(extensionId) {
+ const policy = WebExtensionPolicy.getByID(extensionId);
+ ok(policy, `Got a WebExtensionPolicy instance for ${extensionId}`);
+ ok(policy.extension, `Got an Extension class instance for ${extensionId}`);
+ return policy.extension;
+}
+/* exported getExtensionInstance */
+
+async function triggerExtensionEventPageIdleTimeout(extensionId) {
+ await getExtensionInstance(extensionId).terminateBackground();
+}
+/* exported triggerExtensionEventPageIdleTimeout */
+
+async function wakeupExtensionEventPage(extensionId) {
+ await getExtensionInstance(extensionId).wakeupBackground();
+}
+/* exported wakeupExtensionEventPage */
+
+function promiseTerminateBackgroundScriptIgnored(extensionId) {
+ const extension = getExtensionInstance(extensionId);
+ return new Promise(resolve => {
+ extension.once("background-script-suspend-ignored", resolve);
+ });
+}
+/* exported promiseTerminateBackgroundScriptIgnored */
+
+async function promiseBackgroundStatusUpdate(window) {
+ waitForDispatch(
+ window.AboutDebugging.store,
+ "EXTENSION_BGSCRIPT_STATUS_UPDATED"
+ );
+}
+/* exported promiseBackgroundStatusUpdate */
diff --git a/devtools/client/aboutdebugging/test/browser/helper-collapsibilities.js b/devtools/client/aboutdebugging/test/browser/helper-collapsibilities.js
new file mode 100644
index 0000000000..0f0d28da1d
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/helper-collapsibilities.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TARGET_PANES = [
+ {
+ title: "Temporary Extensions",
+ pref: "devtools.aboutdebugging.collapsibilities.temporaryExtension",
+ },
+ {
+ title: "Extensions",
+ pref: "devtools.aboutdebugging.collapsibilities.installedExtension",
+ },
+ {
+ title: "Tabs",
+ pref: "devtools.aboutdebugging.collapsibilities.tab",
+ },
+ {
+ title: "Service Workers",
+ pref: "devtools.aboutdebugging.collapsibilities.serviceWorker",
+ },
+ {
+ title: "Shared Workers",
+ pref: "devtools.aboutdebugging.collapsibilities.sharedWorker",
+ },
+ {
+ title: "Other Workers",
+ pref: "devtools.aboutdebugging.collapsibilities.otherWorker",
+ },
+];
+/* exported TARGET_PANES */
+
+function prepareCollapsibilitiesTest() {
+ // Make all collapsibilities to be expanded.
+ for (const { pref } of TARGET_PANES) {
+ Services.prefs.setBoolPref(pref, false);
+ }
+}
+/* exported prepareCollapsibilitiesTest */
+
+async function toggleCollapsibility(debugTargetPane) {
+ debugTargetPane.querySelector(".qa-debug-target-pane-title").click();
+ // Wait for animation of collapse/expand.
+ const animations = debugTargetPane.ownerDocument.getAnimations();
+ await Promise.all(animations.map(animation => animation.finished));
+}
+/* exported toggleCollapsibility */
+
+registerCleanupFunction(() => {
+ for (const { pref } of TARGET_PANES) {
+ Services.prefs.clearUserPref(pref);
+ }
+});
diff --git a/devtools/client/aboutdebugging/test/browser/helper-mocks.js b/devtools/client/aboutdebugging/test/browser/helper-mocks.js
new file mode 100644
index 0000000000..76fc414ae8
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/helper-mocks.js
@@ -0,0 +1,262 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../../../shared/test/shared-head.js */
+
+const MOCKS_ROOT = CHROME_URL_ROOT + "mocks/";
+/* import-globals-from mocks/helper-adb-mock.js */
+Services.scriptloader.loadSubScript(MOCKS_ROOT + "helper-adb-mock.js", this);
+/* import-globals-from mocks/helper-client-wrapper-mock.js */
+Services.scriptloader.loadSubScript(
+ MOCKS_ROOT + "helper-client-wrapper-mock.js",
+ this
+);
+/* import-globals-from mocks/helper-runtime-client-factory-mock.js */
+Services.scriptloader.loadSubScript(
+ MOCKS_ROOT + "helper-runtime-client-factory-mock.js",
+ this
+);
+
+const {
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This wrapper around the mocks used in about:debugging tests provides helpers to
+ * quickly setup mocks for runtime tests involving USB, network or wifi runtimes that can
+ * are difficult to setup in a test environment.
+ */
+class Mocks {
+ constructor() {
+ // Setup the adb mock to rely on internal arrays.
+ this.adbMock = createAdbMock();
+ this.adbProcessMock = createAdbProcessMock();
+ this._usbRuntimes = [];
+ this._usbDevices = [];
+ this.adbMock.adb.getRuntimes = () => {
+ return this._usbRuntimes;
+ };
+ this.adbMock.adb.getDevices = () => {
+ const runtimeDevices = this._usbRuntimes.map(r => {
+ return { id: r.deviceId, name: r.deviceName };
+ });
+ return runtimeDevices.concat(this._usbDevices);
+ };
+
+ // adb.updateRuntimes should ultimately fire the "runtime-list-updated" event.
+ this.adbMock.adb.updateRuntimes = () => {
+ this.emitUSBUpdate();
+ };
+
+ this.adbMock.adb.isProcessStarted = () => true;
+
+ // Prepare a fake observer to be able to emit events from this mock.
+ this._observerMock = addObserverMock(this.adbMock.adb);
+
+ // Setup the runtime-client-factory mock to rely on the internal _clients map.
+ this.runtimeClientFactoryMock = createRuntimeClientFactoryMock();
+ this._clients = {
+ [RUNTIMES.NETWORK]: {},
+ [RUNTIMES.THIS_FIREFOX]: {},
+ [RUNTIMES.USB]: {},
+ };
+ this.runtimeClientFactoryMock.createClientForRuntime = runtime => {
+ return this._clients[runtime.type][runtime.id];
+ };
+
+ // Add a client for THIS_FIREFOX, since about:debugging will start on the This Firefox
+ // page.
+ this._thisFirefoxClient = createThisFirefoxClientMock();
+ this._clients[RUNTIMES.THIS_FIREFOX][RUNTIMES.THIS_FIREFOX] =
+ this._thisFirefoxClient;
+
+ // Enable mocks and remove them after the test.
+ this.enableMocks();
+ registerCleanupFunction(() => this.disableMocks());
+ }
+
+ get thisFirefoxClient() {
+ return this._thisFirefoxClient;
+ }
+
+ enableMocks() {
+ enableAdbMock(this.adbMock);
+ enableAdbProcessMock(this.adbProcessMock);
+ enableRuntimeClientFactoryMock(this.runtimeClientFactoryMock);
+ }
+
+ disableMocks() {
+ disableAdbMock();
+ disableAdbProcessMock();
+ disableRuntimeClientFactoryMock();
+
+ for (const host of Object.keys(this._clients[RUNTIMES.NETWORK])) {
+ this.removeNetworkRuntime(host);
+ }
+ }
+
+ createNetworkRuntime(host, runtimeInfo) {
+ const {
+ addNetworkLocation,
+ } = require("resource://devtools/client/aboutdebugging/src/modules/network-locations.js");
+ addNetworkLocation(host);
+
+ // Add a valid client that can be returned for this particular runtime id.
+ const mockNetworkClient = createClientMock();
+ mockNetworkClient.getDeviceDescription = () => {
+ return {
+ name: runtimeInfo.name || "TestBrand",
+ channel: runtimeInfo.channel || "release",
+ version: runtimeInfo.version || "1.0",
+ };
+ };
+ this._clients[RUNTIMES.NETWORK][host] = mockNetworkClient;
+
+ return mockNetworkClient;
+ }
+
+ removeNetworkRuntime(host) {
+ const {
+ removeNetworkLocation,
+ } = require("resource://devtools/client/aboutdebugging/src/modules/network-locations.js");
+ removeNetworkLocation(host);
+
+ delete this._clients[RUNTIMES.NETWORK][host];
+ }
+
+ emitUSBUpdate() {
+ this._observerMock.emit("runtime-list-updated");
+ }
+
+ /**
+ * Creates a USB runtime for which a client conenction can be established.
+ * @param {String} id
+ * The id of the runtime.
+ * @param {Object} optional object used to create the fake runtime & device
+ * - channel: {String} Release channel, for instance "release", "nightly"
+ * - clientWrapper: {ClientWrapper} optional ClientWrapper for this runtime
+ * - deviceId: {String} Device id
+ * - deviceName: {String} Device name
+ * - isFenix: {Boolean} set by ADB if the package name matches a Fenix package
+ * - name: {String} Application name, for instance "Firefox"
+ * - shortName: {String} Short name for the device
+ * - socketPath: {String} (should only be used for connecting, so not here)
+ * - version: {String} Version, for instance "63.0a"
+ * - versionName: {String} Version return by ADB "63.0a"
+ * @return {Object} Returns the mock client created for this runtime so that methods
+ * can be overridden on it.
+ */
+ createUSBRuntime(id, runtimeInfo = {}) {
+ // Add a new runtime to the list of scanned runtimes.
+ this._usbRuntimes.push({
+ deviceId: runtimeInfo.deviceId || "test device id",
+ deviceName: runtimeInfo.deviceName || "test device name",
+ id,
+ isFenix: runtimeInfo.isFenix,
+ shortName: runtimeInfo.shortName || "testshort",
+ socketPath: runtimeInfo.socketPath || "test/path",
+ versionName: runtimeInfo.versionName || "1.0",
+ });
+
+ // Add a valid client that can be returned for this particular runtime id.
+ let mockUsbClient = runtimeInfo.clientWrapper;
+ if (mockUsbClient) {
+ const originalGetDeviceDescription =
+ mockUsbClient.getDeviceDescription.bind(mockUsbClient);
+ mockUsbClient.getDeviceDescription = async () => {
+ const deviceDescription = await originalGetDeviceDescription();
+ return {
+ channel: runtimeInfo.channel || deviceDescription.channel,
+ name: runtimeInfo.name || deviceDescription.name,
+ version: runtimeInfo.version || deviceDescription.version,
+ };
+ };
+ } else {
+ // If no clientWrapper was provided, create a mock client here.
+ mockUsbClient = createClientMock();
+ mockUsbClient.getDeviceDescription = () => {
+ return {
+ channel: runtimeInfo.channel || "release",
+ name: runtimeInfo.name || "TestBrand",
+ version: runtimeInfo.version || "1.0",
+ };
+ };
+ }
+
+ this._clients[RUNTIMES.USB][id] = mockUsbClient;
+
+ return mockUsbClient;
+ }
+
+ removeUSBRuntime(id) {
+ this._usbRuntimes = this._usbRuntimes.filter(runtime => runtime.id !== id);
+ delete this._clients[RUNTIMES.USB][id];
+ }
+
+ addDevice(deviceId, deviceName) {
+ this._usbDevices.push({
+ id: deviceId,
+ name: deviceName,
+ });
+ }
+
+ removeDevice(deviceId) {
+ this._usbDevices = this._usbDevices.filter(d => {
+ return d.id !== deviceId;
+ });
+ }
+
+ removeRuntime(id) {
+ if (this._clients[RUNTIMES.USB][id]) {
+ this.removeUSBRuntime(id);
+ } else if (this._clients[RUNTIMES.NETWORK][id]) {
+ this.removeNetworkRuntime(id);
+ }
+ }
+}
+/* exported Mocks */
+
+const silenceWorkerUpdates = function () {
+ const {
+ removeMockedModule,
+ setMockedModule,
+ } = require("resource://devtools/shared/loader/browser-loader-mocks.js");
+
+ const mock = {
+ WorkersListener: () => {
+ return {
+ addListener: () => {},
+ removeListener: () => {},
+ };
+ },
+ };
+ setMockedModule(mock, "devtools/client/shared/workers-listener");
+
+ registerCleanupFunction(() => {
+ removeMockedModule("devtools/client/shared/workers-listener");
+ });
+};
+/* exported silenceWorkerUpdates */
+
+async function createLocalClientWrapper() {
+ info("Create a local DevToolsClient");
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ const {
+ DevToolsClient,
+ } = require("resource://devtools/client/devtools-client.js");
+ const {
+ ClientWrapper,
+ } = require("resource://devtools/client/aboutdebugging/src/modules/client-wrapper.js");
+
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+
+ await client.connect();
+ return new ClientWrapper(client);
+}
+/* exported createLocalClientWrapper */
diff --git a/devtools/client/aboutdebugging/test/browser/helper-real-usb.js b/devtools/client/aboutdebugging/test/browser/helper-real-usb.js
new file mode 100644
index 0000000000..dd6c8c263d
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/helper-real-usb.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-adb.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-adb.js", this);
+
+async function getExpectedRuntime() {
+ const runtimes = await getExpectedRuntimeAll();
+ return runtimes[0];
+}
+/* exported getExpectedRuntime */
+
+async function getExpectedRuntimeAll() {
+ const runtimesPath = _getExpectedRuntimesPath();
+ const currentPath = Services.env.get("PWD");
+ const path = `${currentPath}/${runtimesPath}`;
+ info(`Load ${path}`);
+ const buffer = await IOUtils.read(path);
+ const data = new TextDecoder().decode(buffer);
+ return JSON.parse(data);
+}
+/* exported getExpectedRuntimeAll */
+
+function isAvailable() {
+ return !!_getExpectedRuntimesPath();
+}
+/* exported isAvailable */
+
+async function openAboutDebuggingWithADB() {
+ const { document, tab, window } = await openAboutDebugging();
+
+ await pushPref(
+ "devtools.remote.adb.extensionURL",
+ CHROME_URL_ROOT + "resources/test-adb-extension/adb-extension-#OS#.xpi"
+ );
+ await checkAdbNotRunning();
+
+ const {
+ adbAddon,
+ } = require("resource://devtools/client/shared/remote-debugging/adb/adb-addon.js");
+ adbAddon.install("internal");
+ const usbStatusElement = document.querySelector(".qa-sidebar-usb-status");
+ await waitUntil(() => usbStatusElement.textContent.includes("USB enabled"));
+ await waitForAdbStart();
+
+ return { document, tab, window };
+}
+/* exported openAboutDebuggingWithADB */
+
+function _getExpectedRuntimesPath() {
+ return Services.env.get("USB_RUNTIMES");
+}
diff --git a/devtools/client/aboutdebugging/test/browser/helper-serviceworker.js b/devtools/client/aboutdebugging/test/browser/helper-serviceworker.js
new file mode 100644
index 0000000000..e57b62b7eb
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/helper-serviceworker.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head.js */
+
+/**
+ * Temporarily flip all the preferences necessary for service worker testing.
+ */
+async function enableServiceWorkerDebugging() {
+ // Enable service workers.
+ await pushPref("dom.serviceWorkers.enabled", true);
+
+ // Accept workers from mochitest's http (normally only available in https).
+ await pushPref("dom.serviceWorkers.testing.enabled", true);
+
+ // Force single content process. Necessary until sw e10s refactor is done (Bug 1231208).
+ await pushPref("dom.ipc.processCount", 1);
+ Services.ppmm.releaseCachedProcesses();
+}
+/* exported enableServiceWorkerDebugging */
+
+/**
+ * Helper to listen once on a message sent using postMessage from the provided tab.
+ *
+ * @param {Tab} tab
+ * The tab on which the message will be received.
+ * @param {String} message
+ * The name of the expected message.
+ */
+function onServiceWorkerMessage(tab, message) {
+ info("Make the test page notify us when the service worker sends a message.");
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [message],
+ function (messageChild) {
+ return new Promise(resolve => {
+ const win = content.wrappedJSObject;
+ win.navigator.serviceWorker.addEventListener(
+ "message",
+ function (event) {
+ if (event.data == messageChild) {
+ resolve();
+ }
+ }
+ );
+ });
+ }
+ );
+}
+/* exported onServiceWorkerMessage */
+
+async function _waitForServiceWorkerStatus(workerText, status, document) {
+ await waitUntil(() => {
+ const target = findDebugTargetByText(workerText, document);
+ const statusElement = target && target.querySelector(".qa-worker-status");
+ return statusElement && statusElement.textContent === status;
+ });
+
+ return findDebugTargetByText(workerText, document);
+}
+/* exported waitForServiceWorkerRunning */
+
+async function waitForServiceWorkerStopped(workerText, document) {
+ return _waitForServiceWorkerStatus(workerText, "Stopped", document);
+}
+/* exported waitForServiceWorkerStopped */
+
+async function waitForServiceWorkerRunning(workerText, document) {
+ return _waitForServiceWorkerStatus(workerText, "Running", document);
+}
+/* exported waitForServiceWorkerRunning */
+
+async function waitForServiceWorkerRegistering(workerText, document) {
+ return _waitForServiceWorkerStatus(workerText, "Registering", document);
+}
+/* exported waitForServiceWorkerRegistering */
+
+async function waitForRegistration(tab) {
+ info("Wait until the registration appears on the window");
+ const swBrowser = tab.linkedBrowser;
+ await asyncWaitUntil(async () =>
+ SpecialPowers.spawn(swBrowser, [], async function () {
+ return !!(await content.wrappedJSObject.getRegistration());
+ })
+ );
+}
+/* exported waitForRegistration */
+
+/**
+ * Unregister the service worker from the content page. The content page should define
+ * `getRegistration` to allow this helper to retrieve the service worker registration that
+ * should be unregistered.
+ *
+ * @param {Tab} tab
+ * The tab on which the service worker should be removed.
+ */
+async function unregisterServiceWorker(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ const win = content.wrappedJSObject;
+ // Check that the content page defines getRegistration.
+ is(
+ typeof win.getRegistration,
+ "function",
+ "getRegistration is a valid function"
+ );
+ win.getRegistration().unregister();
+ });
+}
+/* exported unregisterServiceWorker */
diff --git a/devtools/client/aboutdebugging/test/browser/helper-telemetry.js b/devtools/client/aboutdebugging/test/browser/helper-telemetry.js
new file mode 100644
index 0000000000..228f01aff3
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/helper-telemetry.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head.js */
+
+/**
+ * Reset all telemetry events.
+ */
+function setupTelemetryTest() {
+ // Let's reset the counts.
+ Services.telemetry.clearEvents();
+
+ // Ensure no events have been logged
+ const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS;
+ const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true);
+ ok(!snapshot.parent, "No events have been logged for the main process");
+}
+/* exported setupTelemetryTest */
+
+/**
+ * Check that the logged telemetry events exactly match the array of expected events.
+ * Will compare the number of events, the event methods, and the event extras including
+ * the about:debugging session id.
+ */
+function checkTelemetryEvents(expectedEvents, expectedSessionId) {
+ const evts = readAboutDebuggingEvents();
+ is(evts.length, expectedEvents.length, "Expected number of events");
+
+ function _eventHasExpectedExtras(e, expectedEvent) {
+ const expectedExtras = Object.keys(expectedEvent.extras);
+ return expectedExtras.every(extra => {
+ return e.extras[extra] === expectedEvent.extras[extra];
+ });
+ }
+
+ for (const expectedEvent of expectedEvents) {
+ const sameMethodEvents = evts.filter(
+ e => e.method === expectedEvent.method
+ );
+ ok(
+ !!sameMethodEvents.length,
+ "Found event for method: " + expectedEvent.method
+ );
+
+ const sameExtrasEvents = sameMethodEvents.filter(e =>
+ _eventHasExpectedExtras(e, expectedEvent)
+ );
+ ok(
+ sameExtrasEvents.length === 1,
+ "Found exactly one event matching the expected extras"
+ );
+ if (sameExtrasEvents.length === 0) {
+ info(JSON.stringify(sameMethodEvents));
+ }
+ is(
+ sameExtrasEvents[0].extras.session_id,
+ expectedSessionId,
+ "Select page event has the expected session"
+ );
+ }
+
+ return evts;
+}
+/* exported checkTelemetryEvents */
+
+/**
+ * Retrieve the session id from an "open" event.
+ * Note that calling this will "clear" all the events.
+ */
+function getOpenEventSessionId() {
+ const openEvents = readAboutDebuggingEvents().filter(
+ e => e.method === "open_adbg"
+ );
+ ok(!!openEvents[0], "Found an about:debugging open event");
+ return openEvents[0].extras.session_id;
+}
+/* exported getOpenEventSessionId */
+
+/**
+ * Read all the pending events that have "aboutdebugging" as their object property.
+ * WARNING: Calling this method also flushes/clears the events.
+ */
+function readAboutDebuggingEvents() {
+ const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS;
+ // Retrieve and clear telemetry events.
+ const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true);
+ // about:debugging events are logged in the parent process
+ const parentEvents = snapshot.parent || [];
+
+ return parentEvents
+ .map(_toEventObject)
+ .filter(e => e.object === "aboutdebugging");
+}
+/* exported getLoggedEvents */
+
+/**
+ * The telemetry event data structure is simply an array. This helper remaps the array to
+ * an object with more user friendly properties.
+ */
+function _toEventObject(rawEvent) {
+ return {
+ // Category is typically devtools.main for us.
+ category: rawEvent[1],
+ // Method is the event's name (eg open, select_page etc...)
+ method: rawEvent[2],
+ // Object will usually be aboutdebugging for our tests
+ object: rawEvent[3],
+ // Value is usually empty for devtools events
+ value: rawEvent[4],
+ // Extras contain all the details of the event, including the session_id.
+ extras: rawEvent[5],
+ };
+}
diff --git a/devtools/client/aboutdebugging/test/browser/mocks/helper-adb-mock.js b/devtools/client/aboutdebugging/test/browser/mocks/helper-adb-mock.js
new file mode 100644
index 0000000000..f02ca02ee3
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/mocks/helper-adb-mock.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Setup the loader to return the provided mock object instead of the regular adb module.
+ *
+ * @param {Object}
+ * mock should implement the following methods:
+ * - registerListener(listener)
+ * - getRuntimes()
+ * - getDevices()
+ * - updateRuntimes()
+ * - unregisterListener(listener)
+ */
+function enableAdbMock(mock) {
+ const {
+ setMockedModule,
+ } = require("resource://devtools/shared/loader/browser-loader-mocks.js");
+ setMockedModule(mock, "devtools/client/shared/remote-debugging/adb/adb");
+}
+/* exported enableAdbMock */
+
+/**
+ * Update the loader to clear the mock entry for the adb module.
+ */
+function disableAdbMock() {
+ const {
+ removeMockedModule,
+ } = require("resource://devtools/shared/loader/browser-loader-mocks.js");
+ removeMockedModule("devtools/client/shared/remote-debugging/adb/adb");
+}
+/* exported disableAdbMock */
+
+/**
+ * Creates a simple mock version for adb, implementing all the expected methods
+ * with empty placeholders.
+ */
+function createAdbMock() {
+ const adbMock = {};
+ adbMock.registerListener = function (listener) {
+ console.log("MOCKED METHOD registerListener");
+ };
+
+ adbMock.getRuntimes = function () {
+ console.log("MOCKED METHOD getRuntimes");
+ };
+
+ adbMock.getDevices = function () {
+ console.log("MOCKED METHOD getDevices");
+ };
+
+ adbMock.updateRuntimes = function () {
+ console.log("MOCKED METHOD updateRuntimes");
+ };
+
+ adbMock.unregisterListener = function (listener) {
+ console.log("MOCKED METHOD unregisterListener");
+ };
+
+ adbMock.once = function () {
+ console.log("MOCKED METHOD once");
+ };
+
+ adbMock.isProcessStarted = function () {
+ console.log("MOCKED METHOD isProcessStarted");
+ };
+
+ return { adb: adbMock };
+}
+/* exported createAdbMock */
+
+/**
+ * The adb module allows to observe runtime updates. To simulate this behaviour
+ * the easiest is to use an EventEmitter-decorated object that can accept listeners and
+ * can emit events from the test.
+ *
+ * This method will update the registerListener method of the provided
+ * usbRuntimesMock in order to add listeners to a mockObserver, and returns said observer
+ * so that the test can emit "runtime-list-updated" when needed.
+ */
+function addObserverMock(adbMock) {
+ const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+ const observerMock = {};
+ EventEmitter.decorate(observerMock);
+ adbMock.registerListener = function (listener) {
+ console.log("MOCKED METHOD registerListener with mock scanner");
+ observerMock.on("runtime-list-updated", listener);
+ };
+
+ // NOTE FOR REVIEW: Instead of emitting "runtime-list-updated" events in the test,
+ // this mock could have a emitObservedEvent method, that would just emit the correct
+ // event. This way if the event name changes, everything remains contained in this
+ // method.
+
+ return observerMock;
+}
+/* exported addObserverMock */
+
+function createAdbProcessMock() {
+ const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+ const mock = {};
+ EventEmitter.decorate(mock);
+
+ mock.ready = false;
+
+ mock.start = async () => {
+ console.log("MOCKED METHOD start");
+ mock.ready = true;
+ mock.emit("adb-ready");
+ };
+
+ return { adbProcess: mock };
+}
+/* exported createAdbProcessMock */
+
+function enableAdbProcessMock(mock) {
+ const {
+ setMockedModule,
+ } = require("resource://devtools/shared/loader/browser-loader-mocks.js");
+ setMockedModule(
+ mock,
+ "devtools/client/shared/remote-debugging/adb/adb-process"
+ );
+}
+/* exported enableAdbProcessMock */
+
+function disableAdbProcessMock() {
+ const {
+ removeMockedModule,
+ } = require("resource://devtools/shared/loader/browser-loader-mocks.js");
+ removeMockedModule("devtools/client/shared/remote-debugging/adb/adb-process");
+}
+/* exported disableAdbProcessMock */
diff --git a/devtools/client/aboutdebugging/test/browser/mocks/helper-client-wrapper-mock.js b/devtools/client/aboutdebugging/test/browser/mocks/helper-client-wrapper-mock.js
new file mode 100644
index 0000000000..5748904bff
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/mocks/helper-client-wrapper-mock.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict"; // defined in head.js
+
+/* global CHROME_URL_ROOT */
+
+// This head file contains helpers to create mock versions of the ClientWrapper class
+// defined at devtools/client/aboutdebugging/src/modules/client-wrapper.js .
+
+const {
+ RUNTIME_PREFERENCE,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+// Sensible default values for runtime preferences that should be usable in most
+// situations
+const DEFAULT_PREFERENCES = {
+ [RUNTIME_PREFERENCE.CONNECTION_PROMPT]: true,
+ [RUNTIME_PREFERENCE.PERMANENT_PRIVATE_BROWSING]: false,
+ [RUNTIME_PREFERENCE.SERVICE_WORKERS_ENABLED]: true,
+};
+
+// Creates a simple mock ClientWrapper.
+function createClientMock() {
+ const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+ const eventEmitter = {};
+ EventEmitter.decorate(eventEmitter);
+
+ return {
+ // add a reference to the internal event emitter so that consumers can fire client
+ // events.
+ _eventEmitter: eventEmitter,
+ _preferences: {},
+ contentProcessFronts: [],
+ serviceWorkerRegistrationFronts: [],
+ once: (evt, listener) => {
+ eventEmitter.once(evt, listener);
+ },
+ on: (evt, listener) => {
+ eventEmitter.on(evt, listener);
+ },
+ off: (evt, listener) => {
+ eventEmitter.off(evt, listener);
+ },
+ client: {
+ once: (evt, listener) => {
+ eventEmitter.once(evt, listener);
+ },
+ on: (evt, listener) => {
+ eventEmitter.on(evt, listener);
+ },
+ off: (evt, listener) => {
+ eventEmitter.off(evt, listener);
+ },
+ },
+ // no-op
+ close: () => {},
+ // client is not closed
+ isClosed: () => false,
+ // no-op
+ connect: () => {},
+ // no-op
+ getDeviceDescription: () => {},
+ // Return default preference value or null if no match.
+ getPreference(prefName) {
+ if (prefName in this._preferences) {
+ return this._preferences[prefName];
+ }
+ if (prefName in DEFAULT_PREFERENCES) {
+ return DEFAULT_PREFERENCES[prefName];
+ }
+ return null;
+ },
+ // no-op
+ createRootResourceCommand: () => {
+ return {
+ watchResources: () => new Promise(r => r()),
+ unwatchResources: () => {},
+ };
+ },
+ // Empty array of addons
+ listAddons: () => [],
+ // Empty array of processes
+ listProcesses: () => [],
+ // Empty array of tabs
+ listTabs: () => [],
+ // Empty arrays of workers
+ listWorkers: () => ({
+ otherWorkers: [],
+ serviceWorkers: [],
+ sharedWorkers: [],
+ }),
+ // no-op
+ getMainProcess: () => {},
+ // no-op
+ getFront: () => {},
+ // stores the preference locally (doesn't update about:config)
+ setPreference(prefName, value) {
+ this._preferences[prefName] = value;
+ },
+ getPerformancePanelUrl: () => CHROME_URL_ROOT + "empty.html",
+ loadPerformanceProfiler: () => {},
+ // Valid compatibility report
+ checkVersionCompatibility: () => {
+ const {
+ COMPATIBILITY_STATUS,
+ } = require("resource://devtools/client/shared/remote-debugging/version-checker.js");
+ return { status: COMPATIBILITY_STATUS.COMPATIBLE };
+ },
+ // No traits by default but allow updates.
+ traits: {},
+ };
+}
+
+// Create a ClientWrapper mock that can be used to replace the this-firefox runtime.
+function createThisFirefoxClientMock() {
+ const mockThisFirefoxDescription = {
+ name: "Firefox",
+ channel: "nightly",
+ version: "63.0",
+ };
+
+ // Create a fake about:debugging tab because our test helper openAboutDebugging
+ // waits until about:debugging is displayed in the list of tabs.
+ const mockAboutDebuggingTab = {
+ retrieveFavicon: () => {},
+ outerWindowID: 0,
+ traits: {},
+ url: "about:debugging",
+ };
+
+ const mockThisFirefoxClient = createClientMock();
+ mockThisFirefoxClient.listTabs = () => [mockAboutDebuggingTab];
+ mockThisFirefoxClient.getDeviceDescription = () => mockThisFirefoxDescription;
+
+ return mockThisFirefoxClient;
+}
+/* exported createThisFirefoxClientMock */
diff --git a/devtools/client/aboutdebugging/test/browser/mocks/helper-runtime-client-factory-mock.js b/devtools/client/aboutdebugging/test/browser/mocks/helper-runtime-client-factory-mock.js
new file mode 100644
index 0000000000..b74c229fcf
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/mocks/helper-runtime-client-factory-mock.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Setup the loader to return the provided mock object instead of the regular
+ * runtime-client-factory module.
+ *
+ * @param {Object}
+ * mock should implement the following methods:
+ * - createClientForRuntime(runtime)
+ */
+function enableRuntimeClientFactoryMock(mock) {
+ const {
+ setMockedModule,
+ } = require("resource://devtools/shared/loader/browser-loader-mocks.js");
+ setMockedModule(
+ mock,
+ "devtools/client/aboutdebugging/src/modules/runtime-client-factory"
+ );
+
+ // When using a mocked client, we should not attempt to check default
+ // preferences.
+ mockRuntimeDefaultPreferences();
+}
+/* exported enableRuntimeClientFactoryMock */
+
+const mockRuntimeDefaultPreferences = function () {
+ const {
+ removeMockedModule,
+ setMockedModule,
+ } = require("resource://devtools/shared/loader/browser-loader-mocks.js");
+
+ const mock = {
+ setDefaultPreferencesIfNeeded: () => {},
+ DEFAULT_PREFERENCES: [],
+ };
+ setMockedModule(
+ mock,
+ "devtools/client/aboutdebugging/src/modules/runtime-default-preferences"
+ );
+
+ registerCleanupFunction(() => {
+ removeMockedModule(
+ "devtools/client/aboutdebugging/src/modules/runtime-default-preferences"
+ );
+ });
+};
+
+/**
+ * Update the loader to clear the mock entry for the runtime-client-factory module.
+ */
+function disableRuntimeClientFactoryMock() {
+ const {
+ removeMockedModule,
+ } = require("resource://devtools/shared/loader/browser-loader-mocks.js");
+ removeMockedModule(
+ "devtools/client/aboutdebugging/src/modules/runtime-client-factory"
+ );
+}
+/* exported disableRuntimeClientFactoryMock */
+
+/**
+ * Creates a simple mock version for runtime-client-factory, implementing all the expected
+ * methods with empty placeholders.
+ */
+function createRuntimeClientFactoryMock() {
+ const RuntimeClientFactoryMock = {};
+ RuntimeClientFactoryMock.createClientForRuntime = function (runtime) {
+ console.log("MOCKED METHOD createClientForRuntime");
+ };
+
+ return RuntimeClientFactoryMock;
+}
+/* exported createRuntimeClientFactoryMock */
diff --git a/devtools/client/aboutdebugging/test/browser/resources/bad-extensions/invalid-json/manifest.json b/devtools/client/aboutdebugging/test/browser/resources/bad-extensions/invalid-json/manifest.json
new file mode 100644
index 0000000000..4ab10b4de7
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/bad-extensions/invalid-json/manifest.json
@@ -0,0 +1 @@
+this is not valid json
diff --git a/devtools/client/aboutdebugging/test/browser/resources/bad-extensions/invalid-property/manifest.json b/devtools/client/aboutdebugging/test/browser/resources/bad-extensions/invalid-property/manifest.json
new file mode 100644
index 0000000000..992818bd77
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/bad-extensions/invalid-property/manifest.json
@@ -0,0 +1,23 @@
+{
+ "manifest_version": 2,
+ "name": "test-invalid-extension",
+ "version": "1",
+ "description": "the name says it all",
+ "permissions": ["*://*.foo.com/*", "alarms", "notifications", "tabs"],
+ "background": {
+ "scripts": ["background.js"]
+ },
+ "content_scripts": [
+ {
+ "matches": "*://*.foo.com/*",
+ "js": ["content.js"]
+ }
+ ],
+ "browser_action": {
+ "default_icon": {
+ "32": "home.svg"
+ },
+ "default_title": "foobarbaz (v1)",
+ "browser_style": true
+ }
+}
diff --git a/devtools/client/aboutdebugging/test/browser/resources/doc_aboutdebugging_devtoolstoolbox_breakpoint.html b/devtools/client/aboutdebugging/test/browser/resources/doc_aboutdebugging_devtoolstoolbox_breakpoint.html
new file mode 100644
index 0000000000..bb28556775
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/doc_aboutdebugging_devtoolstoolbox_breakpoint.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>BREAKPOINT TEST PAGE</title>
+</head>
+<body>
+ <script type="text/javascript" src="script_aboutdebugging_devtoolstoolbox_breakpoint.js"></script>
+</body>
+</html>
diff --git a/devtools/client/aboutdebugging/test/browser/resources/packaged-extension/packaged-extension.xpi b/devtools/client/aboutdebugging/test/browser/resources/packaged-extension/packaged-extension.xpi
new file mode 100644
index 0000000000..c1c7af9600
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/packaged-extension/packaged-extension.xpi
Binary files differ
diff --git a/devtools/client/aboutdebugging/test/browser/resources/real/usb-runtimes-sample.json b/devtools/client/aboutdebugging/test/browser/resources/real/usb-runtimes-sample.json
new file mode 100644
index 0000000000..bce334e95a
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/real/usb-runtimes-sample.json
@@ -0,0 +1,14 @@
+[
+ {
+ "sidebarInfo": {
+ "deviceName": "Pixel 2",
+ "shortName": "Firefox Nightly"
+ },
+ "runtimeDetails": {
+ "info": {
+ "name": "Mozilla Nightly",
+ "version": "64.0a1"
+ }
+ }
+ }
+]
diff --git a/devtools/client/aboutdebugging/test/browser/resources/script_aboutdebugging_devtoolstoolbox_breakpoint.js b/devtools/client/aboutdebugging/test/browser/resources/script_aboutdebugging_devtoolstoolbox_breakpoint.js
new file mode 100644
index 0000000000..99401016e4
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/script_aboutdebugging_devtoolstoolbox_breakpoint.js
@@ -0,0 +1,12 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Random method on which a breakpoint will be set from the DevTools UI in the
+// test.
+window.testMethod = function () {
+ const a = 1;
+ const b = 2;
+ return a + b;
+};
diff --git a/devtools/client/aboutdebugging/test/browser/resources/service-workers/controlled-sw.html b/devtools/client/aboutdebugging/test/browser/resources/service-workers/controlled-sw.html
new file mode 100644
index 0000000000..98d3bffd95
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/service-workers/controlled-sw.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Service worker controlled</title>
+</head>
+<body>
+<script type="text/javascript">
+
+"use strict";
+
+let registration;
+
+const registerServiceWorker = async function() {
+ try {
+ registration = await navigator.serviceWorker.register("controlled-sw.js");
+ dump("Controlled service worker registered\n");
+ } catch (e) {
+ dump("Controlled service worker not registered: " + e + "\n");
+ }
+};
+
+// Helper called from helper-serviceworker.js to unregister the service worker.
+window.getRegistration = function() {
+ return registration;
+};
+
+// Called from browser_aboutdebugging_serviceworker_status.js
+window.installServiceWorker = function() {
+ registration.installing.postMessage("install-service-worker");
+};
+
+// Register the service worker.
+registerServiceWorker();
+
+</script>
+</body>
+</html>
diff --git a/devtools/client/aboutdebugging/test/browser/resources/service-workers/controlled-sw.js b/devtools/client/aboutdebugging/test/browser/resources/service-workers/controlled-sw.js
new file mode 100644
index 0000000000..0a6d9cfdc6
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/service-workers/controlled-sw.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env worker */
+
+"use strict";
+
+// Copied from shared-head.js
+function waitUntil(predicate, interval = 10) {
+ if (predicate()) {
+ return Promise.resolve(true);
+ }
+ return new Promise(resolve => {
+ setTimeout(function () {
+ waitUntil(predicate, interval).then(() => resolve(true));
+ }, interval);
+ });
+}
+
+// This flag will be flipped from controlled-sw.html::installServiceWorker()
+let canInstall = false;
+self.addEventListener("message", function (event) {
+ if (event.data === "install-service-worker") {
+ canInstall = true;
+ }
+});
+
+// Wait for the canInstall flag to be flipped before completing the install.
+self.addEventListener("install", function (event) {
+ event.waitUntil(waitUntil(() => canInstall));
+});
diff --git a/devtools/client/aboutdebugging/test/browser/resources/service-workers/empty-sw.html b/devtools/client/aboutdebugging/test/browser/resources/service-workers/empty-sw.html
new file mode 100644
index 0000000000..ab862743a7
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/service-workers/empty-sw.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+let registration;
+
+const registerServiceWorker = async function() {
+ try {
+ registration = await navigator.serviceWorker.register("empty-sw.js");
+ dump("Empty service worker registered\n");
+ } catch (e) {
+ dump("Empty service worker not registered: " + e + "\n");
+ }
+};
+
+// Helper called from helper-serviceworker.js to unregister the service worker.
+window.getRegistration = function() {
+ return registration;
+};
+// Register the service worker.
+registerServiceWorker();
+</script>
+</body>
+</html>
diff --git a/devtools/client/aboutdebugging/test/browser/resources/service-workers/empty-sw.js b/devtools/client/aboutdebugging/test/browser/resources/service-workers/empty-sw.js
new file mode 100644
index 0000000000..1e7226402c
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/service-workers/empty-sw.js
@@ -0,0 +1 @@
+// Empty, just test registering.
diff --git a/devtools/client/aboutdebugging/test/browser/resources/service-workers/fetch-sw.html b/devtools/client/aboutdebugging/test/browser/resources/service-workers/fetch-sw.html
new file mode 100644
index 0000000000..a1bb218341
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/service-workers/fetch-sw.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+let registration;
+
+const registerServiceWorker = async function() {
+ try {
+ registration = await navigator.serviceWorker.register("fetch-sw.js");
+ dump("Empty service worker registered\n");
+ } catch (e) {
+ dump("Empty service worker not registered: " + e + "\n");
+ }
+};
+
+// Helper called from helper-serviceworker.js to unregister the service worker.
+window.getRegistration = function() {
+ return registration;
+};
+// Register the service worker.
+registerServiceWorker();
+</script>
+</body>
+</html>
diff --git a/devtools/client/aboutdebugging/test/browser/resources/service-workers/fetch-sw.js b/devtools/client/aboutdebugging/test/browser/resources/service-workers/fetch-sw.js
new file mode 100644
index 0000000000..de6ee1fb32
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/service-workers/fetch-sw.js
@@ -0,0 +1,6 @@
+"use strict";
+
+// Bug 1328293
+self.onfetch = function (event) {
+ // do nothing.
+};
diff --git a/devtools/client/aboutdebugging/test/browser/resources/service-workers/push-sw.html b/devtools/client/aboutdebugging/test/browser/resources/service-workers/push-sw.html
new file mode 100644
index 0000000000..bf5b0b0b0a
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/service-workers/push-sw.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Service worker push test</title>
+</head>
+<body>
+<script type="text/javascript">
+
+"use strict";
+
+let registration;
+let subscription;
+
+const registerServiceWorker = async function() {
+ const perm = { type: "desktop-notification", allow: true, context: document };
+ await SpecialPowers.pushPermissions([perm]);
+
+ try {
+ registration = await navigator.serviceWorker.register("push-sw.js");
+ dump("Push service worker registered\n");
+ } catch (e) {
+ dump("Push service worker not registered: " + e + "\n");
+ }
+};
+
+// Helper called from helper-serviceworker.js to unregister the service worker.
+window.getRegistration = function() {
+ return registration;
+};
+
+// Helper called from browser_aboutdebugging_serviceworker_pushservice_url.js
+window.subscribeToPush = async function() {
+ try {
+ subscription = await registration.pushManager.subscribe();
+ dump("SW subscribed to push: " + subscription.endpoint + "\n");
+ } catch (e) {
+ dump("SW not subscribed to push: " + e + "\n");
+ }
+};
+
+// Helper called from browser_aboutdebugging_serviceworker_pushservice_url.js
+window.unsubscribeToPush = async function() {
+ subscription.unsubscribe();
+};
+
+// Expose a promise to wait until the service worker is claimed.
+window.onSwClaimed = new Promise(resolve => {
+ navigator.serviceWorker.addEventListener("message", function(event) {
+ if (event.data == "sw-claimed") {
+ resolve();
+ }
+ });
+});
+
+// Register the service worker.
+registerServiceWorker();
+
+</script>
+</body>
+</html>
diff --git a/devtools/client/aboutdebugging/test/browser/resources/service-workers/push-sw.js b/devtools/client/aboutdebugging/test/browser/resources/service-workers/push-sw.js
new file mode 100644
index 0000000000..1231697ddd
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/service-workers/push-sw.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env worker */
+/* global clients */
+
+"use strict";
+
+// Send a message to all controlled windows.
+function postMessage(message) {
+ return clients.matchAll().then(function (clientlist) {
+ clientlist.forEach(function (client) {
+ client.postMessage(message);
+ });
+ });
+}
+
+// Don't wait for the next page load to become the active service worker.
+self.addEventListener("install", function (event) {
+ event.waitUntil(self.skipWaiting());
+});
+
+// Claim control over the currently open test page when activating.
+self.addEventListener("activate", function (event) {
+ event.waitUntil(
+ self.clients.claim().then(function () {
+ return postMessage("sw-claimed");
+ })
+ );
+});
+
+// Forward all "push" events to the controlled window.
+self.addEventListener("push", function (event) {
+ event.waitUntil(postMessage("sw-pushed"));
+});
diff --git a/devtools/client/aboutdebugging/test/browser/resources/test-adb-extension/adb-extension-linux.xpi b/devtools/client/aboutdebugging/test/browser/resources/test-adb-extension/adb-extension-linux.xpi
new file mode 100644
index 0000000000..56054c341c
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/test-adb-extension/adb-extension-linux.xpi
Binary files differ
diff --git a/devtools/client/aboutdebugging/test/browser/resources/test-adb-extension/adb-extension-linux64.xpi b/devtools/client/aboutdebugging/test/browser/resources/test-adb-extension/adb-extension-linux64.xpi
new file mode 100644
index 0000000000..9cd737b017
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/test-adb-extension/adb-extension-linux64.xpi
Binary files differ
diff --git a/devtools/client/aboutdebugging/test/browser/resources/test-adb-extension/adb-extension-mac64.xpi b/devtools/client/aboutdebugging/test/browser/resources/test-adb-extension/adb-extension-mac64.xpi
new file mode 100644
index 0000000000..5af9bc963d
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/test-adb-extension/adb-extension-mac64.xpi
Binary files differ
diff --git a/devtools/client/aboutdebugging/test/browser/resources/test-adb-extension/adb-extension-win32.xpi b/devtools/client/aboutdebugging/test/browser/resources/test-adb-extension/adb-extension-win32.xpi
new file mode 100644
index 0000000000..0c10c8502c
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/test-adb-extension/adb-extension-win32.xpi
Binary files differ
diff --git a/devtools/client/aboutdebugging/test/browser/resources/test-temporary-extension/manifest.json b/devtools/client/aboutdebugging/test/browser/resources/test-temporary-extension/manifest.json
new file mode 100644
index 0000000000..c62b2ddbd6
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/test-temporary-extension/manifest.json
@@ -0,0 +1,13 @@
+{
+ "manifest_version": 2,
+ "name": "test-temporary-extension",
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "test-temporary-extension@mozilla.org"
+ }
+ },
+ "background": {
+ "scripts": ["script.js"]
+ }
+}
diff --git a/devtools/client/aboutdebugging/test/browser/resources/test-temporary-extension/script.js b/devtools/client/aboutdebugging/test/browser/resources/test-temporary-extension/script.js
new file mode 100644
index 0000000000..02d5604c3a
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/resources/test-temporary-extension/script.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env browser */
+
+"use strict";
+
+document.body.innerText = "Background Page Body Test Content";
diff --git a/devtools/client/aboutdebugging/test/browser/test-tab-favicons.html b/devtools/client/aboutdebugging/test/browser/test-tab-favicons.html
new file mode 100644
index 0000000000..35954f67a1
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser/test-tab-favicons.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Favicon tab</title>
+ <link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATklEQVRYhe3SIQ4AIBADwf7/04elBAtrVlSduGnSTDJ7cuT1PQJwwO+Hl7sAGAA07gjAAfgIBeAAoHFHAA7ARygABwCNOwJwAD5CATRgAYXh+kypw86nAAAAAElFTkSuQmCC">
+ <head>
+ <body>Some page with a favicon</body>
+</html>
diff --git a/devtools/client/aboutdebugging/test/node/.eslintrc.js b/devtools/client/aboutdebugging/test/node/.eslintrc.js
new file mode 100644
index 0000000000..ffb3e70473
--- /dev/null
+++ b/devtools/client/aboutdebugging/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/aboutdebugging/test/node/README.md b/devtools/client/aboutdebugging/test/node/README.md
new file mode 100644
index 0000000000..58cbe55691
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/node/README.md
@@ -0,0 +1,22 @@
+# Jest Tests for devtools/client/aboutdebugging
+
+## 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/aboutdebugging/test/node
+ yarn && yarn test
+```
+
+## Run on try
+
+The tests run on try on linux64 platforms. The complete name of 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/aboutdebugging/test/node/babel.config.js b/devtools/client/aboutdebugging/test/node/babel.config.js
new file mode 100644
index 0000000000..a9e47ba7fb
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/node/babel.config.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+module.exports = {
+ plugins: ["@babel/plugin-proposal-async-generator-functions"],
+};
diff --git a/devtools/client/aboutdebugging/test/node/components/__snapshots__/shared-message.test.js.snap b/devtools/client/aboutdebugging/test/node/components/__snapshots__/shared-message.test.js.snap
new file mode 100644
index 0000000000..eb1e3ac6ee
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/node/components/__snapshots__/shared-message.test.js.snap
@@ -0,0 +1,91 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Message component renders the expected snapshot for ERROR level 1`] = `
+<aside
+ className="message message--level-error qa-message some-classname-3"
+>
+ <img
+ className="message__icon"
+ src="chrome://devtools/skin/images/aboutdebugging-error.svg"
+ />
+ <div
+ className="message__body"
+ >
+ <div>
+ Message content
+ </div>
+ </div>
+</aside>
+`;
+
+exports[`Message component renders the expected snapshot for INFO level 1`] = `
+<aside
+ className="message message--level-info qa-message some-classname-1"
+>
+ <img
+ className="message__icon"
+ src="chrome://devtools/skin/images/aboutdebugging-information.svg"
+ />
+ <div
+ className="message__body"
+ >
+ <div>
+ Message content
+ </div>
+ </div>
+</aside>
+`;
+
+exports[`Message component renders the expected snapshot for WARNING level 1`] = `
+<aside
+ className="message message--level-warning qa-message some-classname-2"
+>
+ <img
+ className="message__icon"
+ src="chrome://devtools/skin/images/alert.svg"
+ />
+ <div
+ className="message__body"
+ >
+ <div>
+ Message content
+ </div>
+ </div>
+</aside>
+`;
+
+exports[`Message component renders with closing button renders the expected snapshot for Message with closing button 1`] = `
+<aside
+ className="message message--level-info qa-message some-classname-1"
+>
+ <img
+ className="message__icon"
+ src="chrome://devtools/skin/images/aboutdebugging-information.svg"
+ />
+ <div
+ className="message__body"
+ >
+ <div>
+ Message content
+ </div>
+ </div>
+ <button
+ className="ghost-button message__button message__button--info qa-message-button-close-button"
+ onClick={[Function]}
+ >
+ <Localized
+ attrs={
+ Object {
+ "alt": true,
+ }
+ }
+ id="about-debugging-message-close-icon"
+ >
+ <img
+ className="qa-message-button-close-icon"
+ src="chrome://devtools/skin/images/close.svg"
+ />
+ </Localized>
+ </button>
+</aside>
+`;
diff --git a/devtools/client/aboutdebugging/test/node/components/shared-message.test.js b/devtools/client/aboutdebugging/test/node/components/shared-message.test.js
new file mode 100644
index 0000000000..21a3f44c58
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/node/components/shared-message.test.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Unit tests for the shared/Message component.
+ */
+
+const { shallow } = require("enzyme");
+const React = require("react");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const Message = React.createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/Message.js")
+);
+
+describe("Message component", () => {
+ it("renders the expected snapshot for INFO level", () => {
+ const message = shallow(
+ Message({
+ children: dom.div({}, "Message content"),
+ className: "some-classname-1",
+ level: MESSAGE_LEVEL.INFO,
+ })
+ );
+ expect(message).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot for WARNING level", () => {
+ const message = shallow(
+ Message({
+ children: dom.div({}, "Message content"),
+ className: "some-classname-2",
+ level: MESSAGE_LEVEL.WARNING,
+ })
+ );
+ expect(message).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot for ERROR level", () => {
+ const message = shallow(
+ Message({
+ children: dom.div({}, "Message content"),
+ className: "some-classname-3",
+ level: MESSAGE_LEVEL.ERROR,
+ })
+ );
+ expect(message).toMatchSnapshot();
+ });
+});
+
+describe("Message component renders with closing button", () => {
+ it("renders the expected snapshot for Message with closing button", () => {
+ const message = shallow(
+ Message({
+ children: dom.div({}, "Message content"),
+ className: "some-classname-1",
+ level: MESSAGE_LEVEL.INFO,
+ isCloseable: true,
+ })
+ );
+ expect(message).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/aboutdebugging/test/node/jest.config.js b/devtools/client/aboutdebugging/test/node/jest.config.js
new file mode 100644
index 0000000000..e114658f88
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/node/jest.config.js
@@ -0,0 +1,14 @@
+/* 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"],
+ snapshotSerializers: ["enzyme-to-json/serializer"],
+};
diff --git a/devtools/client/aboutdebugging/test/node/package.json b/devtools/client/aboutdebugging/test/node/package.json
new file mode 100644
index 0000000000..db8fa83e5d
--- /dev/null
+++ b/devtools/client/aboutdebugging/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",
+ "enzyme": "^3.9.0",
+ "enzyme-adapter-react-16": "^1.13.2",
+ "enzyme-to-json": "^3.3.5",
+ "jest": "^24.6.0",
+ "react": "16.4.1",
+ "react-dom": "16.4.1",
+ "react-test-renderer": "16.4.1"
+ }
+}
diff --git a/devtools/client/aboutdebugging/test/node/setup.js b/devtools/client/aboutdebugging/test/node/setup.js
new file mode 100644
index 0000000000..570e4462ae
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/node/setup.js
@@ -0,0 +1,15 @@
+/* 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";
+
+// Configure enzyme with React 16 adapter.
+const Enzyme = require("enzyme");
+const Adapter = require("enzyme-adapter-react-16");
+Enzyme.configure({ adapter: new Adapter() });
+
+const {
+ setMocksInGlobal,
+} = require("resource://devtools/client/shared/test-helpers/shared-node-helpers.js");
+setMocksInGlobal();
diff --git a/devtools/client/aboutdebugging/test/node/yarn.lock b/devtools/client/aboutdebugging/test/node/yarn.lock
new file mode 100644
index 0000000000..396c84ebcd
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/node/yarn.lock
@@ -0,0 +1,4133 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
+ integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==
+ dependencies:
+ "@babel/highlight" "^7.0.0"
+
+"@babel/core@^7.1.0":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.5.tgz#081f97e8ffca65a9b4b0fdc7e274e703f000c06a"
+ integrity sha512-OvjIh6aqXtlsA8ujtGKfC7LYWksYSX8yQcM8Ay3LuvVeQ63lcOKgoZWVqcpFwkd29aYU9rVx7jxhfhiEDV9MZA==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@babel/generator" "^7.4.4"
+ "@babel/helpers" "^7.4.4"
+ "@babel/parser" "^7.4.5"
+ "@babel/template" "^7.4.4"
+ "@babel/traverse" "^7.4.5"
+ "@babel/types" "^7.4.4"
+ convert-source-map "^1.1.0"
+ debug "^4.1.0"
+ json5 "^2.1.0"
+ lodash "^4.17.11"
+ resolve "^1.3.2"
+ semver "^5.4.1"
+ source-map "^0.5.0"
+
+"@babel/generator@^7.4.0", "@babel/generator@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041"
+ integrity sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==
+ dependencies:
+ "@babel/types" "^7.4.4"
+ jsesc "^2.5.1"
+ lodash "^4.17.11"
+ source-map "^0.5.0"
+ trim-right "^1.0.1"
+
+"@babel/helper-annotate-as-pure@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32"
+ integrity sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-function-name@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53"
+ integrity sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==
+ dependencies:
+ "@babel/helper-get-function-arity" "^7.0.0"
+ "@babel/template" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-get-function-arity@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3"
+ integrity sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-plugin-utils@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250"
+ integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==
+
+"@babel/helper-remap-async-to-generator@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz#361d80821b6f38da75bd3f0785ece20a88c5fe7f"
+ integrity sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.0.0"
+ "@babel/helper-wrap-function" "^7.1.0"
+ "@babel/template" "^7.1.0"
+ "@babel/traverse" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-split-export-declaration@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677"
+ integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==
+ dependencies:
+ "@babel/types" "^7.4.4"
+
+"@babel/helper-wrap-function@^7.1.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa"
+ integrity sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==
+ dependencies:
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/template" "^7.1.0"
+ "@babel/traverse" "^7.1.0"
+ "@babel/types" "^7.2.0"
+
+"@babel/helpers@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.4.tgz#868b0ef59c1dd4e78744562d5ce1b59c89f2f2a5"
+ integrity sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A==
+ dependencies:
+ "@babel/template" "^7.4.4"
+ "@babel/traverse" "^7.4.4"
+ "@babel/types" "^7.4.4"
+
+"@babel/highlight@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4"
+ integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==
+ dependencies:
+ chalk "^2.0.0"
+ esutils "^2.0.2"
+ js-tokens "^4.0.0"
+
+"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.4.4", "@babel/parser@^7.4.5":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872"
+ integrity sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==
+
+"@babel/plugin-proposal-async-generator-functions@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e"
+ integrity sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-remap-async-to-generator" "^7.1.0"
+ "@babel/plugin-syntax-async-generators" "^7.2.0"
+
+"@babel/plugin-syntax-async-generators@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz#69e1f0db34c6f5a0cf7e2b3323bf159a76c8cb7f"
+ integrity sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-syntax-object-rest-spread@^7.0.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e"
+ integrity sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237"
+ integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@babel/parser" "^7.4.4"
+ "@babel/types" "^7.4.4"
+
+"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.4", "@babel/traverse@^7.4.5":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.5.tgz#4e92d1728fd2f1897dafdd321efbff92156c3216"
+ integrity sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@babel/generator" "^7.4.4"
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/helper-split-export-declaration" "^7.4.4"
+ "@babel/parser" "^7.4.5"
+ "@babel/types" "^7.4.4"
+ debug "^4.1.0"
+ globals "^11.1.0"
+ lodash "^4.17.11"
+
+"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0"
+ integrity sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==
+ dependencies:
+ esutils "^2.0.2"
+ lodash "^4.17.11"
+ to-fast-properties "^2.0.0"
+
+"@cnakazawa/watch@^1.0.3":
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
+ integrity sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA==
+ dependencies:
+ exec-sh "^0.3.2"
+ minimist "^1.2.0"
+
+"@jest/console@^24.7.1":
+ version "24.7.1"
+ resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545"
+ integrity sha512-iNhtIy2M8bXlAOULWVTUxmnelTLFneTNEkHCgPmgd+zNwy9zVddJ6oS5rZ9iwoscNdT5mMwUd0C51v/fSlzItg==
+ dependencies:
+ "@jest/source-map" "^24.3.0"
+ chalk "^2.0.1"
+ slash "^2.0.0"
+
+"@jest/core@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.8.0.tgz#fbbdcd42a41d0d39cddbc9f520c8bab0c33eed5b"
+ integrity sha512-R9rhAJwCBQzaRnrRgAdVfnglUuATXdwTRsYqs6NMdVcAl5euG8LtWDe+fVkN27YfKVBW61IojVsXKaOmSnqd/A==
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/reporters" "^24.8.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/transform" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ graceful-fs "^4.1.15"
+ jest-changed-files "^24.8.0"
+ jest-config "^24.8.0"
+ jest-haste-map "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-regex-util "^24.3.0"
+ jest-resolve-dependencies "^24.8.0"
+ jest-runner "^24.8.0"
+ jest-runtime "^24.8.0"
+ jest-snapshot "^24.8.0"
+ jest-util "^24.8.0"
+ jest-validate "^24.8.0"
+ jest-watcher "^24.8.0"
+ micromatch "^3.1.10"
+ p-each-series "^1.0.0"
+ pirates "^4.0.1"
+ realpath-native "^1.1.0"
+ rimraf "^2.5.4"
+ strip-ansi "^5.0.0"
+
+"@jest/environment@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.8.0.tgz#0342261383c776bdd652168f68065ef144af0eac"
+ integrity sha512-vlGt2HLg7qM+vtBrSkjDxk9K0YtRBi7HfRFaDxoRtyi+DyVChzhF20duvpdAnKVBV6W5tym8jm0U9EfXbDk1tw==
+ dependencies:
+ "@jest/fake-timers" "^24.8.0"
+ "@jest/transform" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ jest-mock "^24.8.0"
+
+"@jest/fake-timers@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.8.0.tgz#2e5b80a4f78f284bcb4bd5714b8e10dd36a8d3d1"
+ integrity sha512-2M4d5MufVXwi6VzZhJ9f5S/wU4ud2ck0kxPof1Iz3zWx6Y+V2eJrES9jEktB6O3o/oEyk+il/uNu9PvASjWXQw==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-mock "^24.8.0"
+
+"@jest/reporters@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.8.0.tgz#075169cd029bddec54b8f2c0fc489fd0b9e05729"
+ integrity sha512-eZ9TyUYpyIIXfYCrw0UHUWUvE35vx5I92HGMgS93Pv7du+GHIzl+/vh8Qj9MCWFK/4TqyttVBPakWMOfZRIfxw==
+ dependencies:
+ "@jest/environment" "^24.8.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/transform" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ glob "^7.1.2"
+ istanbul-lib-coverage "^2.0.2"
+ istanbul-lib-instrument "^3.0.1"
+ istanbul-lib-report "^2.0.4"
+ istanbul-lib-source-maps "^3.0.1"
+ istanbul-reports "^2.1.1"
+ jest-haste-map "^24.8.0"
+ jest-resolve "^24.8.0"
+ jest-runtime "^24.8.0"
+ jest-util "^24.8.0"
+ jest-worker "^24.6.0"
+ node-notifier "^5.2.1"
+ slash "^2.0.0"
+ source-map "^0.6.0"
+ string-length "^2.0.0"
+
+"@jest/source-map@^24.3.0":
+ version "24.3.0"
+ resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.3.0.tgz#563be3aa4d224caf65ff77edc95cd1ca4da67f28"
+ integrity sha512-zALZt1t2ou8le/crCeeiRYzvdnTzaIlpOWaet45lNSqNJUnXbppUUFR4ZUAlzgDmKee4Q5P/tKXypI1RiHwgag==
+ dependencies:
+ callsites "^3.0.0"
+ graceful-fs "^4.1.15"
+ source-map "^0.6.0"
+
+"@jest/test-result@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.8.0.tgz#7675d0aaf9d2484caa65e048d9b467d160f8e9d3"
+ integrity sha512-+YdLlxwizlfqkFDh7Mc7ONPQAhA4YylU1s529vVM1rsf67vGZH/2GGm5uO8QzPeVyaVMobCQ7FTxl38QrKRlng==
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/types" "^24.8.0"
+ "@types/istanbul-lib-coverage" "^2.0.0"
+
+"@jest/test-sequencer@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.8.0.tgz#2f993bcf6ef5eb4e65e8233a95a3320248cf994b"
+ integrity sha512-OzL/2yHyPdCHXEzhoBuq37CE99nkme15eHkAzXRVqthreWZamEMA0WoetwstsQBCXABhczpK03JNbc4L01vvLg==
+ dependencies:
+ "@jest/test-result" "^24.8.0"
+ jest-haste-map "^24.8.0"
+ jest-runner "^24.8.0"
+ jest-runtime "^24.8.0"
+
+"@jest/transform@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-24.8.0.tgz#628fb99dce4f9d254c6fd9341e3eea262e06fef5"
+ integrity sha512-xBMfFUP7TortCs0O+Xtez2W7Zu1PLH9bvJgtraN1CDST6LBM/eTOZ9SfwS/lvV8yOfcDpFmwf9bq5cYbXvqsvA==
+ dependencies:
+ "@babel/core" "^7.1.0"
+ "@jest/types" "^24.8.0"
+ babel-plugin-istanbul "^5.1.0"
+ chalk "^2.0.1"
+ convert-source-map "^1.4.0"
+ fast-json-stable-stringify "^2.0.0"
+ graceful-fs "^4.1.15"
+ jest-haste-map "^24.8.0"
+ jest-regex-util "^24.3.0"
+ jest-util "^24.8.0"
+ micromatch "^3.1.10"
+ realpath-native "^1.1.0"
+ slash "^2.0.0"
+ source-map "^0.6.1"
+ write-file-atomic "2.4.1"
+
+"@jest/types@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.8.0.tgz#f31e25948c58f0abd8c845ae26fcea1491dea7ad"
+ integrity sha512-g17UxVr2YfBtaMUxn9u/4+siG1ptg9IGYAYwvpwn61nBg779RXnjE/m7CxYcIzEt0AbHZZAHSEZNhkE2WxURVg==
+ dependencies:
+ "@types/istanbul-lib-coverage" "^2.0.0"
+ "@types/istanbul-reports" "^1.1.1"
+ "@types/yargs" "^12.0.9"
+
+"@types/babel__core@^7.1.0":
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f"
+ integrity sha512-cfCCrFmiGY/yq0NuKNxIQvZFy9kY/1immpSpTngOnyIbD4+eJOG5mxphhHDv3CHL9GltO4GcKr54kGBg3RNdbg==
+ 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.0.2"
+ resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.0.2.tgz#d2112a6b21fad600d7674274293c85dce0cb47fc"
+ integrity sha512-NHcOfab3Zw4q5sEE2COkpfXjoE7o+PmqD9DQW4koUT3roNxwziUdXGnRndMat/LJNUtePwn1TlP4do3uoe3KZQ==
+ 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"
+ integrity sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6":
+ version "7.0.6"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.6.tgz#328dd1a8fc4cfe3c8458be9477b219ea158fd7b2"
+ integrity sha512-XYVgHF2sQ0YblLRMLNPB3CkFMewzFmlDsH/TneZFHUXDlABQgh88uOxuez7ZcXxayLFrqLwtDH1t+FmlFwNZxw==
+ dependencies:
+ "@babel/types" "^7.3.0"
+
+"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
+ integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==
+
+"@types/istanbul-lib-report@*":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#e5471e7fa33c61358dd38426189c037a58433b8c"
+ integrity sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==
+ 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"
+ integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==
+ dependencies:
+ "@types/istanbul-lib-coverage" "*"
+ "@types/istanbul-lib-report" "*"
+
+"@types/node@*":
+ version "12.0.3"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.3.tgz#5d8d24e0033fc6393efadc85cb59c1f638095c9a"
+ integrity sha512-zkOxCS/fA+3SsdA+9Yun0iANxzhQRiNwTvJSr6N95JhuJ/x27z9G2URx1Jpt3zYFfCGUXZGL5UDxt5eyLE7wgw==
+
+"@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"
+ integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
+
+"@types/yargs@^12.0.2", "@types/yargs@^12.0.9":
+ version "12.0.12"
+ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916"
+ integrity sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==
+
+abab@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f"
+ integrity sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==
+
+abbrev@1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+ integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
+
+acorn-globals@^4.1.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.0.tgz#e3b6f8da3c1552a95ae627571f7dd6923bb54103"
+ integrity sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==
+ 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"
+ integrity sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==
+
+acorn@^5.5.3:
+ version "5.7.3"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
+ integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
+
+acorn@^6.0.1:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f"
+ integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==
+
+airbnb-prop-types@^2.13.2:
+ version "2.13.2"
+ resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.13.2.tgz#43147a5062dd2a4a5600e748a47b64004cc5f7fc"
+ integrity sha512-2FN6DlHr6JCSxPPi25EnqGaXC4OC3/B3k1lCd6MMYrZ51/Gf/1qDfaR+JElzWa+Tl7cY2aYOlsYJGFeQyVHIeQ==
+ dependencies:
+ array.prototype.find "^2.0.4"
+ function.prototype.name "^1.1.0"
+ has "^1.0.3"
+ is-regex "^1.0.4"
+ object-is "^1.0.1"
+ object.assign "^4.1.0"
+ object.entries "^1.1.0"
+ prop-types "^15.7.2"
+ prop-types-exact "^1.2.0"
+ react-is "^16.8.6"
+
+ajv@^6.5.5:
+ version "6.10.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
+ integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==
+ 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@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
+ integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
+
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+ integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+ integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
+
+ansi-regex@^4.0.0, ansi-regex@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
+ integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
+
+ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+ dependencies:
+ color-convert "^1.9.0"
+
+anymatch@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+ integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==
+ dependencies:
+ micromatch "^3.1.4"
+ normalize-path "^2.1.1"
+
+aproba@^1.0.3:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+ integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
+
+are-we-there-yet@~1.1.2:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
+ integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.6"
+
+arr-diff@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+ integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
+
+arr-flatten@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+ integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
+
+arr-union@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+ integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
+
+array-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
+ integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
+
+array-filter@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
+ integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
+
+array-unique@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+ integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+
+array.prototype.find@^2.0.4:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.0.tgz#630f2eaf70a39e608ac3573e45cf8ccd0ede9ad7"
+ integrity sha512-Wn41+K1yuO5p7wRZDl7890c3xvv5UBrfVXTVIe28rSQb6LS0fZMDrQB6PAcxQFRFy6vJTLDc3A2+3CjQdzVKRg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.13.0"
+
+array.prototype.flat@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz#812db8f02cad24d3fab65dd67eabe3b8903494a4"
+ integrity sha512-rVqIs330nLJvfC7JqYvEWwqVr5QjYF1ib02i3YJtR/fICO6527Tjpc/e4Mvmxh3GIePPreRXMdaGyC99YphWEw==
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.10.0"
+ function-bind "^1.1.1"
+
+asap@~2.0.3:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+ integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
+
+asn1@~0.2.3:
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
+ integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
+ 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"
+ integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
+
+assign-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+ integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
+
+astral-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
+ integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
+
+async-limiter@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
+ integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+ integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+
+atob@^2.1.1:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
+ integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+
+aws-sign2@~0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+ integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
+
+aws4@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
+ integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
+
+babel-jest@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.8.0.tgz#5c15ff2b28e20b0f45df43fe6b7f2aae93dba589"
+ integrity sha512-+5/kaZt4I9efoXzPlZASyK/lN9qdRKmmUav9smVc0ruPQD7IsfucQ87gpOE8mn2jbDuS6M/YOW6n3v9ZoIfgnw==
+ dependencies:
+ "@jest/transform" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ "@types/babel__core" "^7.1.0"
+ babel-plugin-istanbul "^5.1.0"
+ babel-preset-jest "^24.6.0"
+ chalk "^2.4.2"
+ slash "^2.0.0"
+
+babel-plugin-istanbul@^5.1.0:
+ version "5.1.4"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.4.tgz#841d16b9a58eeb407a0ddce622ba02fe87a752ba"
+ integrity sha512-dySz4VJMH+dpndj0wjJ8JPs/7i1TdSPb1nRrn56/92pKOF9VKC1FMFJmMXjzlGGusnCAqujP6PBCiKq0sVA+YQ==
+ dependencies:
+ find-up "^3.0.0"
+ istanbul-lib-instrument "^3.3.0"
+ test-exclude "^5.2.3"
+
+babel-plugin-jest-hoist@^24.6.0:
+ version "24.6.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.6.0.tgz#f7f7f7ad150ee96d7a5e8e2c5da8319579e78019"
+ integrity sha512-3pKNH6hMt9SbOv0F3WVmy5CWQ4uogS3k0GY5XLyQHJ9EGpAT9XWkFd2ZiXXtkwFHdAHa5j7w7kfxSP5lAIwu7w==
+ dependencies:
+ "@types/babel__traverse" "^7.0.6"
+
+babel-preset-jest@^24.6.0:
+ version "24.6.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.6.0.tgz#66f06136eefce87797539c0d63f1769cc3915984"
+ integrity sha512-pdZqLEdmy1ZK5kyRUfvBb2IfTPb2BUvIJczlPspS8fWmBQslNNDBqVfh7BW5leOVJMDZKzjD8XEyABTk6gQ5yw==
+ dependencies:
+ "@babel/plugin-syntax-object-rest-spread" "^7.0.0"
+ babel-plugin-jest-hoist "^24.6.0"
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+ integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+
+base@^0.11.1:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+ integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==
+ 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"
+ integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
+ dependencies:
+ tweetnacl "^0.14.3"
+
+boolbase@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+ integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ 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"
+ integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
+ 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"
+
+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"
+ integrity sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==
+
+browser-resolve@^1.11.3:
+ version "1.11.3"
+ resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6"
+ integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==
+ 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"
+ integrity sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=
+ 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"
+ integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+
+cache-base@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+ integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==
+ 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"
+ integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+camelcase@^5.0.0:
+ version "5.3.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+ integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
+capture-exit@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4"
+ integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==
+ 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"
+ integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
+
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.2:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+ integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+cheerio@^1.0.0-rc.2:
+ version "1.0.0-rc.3"
+ resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6"
+ integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==
+ dependencies:
+ css-select "~1.2.0"
+ dom-serializer "~0.1.1"
+ entities "~1.1.1"
+ htmlparser2 "^3.9.1"
+ lodash "^4.15.0"
+ parse5 "^3.0.1"
+
+chownr@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
+ integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==
+
+ci-info@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
+ integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
+
+class-utils@^0.3.5:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+ integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
+ dependencies:
+ arr-union "^3.1.0"
+ define-property "^0.2.5"
+ isobject "^3.0.0"
+ static-extend "^0.1.1"
+
+cliui@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
+ integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==
+ dependencies:
+ string-width "^2.1.1"
+ strip-ansi "^4.0.0"
+ wrap-ansi "^2.0.0"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+ integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
+
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+ integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
+
+collection-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+ integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=
+ 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"
+ integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+ dependencies:
+ color-name "1.1.3"
+
+color-name@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+ integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+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"
+ integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commander@^2.19.0:
+ version "2.20.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
+ integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
+
+commander@~2.19.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
+ integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==
+
+component-emitter@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
+ integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+ integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
+
+convert-source-map@^1.1.0, 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"
+ integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==
+ 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"
+ integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
+
+core-js@^1.0.0:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
+ integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
+
+core-util-is@1.0.2, core-util-is@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+ integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+
+cross-spawn@^6.0.0:
+ version "6.0.5"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+ integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+ dependencies:
+ nice-try "^1.0.4"
+ path-key "^2.0.1"
+ semver "^5.5.0"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+css-select@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
+ integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
+ dependencies:
+ boolbase "~1.0.0"
+ css-what "2.1"
+ domutils "1.5.1"
+ nth-check "~1.0.1"
+
+css-what@2.1:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
+ integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
+
+cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.6.tgz#f85206cee04efa841f3c5982a74ba96ab20d65ad"
+ integrity sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==
+
+cssstyle@^1.0.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.2.1.tgz#3aceb2759eaf514ac1a21628d723d6043a819495"
+ integrity sha512-7DYm8qe+gPx/h77QlCyFmX80+fGaE/6A/Ekl0zaszYOubvySO2saYFdQ78P29D0UsULxFKCetDGNaNRUdSF+2A==
+ dependencies:
+ cssom "0.3.x"
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
+ dependencies:
+ assert-plus "^1.0.0"
+
+data-urls@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
+ integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==
+ dependencies:
+ abab "^2.0.0"
+ whatwg-mimetype "^2.2.0"
+ whatwg-url "^7.0.0"
+
+debug@^2.1.2, debug@^2.2.0, debug@^2.3.3:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ 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"
+ integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
+ 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"
+ integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+
+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"
+ integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
+
+deep-extend@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
+ integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
+
+deep-is@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+ integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
+
+define-properties@^1.1.2, define-properties@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+ integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+ 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"
+ integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=
+ 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"
+ integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY=
+ 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"
+ integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==
+ 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"
+ integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+ integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
+
+detect-libc@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
+ integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
+
+detect-newline@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
+ integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=
+
+diff-sequences@^24.3.0:
+ version "24.3.0"
+ resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.3.0.tgz#0f20e8a1df1abddaf4d9c226680952e64118b975"
+ integrity sha512-xLqpez+Zj9GKSnPWS0WZw1igGocZ+uua8+y+5dDNTT934N3QuY1sp2LkHzwiaYQGz60hMq0pjAshdeXm5VUOEw==
+
+discontinuous-range@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
+ integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=
+
+dom-serializer@0, dom-serializer@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
+ integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
+ dependencies:
+ domelementtype "^1.3.0"
+ entities "^1.1.1"
+
+domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
+ integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
+
+domexception@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
+ integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==
+ dependencies:
+ webidl-conversions "^4.0.2"
+
+domhandler@^2.3.0:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+ integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
+ dependencies:
+ domelementtype "1"
+
+domutils@1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+ integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+domutils@^1.5.1:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+ integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+ecc-jsbn@~0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
+ integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
+ dependencies:
+ jsbn "~0.1.0"
+ safer-buffer "^2.1.0"
+
+encoding@^0.1.11:
+ version "0.1.12"
+ resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
+ integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=
+ dependencies:
+ iconv-lite "~0.4.13"
+
+end-of-stream@^1.1.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
+ integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==
+ dependencies:
+ once "^1.4.0"
+
+entities@^1.1.1, entities@~1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
+ integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
+
+enzyme-adapter-react-16@^1.13.2:
+ version "1.13.2"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.13.2.tgz#8a574d7cbbef7ef0cab2022e9bfc12aeaebb7ae5"
+ integrity sha512-h0neTuAAFfQUgEZ+PPHVIMDFJ9+CGafI8AjojNlSVh4Fd1pLDgtl2OeVkm4yKF7RSgzrPAwugq4JW8Jjo2iRJA==
+ dependencies:
+ enzyme-adapter-utils "^1.12.0"
+ has "^1.0.3"
+ object.assign "^4.1.0"
+ object.values "^1.1.0"
+ prop-types "^15.7.2"
+ react-is "^16.8.6"
+ react-test-renderer "^16.0.0-0"
+ semver "^5.7.0"
+
+enzyme-adapter-utils@^1.12.0:
+ version "1.12.0"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.12.0.tgz#96e3730d76b872f593e54ce1c51fa3a451422d93"
+ integrity sha512-wkZvE0VxcFx/8ZsBw0iAbk3gR1d9hK447ebnSYBf95+r32ezBq+XDSAvRErkc4LZosgH8J7et7H7/7CtUuQfBA==
+ dependencies:
+ airbnb-prop-types "^2.13.2"
+ function.prototype.name "^1.1.0"
+ object.assign "^4.1.0"
+ object.fromentries "^2.0.0"
+ prop-types "^15.7.2"
+ semver "^5.6.0"
+
+enzyme-to-json@^3.3.5:
+ version "3.3.5"
+ resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.3.5.tgz#f8eb82bd3d5941c9d8bc6fd9140030777d17d0af"
+ integrity sha512-DmH1wJ68HyPqKSYXdQqB33ZotwfUhwQZW3IGXaNXgR69Iodaoj8TF/D9RjLdz4pEhGq2Tx2zwNUIjBuqoZeTgA==
+ dependencies:
+ lodash "^4.17.4"
+
+enzyme@^3.9.0:
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.9.0.tgz#2b491f06ca966eb56b6510068c7894a7e0be3909"
+ integrity sha512-JqxI2BRFHbmiP7/UFqvsjxTirWoM1HfeaJrmVSZ9a1EADKkZgdPcAuISPMpoUiHlac9J4dYt81MC5BBIrbJGMg==
+ dependencies:
+ array.prototype.flat "^1.2.1"
+ cheerio "^1.0.0-rc.2"
+ function.prototype.name "^1.1.0"
+ has "^1.0.3"
+ html-element-map "^1.0.0"
+ is-boolean-object "^1.0.0"
+ is-callable "^1.1.4"
+ is-number-object "^1.0.3"
+ is-regex "^1.0.4"
+ is-string "^1.0.4"
+ is-subset "^0.1.1"
+ lodash.escape "^4.0.1"
+ lodash.isequal "^4.5.0"
+ object-inspect "^1.6.0"
+ object-is "^1.0.1"
+ object.assign "^4.1.0"
+ object.entries "^1.0.4"
+ object.values "^1.0.4"
+ raf "^3.4.0"
+ rst-selector-parser "^2.2.3"
+ string.prototype.trim "^1.1.2"
+
+error-ex@^1.3.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+ integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+ dependencies:
+ is-arrayish "^0.2.1"
+
+es-abstract@^1.10.0, es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.13.0, es-abstract@^1.5.0, es-abstract@^1.5.1:
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
+ integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==
+ 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"
+ integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==
+ 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"
+ integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+escodegen@^1.9.1:
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.1.tgz#c485ff8d6b4cdb89e27f4a856e91f118401ca510"
+ integrity sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==
+ dependencies:
+ esprima "^3.1.3"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ optionator "^0.8.1"
+ optionalDependencies:
+ source-map "~0.6.1"
+
+esprima@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
+ integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=
+
+estraverse@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+ integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=
+
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+ integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=
+
+exec-sh@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b"
+ integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg==
+
+execa@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
+ integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
+ 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"
+
+exit@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
+ integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=
+
+expand-brackets@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+ integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI=
+ 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@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/expect/-/expect-24.8.0.tgz#471f8ec256b7b6129ca2524b2a62f030df38718d"
+ integrity sha512-/zYvP8iMDrzaaxHVa724eJBCKqSHmO0FA7EDkBiRHxg6OipmMn1fN+C8T9L9K8yr7UONkOifu6+LLH+z76CnaA==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ ansi-styles "^3.2.0"
+ jest-get-type "^24.8.0"
+ jest-matcher-utils "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-regex-util "^24.3.0"
+
+extend-shallow@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+ integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
+ 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"
+ integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=
+ 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"
+ integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
+extglob@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+ integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==
+ 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"
+ integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
+
+extsprintf@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+ integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+
+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"
+ integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
+
+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"
+ integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I=
+
+fast-levenshtein@~2.0.4:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+ integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+
+fb-watchman@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
+ integrity sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=
+ 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"
+ integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
+ 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"
+ integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+ to-regex-range "^2.1.0"
+
+find-up@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+ integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+ dependencies:
+ locate-path "^3.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"
+ integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+ integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
+
+form-data@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+ integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+ 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"
+ integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=
+ dependencies:
+ map-cache "^0.2.2"
+
+fs-minipass@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
+ integrity sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==
+ dependencies:
+ minipass "^2.2.1"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+fsevents@^1.2.7:
+ version "1.2.9"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f"
+ integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==
+ dependencies:
+ nan "^2.12.1"
+ node-pre-gyp "^0.12.0"
+
+function-bind@^1.0.2, function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+function.prototype.name@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.0.tgz#8bd763cc0af860a859cc5d49384d74b932cd2327"
+ integrity sha512-Bs0VRrTz4ghD8pTmbJQD1mZ8A/mN0ur/jGz+A6FBxPDUPkm1tNfF6bhTYPA7i7aF4lZJVr+OXTNNrnnIl58Wfg==
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ is-callable "^1.1.3"
+
+gauge@~2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+ integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
+ dependencies:
+ aproba "^1.0.3"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.0"
+ object-assign "^4.1.0"
+ signal-exit "^3.0.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wide-align "^1.1.0"
+
+get-caller-file@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
+ integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
+
+get-stream@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
+ integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
+ 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"
+ integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
+ 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"
+ integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
+ 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"
+ integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2:
+ version "4.1.15"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
+ integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
+
+growly@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
+ integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=
+
+handlebars@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67"
+ integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==
+ dependencies:
+ neo-async "^2.6.0"
+ optimist "^0.6.1"
+ source-map "^0.6.1"
+ optionalDependencies:
+ uglify-js "^3.1.4"
+
+har-schema@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+ integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
+
+har-validator@~5.1.0:
+ version "5.1.3"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+ integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
+ 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"
+ integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
+ integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=
+
+has-unicode@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+ integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
+
+has-value@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+ integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=
+ 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"
+ integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=
+ 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"
+ integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E=
+
+has-values@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+ integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=
+ 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"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+hosted-git-info@^2.1.4:
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
+ integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==
+
+html-element-map@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.0.1.tgz#3c4fcb4874ebddfe4283b51c8994e7713782b592"
+ integrity sha512-BZSfdEm6n706/lBfXKWa4frZRZcT5k1cOusw95ijZsHlI+GdgY0v95h6IzO3iIDf2ROwq570YTwqNPqHcNMozw==
+ dependencies:
+ array-filter "^1.0.0"
+
+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"
+ integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==
+ dependencies:
+ whatwg-encoding "^1.0.1"
+
+htmlparser2@^3.9.1:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
+ integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
+ dependencies:
+ domelementtype "^1.3.1"
+ domhandler "^2.3.0"
+ domutils "^1.5.1"
+ entities "^1.1.1"
+ inherits "^2.0.1"
+ readable-stream "^3.1.1"
+
+http-signature@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+ integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+ dependencies:
+ assert-plus "^1.0.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+iconv-lite@0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+ integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+ignore-walk@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
+ integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==
+ dependencies:
+ minimatch "^3.0.4"
+
+import-local@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
+ integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==
+ dependencies:
+ pkg-dir "^3.0.0"
+ resolve-cwd "^2.0.0"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+ integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+ integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
+
+ini@~1.3.0:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
+ integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+
+invariant@^2.2.4:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+ integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
+ dependencies:
+ loose-envify "^1.0.0"
+
+invert-kv@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
+ integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
+
+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"
+ integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=
+ 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"
+ integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
+ dependencies:
+ kind-of "^6.0.0"
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+ integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
+
+is-boolean-object@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93"
+ integrity sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M=
+
+is-buffer@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+ integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+
+is-callable@^1.1.3, is-callable@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
+ integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==
+
+is-ci@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
+ integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
+ 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"
+ integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=
+ 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"
+ integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
+ 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"
+ integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=
+
+is-descriptor@^0.1.0:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+ integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
+ 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"
+ integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
+ 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"
+ integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
+
+is-extendable@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+ integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
+ dependencies:
+ is-plain-object "^2.0.4"
+
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+ integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+
+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"
+ integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==
+
+is-number-object@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.3.tgz#f265ab89a9f445034ef6aff15a8f00b00f551799"
+ integrity sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=
+
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=
+ dependencies:
+ kind-of "^3.0.2"
+
+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"
+ integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+ 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"
+ integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=
+ 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"
+ integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
+
+is-string@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.4.tgz#cc3a9b69857d621e963725a24caeec873b826e64"
+ integrity sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ=
+
+is-subset@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
+ integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=
+
+is-symbol@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38"
+ integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==
+ dependencies:
+ has-symbols "^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"
+ integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+
+is-windows@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+ integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+
+is-wsl@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
+ integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
+
+isarray@1.0.0, isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+ integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
+ 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"
+ integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+
+isomorphic-fetch@^2.1.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
+ integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
+ 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"
+ integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+
+istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49"
+ integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==
+
+istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630"
+ integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==
+ dependencies:
+ "@babel/generator" "^7.4.0"
+ "@babel/parser" "^7.4.3"
+ "@babel/template" "^7.4.0"
+ "@babel/traverse" "^7.4.3"
+ "@babel/types" "^7.4.0"
+ istanbul-lib-coverage "^2.0.5"
+ semver "^6.0.0"
+
+istanbul-lib-report@^2.0.4:
+ version "2.0.8"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33"
+ integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==
+ dependencies:
+ istanbul-lib-coverage "^2.0.5"
+ make-dir "^2.1.0"
+ supports-color "^6.1.0"
+
+istanbul-lib-source-maps@^3.0.1:
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8"
+ integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==
+ dependencies:
+ debug "^4.1.1"
+ istanbul-lib-coverage "^2.0.5"
+ make-dir "^2.1.0"
+ rimraf "^2.6.3"
+ source-map "^0.6.1"
+
+istanbul-reports@^2.1.1:
+ version "2.2.6"
+ resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.6.tgz#7b4f2660d82b29303a8fe6091f8ca4bf058da1af"
+ integrity sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==
+ dependencies:
+ handlebars "^4.1.2"
+
+jest-changed-files@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.8.0.tgz#7e7eb21cf687587a85e50f3d249d1327e15b157b"
+ integrity sha512-qgANC1Yrivsq+UrLXsvJefBKVoCsKB0Hv+mBb6NMjjZ90wwxCDmU3hsCXBya30cH+LnPYjwgcU65i6yJ5Nfuug==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ execa "^1.0.0"
+ throat "^4.0.0"
+
+jest-cli@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.8.0.tgz#b075ac914492ed114fa338ade7362a301693e989"
+ integrity sha512-+p6J00jSMPQ116ZLlHJJvdf8wbjNbZdeSX9ptfHX06/MSNaXmKihQzx5vQcw0q2G6JsdVkUIdWbOWtSnaYs3yA==
+ dependencies:
+ "@jest/core" "^24.8.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ import-local "^2.0.0"
+ is-ci "^2.0.0"
+ jest-config "^24.8.0"
+ jest-util "^24.8.0"
+ jest-validate "^24.8.0"
+ prompts "^2.0.1"
+ realpath-native "^1.1.0"
+ yargs "^12.0.2"
+
+jest-config@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.8.0.tgz#77db3d265a6f726294687cbbccc36f8a76ee0f4f"
+ integrity sha512-Czl3Nn2uEzVGsOeaewGWoDPD8GStxCpAe0zOYs2x2l0fZAgPbCr3uwUkgNKV3LwE13VXythM946cd5rdGkkBZw==
+ dependencies:
+ "@babel/core" "^7.1.0"
+ "@jest/test-sequencer" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ babel-jest "^24.8.0"
+ chalk "^2.0.1"
+ glob "^7.1.1"
+ jest-environment-jsdom "^24.8.0"
+ jest-environment-node "^24.8.0"
+ jest-get-type "^24.8.0"
+ jest-jasmine2 "^24.8.0"
+ jest-regex-util "^24.3.0"
+ jest-resolve "^24.8.0"
+ jest-util "^24.8.0"
+ jest-validate "^24.8.0"
+ micromatch "^3.1.10"
+ pretty-format "^24.8.0"
+ realpath-native "^1.1.0"
+
+jest-diff@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.8.0.tgz#146435e7d1e3ffdf293d53ff97e193f1d1546172"
+ integrity sha512-wxetCEl49zUpJ/bvUmIFjd/o52J+yWcoc5ZyPq4/W1LUKGEhRYDIbP1KcF6t+PvqNrGAFk4/JhtxDq/Nnzs66g==
+ dependencies:
+ chalk "^2.0.1"
+ diff-sequences "^24.3.0"
+ jest-get-type "^24.8.0"
+ pretty-format "^24.8.0"
+
+jest-docblock@^24.3.0:
+ version "24.3.0"
+ resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.3.0.tgz#b9c32dac70f72e4464520d2ba4aec02ab14db5dd"
+ integrity sha512-nlANmF9Yq1dufhFlKG9rasfQlrY7wINJbo3q01tu56Jv5eBU5jirylhF2O5ZBnLxzOVBGRDz/9NAwNyBtG4Nyg==
+ dependencies:
+ detect-newline "^2.1.0"
+
+jest-each@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.8.0.tgz#a05fd2bf94ddc0b1da66c6d13ec2457f35e52775"
+ integrity sha512-NrwK9gaL5+XgrgoCsd9svsoWdVkK4gnvyhcpzd6m487tXHqIdYeykgq3MKI1u4I+5Zf0tofr70at9dWJDeb+BA==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ chalk "^2.0.1"
+ jest-get-type "^24.8.0"
+ jest-util "^24.8.0"
+ pretty-format "^24.8.0"
+
+jest-environment-jsdom@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.8.0.tgz#300f6949a146cabe1c9357ad9e9ecf9f43f38857"
+ integrity sha512-qbvgLmR7PpwjoFjM/sbuqHJt/NCkviuq9vus9NBn/76hhSidO+Z6Bn9tU8friecegbJL8gzZQEMZBQlFWDCwAQ==
+ dependencies:
+ "@jest/environment" "^24.8.0"
+ "@jest/fake-timers" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ jest-mock "^24.8.0"
+ jest-util "^24.8.0"
+ jsdom "^11.5.1"
+
+jest-environment-node@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.8.0.tgz#d3f726ba8bc53087a60e7a84ca08883a4c892231"
+ integrity sha512-vIGUEScd1cdDgR6sqn2M08sJTRLQp6Dk/eIkCeO4PFHxZMOgy+uYLPMC4ix3PEfM5Au/x3uQ/5Tl0DpXXZsJ/Q==
+ dependencies:
+ "@jest/environment" "^24.8.0"
+ "@jest/fake-timers" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ jest-mock "^24.8.0"
+ jest-util "^24.8.0"
+
+jest-get-type@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.8.0.tgz#a7440de30b651f5a70ea3ed7ff073a32dfe646fc"
+ integrity sha512-RR4fo8jEmMD9zSz2nLbs2j0zvPpk/KCEz3a62jJWbd2ayNo0cb+KFRxPHVhE4ZmgGJEQp0fosmNz84IfqM8cMQ==
+
+jest-haste-map@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.8.0.tgz#51794182d877b3ddfd6e6d23920e3fe72f305800"
+ integrity sha512-ZBPRGHdPt1rHajWelXdqygIDpJx8u3xOoLyUBWRW28r3tagrgoepPrzAozW7kW9HrQfhvmiv1tncsxqHJO1onQ==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ anymatch "^2.0.0"
+ fb-watchman "^2.0.0"
+ graceful-fs "^4.1.15"
+ invariant "^2.2.4"
+ jest-serializer "^24.4.0"
+ jest-util "^24.8.0"
+ jest-worker "^24.6.0"
+ micromatch "^3.1.10"
+ sane "^4.0.3"
+ walker "^1.0.7"
+ optionalDependencies:
+ fsevents "^1.2.7"
+
+jest-jasmine2@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.8.0.tgz#a9c7e14c83dd77d8b15e820549ce8987cc8cd898"
+ integrity sha512-cEky88npEE5LKd5jPpTdDCLvKkdyklnaRycBXL6GNmpxe41F0WN44+i7lpQKa/hcbXaQ+rc9RMaM4dsebrYong==
+ dependencies:
+ "@babel/traverse" "^7.1.0"
+ "@jest/environment" "^24.8.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ chalk "^2.0.1"
+ co "^4.6.0"
+ expect "^24.8.0"
+ is-generator-fn "^2.0.0"
+ jest-each "^24.8.0"
+ jest-matcher-utils "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-runtime "^24.8.0"
+ jest-snapshot "^24.8.0"
+ jest-util "^24.8.0"
+ pretty-format "^24.8.0"
+ throat "^4.0.0"
+
+jest-leak-detector@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.8.0.tgz#c0086384e1f650c2d8348095df769f29b48e6980"
+ integrity sha512-cG0yRSK8A831LN8lIHxI3AblB40uhv0z+SsQdW3GoMMVcK+sJwrIIyax5tu3eHHNJ8Fu6IMDpnLda2jhn2pD/g==
+ dependencies:
+ pretty-format "^24.8.0"
+
+jest-matcher-utils@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.8.0.tgz#2bce42204c9af12bde46f83dc839efe8be832495"
+ integrity sha512-lex1yASY51FvUuHgm0GOVj7DCYEouWSlIYmCW7APSqB9v8mXmKSn5+sWVF0MhuASG0bnYY106/49JU1FZNl5hw==
+ dependencies:
+ chalk "^2.0.1"
+ jest-diff "^24.8.0"
+ jest-get-type "^24.8.0"
+ pretty-format "^24.8.0"
+
+jest-message-util@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.8.0.tgz#0d6891e72a4beacc0292b638685df42e28d6218b"
+ integrity sha512-p2k71rf/b6ns8btdB0uVdljWo9h0ovpnEe05ZKWceQGfXYr4KkzgKo3PBi8wdnd9OtNh46VpNIJynUn/3MKm1g==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ "@types/stack-utils" "^1.0.1"
+ chalk "^2.0.1"
+ micromatch "^3.1.10"
+ slash "^2.0.0"
+ stack-utils "^1.0.1"
+
+jest-mock@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.8.0.tgz#2f9d14d37699e863f1febf4e4d5a33b7fdbbde56"
+ integrity sha512-6kWugwjGjJw+ZkK4mDa0Df3sDlUTsV47MSrT0nGQ0RBWJbpODDQ8MHDVtGtUYBne3IwZUhtB7elxHspU79WH3A==
+ dependencies:
+ "@jest/types" "^24.8.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"
+ integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ==
+
+jest-regex-util@^24.3.0:
+ version "24.3.0"
+ resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.3.0.tgz#d5a65f60be1ae3e310d5214a0307581995227b36"
+ integrity sha512-tXQR1NEOyGlfylyEjg1ImtScwMq8Oh3iJbGTjN7p0J23EuVX1MA8rwU69K4sLbCmwzgCUbVkm0FkSF9TdzOhtg==
+
+jest-resolve-dependencies@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.8.0.tgz#19eec3241f2045d3f990dba331d0d7526acff8e0"
+ integrity sha512-hyK1qfIf/krV+fSNyhyJeq3elVMhK9Eijlwy+j5jqmZ9QsxwKBiP6qukQxaHtK8k6zql/KYWwCTQ+fDGTIJauw==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ jest-regex-util "^24.3.0"
+ jest-snapshot "^24.8.0"
+
+jest-resolve@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.8.0.tgz#84b8e5408c1f6a11539793e2b5feb1b6e722439f"
+ integrity sha512-+hjSzi1PoRvnuOICoYd5V/KpIQmkAsfjFO71458hQ2Whi/yf1GDeBOFj8Gxw4LrApHsVJvn5fmjcPdmoUHaVKw==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ browser-resolve "^1.11.3"
+ chalk "^2.0.1"
+ jest-pnp-resolver "^1.2.1"
+ realpath-native "^1.1.0"
+
+jest-runner@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.8.0.tgz#4f9ae07b767db27b740d7deffad0cf67ccb4c5bb"
+ integrity sha512-utFqC5BaA3JmznbissSs95X1ZF+d+4WuOWwpM9+Ak356YtMhHE/GXUondZdcyAAOTBEsRGAgH/0TwLzfI9h7ow==
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/environment" "^24.8.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ chalk "^2.4.2"
+ exit "^0.1.2"
+ graceful-fs "^4.1.15"
+ jest-config "^24.8.0"
+ jest-docblock "^24.3.0"
+ jest-haste-map "^24.8.0"
+ jest-jasmine2 "^24.8.0"
+ jest-leak-detector "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-resolve "^24.8.0"
+ jest-runtime "^24.8.0"
+ jest-util "^24.8.0"
+ jest-worker "^24.6.0"
+ source-map-support "^0.5.6"
+ throat "^4.0.0"
+
+jest-runtime@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.8.0.tgz#05f94d5b05c21f6dc54e427cd2e4980923350620"
+ integrity sha512-Mq0aIXhvO/3bX44ccT+czU1/57IgOMyy80oM0XR/nyD5zgBcesF84BPabZi39pJVA6UXw+fY2Q1N+4BiVUBWOA==
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/environment" "^24.8.0"
+ "@jest/source-map" "^24.3.0"
+ "@jest/transform" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ "@types/yargs" "^12.0.2"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ glob "^7.1.3"
+ graceful-fs "^4.1.15"
+ jest-config "^24.8.0"
+ jest-haste-map "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-mock "^24.8.0"
+ jest-regex-util "^24.3.0"
+ jest-resolve "^24.8.0"
+ jest-snapshot "^24.8.0"
+ jest-util "^24.8.0"
+ jest-validate "^24.8.0"
+ realpath-native "^1.1.0"
+ slash "^2.0.0"
+ strip-bom "^3.0.0"
+ yargs "^12.0.2"
+
+jest-serializer@^24.4.0:
+ version "24.4.0"
+ resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.4.0.tgz#f70c5918c8ea9235ccb1276d232e459080588db3"
+ integrity sha512-k//0DtglVstc1fv+GY/VHDIjrtNjdYvYjMlbLUed4kxrE92sIUewOi5Hj3vrpB8CXfkJntRPDRjCrCvUhBdL8Q==
+
+jest-snapshot@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.8.0.tgz#3bec6a59da2ff7bc7d097a853fb67f9d415cb7c6"
+ integrity sha512-5ehtWoc8oU9/cAPe6fez6QofVJLBKyqkY2+TlKTOf0VllBB/mqUNdARdcjlZrs9F1Cv+/HKoCS/BknT0+tmfPg==
+ dependencies:
+ "@babel/types" "^7.0.0"
+ "@jest/types" "^24.8.0"
+ chalk "^2.0.1"
+ expect "^24.8.0"
+ jest-diff "^24.8.0"
+ jest-matcher-utils "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-resolve "^24.8.0"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ pretty-format "^24.8.0"
+ semver "^5.5.0"
+
+jest-util@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.8.0.tgz#41f0e945da11df44cc76d64ffb915d0716f46cd1"
+ integrity sha512-DYZeE+XyAnbNt0BG1OQqKy/4GVLPtzwGx5tsnDrFcax36rVE3lTA5fbvgmbVPUZf9w77AJ8otqR4VBbfFJkUZA==
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/fake-timers" "^24.8.0"
+ "@jest/source-map" "^24.3.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ callsites "^3.0.0"
+ chalk "^2.0.1"
+ graceful-fs "^4.1.15"
+ is-ci "^2.0.0"
+ mkdirp "^0.5.1"
+ slash "^2.0.0"
+ source-map "^0.6.0"
+
+jest-validate@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.8.0.tgz#624c41533e6dfe356ffadc6e2423a35c2d3b4849"
+ integrity sha512-+/N7VOEMW1Vzsrk3UWBDYTExTPwf68tavEPKDnJzrC6UlHtUDU/fuEdXqFoHzv9XnQ+zW6X3qMZhJ3YexfeLDA==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ camelcase "^5.0.0"
+ chalk "^2.0.1"
+ jest-get-type "^24.8.0"
+ leven "^2.1.0"
+ pretty-format "^24.8.0"
+
+jest-watcher@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.8.0.tgz#58d49915ceddd2de85e238f6213cef1c93715de4"
+ integrity sha512-SBjwHt5NedQoVu54M5GEx7cl7IGEFFznvd/HNT8ier7cCAx/Qgu9ZMlaTQkvK22G1YOpcWBLQPFSImmxdn3DAw==
+ dependencies:
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ "@types/yargs" "^12.0.9"
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.1"
+ jest-util "^24.8.0"
+ string-length "^2.0.0"
+
+jest-worker@^24.6.0:
+ version "24.6.0"
+ resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.6.0.tgz#7f81ceae34b7cde0c9827a6980c35b7cdc0161b3"
+ integrity sha512-jDwgW5W9qGNvpI1tNnvajh0a5IE/PuGLFmHk6aR/BZFz8tSgGw17GsDPXAJ6p91IvYDjOw8GpFbvvZGAK+DPQQ==
+ dependencies:
+ merge-stream "^1.0.1"
+ supports-color "^6.1.0"
+
+jest@^24.6.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest/-/jest-24.8.0.tgz#d5dff1984d0d1002196e9b7f12f75af1b2809081"
+ integrity sha512-o0HM90RKFRNWmAWvlyV8i5jGZ97pFwkeVoGvPW1EtLTgJc2+jcuqcbbqcSZLE/3f2S5pt0y2ZBETuhpWNl1Reg==
+ dependencies:
+ import-local "^2.0.0"
+ jest-cli "^24.8.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"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+ integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
+
+jsdom@^11.5.1:
+ version "11.12.0"
+ resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8"
+ integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==
+ dependencies:
+ abab "^2.0.0"
+ acorn "^5.5.3"
+ acorn-globals "^4.1.0"
+ array-equal "^1.0.0"
+ cssom ">= 0.3.2 < 0.4.0"
+ cssstyle "^1.0.0"
+ data-urls "^1.0.0"
+ domexception "^1.0.1"
+ escodegen "^1.9.1"
+ html-encoding-sniffer "^1.0.2"
+ left-pad "^1.3.0"
+ nwsapi "^2.0.7"
+ parse5 "4.0.0"
+ pn "^1.1.0"
+ request "^2.87.0"
+ request-promise-native "^1.0.5"
+ sax "^1.2.4"
+ symbol-tree "^3.2.2"
+ tough-cookie "^2.3.4"
+ w3c-hr-time "^1.0.1"
+ webidl-conversions "^4.0.2"
+ whatwg-encoding "^1.0.3"
+ whatwg-mimetype "^2.1.0"
+ whatwg-url "^6.4.1"
+ ws "^5.2.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"
+ integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
+
+json-parse-better-errors@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+ integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
+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"
+ integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+ integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+
+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"
+ integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+
+json5@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850"
+ integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==
+ dependencies:
+ minimist "^1.2.0"
+
+jsprim@^1.2.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+ integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+ 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"
+ integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
+ 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"
+ integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc=
+ 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"
+ integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
+
+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"
+ integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
+
+kleur@^3.0.2:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
+ integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
+
+lcid@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf"
+ integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==
+ dependencies:
+ invert-kv "^2.0.0"
+
+left-pad@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
+ integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==
+
+leven@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
+ integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA=
+
+levn@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+ integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
+ dependencies:
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+
+load-json-file@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
+ integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^4.0.0"
+ pify "^3.0.0"
+ strip-bom "^3.0.0"
+
+locate-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+ integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+ dependencies:
+ p-locate "^3.0.0"
+ path-exists "^3.0.0"
+
+lodash.escape@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
+ integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=
+
+lodash.flattendeep@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
+ integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=
+
+lodash.isequal@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+ integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
+
+lodash.sortby@^4.7.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
+ integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
+
+lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.4:
+ version "4.17.11"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
+ integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
+
+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"
+ integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+ dependencies:
+ js-tokens "^3.0.0 || ^4.0.0"
+
+make-dir@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
+ integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==
+ dependencies:
+ pify "^4.0.1"
+ semver "^5.6.0"
+
+makeerror@1.0.x:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
+ integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=
+ dependencies:
+ tmpl "1.0.x"
+
+map-age-cleaner@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
+ integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
+ dependencies:
+ p-defer "^1.0.0"
+
+map-cache@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+ integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
+
+map-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+ integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=
+ dependencies:
+ object-visit "^1.0.0"
+
+mem@^4.0.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178"
+ integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==
+ dependencies:
+ map-age-cleaner "^0.1.1"
+ mimic-fn "^2.0.0"
+ p-is-promise "^2.0.0"
+
+merge-stream@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1"
+ integrity sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=
+ dependencies:
+ readable-stream "^2.0.1"
+
+micromatch@^3.1.10, micromatch@^3.1.4:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+ integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
+ 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"
+
+mime-db@~1.38.0:
+ version "1.38.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad"
+ integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==
+
+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"
+ integrity sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==
+ dependencies:
+ mime-db "~1.38.0"
+
+mimic-fn@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+ integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+
+minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+ 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"
+ integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
+
+minimist@^1.1.1, minimist@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+ integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
+
+minimist@~0.0.1:
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
+ integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=
+
+minipass@^2.2.1, minipass@^2.3.4:
+ version "2.3.5"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848"
+ integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==
+ dependencies:
+ safe-buffer "^5.1.2"
+ yallist "^3.0.0"
+
+minizlib@^1.1.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614"
+ integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==
+ dependencies:
+ minipass "^2.2.1"
+
+mixin-deep@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
+ integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==
+ dependencies:
+ for-in "^1.0.2"
+ is-extendable "^1.0.1"
+
+mkdirp@^0.5.0, mkdirp@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
+ dependencies:
+ minimist "0.0.8"
+
+moo@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e"
+ integrity sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw==
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+ integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+ms@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
+ integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
+
+nan@^2.12.1:
+ version "2.13.2"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
+ integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
+
+nanomatch@^1.2.9:
+ version "1.2.13"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
+ integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==
+ 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"
+ integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+
+nearley@^2.7.10:
+ version "2.16.0"
+ resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.16.0.tgz#77c297d041941d268290ec84b739d0ee297e83a7"
+ integrity sha512-Tr9XD3Vt/EujXbZBv6UAHYoLUSMQAxSsTnm9K3koXzjzNWY195NqALeyrzLZBKzAkL3gl92BcSogqrHjD8QuUg==
+ dependencies:
+ commander "^2.19.0"
+ moo "^0.4.3"
+ railroad-diagrams "^1.0.0"
+ randexp "0.4.6"
+ semver "^5.4.1"
+
+needle@^2.2.1:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e"
+ integrity sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==
+ dependencies:
+ debug "^2.1.2"
+ iconv-lite "^0.4.4"
+ sax "^1.2.4"
+
+neo-async@^2.6.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.0.tgz#b9d15e4d71c6762908654b5183ed38b753340835"
+ integrity sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==
+
+nice-try@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+ integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
+node-fetch@^1.0.1:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
+ integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==
+ 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"
+ integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=
+
+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"
+ integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=
+
+node-notifier@^5.2.1:
+ version "5.4.0"
+ resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.0.tgz#7b455fdce9f7de0c63538297354f3db468426e6a"
+ integrity sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ==
+ dependencies:
+ growly "^1.3.0"
+ is-wsl "^1.1.0"
+ semver "^5.5.0"
+ shellwords "^0.1.1"
+ which "^1.3.0"
+
+node-pre-gyp@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149"
+ integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==
+ dependencies:
+ detect-libc "^1.0.2"
+ mkdirp "^0.5.1"
+ needle "^2.2.1"
+ nopt "^4.0.1"
+ npm-packlist "^1.1.6"
+ npmlog "^4.0.2"
+ rc "^1.2.7"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^4"
+
+nopt@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
+ integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=
+ dependencies:
+ abbrev "1"
+ osenv "^0.1.4"
+
+normalize-package-data@^2.3.2:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
+ integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
+ dependencies:
+ hosted-git-info "^2.1.4"
+ resolve "^1.10.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-path@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+npm-bundled@^1.0.1:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd"
+ integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==
+
+npm-packlist@^1.1.6:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc"
+ integrity sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==
+ dependencies:
+ ignore-walk "^3.0.1"
+ npm-bundled "^1.0.1"
+
+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"
+ integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
+ dependencies:
+ path-key "^2.0.0"
+
+npmlog@^4.0.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+ integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
+ dependencies:
+ are-we-there-yet "~1.1.2"
+ console-control-strings "~1.1.0"
+ gauge "~2.7.3"
+ set-blocking "~2.0.0"
+
+nth-check@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
+ integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
+ dependencies:
+ boolbase "~1.0.0"
+
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+ integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
+
+nwsapi@^2.0.7:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.1.tgz#08d6d75e69fd791bdea31507ffafe8c843b67e9c"
+ integrity sha512-T5GaA1J/d34AC8mkrFD2O0DR17kwJ702ZOtJOsS8RpbsQZVOC2/xYFb1i/cw+xdM54JIlMuojjDOYct8GIWtwg==
+
+oauth-sign@~0.9.0:
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+ integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
+
+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"
+ integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+
+object-copy@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+ integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw=
+ dependencies:
+ copy-descriptor "^0.1.0"
+ define-property "^0.2.5"
+ kind-of "^3.0.3"
+
+object-inspect@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b"
+ integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==
+
+object-is@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
+ integrity sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=
+
+object-keys@^1.0.11:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+ integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object-keys@^1.0.12:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.0.tgz#11bd22348dd2e096a045ab06f6c85bcc340fa032"
+ integrity sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==
+
+object-visit@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+ integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=
+ dependencies:
+ isobject "^3.0.0"
+
+object.assign@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+ integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ has-symbols "^1.0.0"
+ object-keys "^1.0.11"
+
+object.entries@^1.0.4, object.entries@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519"
+ integrity sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.12.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
+object.fromentries@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.0.tgz#49a543d92151f8277b3ac9600f1e930b189d30ab"
+ integrity sha512-9iLiI6H083uiqUuvzyY6qrlmc/Gz8hLQFOcb/Ri/0xXFkSNS3ctV+CbE6yM2+AnkYfOB3dGjdzC0wrMLIhQICA==
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.11.0"
+ function-bind "^1.1.1"
+ has "^1.0.1"
+
+object.getownpropertydescriptors@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
+ integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=
+ 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"
+ integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=
+ dependencies:
+ isobject "^3.0.1"
+
+object.values@^1.0.4, object.values@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9"
+ integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.12.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
+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"
+ integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+ dependencies:
+ wrappy "1"
+
+optimist@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
+ integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY=
+ dependencies:
+ minimist "~0.0.1"
+ wordwrap "~0.0.2"
+
+optionator@^0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
+ integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=
+ 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"
+
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+ integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
+
+os-locale@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
+ integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==
+ dependencies:
+ execa "^1.0.0"
+ lcid "^2.0.0"
+ mem "^4.0.0"
+
+os-tmpdir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+ integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
+
+osenv@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
+ integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.0"
+
+p-defer@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
+ integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
+
+p-each-series@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71"
+ integrity sha1-kw89Et0fUOdDRFeiLNbwSsatf3E=
+ dependencies:
+ p-reduce "^1.0.0"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+ integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
+
+p-is-promise@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e"
+ integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==
+
+p-limit@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2"
+ integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==
+ dependencies:
+ p-try "^2.0.0"
+
+p-locate@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+ integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+ dependencies:
+ p-limit "^2.0.0"
+
+p-reduce@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa"
+ integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=
+
+p-try@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+ integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+parse-json@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+ integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
+ dependencies:
+ error-ex "^1.3.1"
+ json-parse-better-errors "^1.0.1"
+
+parse5@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
+ integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==
+
+parse5@^3.0.1:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
+ integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==
+ dependencies:
+ "@types/node" "*"
+
+pascalcase@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+ integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+ integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+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"
+ integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+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"
+ integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+
+path-parse@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+ integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
+
+path-type@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
+ integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
+ dependencies:
+ pify "^3.0.0"
+
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+ integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+
+pify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+ integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
+
+pify@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
+ integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
+
+pirates@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87"
+ integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==
+ dependencies:
+ node-modules-regexp "^1.0.0"
+
+pkg-dir@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
+ integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==
+ dependencies:
+ find-up "^3.0.0"
+
+pn@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
+ integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
+
+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"
+ integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+ integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+
+pretty-format@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2"
+ integrity sha512-P952T7dkrDEplsR+TuY7q3VXDae5Sr7zmQb12JU/NDQa/3CH7/QW0yvqLcGN6jL+zQFKaoJcPc+yJxMTGmosqw==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ ansi-regex "^4.0.0"
+ ansi-styles "^3.2.0"
+ react-is "^16.8.4"
+
+process-nextick-args@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
+ integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==
+
+promise@^7.1.1:
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+ integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
+ dependencies:
+ asap "~2.0.3"
+
+prompts@^2.0.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.1.0.tgz#bf90bc71f6065d255ea2bdc0fe6520485c1b45db"
+ integrity sha512-+x5TozgqYdOwWsQFZizE/Tra3fKvAoy037kOyU6cgz84n8f6zxngLOV4O32kTwt9FcLCxAqw0P/c8rOr9y+Gfg==
+ dependencies:
+ kleur "^3.0.2"
+ sisteransi "^1.0.0"
+
+prop-types-exact@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869"
+ integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==
+ dependencies:
+ has "^1.0.3"
+ object.assign "^4.1.0"
+ reflect.ownkeys "^0.2.0"
+
+prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
+ version "15.7.2"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
+ integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
+ dependencies:
+ loose-envify "^1.4.0"
+ object-assign "^4.1.1"
+ react-is "^16.8.1"
+
+psl@^1.1.24, psl@^1.1.28:
+ version "1.1.31"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
+ integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==
+
+pump@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+ integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+ integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
+
+punycode@^2.1.0, punycode@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+ integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+qs@~6.5.2:
+ version "6.5.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+ integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+
+raf@^3.4.0:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
+ integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
+ dependencies:
+ performance-now "^2.1.0"
+
+railroad-diagrams@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
+ integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=
+
+randexp@0.4.6:
+ version "0.4.6"
+ resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
+ integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==
+ dependencies:
+ discontinuous-range "1.0.0"
+ ret "~0.1.10"
+
+rc@^1.2.7:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
+ integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
+ dependencies:
+ deep-extend "^0.6.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+react-dom@16.4.1:
+ version "16.4.1"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.1.tgz#7f8b0223b3a5fbe205116c56deb85de32685dad6"
+ integrity sha512-1Gin+wghF/7gl4Cqcvr1DxFX2Osz7ugxSwl6gBqCMpdrxHjIFUS7GYxrFftZ9Ln44FHw0JxCFD9YtZsrbR5/4A==
+ dependencies:
+ fbjs "^0.8.16"
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.0"
+
+react-is@^16.4.1, react-is@^16.8.4, react-is@^16.8.6:
+ version "16.8.6"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
+ integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
+
+react-is@^16.8.1:
+ version "16.8.5"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.5.tgz#c54ac229dd66b5afe0de5acbe47647c3da692ff8"
+ integrity sha512-sudt2uq5P/2TznPV4Wtdi+Lnq3yaYW8LfvPKLM9BKD8jJNBkxMVyB0C9/GmVhLw7Jbdmndk/73n7XQGeN9A3QQ==
+
+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"
+ integrity sha512-wyyiPxRZOTpKnNIgUBOB6xPLTpIzwcQMIURhZvzUqZzezvHjaGNsDPBhMac5fIY3Jf5NuKxoGvV64zDSOECPPQ==
+ dependencies:
+ fbjs "^0.8.16"
+ object-assign "^4.1.1"
+ prop-types "^15.6.0"
+ react-is "^16.4.1"
+
+react-test-renderer@^16.0.0-0:
+ version "16.8.6"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.6.tgz#188d8029b8c39c786f998aa3efd3ffe7642d5ba1"
+ integrity sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw==
+ dependencies:
+ object-assign "^4.1.1"
+ prop-types "^15.6.2"
+ react-is "^16.8.6"
+ scheduler "^0.13.6"
+
+react@16.4.1:
+ version "16.4.1"
+ resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32"
+ integrity sha512-3GEs0giKp6E0Oh/Y9ZC60CmYgUPnp7voH9fbjWsvXtYFb4EWtgQub0ADSq0sJR0BbHc4FThLLtzlcFaFXIorwg==
+ dependencies:
+ fbjs "^0.8.16"
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.0"
+
+read-pkg-up@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978"
+ integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==
+ dependencies:
+ find-up "^3.0.0"
+ read-pkg "^3.0.0"
+
+read-pkg@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
+ integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
+ dependencies:
+ load-json-file "^4.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^3.0.0"
+
+readable-stream@^2.0.1, readable-stream@^2.0.6:
+ version "2.3.6"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
+ integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.1.1"
+ util-deprecate "~1.0.1"
+
+readable-stream@^3.1.1:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc"
+ integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+realpath-native@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c"
+ integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==
+ dependencies:
+ util.promisify "^1.0.0"
+
+reflect.ownkeys@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
+ integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=
+
+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"
+ integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==
+ 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"
+ integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
+
+repeat-element@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
+ integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
+
+repeat-string@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+ integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
+
+request-promise-core@1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346"
+ integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==
+ dependencies:
+ lodash "^4.17.11"
+
+request-promise-native@^1.0.5:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59"
+ integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==
+ dependencies:
+ request-promise-core "1.1.2"
+ stealthy-require "^1.1.1"
+ tough-cookie "^2.3.3"
+
+request@^2.87.0:
+ version "2.88.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
+ integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
+ 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.0"
+ 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.4.3"
+ 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"
+ integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+require-main-filename@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+ integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
+
+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"
+ integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
+
+resolve-cwd@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
+ integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=
+ dependencies:
+ resolve-from "^3.0.0"
+
+resolve-from@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
+ integrity sha1-six699nWiBvItuZTM17rywoYh0g=
+
+resolve-url@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+ integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
+
+resolve@1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
+ integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
+
+resolve@^1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba"
+ integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==
+ dependencies:
+ path-parse "^1.0.6"
+
+resolve@^1.3.2:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.0.tgz#4014870ba296176b86343d50b60f3b50609ce232"
+ integrity sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==
+ 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"
+ integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
+
+rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3:
+ version "2.6.3"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
+ integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
+ dependencies:
+ glob "^7.1.3"
+
+rst-selector-parser@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
+ integrity sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=
+ dependencies:
+ lodash.flattendeep "^4.4.0"
+ nearley "^2.7.10"
+
+rsvp@^4.8.4:
+ version "4.8.4"
+ resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.4.tgz#b50e6b34583f3dd89329a2f23a8a2be072845911"
+ integrity sha512-6FomvYPfs+Jy9TfXmBpBuMWNH94SgCsZmJKcanySzgNNP6LjWxBvyLTa9KaMfDDM5oxRfrKDB0r/qeRsLwnBfA==
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+ integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+ integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4=
+ 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"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+sane@^4.0.3:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded"
+ integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==
+ 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"
+
+sax@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+ integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
+scheduler@^0.13.6:
+ version "0.13.6"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889"
+ integrity sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+
+"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0:
+ version "5.6.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
+ integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
+
+semver@^5.4.1, semver@^5.6.0, semver@^5.7.0:
+ version "5.7.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
+ integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==
+
+semver@^6.0.0:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.1.tgz#53f53da9b30b2103cd4f15eab3a18ecbcb210c9b"
+ integrity sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==
+
+set-blocking@^2.0.0, set-blocking@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+ integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+
+set-value@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1"
+ integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE=
+ 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"
+ integrity sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==
+ 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"
+ integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+ dependencies:
+ shebang-regex "^1.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"
+ integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+
+shellwords@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
+ integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
+
+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"
+ integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
+
+sisteransi@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.0.tgz#77d9622ff909080f1c19e5f4a1df0c1b0a27b88c"
+ integrity sha512-N+z4pHB4AmUv0SjveWRd6q1Nj5w62m5jodv+GD8lvmbY/83T/rpbJGZOnK5T149OldDj4Db07BSv9xY4K6NTPQ==
+
+slash@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
+ integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
+
+snapdragon-node@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+ integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==
+ 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"
+ integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==
+ 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"
+ integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==
+ 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"
+ integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==
+ 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"
+ integrity sha512-//sajEx/fGL3iw6fltKMdPvy8kL3kJ2O3iuYlRoT3k9Kb4BjOoZ+BZzaNHeuaruSt+Kf3Zk9tnfAQg9/AJqUVQ==
+ 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"
+ integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=
+
+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"
+ integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
+
+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"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+spdx-correct@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4"
+ integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==
+ dependencies:
+ spdx-expression-parse "^3.0.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-exceptions@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977"
+ integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==
+
+spdx-expression-parse@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0"
+ integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==
+ dependencies:
+ spdx-exceptions "^2.1.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-license-ids@^3.0.0:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz#81c0ce8f21474756148bbb5f3bfc0f36bf15d76e"
+ integrity sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==
+
+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"
+ integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==
+ dependencies:
+ extend-shallow "^3.0.0"
+
+sshpk@^1.7.0:
+ version "1.16.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
+ integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
+ 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"
+ integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==
+
+static-extend@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+ integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=
+ 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"
+ integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
+
+string-length@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
+ integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=
+ dependencies:
+ astral-regex "^1.0.0"
+ strip-ansi "^4.0.0"
+
+string-width@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+ integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
+string.prototype.trim@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea"
+ integrity sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.5.0"
+ function-bind "^1.0.2"
+
+string_decoder@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
+ integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+string_decoder@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+ integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
+ dependencies:
+ ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-ansi@^5.0.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
+ integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
+ dependencies:
+ ansi-regex "^4.1.0"
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+ integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
+
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+ integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
+
+strip-json-comments@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+ integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
+
+supports-color@^5.3.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+ integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+ dependencies:
+ has-flag "^3.0.0"
+
+supports-color@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
+ integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
+ dependencies:
+ has-flag "^3.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"
+ integrity sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=
+
+tar@^4:
+ version "4.4.8"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d"
+ integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==
+ dependencies:
+ chownr "^1.1.1"
+ fs-minipass "^1.2.5"
+ minipass "^2.3.4"
+ minizlib "^1.1.1"
+ mkdirp "^0.5.0"
+ safe-buffer "^5.1.2"
+ yallist "^3.0.2"
+
+test-exclude@^5.2.3:
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0"
+ integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==
+ dependencies:
+ glob "^7.1.3"
+ minimatch "^3.0.4"
+ read-pkg-up "^4.0.0"
+ require-main-filename "^2.0.0"
+
+throat@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
+ integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=
+
+tmpl@1.0.x:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
+ integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=
+
+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"
+ integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
+
+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"
+ integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=
+ 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"
+ integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=
+ dependencies:
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+
+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"
+ integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==
+ 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.3.4:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+ integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+ dependencies:
+ psl "^1.1.28"
+ punycode "^2.1.1"
+
+tough-cookie@~2.4.3:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
+ integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==
+ dependencies:
+ psl "^1.1.24"
+ punycode "^1.4.1"
+
+tr46@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
+ integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=
+ dependencies:
+ punycode "^2.1.0"
+
+trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+ integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
+ 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"
+ integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
+
+type-check@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
+ dependencies:
+ prelude-ls "~1.1.2"
+
+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"
+ integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==
+
+uglify-js@^3.1.4:
+ version "3.5.2"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.5.2.tgz#dc0c7ac2da0a4b7d15e84266818ff30e82529474"
+ integrity sha512-imog1WIsi9Yb56yRt5TfYVxGmnWs3WSGU73ieSOlMVFwhJCA9W8fqFFMMj4kgDqiS/80LGdsYnWL7O9UcjEBlg==
+ dependencies:
+ commander "~2.19.0"
+ source-map "~0.6.1"
+
+union-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
+ integrity sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=
+ 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"
+ integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=
+ 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"
+ integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==
+ 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"
+ integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
+
+use@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
+ integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
+
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+ integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+util.promisify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"
+ integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==
+ 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"
+ integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
+ integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
+ dependencies:
+ spdx-correct "^3.0.0"
+ spdx-expression-parse "^3.0.0"
+
+verror@1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+ integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
+ 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"
+ integrity sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=
+ dependencies:
+ browser-process-hrtime "^0.1.2"
+
+walker@^1.0.7, walker@~1.0.5:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
+ integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=
+ 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"
+ integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
+
+whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
+ integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==
+ 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"
+ integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
+
+whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
+ integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
+
+whatwg-url@^6.4.1:
+ version "6.5.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
+ integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==
+ dependencies:
+ lodash.sortby "^4.7.0"
+ tr46 "^1.0.1"
+ webidl-conversions "^4.0.2"
+
+whatwg-url@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd"
+ integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==
+ 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"
+ integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+
+which@^1.2.9, which@^1.3.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+ dependencies:
+ isexe "^2.0.0"
+
+wide-align@^1.1.0:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
+ integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
+ dependencies:
+ string-width "^1.0.2 || 2"
+
+wordwrap@~0.0.2:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+ integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc=
+
+wordwrap@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+ integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
+
+wrap-ansi@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+ integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+write-file-atomic@2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.1.tgz#d0b05463c188ae804396fd5ab2a370062af87529"
+ integrity sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==
+ dependencies:
+ graceful-fs "^4.1.11"
+ imurmurhash "^0.1.4"
+ signal-exit "^3.0.2"
+
+ws@^5.2.0:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"
+ integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==
+ dependencies:
+ async-limiter "~1.0.0"
+
+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"
+ integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
+
+"y18n@^3.2.1 || ^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
+ integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
+
+yallist@^3.0.0, yallist@^3.0.2:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9"
+ integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==
+
+yargs-parser@^11.1.1:
+ version "11.1.1"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
+ integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==
+ dependencies:
+ camelcase "^5.0.0"
+ decamelize "^1.2.0"
+
+yargs@^12.0.2:
+ version "12.0.5"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
+ integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==
+ dependencies:
+ cliui "^4.0.0"
+ decamelize "^1.2.0"
+ find-up "^3.0.0"
+ get-caller-file "^1.0.1"
+ os-locale "^3.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1 || ^4.0.0"
+ yargs-parser "^11.1.1"
diff --git a/devtools/client/aboutdebugging/test/xpcshell/.eslintrc.js b/devtools/client/aboutdebugging/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..8611c174f5
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/client/aboutdebugging/test/xpcshell/test_extensions_path.js b/devtools/client/aboutdebugging/test/xpcshell/test_extensions_path.js
new file mode 100644
index 0000000000..1ec9f0d7c2
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/xpcshell/test_extensions_path.js
@@ -0,0 +1,27 @@
+/* global equal */
+
+"use strict";
+
+const {
+ parseFileUri,
+} = require("resource://devtools/client/aboutdebugging/src/modules/extensions-helper.js");
+
+add_task(async function testParseFileUri() {
+ equal(
+ parseFileUri("file:///home/me/my-extension/"),
+ "/home/me/my-extension/",
+ "UNIX paths are supported"
+ );
+
+ equal(
+ parseFileUri("file:///C:/Documents/my-extension/"),
+ "C:/Documents/my-extension/",
+ "Windows paths are supported"
+ );
+
+ equal(
+ parseFileUri("file://home/Documents/my-extension/"),
+ "home/Documents/my-extension/",
+ "Windows network paths are supported"
+ );
+});
diff --git a/devtools/client/aboutdebugging/test/xpcshell/test_runtime_default_preferences.js b/devtools/client/aboutdebugging/test/xpcshell/test_runtime_default_preferences.js
new file mode 100644
index 0000000000..637e42e078
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/xpcshell/test_runtime_default_preferences.js
@@ -0,0 +1,203 @@
+"use strict";
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+const {
+ setDefaultPreferencesIfNeeded,
+ PREFERENCE_TYPES,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtime-default-preferences.js");
+
+const CHAR_PREF = "some.char.pref";
+const BOOL_PREF = "some.bool.pref";
+const INT_PREF = "some.int.pref";
+
+const TEST_PREFERENCES = [
+ {
+ prefName: BOOL_PREF,
+ defaultValue: false,
+ trait: "boolPrefTrait",
+ type: PREFERENCE_TYPES.BOOL,
+ },
+ {
+ prefName: CHAR_PREF,
+ defaultValue: "",
+ trait: "charPrefTrait",
+ type: PREFERENCE_TYPES.CHAR,
+ },
+ {
+ prefName: INT_PREF,
+ defaultValue: 1,
+ trait: "intPrefTrait",
+ type: PREFERENCE_TYPES.INT,
+ },
+];
+
+add_task(async function test_with_traits() {
+ // Create a front that indicates that the preferences should be safe to query.
+ // We should not perform any additional call to the preferences front.
+ const preferencesFront = {
+ getTraits: () => ({
+ boolPrefTrait: true,
+ charPrefTrait: true,
+ intPrefTrait: true,
+ }),
+
+ setBoolPref: sinon.spy(),
+ getBoolPref: sinon.spy(),
+ setCharPref: sinon.spy(),
+ getCharPref: sinon.spy(),
+ setIntPref: sinon.spy(),
+ getIntPref: sinon.spy(),
+ };
+
+ const clientWrapper = createClientWrapper(preferencesFront);
+ await setDefaultPreferencesIfNeeded(clientWrapper, TEST_PREFERENCES);
+
+ // Check get/setBoolPref spies
+ ok(preferencesFront.getBoolPref.notCalled, "getBoolPref was not called");
+ ok(preferencesFront.setBoolPref.notCalled, "setBoolPref was not called");
+
+ // Check get/setCharPref spies
+ ok(preferencesFront.getCharPref.notCalled, "getCharPref was not called");
+ ok(preferencesFront.setCharPref.notCalled, "setCharPref was not called");
+
+ // Check get/setIntPref spies
+ ok(preferencesFront.getIntPref.notCalled, "getIntPref was not called");
+ ok(preferencesFront.setIntPref.notCalled, "setIntPref was not called");
+});
+
+add_task(async function test_without_traits_no_error() {
+ // Create a front that indicates that the preferences are missing, but which
+ // doesn't fail when getting the preferences. This will typically happen when
+ // the user managed to set the preference on the remote runtime.
+ // We should not erase user values, so we should not call the set*Pref APIs.
+ const preferencesFront = {
+ getTraits: () => ({
+ boolPrefTrait: false,
+ charPrefTrait: false,
+ intPrefTrait: false,
+ }),
+
+ setBoolPref: sinon.spy(),
+ getBoolPref: sinon.spy(),
+ setCharPref: sinon.spy(),
+ getCharPref: sinon.spy(),
+ setIntPref: sinon.spy(),
+ getIntPref: sinon.spy(),
+ };
+
+ const clientWrapper = createClientWrapper(preferencesFront);
+ await setDefaultPreferencesIfNeeded(clientWrapper, TEST_PREFERENCES);
+
+ // Check get/setBoolPref spies
+ ok(
+ preferencesFront.getBoolPref.calledWith(BOOL_PREF),
+ "getBoolPref was called with the proper preference name"
+ );
+ ok(preferencesFront.getBoolPref.calledOnce, "getBoolPref was called once");
+ ok(preferencesFront.setBoolPref.notCalled, "setBoolPref was not called");
+
+ // Check get/setCharPref spies
+ ok(
+ preferencesFront.getCharPref.calledWith(CHAR_PREF),
+ "getCharPref was called with the proper preference name"
+ );
+ ok(preferencesFront.getCharPref.calledOnce, "getCharPref was called once");
+ ok(preferencesFront.setCharPref.notCalled, "setCharPref was not called");
+
+ // Check get/setIntPref spies
+ ok(
+ preferencesFront.getIntPref.calledWith(INT_PREF),
+ "getIntPref was called with the proper preference name"
+ );
+ ok(preferencesFront.getIntPref.calledOnce, "getIntPref was called once");
+ ok(preferencesFront.setIntPref.notCalled, "setIntPref was not called");
+});
+
+add_task(async function test_without_traits_with_error() {
+ // Create a front that indicates that the preferences are missing, and which
+ // will also throw when attempting to get said preferences.
+ // This should lead to create default values for the preferences.
+ const preferencesFront = {
+ getTraits: () => ({
+ boolPrefTrait: false,
+ charPrefTrait: false,
+ intPrefTrait: false,
+ }),
+
+ setBoolPref: sinon.spy(),
+ getBoolPref: sinon.spy(pref => {
+ if (pref === BOOL_PREF) {
+ throw new Error("Invalid preference");
+ }
+ }),
+ setCharPref: sinon.spy(),
+ getCharPref: sinon.spy(pref => {
+ if (pref === CHAR_PREF) {
+ throw new Error("Invalid preference");
+ }
+ }),
+ setIntPref: sinon.spy(),
+ getIntPref: sinon.spy(pref => {
+ if (pref === INT_PREF) {
+ throw new Error("Invalid preference");
+ }
+ }),
+ };
+
+ const clientWrapper = createClientWrapper(preferencesFront);
+ await setDefaultPreferencesIfNeeded(clientWrapper, TEST_PREFERENCES);
+
+ // Check get/setBoolPref spies
+ ok(preferencesFront.getBoolPref.calledOnce, "getBoolPref was called once");
+ ok(preferencesFront.getBoolPref.threw(), "getBoolPref threw");
+ ok(
+ preferencesFront.getBoolPref.calledWith(BOOL_PREF),
+ "getBoolPref was called with the proper preference name"
+ );
+
+ ok(preferencesFront.setBoolPref.calledOnce, "setBoolPref was called once");
+ ok(
+ preferencesFront.setBoolPref.calledWith(BOOL_PREF, false),
+ "setBoolPref was called with the proper preference name and value"
+ );
+
+ // Check get/setCharPref spies
+ ok(preferencesFront.getCharPref.calledOnce, "getCharPref was called once");
+ ok(preferencesFront.getCharPref.threw(), "getCharPref threw");
+ ok(
+ preferencesFront.getCharPref.calledWith(CHAR_PREF),
+ "getCharPref was called with the proper preference name"
+ );
+
+ ok(preferencesFront.setCharPref.calledOnce, "setCharPref was called once");
+ ok(
+ preferencesFront.setCharPref.calledWith(CHAR_PREF, ""),
+ "setCharPref was called with the proper preference name and value"
+ );
+
+ // Check get/setIntPref spies
+ ok(preferencesFront.getIntPref.calledOnce, "getIntPref was called once");
+ ok(preferencesFront.getIntPref.threw(), "getIntPref threw");
+ ok(
+ preferencesFront.getIntPref.calledWith(INT_PREF),
+ "getIntPref was called with the proper preference name"
+ );
+
+ ok(preferencesFront.setIntPref.calledOnce, "setIntPref was called once");
+ ok(
+ preferencesFront.setIntPref.calledWith(INT_PREF, 1),
+ "setIntPref was called with the proper preference name and value"
+ );
+});
+
+function createClientWrapper(preferencesFront) {
+ const clientWrapper = {
+ getFront: name => {
+ return preferencesFront;
+ },
+ };
+
+ return clientWrapper;
+}
diff --git a/devtools/client/aboutdebugging/test/xpcshell/xpcshell-head.js b/devtools/client/aboutdebugging/test/xpcshell/xpcshell-head.js
new file mode 100644
index 0000000000..733c0400da
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/xpcshell/xpcshell-head.js
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
diff --git a/devtools/client/aboutdebugging/test/xpcshell/xpcshell.ini b/devtools/client/aboutdebugging/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..42954757e8
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/xpcshell/xpcshell.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+tags = devtools
+head = xpcshell-head.js
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_extensions_path.js]
+[test_runtime_default_preferences.js]