summaryrefslogtreecommitdiffstats
path: root/devtools/client/aboutdebugging
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /devtools/client/aboutdebugging
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/aboutdebugging')
-rw-r--r--devtools/client/aboutdebugging/aboutdebugging.css199
-rw-r--r--devtools/client/aboutdebugging/aboutdebugging.xhtml22
-rw-r--r--devtools/client/aboutdebugging/components/aboutdebugging.js111
-rw-r--r--devtools/client/aboutdebugging/components/addons/controls.js97
-rw-r--r--devtools/client/aboutdebugging/components/addons/install-error.js26
-rw-r--r--devtools/client/aboutdebugging/components/addons/moz.build10
-rw-r--r--devtools/client/aboutdebugging/components/addons/panel.js146
-rw-r--r--devtools/client/aboutdebugging/components/addons/target.js84
-rw-r--r--devtools/client/aboutdebugging/components/moz.build17
-rw-r--r--devtools/client/aboutdebugging/components/panel-header.js24
-rw-r--r--devtools/client/aboutdebugging/components/panel-menu-entry.js48
-rw-r--r--devtools/client/aboutdebugging/components/panel-menu.js41
-rw-r--r--devtools/client/aboutdebugging/components/tabs/moz.build8
-rw-r--r--devtools/client/aboutdebugging/components/tabs/panel.js98
-rw-r--r--devtools/client/aboutdebugging/components/tabs/target.js53
-rw-r--r--devtools/client/aboutdebugging/components/target-list.js56
-rw-r--r--devtools/client/aboutdebugging/components/workers/moz.build9
-rw-r--r--devtools/client/aboutdebugging/components/workers/panel.js193
-rw-r--r--devtools/client/aboutdebugging/components/workers/service-worker-target.js231
-rw-r--r--devtools/client/aboutdebugging/components/workers/target.js57
-rw-r--r--devtools/client/aboutdebugging/initializer.js67
-rw-r--r--devtools/client/aboutdebugging/modules/addon.js23
-rw-r--r--devtools/client/aboutdebugging/modules/moz.build8
-rw-r--r--devtools/client/aboutdebugging/modules/worker.js77
-rw-r--r--devtools/client/aboutdebugging/moz.build14
-rw-r--r--devtools/client/aboutdebugging/test/.eslintrc.js26
-rw-r--r--devtools/client/aboutdebugging/test/addons/bad/manifest.json1
-rw-r--r--devtools/client/aboutdebugging/test/addons/bug1273184.xpibin0 -> 4246 bytes
-rw-r--r--devtools/client/aboutdebugging/test/addons/test-devtools-webextension-nobg/manifest.json10
-rw-r--r--devtools/client/aboutdebugging/test/addons/test-devtools-webextension/bg.js20
-rw-r--r--devtools/client/aboutdebugging/test/addons/test-devtools-webextension/manifest.json17
-rw-r--r--devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.html10
-rw-r--r--devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.js13
-rw-r--r--devtools/client/aboutdebugging/test/addons/unpacked/bootstrap.js22
-rw-r--r--devtools/client/aboutdebugging/test/addons/unpacked/install.rdf26
-rw-r--r--devtools/client/aboutdebugging/test/browser.ini44
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js83
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_debug_webextension.js74
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_debug_webextension_inspector.js82
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_debug_webextension_nobg.js84
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_debug_webextension_popup.js189
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js73
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_install.js51
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_reload.js207
-rw-r--r--devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js65
-rw-r--r--devtools/client/aboutdebugging/test/browser_page_not_found.js37
-rw-r--r--devtools/client/aboutdebugging/test/browser_service_workers.js51
-rw-r--r--devtools/client/aboutdebugging/test/browser_service_workers_not_compatible.js60
-rw-r--r--devtools/client/aboutdebugging/test/browser_service_workers_push.js105
-rw-r--r--devtools/client/aboutdebugging/test/browser_service_workers_push_service.js122
-rw-r--r--devtools/client/aboutdebugging/test/browser_service_workers_start.js97
-rw-r--r--devtools/client/aboutdebugging/test/browser_service_workers_status.js72
-rw-r--r--devtools/client/aboutdebugging/test/browser_service_workers_timeout.js92
-rw-r--r--devtools/client/aboutdebugging/test/browser_service_workers_unregister.js77
-rw-r--r--devtools/client/aboutdebugging/test/browser_tabs.js59
-rw-r--r--devtools/client/aboutdebugging/test/head.js367
-rw-r--r--devtools/client/aboutdebugging/test/service-workers/delay-sw.html22
-rw-r--r--devtools/client/aboutdebugging/test/service-workers/delay-sw.js17
-rw-r--r--devtools/client/aboutdebugging/test/service-workers/empty-sw.html22
-rw-r--r--devtools/client/aboutdebugging/test/service-workers/empty-sw.js1
-rw-r--r--devtools/client/aboutdebugging/test/service-workers/push-sw.html32
-rw-r--r--devtools/client/aboutdebugging/test/service-workers/push-sw.js33
62 files changed, 4082 insertions, 0 deletions
diff --git a/devtools/client/aboutdebugging/aboutdebugging.css b/devtools/client/aboutdebugging/aboutdebugging.css
new file mode 100644
index 000000000..5079c4928
--- /dev/null
+++ b/devtools/client/aboutdebugging/aboutdebugging.css
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+html, body {
+ height: 100%;
+ width: 100%;
+}
+
+h2, h3, h4 {
+ margin-bottom: 10px;
+}
+
+button {
+ padding-left: 20px;
+ padding-right: 20px;
+ min-width: 100px;
+ margin: 0 4px;
+}
+
+/* Category panels */
+
+.category {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.category-name {
+ cursor: default;
+}
+
+.app {
+ height: 100%;
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+}
+
+.main-content {
+ flex: 1;
+}
+
+.panel {
+ max-width: 800px;
+}
+
+/* Targets */
+
+.targets {
+ margin-bottom: 35px;
+}
+
+.target-list {
+ margin: 0;
+ padding: 0;
+}
+
+.target-container {
+ margin-top: 5px;
+ min-height: 34px;
+ display: flex;
+ flex-direction: row;
+ align-items: start;
+}
+
+.target-icon {
+ height: 24px;
+ margin: 0 5px 0 0;
+}
+
+.target-icon:not([src]) {
+ display: none;
+}
+
+.inverted-icons .target-icon {
+ filter: invert(30%);
+}
+
+.target {
+ flex: 1;
+ margin-top: 2px;
+ /* This is silly: https://bugzilla.mozilla.org/show_bug.cgi?id=1086218#c4. */
+ min-width: 0;
+}
+
+.target-details {
+ margin: 0;
+ padding: 0;
+ list-style-type: none
+}
+
+.target-detail {
+ display: flex;
+ font-size: 12px;
+ margin-top: 7px;
+ margin-bottom: 7px;
+}
+
+.target-detail a {
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+.target-detail strong {
+ white-space: nowrap;
+}
+
+.target-detail span {
+ /* Truncate items that are too long (e.g. URLs that would break the UI). */
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.target-detail > :not(:first-child) {
+ margin-left: 8px;
+}
+
+.target-status {
+ box-sizing: border-box;
+ display: inline-block;
+
+ min-width: 50px;
+ margin: 4px 5px 0 0;
+ padding: 2px;
+
+ border-width: 1px;
+ border-style: solid;
+
+ font-size: 0.6em;
+ text-align: center;
+}
+
+.target-status-stopped {
+ border-color: grey;
+ background-color: lightgrey;
+}
+
+.target-status-running {
+ border-color: limegreen;
+ background-color: palegreen;
+}
+
+.target-name {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.addons-controls {
+ display: flex;
+ flex-direction: row;
+}
+
+.addons-install-error {
+ background-color: #f3b0b0;
+ padding: 5px 10px;
+ margin: 5px 4px 5px 0px;
+}
+
+.service-worker-disabled .warning,
+.addons-install-error .warning {
+ background-image: url(chrome://devtools/skin/images/alerticon-warning.png);
+ background-size: 13px 12px;
+ margin-right: 10px;
+ display: inline-block;
+ width: 13px;
+ height: 12px;
+}
+
+@media (min-resolution: 1.1dppx) {
+ .service-worker-disabled .warning,
+ .addons-install-error .warning {
+ background-image: url(chrome://devtools/skin/images/alerticon-warning@2x.png);
+ }
+}
+
+.addons-options {
+ flex: 1;
+}
+
+.addons-debugging-label {
+ display: inline-block;
+ margin-inline-end: 1ch;
+}
+
+.error-page {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+}
+
+.error-page .error-page-details {
+ color: gray;
+}
diff --git a/devtools/client/aboutdebugging/aboutdebugging.xhtml b/devtools/client/aboutdebugging/aboutdebugging.xhtml
new file mode 100644
index 000000000..95f74b2b9
--- /dev/null
+++ b/devtools/client/aboutdebugging/aboutdebugging.xhtml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE html [
+<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD;
+<!ENTITY % toolboxDTD SYSTEM "chrome://devtools/locale/toolbox.dtd"> %toolboxDTD;
+<!ENTITY % aboutdebuggingDTD SYSTEM "chrome://devtools/locale/aboutdebugging.dtd"> %aboutdebuggingDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&aboutDebugging.fullTitle;</title>
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/content/aboutdebugging/aboutdebugging.css" type="text/css"/>
+ <script type="application/javascript" src="resource://devtools/client/shared/vendor/react.js"></script>
+ <script type="application/javascript;version=1.8" src="chrome://devtools/content/aboutdebugging/initializer.js"></script>
+ </head>
+ <body id="body">
+ </body>
+</html>
diff --git a/devtools/client/aboutdebugging/components/aboutdebugging.js b/devtools/client/aboutdebugging/components/aboutdebugging.js
new file mode 100644
index 000000000..601574dcb
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/aboutdebugging.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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { createFactory, createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const Services = require("Services");
+
+const PanelMenu = createFactory(require("./panel-menu"));
+
+loader.lazyGetter(this, "AddonsPanel",
+ () => createFactory(require("./addons/panel")));
+loader.lazyGetter(this, "TabsPanel",
+ () => createFactory(require("./tabs/panel")));
+loader.lazyGetter(this, "WorkersPanel",
+ () => createFactory(require("./workers/panel")));
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "Telemetry",
+ "devtools/client/shared/telemetry");
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+const panels = [{
+ id: "addons",
+ name: Strings.GetStringFromName("addons"),
+ icon: "chrome://devtools/skin/images/debugging-addons.svg",
+ component: AddonsPanel
+}, {
+ id: "tabs",
+ name: Strings.GetStringFromName("tabs"),
+ icon: "chrome://devtools/skin/images/debugging-tabs.svg",
+ component: TabsPanel
+}, {
+ id: "workers",
+ name: Strings.GetStringFromName("workers"),
+ icon: "chrome://devtools/skin/images/debugging-workers.svg",
+ component: WorkersPanel
+}];
+
+const defaultPanelId = "addons";
+
+module.exports = createClass({
+ displayName: "AboutDebuggingApp",
+
+ propTypes: {
+ client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ telemetry: PropTypes.instanceOf(Telemetry).isRequired
+ },
+
+ getInitialState() {
+ return {
+ selectedPanelId: defaultPanelId
+ };
+ },
+
+ componentDidMount() {
+ window.addEventListener("hashchange", this.onHashChange);
+ this.onHashChange();
+ this.props.telemetry.toolOpened("aboutdebugging");
+ },
+
+ componentWillUnmount() {
+ window.removeEventListener("hashchange", this.onHashChange);
+ this.props.telemetry.toolClosed("aboutdebugging");
+ this.props.telemetry.destroy();
+ },
+
+ onHashChange() {
+ this.setState({
+ selectedPanelId: window.location.hash.substr(1) || defaultPanelId
+ });
+ },
+
+ selectPanel(panelId) {
+ window.location.hash = "#" + panelId;
+ },
+
+ render() {
+ let { client } = this.props;
+ let { selectedPanelId } = this.state;
+ let selectPanel = this.selectPanel;
+ let selectedPanel = panels.find(p => p.id == selectedPanelId);
+ let panel;
+
+ if (selectedPanel) {
+ panel = selectedPanel.component({ client, id: selectedPanel.id });
+ } else {
+ panel = (
+ dom.div({ className: "error-page" },
+ dom.h1({ className: "header-name" },
+ Strings.GetStringFromName("pageNotFound")
+ ),
+ dom.h4({ className: "error-page-details" },
+ Strings.formatStringFromName("doesNotExist", [selectedPanelId], 1))
+ )
+ );
+ }
+
+ return dom.div({ className: "app" },
+ PanelMenu({ panels, selectedPanelId, selectPanel }),
+ dom.div({ className: "main-content" }, panel)
+ );
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/addons/controls.js b/devtools/client/aboutdebugging/components/addons/controls.js
new file mode 100644
index 000000000..7f985528c
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/addons/controls.js
@@ -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/. */
+
+/* eslint-env browser */
+/* globals AddonManager */
+
+"use strict";
+
+loader.lazyImporter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+
+const { Cc, Ci } = require("chrome");
+const { createFactory, createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const Services = require("Services");
+const AddonsInstallError = createFactory(require("./install-error"));
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+const MORE_INFO_URL = "https://developer.mozilla.org/docs/Tools" +
+ "/about:debugging#Enabling_add-on_debugging";
+
+module.exports = createClass({
+ displayName: "AddonsControls",
+
+ propTypes: {
+ debugDisabled: PropTypes.bool
+ },
+
+ getInitialState() {
+ return {
+ installError: null,
+ };
+ },
+
+ onEnableAddonDebuggingChange(event) {
+ let enabled = event.target.checked;
+ Services.prefs.setBoolPref("devtools.chrome.enabled", enabled);
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled", enabled);
+ },
+
+ loadAddonFromFile() {
+ this.setState({ installError: null });
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(window,
+ Strings.GetStringFromName("selectAddonFromFile2"),
+ Ci.nsIFilePicker.modeOpen);
+ let res = fp.show();
+ 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 = file.parent;
+ }
+
+ AddonManager.installTemporaryAddon(file)
+ .catch(e => {
+ console.error(e);
+ this.setState({ installError: e.message });
+ });
+ },
+
+ render() {
+ let { debugDisabled } = this.props;
+
+ return dom.div({ className: "addons-top" },
+ dom.div({ className: "addons-controls" },
+ dom.div({ className: "addons-options toggle-container-with-text" },
+ dom.input({
+ id: "enable-addon-debugging",
+ type: "checkbox",
+ checked: !debugDisabled,
+ onChange: this.onEnableAddonDebuggingChange,
+ }),
+ dom.label({
+ className: "addons-debugging-label",
+ htmlFor: "enable-addon-debugging",
+ title: Strings.GetStringFromName("addonDebugging.tooltip")
+ }, Strings.GetStringFromName("addonDebugging.label")),
+ "(",
+ dom.a({ href: MORE_INFO_URL, target: "_blank" },
+ Strings.GetStringFromName("moreInfo")),
+ ")"
+ ),
+ dom.button({
+ id: "load-addon-from-file",
+ onClick: this.loadAddonFromFile,
+ }, Strings.GetStringFromName("loadTemporaryAddon"))
+ ),
+ AddonsInstallError({ error: this.state.installError }));
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/addons/install-error.js b/devtools/client/aboutdebugging/components/addons/install-error.js
new file mode 100644
index 000000000..aea1c4f09
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/addons/install-error.js
@@ -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/. */
+
+/* eslint-env browser */
+"use strict";
+
+const { createClass, DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react");
+
+module.exports = createClass({
+ displayName: "AddonsInstallError",
+
+ propTypes: {
+ error: PropTypes.string
+ },
+
+ render() {
+ if (!this.props.error) {
+ return null;
+ }
+ let text = `There was an error during installation: ${this.props.error}`;
+ return dom.div({ className: "addons-install-error" },
+ dom.div({ className: "warning" }),
+ dom.span({}, text));
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/addons/moz.build b/devtools/client/aboutdebugging/components/addons/moz.build
new file mode 100644
index 000000000..378554f78
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/addons/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(
+ 'controls.js',
+ 'install-error.js',
+ 'panel.js',
+ 'target.js',
+)
diff --git a/devtools/client/aboutdebugging/components/addons/panel.js b/devtools/client/aboutdebugging/components/addons/panel.js
new file mode 100644
index 000000000..425a10a8d
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/addons/panel.js
@@ -0,0 +1,146 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
+const { createFactory, createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const Services = require("Services");
+
+const AddonsControls = createFactory(require("./controls"));
+const AddonTarget = createFactory(require("./target"));
+const PanelHeader = createFactory(require("../panel-header"));
+const TargetList = createFactory(require("../target-list"));
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+const ExtensionIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+const CHROME_ENABLED_PREF = "devtools.chrome.enabled";
+const REMOTE_ENABLED_PREF = "devtools.debugger.remote-enabled";
+
+module.exports = createClass({
+ displayName: "AddonsPanel",
+
+ propTypes: {
+ client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ id: PropTypes.string.isRequired
+ },
+
+ getInitialState() {
+ return {
+ extensions: [],
+ debugDisabled: false,
+ };
+ },
+
+ componentDidMount() {
+ AddonManager.addAddonListener(this);
+
+ Services.prefs.addObserver(CHROME_ENABLED_PREF,
+ this.updateDebugStatus, false);
+ Services.prefs.addObserver(REMOTE_ENABLED_PREF,
+ this.updateDebugStatus, false);
+
+ this.updateDebugStatus();
+ this.updateAddonsList();
+ },
+
+ componentWillUnmount() {
+ AddonManager.removeAddonListener(this);
+ Services.prefs.removeObserver(CHROME_ENABLED_PREF,
+ this.updateDebugStatus);
+ Services.prefs.removeObserver(REMOTE_ENABLED_PREF,
+ this.updateDebugStatus);
+ },
+
+ updateDebugStatus() {
+ let debugDisabled =
+ !Services.prefs.getBoolPref(CHROME_ENABLED_PREF) ||
+ !Services.prefs.getBoolPref(REMOTE_ENABLED_PREF);
+
+ this.setState({ debugDisabled });
+ },
+
+ updateAddonsList() {
+ this.props.client.listAddons()
+ .then(({addons}) => {
+ let extensions = addons.filter(addon => addon.debuggable).map(addon => {
+ return {
+ name: addon.name,
+ icon: addon.iconURL || ExtensionIcon,
+ addonID: addon.id,
+ addonActor: addon.actor,
+ temporarilyInstalled: addon.temporarilyInstalled
+ };
+ });
+
+ this.setState({ extensions });
+ }, error => {
+ throw new Error("Client error while listing addons: " + error);
+ });
+ },
+
+ /**
+ * Mandatory callback as AddonManager listener.
+ */
+ onInstalled() {
+ this.updateAddonsList();
+ },
+
+ /**
+ * Mandatory callback as AddonManager listener.
+ */
+ onUninstalled() {
+ this.updateAddonsList();
+ },
+
+ /**
+ * Mandatory callback as AddonManager listener.
+ */
+ onEnabled() {
+ this.updateAddonsList();
+ },
+
+ /**
+ * Mandatory callback as AddonManager listener.
+ */
+ onDisabled() {
+ this.updateAddonsList();
+ },
+
+ render() {
+ let { client, id } = this.props;
+ let { debugDisabled, extensions: targets } = this.state;
+ let name = Strings.GetStringFromName("extensions");
+ let targetClass = AddonTarget;
+
+ return dom.div({
+ id: id + "-panel",
+ className: "panel",
+ role: "tabpanel",
+ "aria-labelledby": id + "-header"
+ },
+ PanelHeader({
+ id: id + "-header",
+ name: Strings.GetStringFromName("addons")
+ }),
+ AddonsControls({ debugDisabled }),
+ dom.div({ id: "addons" },
+ TargetList({
+ id: "extensions",
+ name,
+ targets,
+ client,
+ debugDisabled,
+ targetClass,
+ sort: true
+ })
+ ));
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/addons/target.js b/devtools/client/aboutdebugging/components/addons/target.js
new file mode 100644
index 000000000..c21499650
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/addons/target.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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const { debugAddon } = require("../../modules/addon");
+const Services = require("Services");
+
+loader.lazyImporter(this, "BrowserToolboxProcess",
+ "resource://devtools/client/framework/ToolboxProcess.jsm");
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+module.exports = createClass({
+ displayName: "AddonTarget",
+
+ propTypes: {
+ client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ debugDisabled: PropTypes.bool,
+ target: PropTypes.shape({
+ addonActor: PropTypes.string.isRequired,
+ addonID: PropTypes.string.isRequired,
+ icon: PropTypes.string,
+ name: PropTypes.string.isRequired,
+ temporarilyInstalled: PropTypes.bool
+ }).isRequired
+ },
+
+ debug() {
+ let { target } = this.props;
+ debugAddon(target.addonID);
+ },
+
+ reload() {
+ let { client, target } = this.props;
+ // This function sometimes returns a partial promise that only
+ // implements then().
+ client.request({
+ to: target.addonActor,
+ type: "reload"
+ }).then(() => {}, error => {
+ throw new Error(
+ "Error reloading addon " + target.addonID + ": " + error);
+ });
+ },
+
+ render() {
+ let { target, debugDisabled } = this.props;
+ // Only temporarily installed add-ons can be reloaded.
+ const canBeReloaded = target.temporarilyInstalled;
+
+ return dom.li({ className: "target-container" },
+ dom.img({
+ className: "target-icon",
+ role: "presentation",
+ src: target.icon
+ }),
+ dom.div({ className: "target" },
+ dom.div({ className: "target-name", title: target.name }, target.name)
+ ),
+ dom.button({
+ className: "debug-button",
+ onClick: this.debug,
+ disabled: debugDisabled,
+ }, Strings.GetStringFromName("debug")),
+ dom.button({
+ className: "reload-button",
+ onClick: this.reload,
+ disabled: !canBeReloaded,
+ title: !canBeReloaded ?
+ Strings.GetStringFromName("reloadDisabledTooltip") : ""
+ }, Strings.GetStringFromName("reload"))
+ );
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/moz.build b/devtools/client/aboutdebugging/components/moz.build
new file mode 100644
index 000000000..829979dcc
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/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 += [
+ 'addons',
+ 'tabs',
+ 'workers',
+]
+
+DevToolsModules(
+ 'aboutdebugging.js',
+ 'panel-header.js',
+ 'panel-menu-entry.js',
+ 'panel-menu.js',
+ 'target-list.js',
+)
diff --git a/devtools/client/aboutdebugging/components/panel-header.js b/devtools/client/aboutdebugging/components/panel-header.js
new file mode 100644
index 000000000..5629018f7
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/panel-header.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 { createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+
+module.exports = createClass({
+ displayName: "PanelHeader",
+
+ propTypes: {
+ id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired
+ },
+
+ render() {
+ let { name, id } = this.props;
+
+ return dom.div({ className: "header" },
+ dom.h1({ id, className: "header-name" }, name));
+ },
+});
diff --git a/devtools/client/aboutdebugging/components/panel-menu-entry.js b/devtools/client/aboutdebugging/components/panel-menu-entry.js
new file mode 100644
index 000000000..1af02d435
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/panel-menu-entry.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 { createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+
+module.exports = createClass({
+ displayName: "PanelMenuEntry",
+
+ propTypes: {
+ icon: PropTypes.string.isRequired,
+ id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ selected: PropTypes.bool,
+ selectPanel: PropTypes.func.isRequired
+ },
+
+ onClick() {
+ this.props.selectPanel(this.props.id);
+ },
+
+ onKeyDown(event) {
+ if ([" ", "Enter"].includes(event.key)) {
+ this.props.selectPanel(this.props.id);
+ }
+ },
+
+ render() {
+ let { id, name, icon, selected } = this.props;
+
+ // Here .category, .category-icon, .category-name classnames are used to
+ // apply common styles defined.
+ let className = "category" + (selected ? " selected" : "");
+ return dom.div({
+ "aria-selected": selected,
+ "aria-controls": id + "-panel",
+ className,
+ onClick: this.onClick,
+ onKeyDown: this.onKeyDown,
+ tabIndex: "0",
+ role: "tab" },
+ dom.img({ className: "category-icon", src: icon, role: "presentation" }),
+ dom.div({ className: "category-name" }, name));
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/panel-menu.js b/devtools/client/aboutdebugging/components/panel-menu.js
new file mode 100644
index 000000000..b24493d78
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/panel-menu.js
@@ -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/. */
+
+"use strict";
+
+const { createClass, createFactory, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const PanelMenuEntry = createFactory(require("./panel-menu-entry"));
+
+module.exports = createClass({
+ displayName: "PanelMenu",
+
+ propTypes: {
+ panels: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ icon: PropTypes.string.isRequired,
+ component: PropTypes.func.isRequired
+ })).isRequired,
+ selectPanel: PropTypes.func.isRequired,
+ selectedPanelId: PropTypes.string
+ },
+
+ render() {
+ let { panels, selectedPanelId, selectPanel } = this.props;
+ let panelLinks = panels.map(({ id, name, icon }) => {
+ let selected = id == selectedPanelId;
+ return PanelMenuEntry({
+ id,
+ name,
+ icon,
+ selected,
+ selectPanel
+ });
+ });
+
+ // "categories" id used for styling purposes
+ return dom.div({ id: "categories", role: "tablist" }, panelLinks);
+ },
+});
diff --git a/devtools/client/aboutdebugging/components/tabs/moz.build b/devtools/client/aboutdebugging/components/tabs/moz.build
new file mode 100644
index 000000000..ee6a89e37
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/tabs/moz.build
@@ -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/.
+
+DevToolsModules(
+ 'panel.js',
+ 'target.js',
+)
diff --git a/devtools/client/aboutdebugging/components/tabs/panel.js b/devtools/client/aboutdebugging/components/tabs/panel.js
new file mode 100644
index 000000000..e280ce7f1
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/tabs/panel.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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { createClass, createFactory, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const Services = require("Services");
+
+const PanelHeader = createFactory(require("../panel-header"));
+const TargetList = createFactory(require("../target-list"));
+const TabTarget = createFactory(require("./target"));
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+module.exports = createClass({
+ displayName: "TabsPanel",
+
+ propTypes: {
+ client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ id: PropTypes.string.isRequired
+ },
+
+ getInitialState() {
+ return {
+ tabs: []
+ };
+ },
+
+ componentDidMount() {
+ let { client } = this.props;
+ client.addListener("tabListChanged", this.update);
+ this.update();
+ },
+
+ componentWillUnmount() {
+ let { client } = this.props;
+ client.removeListener("tabListChanged", this.update);
+ },
+
+ update() {
+ this.props.client.mainRoot.listTabs().then(({ tabs }) => {
+ // Filter out closed tabs (represented as `null`).
+ tabs = tabs.filter(tab => !!tab);
+ tabs.forEach(tab => {
+ // FIXME Also try to fetch low-res favicon. But we should use actor
+ // support for this to get the high-res one (bug 1061654).
+ let url = new URL(tab.url);
+ if (url.protocol.startsWith("http")) {
+ let prePath = url.origin;
+ let idx = url.pathname.lastIndexOf("/");
+ if (idx === -1) {
+ prePath += url.pathname;
+ } else {
+ prePath += url.pathname.substr(0, idx);
+ }
+ tab.icon = prePath + "/favicon.ico";
+ } else {
+ tab.icon = "chrome://devtools/skin/images/globe.svg";
+ }
+ });
+ this.setState({ tabs });
+ });
+ },
+
+ render() {
+ let { client, id } = this.props;
+ let { tabs } = this.state;
+
+ return dom.div({
+ id: id + "-panel",
+ className: "panel",
+ role: "tabpanel",
+ "aria-labelledby": id + "-header"
+ },
+ PanelHeader({
+ id: id + "-header",
+ name: Strings.GetStringFromName("tabs")
+ }),
+ dom.div({},
+ TargetList({
+ client,
+ id: "tabs",
+ name: Strings.GetStringFromName("tabs"),
+ sort: false,
+ targetClass: TabTarget,
+ targets: tabs
+ })
+ ));
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/tabs/target.js b/devtools/client/aboutdebugging/components/tabs/target.js
new file mode 100644
index 000000000..d946f8f61
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/tabs/target.js
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const Services = require("Services");
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+module.exports = createClass({
+ displayName: "TabTarget",
+
+ propTypes: {
+ target: PropTypes.shape({
+ icon: PropTypes.string,
+ outerWindowID: PropTypes.number.isRequired,
+ title: PropTypes.string,
+ url: PropTypes.string.isRequired
+ }).isRequired
+ },
+
+ debug() {
+ let { target } = this.props;
+ window.open("about:devtools-toolbox?type=tab&id=" + target.outerWindowID);
+ },
+
+ render() {
+ let { target } = this.props;
+
+ return dom.div({ className: "target-container" },
+ dom.img({
+ className: "target-icon",
+ role: "presentation",
+ src: target.icon
+ }),
+ dom.div({ className: "target" },
+ // If the title is empty, display the url instead.
+ dom.div({ className: "target-name", title: target.url },
+ target.title || target.url)
+ ),
+ dom.button({
+ className: "debug-button",
+ onClick: this.debug,
+ }, Strings.GetStringFromName("debug"))
+ );
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/target-list.js b/devtools/client/aboutdebugging/components/target-list.js
new file mode 100644
index 000000000..e2d5669e7
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/target-list.js
@@ -0,0 +1,56 @@
+/* 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 { createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const Services = require("Services");
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+const LocaleCompare = (a, b) => {
+ return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
+};
+
+module.exports = createClass({
+ displayName: "TargetList",
+
+ propTypes: {
+ client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ debugDisabled: PropTypes.bool,
+ error: PropTypes.node,
+ id: PropTypes.string.isRequired,
+ name: PropTypes.string,
+ sort: PropTypes.bool,
+ targetClass: PropTypes.func.isRequired,
+ targets: PropTypes.arrayOf(PropTypes.object).isRequired
+ },
+
+ render() {
+ let { client, debugDisabled, error, targetClass, targets, sort } = this.props;
+ if (sort) {
+ targets = targets.sort(LocaleCompare);
+ }
+ targets = targets.map(target => {
+ return targetClass({ client, target, debugDisabled });
+ });
+
+ let content = "";
+ if (error) {
+ content = error;
+ } else if (targets.length > 0) {
+ content = dom.ul({ className: "target-list" }, targets);
+ } else {
+ content = dom.p(null, Strings.GetStringFromName("nothing"));
+ }
+
+ return dom.div({ id: this.props.id, className: "targets" },
+ dom.h2(null, this.props.name), content);
+ },
+});
diff --git a/devtools/client/aboutdebugging/components/workers/moz.build b/devtools/client/aboutdebugging/components/workers/moz.build
new file mode 100644
index 000000000..ff33a5b28
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/workers/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(
+ 'panel.js',
+ 'service-worker-target.js',
+ 'target.js',
+)
diff --git a/devtools/client/aboutdebugging/components/workers/panel.js b/devtools/client/aboutdebugging/components/workers/panel.js
new file mode 100644
index 000000000..b1bab2b99
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/workers/panel.js
@@ -0,0 +1,193 @@
+/* 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/. */
+/* globals window */
+"use strict";
+
+loader.lazyImporter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+const { Ci } = require("chrome");
+const { createClass, createFactory, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const { getWorkerForms } = require("../../modules/worker");
+const Services = require("Services");
+
+const PanelHeader = createFactory(require("../panel-header"));
+const TargetList = createFactory(require("../target-list"));
+const WorkerTarget = createFactory(require("./target"));
+const ServiceWorkerTarget = createFactory(require("./service-worker-target"));
+
+loader.lazyImporter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+const WorkerIcon = "chrome://devtools/skin/images/debugging-workers.svg";
+const MORE_INFO_URL = "https://developer.mozilla.org/en-US/docs/Tools/about%3Adebugging";
+
+module.exports = createClass({
+ displayName: "WorkersPanel",
+
+ propTypes: {
+ client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ id: PropTypes.string.isRequired
+ },
+
+ getInitialState() {
+ return {
+ workers: {
+ service: [],
+ shared: [],
+ other: []
+ }
+ };
+ },
+
+ componentDidMount() {
+ let client = this.props.client;
+ client.addListener("workerListChanged", this.update);
+ client.addListener("serviceWorkerRegistrationListChanged", this.update);
+ client.addListener("processListChanged", this.update);
+ client.addListener("registration-changed", this.update);
+
+ this.update();
+ },
+
+ componentWillUnmount() {
+ let client = this.props.client;
+ client.removeListener("processListChanged", this.update);
+ client.removeListener("serviceWorkerRegistrationListChanged", this.update);
+ client.removeListener("workerListChanged", this.update);
+ client.removeListener("registration-changed", this.update);
+ },
+
+ update() {
+ let workers = this.getInitialState().workers;
+
+ getWorkerForms(this.props.client).then(forms => {
+ forms.registrations.forEach(form => {
+ workers.service.push({
+ icon: WorkerIcon,
+ name: form.url,
+ url: form.url,
+ scope: form.scope,
+ registrationActor: form.actor,
+ active: form.active
+ });
+ });
+
+ forms.workers.forEach(form => {
+ let worker = {
+ icon: WorkerIcon,
+ name: form.url,
+ url: form.url,
+ workerActor: form.actor
+ };
+ switch (form.type) {
+ case Ci.nsIWorkerDebugger.TYPE_SERVICE:
+ let registration = this.getRegistrationForWorker(form, workers.service);
+ if (registration) {
+ // XXX: Race, sometimes a ServiceWorkerRegistrationInfo doesn't
+ // have a scriptSpec, but its associated WorkerDebugger does.
+ if (!registration.url) {
+ registration.name = registration.url = form.url;
+ }
+ registration.workerActor = form.actor;
+ } else {
+ // If a service worker registration could not be found, this means we are in
+ // e10s, and registrations are not forwarded to other processes until they
+ // reach the activated state. Augment the worker as a registration worker to
+ // display it in aboutdebugging.
+ worker.scope = form.scope;
+ worker.active = false;
+ workers.service.push(worker);
+ }
+ break;
+ case Ci.nsIWorkerDebugger.TYPE_SHARED:
+ workers.shared.push(worker);
+ break;
+ default:
+ workers.other.push(worker);
+ }
+ });
+
+ // XXX: Filter out the service worker registrations for which we couldn't
+ // find the scriptSpec.
+ workers.service = workers.service.filter(reg => !!reg.url);
+
+ this.setState({ workers });
+ });
+ },
+
+ getRegistrationForWorker(form, registrations) {
+ for (let registration of registrations) {
+ if (registration.scope === form.scope) {
+ return registration;
+ }
+ }
+ return null;
+ },
+
+ render() {
+ let { client, id } = this.props;
+ let { workers } = this.state;
+
+ let isWindowPrivate = PrivateBrowsingUtils.isContentWindowPrivate(window);
+ let isPrivateBrowsingMode = PrivateBrowsingUtils.permanentPrivateBrowsing;
+ let isServiceWorkerDisabled = !Services.prefs
+ .getBoolPref("dom.serviceWorkers.enabled");
+ let errorMsg = isWindowPrivate || isPrivateBrowsingMode ||
+ isServiceWorkerDisabled ?
+ dom.p({ className: "service-worker-disabled" },
+ dom.div({ className: "warning" }),
+ Strings.GetStringFromName("configurationIsNotCompatible"),
+ " (",
+ dom.a({ href: MORE_INFO_URL, target: "_blank" },
+ Strings.GetStringFromName("moreInfo")),
+ ")"
+ ) : "";
+
+ return dom.div({
+ id: id + "-panel",
+ className: "panel",
+ role: "tabpanel",
+ "aria-labelledby": id + "-header"
+ },
+ PanelHeader({
+ id: id + "-header",
+ name: Strings.GetStringFromName("workers")
+ }),
+ dom.div({ id: "workers", className: "inverted-icons" },
+ TargetList({
+ client,
+ error: errorMsg,
+ id: "service-workers",
+ name: Strings.GetStringFromName("serviceWorkers"),
+ sort: true,
+ targetClass: ServiceWorkerTarget,
+ targets: workers.service
+ }),
+ TargetList({
+ client,
+ id: "shared-workers",
+ name: Strings.GetStringFromName("sharedWorkers"),
+ sort: true,
+ targetClass: WorkerTarget,
+ targets: workers.shared
+ }),
+ TargetList({
+ client,
+ id: "other-workers",
+ name: Strings.GetStringFromName("otherWorkers"),
+ sort: true,
+ targetClass: WorkerTarget,
+ targets: workers.other
+ })
+ ));
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/workers/service-worker-target.js b/devtools/client/aboutdebugging/components/workers/service-worker-target.js
new file mode 100644
index 000000000..d46f6f20f
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/workers/service-worker-target.js
@@ -0,0 +1,231 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const { debugWorker } = require("../../modules/worker");
+const Services = require("Services");
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+module.exports = createClass({
+ displayName: "ServiceWorkerTarget",
+
+ propTypes: {
+ client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ debugDisabled: PropTypes.bool,
+ target: PropTypes.shape({
+ active: PropTypes.bool,
+ icon: PropTypes.string,
+ name: PropTypes.string.isRequired,
+ url: PropTypes.string,
+ scope: PropTypes.string.isRequired,
+ // registrationActor can be missing in e10s.
+ registrationActor: PropTypes.string,
+ workerActor: PropTypes.string
+ }).isRequired
+ },
+
+ getInitialState() {
+ return {
+ pushSubscription: null
+ };
+ },
+
+ componentDidMount() {
+ let { client } = this.props;
+ client.addListener("push-subscription-modified", this.onPushSubscriptionModified);
+ this.updatePushSubscription();
+ },
+
+ componentDidUpdate(oldProps, oldState) {
+ let wasActive = oldProps.target.active;
+ if (!wasActive && this.isActive()) {
+ // While the service worker isn't active, any calls to `updatePushSubscription`
+ // won't succeed. If we just became active, make sure we didn't miss a push
+ // subscription change by updating it now.
+ this.updatePushSubscription();
+ }
+ },
+
+ componentWillUnmount() {
+ let { client } = this.props;
+ client.removeListener("push-subscription-modified", this.onPushSubscriptionModified);
+ },
+
+ debug() {
+ if (!this.isRunning()) {
+ // If the worker is not running, we can't debug it.
+ return;
+ }
+
+ let { client, target } = this.props;
+ debugWorker(client, target.workerActor);
+ },
+
+ push() {
+ if (!this.isActive() || !this.isRunning()) {
+ // If the worker is not running, we can't push to it.
+ // If the worker is not active, the registration might be unavailable and the
+ // push will not succeed.
+ return;
+ }
+
+ let { client, target } = this.props;
+ client.request({
+ to: target.workerActor,
+ type: "push"
+ });
+ },
+
+ start() {
+ if (!this.isActive() || this.isRunning()) {
+ // If the worker is not active or if it is already running, we can't start it.
+ return;
+ }
+
+ let { client, target } = this.props;
+ client.request({
+ to: target.registrationActor,
+ type: "start"
+ });
+ },
+
+ unregister() {
+ let { client, target } = this.props;
+ client.request({
+ to: target.registrationActor,
+ type: "unregister"
+ });
+ },
+
+ onPushSubscriptionModified(type, data) {
+ let { target } = this.props;
+ if (data.from === target.registrationActor) {
+ this.updatePushSubscription();
+ }
+ },
+
+ updatePushSubscription() {
+ if (!this.props.target.registrationActor) {
+ // A valid registrationActor is needed to retrieve the push subscription.
+ return;
+ }
+
+ let { client, target } = this.props;
+ client.request({
+ to: target.registrationActor,
+ type: "getPushSubscription"
+ }, ({ subscription }) => {
+ this.setState({ pushSubscription: subscription });
+ });
+ },
+
+ isRunning() {
+ // We know the target is running if it has a worker actor.
+ return !!this.props.target.workerActor;
+ },
+
+ isActive() {
+ return this.props.target.active;
+ },
+
+ getServiceWorkerStatus() {
+ if (this.isActive() && this.isRunning()) {
+ return "running";
+ } else if (this.isActive()) {
+ return "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 "registering";
+ },
+
+ renderButtons() {
+ let pushButton = dom.button({
+ className: "push-button",
+ onClick: this.push
+ }, Strings.GetStringFromName("push"));
+
+ let debugButton = dom.button({
+ className: "debug-button",
+ onClick: this.debug,
+ disabled: this.props.debugDisabled
+ }, Strings.GetStringFromName("debug"));
+
+ let startButton = dom.button({
+ className: "start-button",
+ onClick: this.start,
+ }, Strings.GetStringFromName("start"));
+
+ if (this.isRunning()) {
+ if (this.isActive()) {
+ return [pushButton, debugButton];
+ }
+ // Only debug button is available if the service worker is not active.
+ return debugButton;
+ }
+ return startButton;
+ },
+
+ renderUnregisterLink() {
+ if (!this.isActive()) {
+ // If not active, there might be no registrationActor available.
+ return null;
+ }
+
+ return dom.a({
+ onClick: this.unregister,
+ className: "unregister-link"
+ }, Strings.GetStringFromName("unregister"));
+ },
+
+ render() {
+ let { target } = this.props;
+ let { pushSubscription } = this.state;
+ let status = this.getServiceWorkerStatus();
+
+ return dom.div({ className: "target-container" },
+ dom.img({
+ className: "target-icon",
+ role: "presentation",
+ src: target.icon
+ }),
+ dom.span({ className: `target-status target-status-${status}` },
+ Strings.GetStringFromName(status)),
+ dom.div({ className: "target" },
+ dom.div({ className: "target-name", title: target.name }, target.name),
+ dom.ul({ className: "target-details" },
+ (pushSubscription ?
+ dom.li({ className: "target-detail" },
+ dom.strong(null, Strings.GetStringFromName("pushService")),
+ dom.span({
+ className: "service-worker-push-url",
+ title: pushSubscription.endpoint
+ }, pushSubscription.endpoint)) :
+ null
+ ),
+ dom.li({ className: "target-detail" },
+ dom.strong(null, Strings.GetStringFromName("scope")),
+ dom.span({
+ className: "service-worker-scope",
+ title: target.scope
+ }, target.scope),
+ this.renderUnregisterLink()
+ )
+ )
+ ),
+ this.renderButtons()
+ );
+ }
+});
diff --git a/devtools/client/aboutdebugging/components/workers/target.js b/devtools/client/aboutdebugging/components/workers/target.js
new file mode 100644
index 000000000..c1f6420ac
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/workers/target.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { createClass, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+const { debugWorker } = require("../../modules/worker");
+const Services = require("Services");
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+module.exports = createClass({
+ displayName: "WorkerTarget",
+
+ propTypes: {
+ client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ debugDisabled: PropTypes.bool,
+ target: PropTypes.shape({
+ icon: PropTypes.string,
+ name: PropTypes.string.isRequired,
+ workerActor: PropTypes.string
+ }).isRequired
+ },
+
+ debug() {
+ let { client, target } = this.props;
+ debugWorker(client, target.workerActor);
+ },
+
+ render() {
+ let { target, debugDisabled } = this.props;
+
+ return dom.li({ className: "target-container" },
+ dom.img({
+ className: "target-icon",
+ role: "presentation",
+ src: target.icon
+ }),
+ dom.div({ className: "target" },
+ dom.div({ className: "target-name", title: target.name }, target.name)
+ ),
+ dom.button({
+ className: "debug-button",
+ onClick: this.debug,
+ disabled: debugDisabled
+ }, Strings.GetStringFromName("debug"))
+ );
+ }
+});
diff --git a/devtools/client/aboutdebugging/initializer.js b/devtools/client/aboutdebugging/initializer.js
new file mode 100644
index 000000000..f1b91f14d
--- /dev/null
+++ b/devtools/client/aboutdebugging/initializer.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/. */
+
+/* eslint-env browser */
+/* globals DebuggerClient, DebuggerServer, Telemetry */
+
+"use strict";
+
+const { loader } = Components.utils.import(
+ "resource://devtools/shared/Loader.jsm", {});
+const { BrowserLoader } = Components.utils.import(
+ "resource://devtools/client/shared/browser-loader.js", {});
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "DebuggerServer",
+ "devtools/server/main", true);
+loader.lazyRequireGetter(this, "Telemetry",
+ "devtools/client/shared/telemetry");
+
+const { require } = BrowserLoader({
+ baseURI: "resource://devtools/client/aboutdebugging/",
+ window
+});
+
+const { createFactory } = require("devtools/client/shared/vendor/react");
+const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
+
+const AboutDebuggingApp = createFactory(require("./components/aboutdebugging"));
+
+var AboutDebugging = {
+ init() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ DebuggerServer.allowChromeProcess = true;
+
+ this.client = new DebuggerClient(DebuggerServer.connectPipe());
+
+ this.client.connect().then(() => {
+ let client = this.client;
+ let telemetry = new Telemetry();
+
+ render(AboutDebuggingApp({ client, telemetry }),
+ document.querySelector("#body"));
+ });
+ },
+
+ destroy() {
+ unmountComponentAtNode(document.querySelector("#body"));
+
+ this.client.close();
+ this.client = null;
+ },
+};
+
+window.addEventListener("DOMContentLoaded", function load() {
+ window.removeEventListener("DOMContentLoaded", load);
+ AboutDebugging.init();
+});
+
+window.addEventListener("unload", function unload() {
+ window.removeEventListener("unload", unload);
+ AboutDebugging.destroy();
+});
diff --git a/devtools/client/aboutdebugging/modules/addon.js b/devtools/client/aboutdebugging/modules/addon.js
new file mode 100644
index 000000000..d800462b9
--- /dev/null
+++ b/devtools/client/aboutdebugging/modules/addon.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";
+
+loader.lazyImporter(this, "BrowserToolboxProcess",
+ "resource://devtools/client/framework/ToolboxProcess.jsm");
+
+let toolbox = null;
+
+exports.debugAddon = function (addonID) {
+ if (toolbox) {
+ toolbox.close();
+ }
+
+ toolbox = BrowserToolboxProcess.init({
+ addonID,
+ onClose: () => {
+ toolbox = null;
+ }
+ });
+};
diff --git a/devtools/client/aboutdebugging/modules/moz.build b/devtools/client/aboutdebugging/modules/moz.build
new file mode 100644
index 000000000..de840f957
--- /dev/null
+++ b/devtools/client/aboutdebugging/modules/moz.build
@@ -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/.
+
+DevToolsModules(
+ 'addon.js',
+ 'worker.js',
+)
diff --git a/devtools/client/aboutdebugging/modules/worker.js b/devtools/client/aboutdebugging/modules/worker.js
new file mode 100644
index 000000000..1623088c6
--- /dev/null
+++ b/devtools/client/aboutdebugging/modules/worker.js
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Task } = require("devtools/shared/task");
+
+loader.lazyRequireGetter(this, "gDevTools",
+ "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "TargetFactory",
+ "devtools/client/framework/target", true);
+loader.lazyRequireGetter(this, "Toolbox",
+ "devtools/client/framework/toolbox", true);
+
+/**
+ * Open a window-hosted toolbox to debug the worker associated to the provided
+ * worker actor.
+ *
+ * @param {DebuggerClient} client
+ * @param {Object} workerActor
+ * worker actor form to debug
+ */
+exports.debugWorker = function (client, workerActor) {
+ client.attachWorker(workerActor, (response, workerClient) => {
+ let workerTarget = TargetFactory.forWorker(workerClient);
+ gDevTools.showToolbox(workerTarget, "jsdebugger", Toolbox.HostType.WINDOW)
+ .then(toolbox => {
+ toolbox.once("destroy", () => workerClient.detach());
+ });
+ });
+};
+
+/**
+ * Retrieve all service worker registrations as well as workers from the parent
+ * and child processes.
+ *
+ * @param {DebuggerClient} client
+ * @return {Object}
+ * - {Array} registrations
+ * Array of ServiceWorkerRegistrationActor forms
+ * - {Array} workers
+ * Array of WorkerActor forms
+ */
+exports.getWorkerForms = Task.async(function* (client) {
+ let registrations = [];
+ let workers = [];
+
+ try {
+ // List service worker registrations
+ ({ registrations } =
+ yield client.mainRoot.listServiceWorkerRegistrations());
+
+ // List workers from the Parent process
+ ({ workers } = yield client.mainRoot.listWorkers());
+
+ // And then from the Child processes
+ let { processes } = yield client.mainRoot.listProcesses();
+ for (let process of processes) {
+ // Ignore parent process
+ if (process.parent) {
+ continue;
+ }
+ let { form } = yield client.getProcess(process.id);
+ let processActor = form.actor;
+ let response = yield client.request({
+ to: processActor,
+ type: "listWorkers"
+ });
+ workers = workers.concat(response.workers);
+ }
+ } catch (e) {
+ // Something went wrong, maybe our client is disconnected?
+ }
+
+ return { registrations, workers };
+});
diff --git a/devtools/client/aboutdebugging/moz.build b/devtools/client/aboutdebugging/moz.build
new file mode 100644
index 000000000..fd180ba46
--- /dev/null
+++ b/devtools/client/aboutdebugging/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ 'components',
+ 'modules',
+]
+
+BROWSER_CHROME_MANIFESTS += [
+ 'test/browser.ini'
+]
diff --git a/devtools/client/aboutdebugging/test/.eslintrc.js b/devtools/client/aboutdebugging/test/.eslintrc.js
new file mode 100644
index 000000000..8c4bee0ef
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/.eslintrc.js
@@ -0,0 +1,26 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js",
+ // All globals made available in aboutdebugging head.js file.
+ "globals": {
+ "AddonManager": true,
+ "addTab": true,
+ "assertHasTarget": true,
+ "CHROME_ROOT": true,
+ "changeAboutDebuggingHash": true,
+ "closeAboutDebugging": true,
+ "getServiceWorkerList": true,
+ "getSupportsFile": true,
+ "installAddon": true,
+ "openAboutDebugging": true,
+ "openPanel": true,
+ "removeTab": true,
+ "uninstallAddon": true,
+ "unregisterServiceWorker": true,
+ "waitForInitialAddonList": true,
+ "waitForMutation": true,
+ "waitForServiceWorkerRegistered": true
+ }
+};
diff --git a/devtools/client/aboutdebugging/test/addons/bad/manifest.json b/devtools/client/aboutdebugging/test/addons/bad/manifest.json
new file mode 100644
index 000000000..4ab10b4de
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/bad/manifest.json
@@ -0,0 +1 @@
+this is not valid json
diff --git a/devtools/client/aboutdebugging/test/addons/bug1273184.xpi b/devtools/client/aboutdebugging/test/addons/bug1273184.xpi
new file mode 100644
index 000000000..e1c42376e
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/bug1273184.xpi
Binary files differ
diff --git a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension-nobg/manifest.json b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension-nobg/manifest.json
new file mode 100644
index 000000000..289d8b918
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension-nobg/manifest.json
@@ -0,0 +1,10 @@
+{
+ "manifest_version": 2,
+ "name": "test-devtools-webextension-nobg",
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": "test-devtools-webextension-nobg@mozilla.org"
+ }
+ }
+}
diff --git a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/bg.js b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/bg.js
new file mode 100644
index 000000000..7ab93c46a
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/bg.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env browser */
+/* global browser */
+
+"use strict";
+
+document.body.innerText = "Background Page Body Test Content";
+
+// This function are called from the webconsole test:
+// browser_addons_debug_webextension.js
+
+function myWebExtensionAddonFunction() { // eslint-disable-line no-unused-vars
+ console.log("Background page function called", browser.runtime.getManifest());
+}
+
+function myWebExtensionShowPopup() { // eslint-disable-line no-unused-vars
+ console.log("readyForOpenPopup");
+}
diff --git a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/manifest.json b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/manifest.json
new file mode 100644
index 000000000..f224e5dcf
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/manifest.json
@@ -0,0 +1,17 @@
+{
+ "manifest_version": 2,
+ "name": "test-devtools-webextension",
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": "test-devtools-webextension@mozilla.org"
+ }
+ },
+ "background": {
+ "scripts": ["bg.js"]
+ },
+ "browser_action": {
+ "default_title": "WebExtension Popup Debugging",
+ "default_popup": "popup.html"
+ }
+}
diff --git a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.html b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.html
new file mode 100644
index 000000000..4e3f7aba2
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <script src="popup.js"></script>
+ </head>
+ <body>
+ Background Page Body Test Content
+ </body>
+</html>
diff --git a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.js b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.js
new file mode 100644
index 000000000..035375682
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env browser */
+/* global browser */
+
+"use strict";
+
+// This function is called from the webconsole test:
+// browser_addons_debug_webextension.js
+function myWebExtensionPopupAddonFunction() { // eslint-disable-line no-unused-vars
+ console.log("Popup page function called", browser.runtime.getManifest());
+}
diff --git a/devtools/client/aboutdebugging/test/addons/unpacked/bootstrap.js b/devtools/client/aboutdebugging/test/addons/unpacked/bootstrap.js
new file mode 100644
index 000000000..d96e31e5e
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/unpacked/bootstrap.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env browser */
+/* exported startup, shutdown, install, uninstall */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+// This function is called from the webconsole test:
+// browser_addons_debug_bootstrapped.js
+function myBootstrapAddonFunction() { // eslint-disable-line no-unused-vars
+ Services.obs.notifyObservers(null, "addon-console-works", null);
+}
+
+function startup() {
+ Services.obs.notifyObservers(null, "test-devtools", null);
+}
+function shutdown() {}
+function install() {}
+function uninstall() {}
diff --git a/devtools/client/aboutdebugging/test/addons/unpacked/install.rdf b/devtools/client/aboutdebugging/test/addons/unpacked/install.rdf
new file mode 100644
index 000000000..91c7474cc
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/unpacked/install.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<!--
+# 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/.
+-->
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest"
+ em:id="test-devtools@mozilla.org"
+ em:name="test-devtools"
+ em:version="1.0"
+ em:type="2"
+ em:creator="Mozilla">
+
+ <em:bootstrap>true</em:bootstrap>
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>44.0a1</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/devtools/client/aboutdebugging/test/browser.ini b/devtools/client/aboutdebugging/test/browser.ini
new file mode 100644
index 000000000..90ed59d21
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser.ini
@@ -0,0 +1,44 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ addons/unpacked/bootstrap.js
+ addons/unpacked/install.rdf
+ addons/bad/manifest.json
+ addons/bug1273184.xpi
+ addons/test-devtools-webextension/*
+ addons/test-devtools-webextension-nobg/*
+ service-workers/delay-sw.html
+ service-workers/delay-sw.js
+ service-workers/empty-sw.html
+ service-workers/empty-sw.js
+ service-workers/push-sw.html
+ service-workers/push-sw.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_addons_debug_bootstrapped.js]
+[browser_addons_debug_webextension.js]
+tags = webextensions
+[browser_addons_debug_webextension_inspector.js]
+tags = webextensions
+[browser_addons_debug_webextension_nobg.js]
+tags = webextensions
+[browser_addons_debug_webextension_popup.js]
+tags = webextensions
+[browser_addons_debugging_initial_state.js]
+[browser_addons_install.js]
+[browser_addons_reload.js]
+[browser_addons_toggle_debug.js]
+[browser_page_not_found.js]
+[browser_service_workers.js]
+[browser_service_workers_not_compatible.js]
+[browser_service_workers_push.js]
+[browser_service_workers_push_service.js]
+[browser_service_workers_start.js]
+[browser_service_workers_status.js]
+[browser_service_workers_timeout.js]
+skip-if = true # Bug 1232931
+[browser_service_workers_unregister.js]
+[browser_tabs.js]
+skip-if = true # Bug 1304941
diff --git a/devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js b/devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js
new file mode 100644
index 000000000..982c4c726
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
+requestLongerTimeout(2);
+
+const ADDON_ID = "test-devtools@mozilla.org";
+const ADDON_NAME = "test-devtools";
+
+const { BrowserToolboxProcess } = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+add_task(function* () {
+ yield new Promise(resolve => {
+ let options = {"set": [
+ // Force enabling of addons debugging
+ ["devtools.chrome.enabled", true],
+ ["devtools.debugger.remote-enabled", true],
+ // Disable security prompt
+ ["devtools.debugger.prompt-connection", false],
+ // Enable Browser toolbox test script execution via env variable
+ ["devtools.browser-toolbox.allow-unsafe-script", true],
+ ]};
+ SpecialPowers.pushPrefEnv(options, resolve);
+ });
+
+ let { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+ yield installAddon({
+ document,
+ path: "addons/unpacked/install.rdf",
+ name: ADDON_NAME,
+ });
+
+ // Retrieve the DEBUG button for the addon
+ let names = [...document.querySelectorAll("#addons .target-name")];
+ let name = names.filter(element => element.textContent === ADDON_NAME)[0];
+ ok(name, "Found the addon in the list");
+ let targetElement = name.parentNode.parentNode;
+ let debugBtn = targetElement.querySelector(".debug-button");
+ ok(debugBtn, "Found its debug button");
+
+ // Wait for a notification sent by a script evaluated the test addon via
+ // the web console.
+ let onCustomMessage = new Promise(done => {
+ Services.obs.addObserver(function listener() {
+ Services.obs.removeObserver(listener, "addon-console-works");
+ done();
+ }, "addon-console-works", false);
+ });
+
+ // Be careful, this JS function is going to be executed in the addon toolbox,
+ // which lives in another process. So do not try to use any scope variable!
+ let env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment);
+ let testScript = function () {
+ /* eslint-disable no-undef */
+ toolbox.selectTool("webconsole")
+ .then(console => {
+ let { jsterm } = console.hud;
+ return jsterm.execute("myBootstrapAddonFunction()");
+ })
+ .then(() => toolbox.destroy());
+ /* eslint-enable no-undef */
+ };
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+ registerCleanupFunction(() => {
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+ });
+
+ let onToolboxClose = BrowserToolboxProcess.once("close");
+
+ debugBtn.click();
+
+ yield onCustomMessage;
+ ok(true, "Received the notification message from the bootstrap.js function");
+
+ yield onToolboxClose;
+ ok(true, "Addon toolbox closed");
+
+ yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_addons_debug_webextension.js b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension.js
new file mode 100644
index 000000000..5082c1f3f
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// 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 ADDON_MANIFEST_PATH = "addons/test-devtools-webextension/manifest.json";
+
+const {
+ BrowserToolboxProcess
+} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+/**
+ * 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(function* testWebExtensionsToolboxWebConsole() {
+ let {
+ tab, document, debugBtn,
+ } = yield setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_MANIFEST_PATH);
+
+ // Wait for a notification sent by a script evaluated the test addon via
+ // the web console.
+ let onCustomMessage = new Promise(done => {
+ Services.obs.addObserver(function listener(message, topic) {
+ let apiMessage = message.wrappedJSObject;
+ if (!apiMessage.originAttributes ||
+ apiMessage.originAttributes.addonId != ADDON_ID) {
+ return;
+ }
+ Services.obs.removeObserver(listener, "console-api-log-event");
+ done(apiMessage.arguments);
+ }, "console-api-log-event", false);
+ });
+
+ // Be careful, this JS function is going to be executed in the addon toolbox,
+ // which lives in another process. So do not try to use any scope variable!
+ let env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment);
+ let testScript = function () {
+ /* eslint-disable no-undef */
+ toolbox.selectTool("webconsole")
+ .then(console => {
+ let { jsterm } = console.hud;
+ return jsterm.execute("myWebExtensionAddonFunction()");
+ })
+ .then(() => toolbox.destroy());
+ /* eslint-enable no-undef */
+ };
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+ registerCleanupFunction(() => {
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+ });
+
+ let onToolboxClose = BrowserToolboxProcess.once("close");
+
+ debugBtn.click();
+
+ let args = yield onCustomMessage;
+ ok(true, "Received console message from the background page function as expected");
+ is(args[0], "Background page function called", "Got the expected console message");
+ is(args[1] && args[1].name, ADDON_NAME,
+ "Got the expected manifest from WebExtension API");
+
+ yield onToolboxClose;
+ ok(true, "Addon toolbox closed");
+
+ yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_inspector.js b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_inspector.js
new file mode 100644
index 000000000..3adc918d8
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_inspector.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// 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 ADDON_PATH = "addons/test-devtools-webextension/manifest.json";
+
+const {
+ BrowserToolboxProcess
+} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+/**
+ * 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(function* testWebExtensionsToolboxInspector() {
+ let {
+ tab, document, debugBtn,
+ } = yield setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_PATH);
+
+ // Be careful, this JS function is going to be executed in the addon toolbox,
+ // which lives in another process. So do not try to use any scope variable!
+ let env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment);
+ let testScript = function () {
+ /* eslint-disable no-undef */
+ toolbox.selectTool("inspector")
+ .then(inspector => {
+ return inspector.walker.querySelector(inspector.walker.rootNode, "body");
+ })
+ .then((nodeActor) => {
+ if (!nodeActor) {
+ throw new Error("nodeActor not found");
+ }
+
+ dump("Got a nodeActor\n");
+
+ if (!(nodeActor.inlineTextChild)) {
+ throw new Error("inlineTextChild not found");
+ }
+
+ dump("Got a nodeActor with an inline text child\n");
+
+ let expectedValue = "Background Page Body Test Content";
+ let actualValue = nodeActor.inlineTextChild._form.nodeValue;
+
+ if (String(actualValue).trim() !== String(expectedValue).trim()) {
+ throw new Error(
+ `mismatched inlineTextchild value: "${actualValue}" !== "${expectedValue}"`
+ );
+ }
+
+ dump("Got the expected inline text content in the selected node\n");
+ return Promise.resolve();
+ })
+ .then(() => toolbox.destroy())
+ .catch((error) => {
+ dump("Error while running code in the browser toolbox process:\n");
+ dump(error + "\n");
+ dump("stack:\n" + error.stack + "\n");
+ });
+ /* eslint-enable no-undef */
+ };
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+ registerCleanupFunction(() => {
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+ });
+
+ let onToolboxClose = BrowserToolboxProcess.once("close");
+ debugBtn.click();
+ yield onToolboxClose;
+
+ ok(true, "Addon toolbox closed");
+
+ yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_nobg.js b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_nobg.js
new file mode 100644
index 000000000..0e731fc5d
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_nobg.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
+requestLongerTimeout(2);
+
+const ADDON_NOBG_ID = "test-devtools-webextension-nobg@mozilla.org";
+const ADDON_NOBG_NAME = "test-devtools-webextension-nobg";
+const ADDON_NOBG_PATH = "addons/test-devtools-webextension-nobg/manifest.json";
+
+const {
+ BrowserToolboxProcess
+} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+/**
+ * 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(function* testWebExtensionsToolboxNoBackgroundPage() {
+ let {
+ tab, document, debugBtn,
+ } = yield setupTestAboutDebuggingWebExtension(ADDON_NOBG_NAME, ADDON_NOBG_PATH);
+
+ // Be careful, this JS function is going to be executed in the addon toolbox,
+ // which lives in another process. So do not try to use any scope variable!
+ let env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment);
+ let testScript = function () {
+ /* eslint-disable no-undef */
+ toolbox.selectTool("inspector")
+ .then(inspector => {
+ return inspector.walker.querySelector(inspector.walker.rootNode, "body");
+ })
+ .then((nodeActor) => {
+ if (!nodeActor) {
+ throw new Error("nodeActor not found");
+ }
+
+ dump("Got a nodeActor\n");
+
+ if (!(nodeActor.inlineTextChild)) {
+ throw new Error("inlineTextChild not found");
+ }
+
+ dump("Got a nodeActor with an inline text child\n");
+
+ let expectedValue = "Your addon does not have any document opened yet.";
+ let actualValue = nodeActor.inlineTextChild._form.nodeValue;
+
+ if (actualValue !== expectedValue) {
+ throw new Error(
+ `mismatched inlineTextchild value: "${actualValue}" !== "${expectedValue}"`
+ );
+ }
+
+ dump("Got the expected inline text content in the selected node\n");
+ return Promise.resolve();
+ })
+ .then(() => toolbox.destroy())
+ .catch((error) => {
+ dump("Error while running code in the browser toolbox process:\n");
+ dump(error + "\n");
+ dump("stack:\n" + error.stack + "\n");
+ });
+ /* eslint-enable no-undef */
+ };
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+ registerCleanupFunction(() => {
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+ });
+
+ let onToolboxClose = BrowserToolboxProcess.once("close");
+ debugBtn.click();
+ yield onToolboxClose;
+
+ ok(true, "Addon toolbox closed");
+
+ yield uninstallAddon({document, id: ADDON_NOBG_ID, name: ADDON_NOBG_NAME});
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_popup.js b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_popup.js
new file mode 100644
index 000000000..d2cb8031e
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_popup.js
@@ -0,0 +1,189 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// 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 ADDON_MANIFEST_PATH = "addons/test-devtools-webextension/manifest.json";
+
+const {
+ BrowserToolboxProcess
+} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+/**
+ * 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;
+ * - the webextension developer toolbox has a working Inspector panel, with the
+ * background page as default target;
+ * - 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);
+ * - the webextension 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.
+ */
+
+/**
+ * Returns the widget id for an extension with the passed id.
+ */
+function makeWidgetId(id) {
+ id = id.toLowerCase();
+ return id.replace(/[^a-z0-9_-]/g, "_");
+}
+
+add_task(function* testWebExtensionsToolboxSwitchToPopup() {
+ let {
+ tab, document, debugBtn,
+ } = yield setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_MANIFEST_PATH);
+
+ let onReadyForOpenPopup = new Promise(done => {
+ Services.obs.addObserver(function listener(message, topic) {
+ let apiMessage = message.wrappedJSObject;
+ if (!apiMessage.originAttributes ||
+ apiMessage.originAttributes.addonId != ADDON_ID) {
+ return;
+ }
+
+ if (apiMessage.arguments[0] == "readyForOpenPopup") {
+ Services.obs.removeObserver(listener, "console-api-log-event");
+ done();
+ }
+ }, "console-api-log-event", false);
+ });
+
+ // Be careful, this JS function is going to be executed in the addon toolbox,
+ // which lives in another process. So do not try to use any scope variable!
+ let env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment);
+ let testScript = function () {
+ /* eslint-disable no-undef */
+
+ let jsterm;
+ let popupFramePromise;
+
+ toolbox.selectTool("webconsole")
+ .then(console => {
+ dump(`Clicking the noautohide button\n`);
+ toolbox.doc.getElementById("command-button-noautohide").click();
+ dump(`Clicked the noautohide button\n`);
+
+ popupFramePromise = new Promise(resolve => {
+ let listener = (event, data) => {
+ if (data.frames.some(({url}) => url && url.endsWith("popup.html"))) {
+ toolbox.target.off("frame-update", listener);
+ resolve();
+ }
+ };
+ toolbox.target.on("frame-update", listener);
+ });
+
+ let waitForFrameListUpdate = new Promise((done) => {
+ toolbox.target.once("frame-update", () => {
+ done(console);
+ });
+ });
+
+ jsterm = console.hud.jsterm;
+ jsterm.execute("myWebExtensionShowPopup()");
+
+ // Wait the initial frame update (which list the background page).
+ return waitForFrameListUpdate;
+ })
+ .then((console) => {
+ // Wait the new frame update (once the extension popup has been opened).
+ return popupFramePromise;
+ })
+ .then(() => {
+ dump(`Clicking the frame list button\n`);
+ let btn = toolbox.doc.getElementById("command-button-frames");
+ let menu = toolbox.showFramesMenu({target: btn});
+ dump(`Clicked the frame list button\n`);
+ return menu.once("open").then(() => {
+ return menu;
+ });
+ })
+ .then(frameMenu => {
+ let frames = frameMenu.items;
+
+ if (frames.length != 2) {
+ throw Error(`Number of frames found is wrong: ${frames.length} != 2`);
+ }
+
+ let popupFrameBtn = frames.filter((frame) => {
+ return frame.label.endsWith("popup.html");
+ }).pop();
+
+ if (!popupFrameBtn) {
+ throw Error("Extension Popup frame not found in the listed frames");
+ }
+
+ let waitForNavigated = toolbox.target.once("navigate");
+
+ popupFrameBtn.click();
+
+ return waitForNavigated;
+ })
+ .then(() => {
+ return jsterm.execute("myWebExtensionPopupAddonFunction()");
+ })
+ .then(() => toolbox.destroy())
+ .catch((error) => {
+ dump("Error while running code in the browser toolbox process:\n");
+ dump(error + "\n");
+ dump("stack:\n" + error.stack + "\n");
+ });
+ /* eslint-enable no-undef */
+ };
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+ registerCleanupFunction(() => {
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+ });
+
+ // Wait for a notification sent by a script evaluated the test addon via
+ // the web console.
+ let onPopupCustomMessage = new Promise(done => {
+ Services.obs.addObserver(function listener(message, topic) {
+ let apiMessage = message.wrappedJSObject;
+ if (!apiMessage.originAttributes ||
+ apiMessage.originAttributes.addonId != ADDON_ID) {
+ return;
+ }
+
+ if (apiMessage.arguments[0] == "Popup page function called") {
+ Services.obs.removeObserver(listener, "console-api-log-event");
+ done(apiMessage.arguments);
+ }
+ }, "console-api-log-event", false);
+ });
+
+ let onToolboxClose = BrowserToolboxProcess.once("close");
+
+ debugBtn.click();
+
+ yield onReadyForOpenPopup;
+
+ let browserActionId = makeWidgetId(ADDON_ID) + "-browser-action";
+ let browserActionEl = window.document.getElementById(browserActionId);
+
+ ok(browserActionEl, "Got the browserAction button from the browser UI");
+ browserActionEl.click();
+ info("Clicked on the browserAction button");
+
+ let args = yield onPopupCustomMessage;
+ ok(true, "Received console message from the popup page function as expected");
+ is(args[0], "Popup page function called", "Got the expected console message");
+ is(args[1] && args[1].name, ADDON_NAME,
+ "Got the expected manifest from WebExtension API");
+
+ yield onToolboxClose;
+
+ ok(true, "Addon toolbox closed");
+
+ yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js b/devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js
new file mode 100644
index 000000000..8a63d0061
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that addons debugging controls are properly enabled/disabled depending
+// on the values of the relevant preferences:
+// - devtools.chrome.enabled
+// - devtools.debugger.remote-enabled
+
+const ADDON_ID = "test-devtools@mozilla.org";
+const ADDON_NAME = "test-devtools";
+
+const TEST_DATA = [
+ {
+ chromeEnabled: false,
+ debuggerRemoteEnable: false,
+ expected: false,
+ }, {
+ chromeEnabled: false,
+ debuggerRemoteEnable: true,
+ expected: false,
+ }, {
+ chromeEnabled: true,
+ debuggerRemoteEnable: false,
+ expected: false,
+ }, {
+ chromeEnabled: true,
+ debuggerRemoteEnable: true,
+ expected: true,
+ }
+];
+
+add_task(function* () {
+ for (let testData of TEST_DATA) {
+ yield testCheckboxState(testData);
+ }
+});
+
+function* testCheckboxState(testData) {
+ info("Set preferences as defined by the current test data.");
+ yield new Promise(resolve => {
+ let options = {"set": [
+ ["devtools.chrome.enabled", testData.chromeEnabled],
+ ["devtools.debugger.remote-enabled", testData.debuggerRemoteEnable],
+ ]};
+ SpecialPowers.pushPrefEnv(options, resolve);
+ });
+
+ let { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+
+ info("Install a test addon.");
+ yield installAddon({
+ document,
+ path: "addons/unpacked/install.rdf",
+ name: ADDON_NAME,
+ });
+
+ info("Test checkbox checked state.");
+ let addonDebugCheckbox = document.querySelector("#enable-addon-debugging");
+ is(addonDebugCheckbox.checked, testData.expected,
+ "Addons debugging checkbox should be in expected state.");
+
+ info("Test debug buttons disabled state.");
+ let debugButtons = [...document.querySelectorAll("#addons .debug-button")];
+ ok(debugButtons.every(b => b.disabled != testData.expected),
+ "Debug buttons should be in the expected state");
+
+ info("Uninstall test addon installed earlier.");
+ yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+
+ yield closeAboutDebugging(tab);
+}
diff --git a/devtools/client/aboutdebugging/test/browser_addons_install.js b/devtools/client/aboutdebugging/test/browser_addons_install.js
new file mode 100644
index 000000000..4c3a97c9f
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_install.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const ADDON_ID = "test-devtools@mozilla.org";
+const ADDON_NAME = "test-devtools";
+
+add_task(function* () {
+ let { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+
+ // Install this add-on, and verify that it appears in the about:debugging UI
+ yield installAddon({
+ document,
+ path: "addons/unpacked/install.rdf",
+ name: ADDON_NAME,
+ });
+
+ // Install the add-on, and verify that it disappears in the about:debugging UI
+ yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+
+ yield closeAboutDebugging(tab);
+});
+
+add_task(function* () {
+ let { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+
+ // Start an observer that looks for the install error before
+ // actually doing the install
+ let top = document.querySelector(".addons-top");
+ let promise = waitForMutation(top, { childList: true });
+
+ // Mock the file picker to select a test addon
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(null);
+ let file = getSupportsFile("addons/bad/manifest.json");
+ MockFilePicker.returnFiles = [file.file];
+
+ // Trigger the file picker by clicking on the button
+ document.getElementById("load-addon-from-file").click();
+
+ // Now wait for the install error to appear.
+ yield promise;
+
+ // And check that it really is there.
+ let err = document.querySelector(".addons-install-error");
+ isnot(err, null, "Addon install error message appeared");
+
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_addons_reload.js b/devtools/client/aboutdebugging/test/browser_addons_reload.js
new file mode 100644
index 000000000..506495a60
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_reload.js
@@ -0,0 +1,207 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const ADDON_ID = "test-devtools@mozilla.org";
+const ADDON_NAME = "test-devtools";
+
+/**
+ * Returns a promise that resolves when the given add-on event is fired. The
+ * resolved value is an array of arguments passed for the event.
+ */
+function promiseAddonEvent(event) {
+ return new Promise(resolve => {
+ let listener = {
+ [event]: function (...args) {
+ AddonManager.removeAddonListener(listener);
+ resolve(args);
+ }
+ };
+
+ AddonManager.addAddonListener(listener);
+ });
+}
+
+function* tearDownAddon(addon) {
+ const onUninstalled = promiseAddonEvent("onUninstalled");
+ addon.uninstall();
+ const [uninstalledAddon] = yield onUninstalled;
+ is(uninstalledAddon.id, addon.id,
+ `Add-on was uninstalled: ${uninstalledAddon.id}`);
+}
+
+function getReloadButton(document, addonName) {
+ const names = [...document.querySelectorAll("#addons .target-name")];
+ const name = names.filter(element => element.textContent === addonName)[0];
+ ok(name, `Found ${addonName} add-on in the list`);
+ const targetElement = name.parentNode.parentNode;
+ const reloadButton = targetElement.querySelector(".reload-button");
+ info(`Found reload button for ${addonName}`);
+ return reloadButton;
+}
+
+function installAddonWithManager(filePath) {
+ return new Promise((resolve, reject) => {
+ AddonManager.getInstallForFile(filePath, install => {
+ if (!install) {
+ throw new Error(`An install was not created for ${filePath}`);
+ }
+ install.addListener({
+ onDownloadFailed: reject,
+ onDownloadCancelled: reject,
+ onInstallFailed: reject,
+ onInstallCancelled: reject,
+ onInstallEnded: resolve
+ });
+ install.install();
+ });
+ });
+}
+
+function getAddonByID(addonId) {
+ return new Promise(resolve => {
+ AddonManager.getAddonByID(addonId, addon => resolve(addon));
+ });
+}
+
+/**
+ * Creates a web extension from scratch in a temporary location.
+ * The object must be removed when you're finished working with it.
+ */
+class TempWebExt {
+ constructor(addonId) {
+ this.addonId = addonId;
+ this.tmpDir = FileUtils.getDir("TmpD", ["browser_addons_reload"]);
+ if (!this.tmpDir.exists()) {
+ this.tmpDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ }
+ this.sourceDir = this.tmpDir.clone();
+ this.sourceDir.append(this.addonId);
+ if (!this.sourceDir.exists()) {
+ this.sourceDir.create(Ci.nsIFile.DIRECTORY_TYPE,
+ FileUtils.PERMS_DIRECTORY);
+ }
+ }
+
+ writeManifest(manifestData) {
+ const manifest = this.sourceDir.clone();
+ manifest.append("manifest.json");
+ if (manifest.exists()) {
+ manifest.remove(true);
+ }
+ const fos = Cc["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream);
+ fos.init(manifest,
+ FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
+ FileUtils.MODE_TRUNCATE,
+ FileUtils.PERMS_FILE, 0);
+
+ const manifestString = JSON.stringify(manifestData);
+ fos.write(manifestString, manifestString.length);
+ fos.close();
+ }
+
+ remove() {
+ return this.tmpDir.remove(true);
+ }
+}
+
+add_task(function* reloadButtonReloadsAddon() {
+ const { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+ yield installAddon({
+ document,
+ path: "addons/unpacked/install.rdf",
+ name: ADDON_NAME,
+ });
+
+ const reloadButton = getReloadButton(document, ADDON_NAME);
+ is(reloadButton.disabled, false, "Reload button should not be disabled");
+ is(reloadButton.title, "", "Reload button should not have a tooltip");
+ const onInstalled = promiseAddonEvent("onInstalled");
+
+ const onBootstrapInstallCalled = new Promise(done => {
+ Services.obs.addObserver(function listener() {
+ Services.obs.removeObserver(listener, ADDON_NAME, false);
+ info("Add-on was re-installed: " + ADDON_NAME);
+ done();
+ }, ADDON_NAME, false);
+ });
+
+ reloadButton.click();
+
+ const [reloadedAddon] = yield onInstalled;
+ is(reloadedAddon.name, ADDON_NAME,
+ "Add-on was reloaded: " + reloadedAddon.name);
+
+ yield onBootstrapInstallCalled;
+ yield tearDownAddon(reloadedAddon);
+ yield closeAboutDebugging(tab);
+});
+
+add_task(function* reloadButtonRefreshesMetadata() {
+ const { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+
+ const manifestBase = {
+ "manifest_version": 2,
+ "name": "Temporary web extension",
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": ADDON_ID
+ }
+ }
+ };
+
+ const tempExt = new TempWebExt(ADDON_ID);
+ tempExt.writeManifest(manifestBase);
+
+ const onAddonListUpdated = waitForMutation(getAddonList(document),
+ { childList: true });
+ const onInstalled = promiseAddonEvent("onInstalled");
+ yield AddonManager.installTemporaryAddon(tempExt.sourceDir);
+ const [addon] = yield onInstalled;
+ info(`addon installed: ${addon.id}`);
+ yield onAddonListUpdated;
+
+ const newName = "Temporary web extension (updated)";
+ tempExt.writeManifest(Object.assign({}, manifestBase, {name: newName}));
+
+ // Wait for the add-on list to be updated with the reloaded name.
+ const onReInstall = promiseAddonEvent("onInstalled");
+ const onAddonReloaded = waitForContentMutation(getAddonList(document));
+
+ const reloadButton = getReloadButton(document, manifestBase.name);
+ reloadButton.click();
+
+ yield onAddonReloaded;
+ const [reloadedAddon] = yield onReInstall;
+ // Make sure the name was updated correctly.
+ const allAddons = [...document.querySelectorAll("#addons .target-name")]
+ .map(element => element.textContent);
+ const nameWasUpdated = allAddons.some(name => name === newName);
+ ok(nameWasUpdated, `New name appeared in reloaded add-ons: ${allAddons}`);
+
+ yield tearDownAddon(reloadedAddon);
+ tempExt.remove();
+ yield closeAboutDebugging(tab);
+});
+
+add_task(function* onlyTempInstalledAddonsCanBeReloaded() {
+ const { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+ const onAddonListUpdated = waitForMutation(getAddonList(document),
+ { childList: true });
+ yield installAddonWithManager(getSupportsFile("addons/bug1273184.xpi").file);
+ yield onAddonListUpdated;
+ const addon = yield getAddonByID("bug1273184@tests");
+
+ const reloadButton = getReloadButton(document, addon.name);
+ ok(reloadButton, "Reload button exists");
+ is(reloadButton.disabled, true, "Reload button should be disabled");
+ ok(reloadButton.title, "Disabled reload button should have a tooltip");
+
+ yield tearDownAddon(addon);
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js b/devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js
new file mode 100644
index 000000000..1f67cac5b
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that individual Debug buttons are disabled when "Addons debugging"
+// is disabled.
+// Test that the buttons are updated dynamically if the preference changes.
+
+const ADDON_ID = "test-devtools@mozilla.org";
+const ADDON_NAME = "test-devtools";
+
+add_task(function* () {
+ info("Turn off addon debugging.");
+ yield new Promise(resolve => {
+ let options = {"set": [
+ ["devtools.chrome.enabled", false],
+ ["devtools.debugger.remote-enabled", false],
+ ]};
+ SpecialPowers.pushPrefEnv(options, resolve);
+ });
+
+ let { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+
+ info("Install a test addon.");
+ yield installAddon({
+ document,
+ path: "addons/unpacked/install.rdf",
+ name: ADDON_NAME,
+ });
+
+ let addonDebugCheckbox = document.querySelector("#enable-addon-debugging");
+ ok(!addonDebugCheckbox.checked, "Addons debugging should be disabled.");
+
+ info("Check all debug buttons are disabled.");
+ let debugButtons = [...document.querySelectorAll("#addons .debug-button")];
+ ok(debugButtons.every(b => b.disabled), "Debug buttons should be disabled");
+
+ info("Click on 'Enable addons debugging' checkbox.");
+ let addonsContainer = document.getElementById("addons");
+ let onAddonsMutation = waitForMutation(addonsContainer,
+ { subtree: true, attributes: true });
+ addonDebugCheckbox.click();
+ yield onAddonsMutation;
+
+ info("Check all debug buttons are enabled.");
+ ok(addonDebugCheckbox.checked, "Addons debugging should be enabled.");
+ debugButtons = [...document.querySelectorAll("#addons .debug-button")];
+ ok(debugButtons.every(b => !b.disabled), "Debug buttons should be enabled");
+
+ info("Click again on 'Enable addons debugging' checkbox.");
+ onAddonsMutation = waitForMutation(addonsContainer,
+ { subtree: true, attributes: true });
+ addonDebugCheckbox.click();
+ yield onAddonsMutation;
+
+ info("Check all debug buttons are disabled again.");
+ debugButtons = [...document.querySelectorAll("#addons .debug-button")];
+ ok(debugButtons.every(b => b.disabled), "Debug buttons should be disabled");
+
+ info("Uninstall addon installed earlier.");
+ yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_page_not_found.js b/devtools/client/aboutdebugging/test/browser_page_not_found.js
new file mode 100644
index 000000000..107bc8b91
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_page_not_found.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that navigating to a about:debugging#invalid-hash should show up an
+// error page.
+// Every url navigating including #invalid-hash should be kept in history and
+// navigate back as expected.
+add_task(function* () {
+ let { tab, document } = yield openAboutDebugging("invalid-hash");
+ let element = document.querySelector(".header-name");
+ is(element.textContent, "Page not found", "Show error page");
+
+ yield openPanel(document, "addons-panel");
+ yield waitForInitialAddonList(document);
+ element = document.querySelector(".header-name");
+ is(element.textContent, "Add-ons", "Show Addons");
+
+ yield changeAboutDebuggingHash(document, "invalid-hash");
+ element = document.querySelector(".header-name");
+ is(element.textContent, "Page not found", "Show error page");
+
+ gBrowser.goBack();
+ yield waitForMutation(
+ document.querySelector(".main-content"), {childList: true});
+ yield waitForInitialAddonList(document);
+ element = document.querySelector(".header-name");
+ is(element.textContent, "Add-ons", "Show Addons");
+
+ gBrowser.goBack();
+ yield waitForMutation(
+ document.querySelector(".main-content"), {childList: true});
+ element = document.querySelector(".header-name");
+ is(element.textContent, "Page not found", "Show error page");
+
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_service_workers.js b/devtools/client/aboutdebugging/test/browser_service_workers.js
new file mode 100644
index 000000000..74e4efb3e
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Service workers can't be loaded from chrome://,
+// but http:// is ok with dom.serviceWorkers.testing.enabled turned on.
+const SERVICE_WORKER = URL_ROOT + "service-workers/empty-sw.js";
+const TAB_URL = URL_ROOT + "service-workers/empty-sw.html";
+
+add_task(function* () {
+ yield new Promise(done => {
+ let options = {"set": [
+ // Accept workers from mochitest's http.
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let { tab, document } = yield openAboutDebugging("workers");
+
+ let swTab = yield addTab(TAB_URL);
+
+ let serviceWorkersElement = getServiceWorkerList(document);
+
+ yield waitForMutation(serviceWorkersElement, { childList: true });
+
+ // Check that the service worker appears in the UI
+ let names = [...document.querySelectorAll("#service-workers .target-name")];
+ names = names.map(element => element.textContent);
+ ok(names.includes(SERVICE_WORKER),
+ "The service worker url appears in the list: " + names);
+
+ try {
+ yield unregisterServiceWorker(swTab, serviceWorkersElement);
+ ok(true, "Service worker registration unregistered");
+ } catch (e) {
+ ok(false, "SW not unregistered; " + e);
+ }
+
+ // Check that the service worker disappeared from the UI
+ names = [...document.querySelectorAll("#service-workers .target-name")];
+ names = names.map(element => element.textContent);
+ ok(!names.includes(SERVICE_WORKER),
+ "The service worker url is no longer in the list: " + names);
+
+ yield removeTab(swTab);
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_service_workers_not_compatible.js b/devtools/client/aboutdebugging/test/browser_service_workers_not_compatible.js
new file mode 100644
index 000000000..6221230b5
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_not_compatible.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that Service Worker section should show warning message in
+// about:debugging if any of following conditions is met:
+// 1. service worker is disabled
+// 2. the about:debugging pannel is openned in private browsing mode
+// 3. the about:debugging pannel is openned in private content window
+
+var imgClass = ".service-worker-disabled .warning";
+
+add_task(function* () {
+ yield new Promise(done => {
+ info("disable service workers");
+ let options = {"set": [
+ ["dom.serviceWorkers.enabled", false],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let { tab, document } = yield openAboutDebugging("workers");
+ // Check that the warning img appears in the UI
+ let img = document.querySelector(imgClass);
+ ok(img, "warning message is rendered");
+
+ yield closeAboutDebugging(tab);
+});
+
+add_task(function* () {
+ yield new Promise(done => {
+ info("set private browsing mode as default");
+ let options = {"set": [
+ ["browser.privatebrowsing.autostart", true],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let { tab, document } = yield openAboutDebugging("workers");
+ // Check that the warning img appears in the UI
+ let img = document.querySelector(imgClass);
+ ok(img, "warning message is rendered");
+
+ yield closeAboutDebugging(tab);
+});
+
+add_task(function* () {
+ info("Opening a new private window");
+ let win = OpenBrowserWindow({private: true});
+ yield waitForDelayedStartupFinished(win);
+
+ let { tab, document } = yield openAboutDebugging("workers", win);
+ // Check that the warning img appears in the UI
+ let img = document.querySelector(imgClass);
+ ok(img, "warning message is rendered");
+
+ yield closeAboutDebugging(tab);
+ win.close();
+});
diff --git a/devtools/client/aboutdebugging/test/browser_service_workers_push.js b/devtools/client/aboutdebugging/test/browser_service_workers_push.js
new file mode 100644
index 000000000..ff7789458
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_push.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global sendAsyncMessage */
+
+"use strict";
+
+// Test that clicking on the Push button next to a Service Worker works as
+// intended in about:debugging.
+// It should trigger a "push" notification in the worker.
+
+// Service workers can't be loaded from chrome://, but http:// is ok with
+// dom.serviceWorkers.testing.enabled turned on.
+const SERVICE_WORKER = URL_ROOT + "service-workers/push-sw.js";
+const TAB_URL = URL_ROOT + "service-workers/push-sw.html";
+
+add_task(function* () {
+ info("Turn on workers via mochitest http.");
+ yield new Promise(done => {
+ let options = { "set": [
+ // Accept workers from mochitest's http.
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let { tab, document } = yield openAboutDebugging("workers");
+
+ // Listen for mutations in the service-workers list.
+ let serviceWorkersElement = getServiceWorkerList(document);
+ let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+
+ // Open a tab that registers a push service worker.
+ let swTab = yield addTab(TAB_URL);
+
+ info("Make the test page notify us when the service worker sends a message.");
+
+ yield ContentTask.spawn(swTab.linkedBrowser, {}, function () {
+ let win = content.wrappedJSObject;
+ win.navigator.serviceWorker.addEventListener("message", function (event) {
+ sendAsyncMessage(event.data);
+ }, false);
+ });
+
+ // Expect the service worker to claim the test window when activating.
+ let mm = swTab.linkedBrowser.messageManager;
+ let onClaimed = new Promise(done => {
+ mm.addMessageListener("sw-claimed", function listener() {
+ mm.removeMessageListener("sw-claimed", listener);
+ done();
+ });
+ });
+
+ // Wait for the service-workers list to update.
+ yield onMutation;
+
+ // Check that the service worker appears in the UI.
+ assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
+
+ info("Ensure that the registration resolved before trying to interact with " +
+ "the service worker.");
+ yield waitForServiceWorkerRegistered(swTab);
+ ok(true, "Service worker registration resolved");
+
+ yield waitForServiceWorkerActivation(SERVICE_WORKER, document);
+
+ // Retrieve the Push button for the worker.
+ let names = [...document.querySelectorAll("#service-workers .target-name")];
+ let name = names.filter(element => element.textContent === SERVICE_WORKER)[0];
+ ok(name, "Found the service worker in the list");
+
+ let targetElement = name.parentNode.parentNode;
+
+ let pushBtn = targetElement.querySelector(".push-button");
+ ok(pushBtn, "Found its push button");
+
+ info("Wait for the service worker to claim the test window before " +
+ "proceeding.");
+ yield onClaimed;
+
+ info("Click on the Push button and wait for the service worker to receive " +
+ "a push notification");
+ let onPushNotification = new Promise(done => {
+ mm.addMessageListener("sw-pushed", function listener() {
+ mm.removeMessageListener("sw-pushed", listener);
+ done();
+ });
+ });
+ pushBtn.click();
+ yield onPushNotification;
+ ok(true, "Service worker received a push notification");
+
+ // Finally, unregister the service worker itself.
+ try {
+ yield unregisterServiceWorker(swTab, serviceWorkersElement);
+ ok(true, "Service worker registration unregistered");
+ } catch (e) {
+ ok(false, "SW not unregistered; " + e);
+ }
+
+ yield removeTab(swTab);
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_service_workers_push_service.js b/devtools/client/aboutdebugging/test/browser_service_workers_push_service.js
new file mode 100644
index 000000000..732380a4c
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_push_service.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that a Service Worker registration's Push Service subscription appears
+// in about:debugging if it exists, and disappears when unregistered.
+
+// Service workers can't be loaded from chrome://, but http:// is ok with
+// dom.serviceWorkers.testing.enabled turned on.
+const SERVICE_WORKER = URL_ROOT + "service-workers/push-sw.js";
+const TAB_URL = URL_ROOT + "service-workers/push-sw.html";
+
+const FAKE_ENDPOINT = "https://fake/endpoint";
+
+const PushService = Cc["@mozilla.org/push/Service;1"]
+ .getService(Ci.nsIPushService).wrappedJSObject;
+
+add_task(function* () {
+ info("Turn on workers via mochitest http.");
+ yield SpecialPowers.pushPrefEnv({
+ "set": [
+ // Accept workers from mochitest's http.
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ // Enable the push service.
+ ["dom.push.enabled", true],
+ ["dom.push.connection.enabled", true],
+ ]
+ });
+
+ info("Mock the push service");
+ PushService.service = {
+ _registrations: new Map(),
+ _notify(scope) {
+ Services.obs.notifyObservers(
+ null,
+ PushService.subscriptionModifiedTopic,
+ scope);
+ },
+ init() {},
+ register(pageRecord) {
+ let registration = {
+ endpoint: FAKE_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) {
+ let deleted = this._registrations.delete(pageRecord.scope);
+ if (deleted) {
+ this._notify(pageRecord.scope);
+ }
+ return Promise.resolve(deleted);
+ },
+ };
+
+ let { tab, document } = yield openAboutDebugging("workers");
+
+ // Listen for mutations in the service-workers list.
+ let serviceWorkersElement = document.getElementById("service-workers");
+ let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+
+ // Open a tab that registers a push service worker.
+ let swTab = yield addTab(TAB_URL);
+
+ // Wait for the service-workers list to update.
+ yield onMutation;
+
+ // Check that the service worker appears in the UI.
+ assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
+
+ yield waitForServiceWorkerActivation(SERVICE_WORKER, document);
+
+ // Wait for the service worker details to update.
+ let names = [...document.querySelectorAll("#service-workers .target-name")];
+ let name = names.filter(element => element.textContent === SERVICE_WORKER)[0];
+ ok(name, "Found the service worker in the list");
+
+ let targetContainer = name.parentNode.parentNode;
+ let targetDetailsElement = targetContainer.querySelector(".target-details");
+
+ // Retrieve the push subscription endpoint URL, and verify it looks good.
+ let pushURL = targetContainer.querySelector(".service-worker-push-url");
+ if (!pushURL) {
+ yield waitForMutation(targetDetailsElement, { childList: true });
+ pushURL = targetContainer.querySelector(".service-worker-push-url");
+ }
+
+ ok(pushURL, "Found the push service URL in the service worker details");
+ is(pushURL.textContent, FAKE_ENDPOINT, "The push service URL looks correct");
+
+ // Unsubscribe from the push service.
+ ContentTask.spawn(swTab.linkedBrowser, {}, function () {
+ let win = content.wrappedJSObject;
+ return win.sub.unsubscribe();
+ });
+
+ // Wait for the service worker details to update again.
+ yield waitForMutation(targetDetailsElement, { childList: true });
+ ok(!targetContainer.querySelector(".service-worker-push-url"),
+ "The push service URL should be removed");
+
+ // Finally, unregister the service worker itself.
+ try {
+ yield unregisterServiceWorker(swTab, serviceWorkersElement);
+ ok(true, "Service worker registration unregistered");
+ } catch (e) {
+ ok(false, "SW not unregistered; " + e);
+ }
+
+ info("Unmock the push service");
+ PushService.service = null;
+
+ yield removeTab(swTab);
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_service_workers_start.js b/devtools/client/aboutdebugging/test/browser_service_workers_start.js
new file mode 100644
index 000000000..8c15d97c4
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_start.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that clicking on the Start button next to a Service Worker works as
+// intended in about:debugging.
+// It should cause a worker to start running in a child process.
+
+// Service workers can't be loaded from chrome://, but http:// is ok with
+// dom.serviceWorkers.testing.enabled turned on.
+const SERVICE_WORKER = URL_ROOT + "service-workers/empty-sw.js";
+const TAB_URL = URL_ROOT + "service-workers/empty-sw.html";
+
+const SW_TIMEOUT = 1000;
+
+add_task(function* () {
+ info("Turn on workers via mochitest http.");
+ yield new Promise(done => {
+ let options = { "set": [
+ // Accept workers from mochitest's http.
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ // Reduce the timeout to accelerate service worker freezing
+ ["dom.serviceWorkers.idle_timeout", SW_TIMEOUT],
+ ["dom.serviceWorkers.idle_extended_timeout", SW_TIMEOUT],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let { tab, document } = yield openAboutDebugging("workers");
+
+ // Listen for mutations in the service-workers list.
+ let serviceWorkersElement = getServiceWorkerList(document);
+ let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+
+ // Open a tab that registers an empty service worker.
+ let swTab = yield addTab(TAB_URL);
+
+ // Wait for the service-workers list to update.
+ yield onMutation;
+
+ // Check that the service worker appears in the UI.
+ assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
+
+ info("Ensure that the registration resolved before trying to interact with " +
+ "the service worker.");
+ yield waitForServiceWorkerRegistered(swTab);
+ ok(true, "Service worker registration resolved");
+
+ yield waitForServiceWorkerActivation(SERVICE_WORKER, document);
+
+ // Retrieve the Target element corresponding to the service worker.
+ let names = [...document.querySelectorAll("#service-workers .target-name")];
+ let name = names.filter(element => element.textContent === SERVICE_WORKER)[0];
+ ok(name, "Found the service worker in the list");
+ let targetElement = name.parentNode.parentNode;
+
+ // The service worker may already be killed with the low 1s timeout
+ if (!targetElement.querySelector(".start-button")) {
+ // Check that there is a Debug button but not a Start button.
+ ok(targetElement.querySelector(".debug-button"), "Found its debug button");
+
+ // Wait for the service worker to be killed due to inactivity.
+ yield waitForMutation(targetElement, { childList: true });
+ } else {
+ // Check that there is no Debug button when the SW is already shut down.
+ ok(!targetElement.querySelector(".debug-button"), "No debug button when " +
+ "the worker is already killed");
+ }
+
+ // We should now have a Start button but no Debug button.
+ let startBtn = targetElement.querySelector(".start-button");
+ ok(startBtn, "Found its start button");
+ ok(!targetElement.querySelector(".debug-button"), "No debug button");
+
+ // Click on the Start button and wait for the service worker to be back.
+ let onStarted = waitForMutation(targetElement, { childList: true });
+ startBtn.click();
+ yield onStarted;
+
+ // Check that we have a Debug button but not a Start button again.
+ ok(targetElement.querySelector(".debug-button"), "Found its debug button");
+ ok(!targetElement.querySelector(".start-button"), "No start button");
+
+ // Finally, unregister the service worker itself.
+ try {
+ yield unregisterServiceWorker(swTab, serviceWorkersElement);
+ ok(true, "Service worker registration unregistered");
+ } catch (e) {
+ ok(false, "SW not unregistered; " + e);
+ }
+
+ yield removeTab(swTab);
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_service_workers_status.js b/devtools/client/aboutdebugging/test/browser_service_workers_status.js
new file mode 100644
index 000000000..dff384ee5
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_status.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Service workers can't be loaded from chrome://,
+// but http:// is ok with dom.serviceWorkers.testing.enabled turned on.
+const SERVICE_WORKER = URL_ROOT + "service-workers/delay-sw.js";
+const TAB_URL = URL_ROOT + "service-workers/delay-sw.html";
+const SW_TIMEOUT = 2000;
+
+requestLongerTimeout(2);
+
+add_task(function* () {
+ yield SpecialPowers.pushPrefEnv({
+ "set": [
+ // Accept workers from mochitest's http.
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ // Reduce the timeout to expose issues when service worker
+ // freezing is broken
+ ["dom.serviceWorkers.idle_timeout", SW_TIMEOUT],
+ ["dom.serviceWorkers.idle_extended_timeout", SW_TIMEOUT],
+ ["dom.ipc.processCount", 1],
+ ]
+ });
+
+ let { tab, document } = yield openAboutDebugging("workers");
+
+ // Listen for mutations in the service-workers list.
+ let serviceWorkersElement = getServiceWorkerList(document);
+ let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+
+ let swTab = yield addTab(TAB_URL);
+
+ info("Make the test page notify us when the service worker sends a message.");
+
+ // Wait for the service-workers list to update.
+ yield onMutation;
+
+ // Check that the service worker appears in the UI
+ let names = [...document.querySelectorAll("#service-workers .target-name")];
+ let name = names.filter(element => element.textContent === SERVICE_WORKER)[0];
+ ok(name, "Found the service worker in the list");
+
+ let targetElement = name.parentNode.parentNode;
+ let status = targetElement.querySelector(".target-status");
+ is(status.textContent, "Registering", "Service worker is currently registering");
+
+ yield waitForMutation(serviceWorkersElement, { childList: true, subtree: true });
+ is(status.textContent, "Running", "Service worker is currently running");
+
+ yield waitForMutation(serviceWorkersElement, { attributes: true, subtree: true });
+ is(status.textContent, "Stopped", "Service worker is currently stopped");
+
+ try {
+ yield unregisterServiceWorker(swTab, serviceWorkersElement);
+ ok(true, "Service worker unregistered");
+ } catch (e) {
+ ok(false, "Service worker not unregistered; " + e);
+ }
+
+ // Check that the service worker disappeared from the UI
+ names = [...document.querySelectorAll("#service-workers .target-name")];
+ names = names.map(element => element.textContent);
+ ok(!names.includes(SERVICE_WORKER),
+ "The service worker url is no longer in the list: " + names);
+
+ yield removeTab(swTab);
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_service_workers_timeout.js b/devtools/client/aboutdebugging/test/browser_service_workers_timeout.js
new file mode 100644
index 000000000..94e064029
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_timeout.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Service workers can't be loaded from chrome://,
+// but http:// is ok with dom.serviceWorkers.testing.enabled turned on.
+const SERVICE_WORKER = URL_ROOT + "service-workers/empty-sw.js";
+const TAB_URL = URL_ROOT + "service-workers/empty-sw.html";
+
+const SW_TIMEOUT = 1000;
+
+add_task(function* () {
+ yield new Promise(done => {
+ let options = {"set": [
+ // Accept workers from mochitest's http.
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ // Reduce the timeout to expose issues when service worker
+ // freezing is broken
+ ["dom.serviceWorkers.idle_timeout", SW_TIMEOUT],
+ ["dom.serviceWorkers.idle_extended_timeout", SW_TIMEOUT],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let { tab, document } = yield openAboutDebugging("workers");
+
+ let swTab = yield addTab(TAB_URL);
+
+ let serviceWorkersElement = getServiceWorkerList(document);
+ yield waitForMutation(serviceWorkersElement, { childList: true });
+
+ assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
+
+ // Ensure that the registration resolved before trying to connect to the sw
+ yield waitForServiceWorkerRegistered(swTab);
+ ok(true, "Service worker registration resolved");
+
+ // Retrieve the DEBUG button for the worker
+ let names = [...document.querySelectorAll("#service-workers .target-name")];
+ let name = names.filter(element => element.textContent === SERVICE_WORKER)[0];
+ ok(name, "Found the service worker in the list");
+ let targetElement = name.parentNode.parentNode;
+ let debugBtn = targetElement.querySelector(".debug-button");
+ ok(debugBtn, "Found its debug button");
+
+ // Click on it and wait for the toolbox to be ready
+ let onToolboxReady = new Promise(done => {
+ gDevTools.once("toolbox-ready", function (e, toolbox) {
+ done(toolbox);
+ });
+ });
+ debugBtn.click();
+
+ let toolbox = yield onToolboxReady;
+
+ // Wait for more than the regular timeout,
+ // so that if the worker freezing doesn't work,
+ // it will be destroyed and removed from the list
+ yield new Promise(done => {
+ setTimeout(done, SW_TIMEOUT * 2);
+ });
+
+ assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
+ ok(targetElement.querySelector(".debug-button"),
+ "The debug button is still there");
+
+ yield toolbox.destroy();
+ toolbox = null;
+
+ // Now ensure that the worker is correctly destroyed
+ // after we destroy the toolbox.
+ // The DEBUG button should disappear once the worker is destroyed.
+ yield waitForMutation(targetElement, { childList: true });
+ ok(!targetElement.querySelector(".debug-button"),
+ "The debug button was removed when the worker was killed");
+
+ // Finally, unregister the service worker itself.
+ try {
+ yield unregisterServiceWorker(swTab, serviceWorkersElement);
+ ok(true, "Service worker registration unregistered");
+ } catch (e) {
+ ok(false, "SW not unregistered; " + e);
+ }
+
+ assertHasTarget(false, document, "service-workers", SERVICE_WORKER);
+
+ yield removeTab(swTab);
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_service_workers_unregister.js b/devtools/client/aboutdebugging/test/browser_service_workers_unregister.js
new file mode 100644
index 000000000..b6076ea07
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_unregister.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that clicking on the unregister link in the Service Worker details works
+// as intended in about:debugging.
+// It should unregister the service worker, which should trigger an update of
+// the displayed list of service workers.
+
+// Service workers can't be loaded from chrome://, but http:// is ok with
+// dom.serviceWorkers.testing.enabled turned on.
+const SCOPE = URL_ROOT + "service-workers/";
+const SERVICE_WORKER = SCOPE + "empty-sw.js";
+const TAB_URL = SCOPE + "empty-sw.html";
+
+add_task(function* () {
+ info("Turn on workers via mochitest http.");
+ yield new Promise(done => {
+ let options = { "set": [
+ // Accept workers from mochitest's http.
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.ipc.processCount", 1],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let { tab, document } = yield openAboutDebugging("workers");
+
+ // Listen for mutations in the service-workers list.
+ let serviceWorkersElement = getServiceWorkerList(document);
+ let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+
+ // Open a tab that registers an empty service worker.
+ let swTab = yield addTab(TAB_URL);
+
+ // Wait for the service workers-list to update.
+ yield onMutation;
+
+ // Check that the service worker appears in the UI.
+ assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
+
+ yield waitForServiceWorkerActivation(SERVICE_WORKER, document);
+
+ info("Ensure that the registration resolved before trying to interact with " +
+ "the service worker.");
+ yield waitForServiceWorkerRegistered(swTab);
+ ok(true, "Service worker registration resolved");
+
+ let targets = document.querySelectorAll("#service-workers .target");
+ is(targets.length, 1, "One service worker is now displayed.");
+
+ let target = targets[0];
+ let name = target.querySelector(".target-name");
+ is(name.textContent, SERVICE_WORKER, "Found the service worker in the list");
+
+ info("Check the scope displayed scope is correct");
+ let scope = target.querySelector(".service-worker-scope");
+ is(scope.textContent, SCOPE,
+ "The expected scope is displayed in the service worker info.");
+
+ info("Unregister the service worker via the unregister link.");
+ let unregisterLink = target.querySelector(".unregister-link");
+ ok(unregisterLink, "Found the unregister link");
+
+ onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+ unregisterLink.click();
+ yield onMutation;
+
+ is(document.querySelector("#service-workers .target"), null,
+ "No service worker displayed anymore.");
+
+ yield removeTab(swTab);
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/browser_tabs.js b/devtools/client/aboutdebugging/test/browser_tabs.js
new file mode 100644
index 000000000..8cdeef17d
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_tabs.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TAB_URL = "data:text/html,<title>foo</title>";
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", 1]]
+ });
+});
+
+add_task(function* () {
+ let { tab, document } = yield openAboutDebugging("tabs");
+
+ // Wait for initial tabs list which may be empty
+ let tabsElement = getTabList(document);
+ if (tabsElement.querySelectorAll(".target-name").length == 0) {
+ yield waitForMutation(tabsElement, { childList: true });
+ }
+ // Refresh tabsElement to get the .target-list element
+ tabsElement = getTabList(document);
+
+ let names = [...tabsElement.querySelectorAll(".target-name")];
+ let initialTabCount = names.length;
+
+ // Open a new tab in background and wait for its addition in the UI
+ let onNewTab = waitForMutation(tabsElement, { childList: true });
+ let newTab = yield addTab(TAB_URL, { background: true });
+ yield onNewTab;
+
+ // Check that the new tab appears in the UI, but with an empty name
+ let newNames = [...tabsElement.querySelectorAll(".target-name")];
+ newNames = newNames.filter(node => !names.includes(node));
+ is(newNames.length, 1, "A new tab appeared in the list");
+ let newTabTarget = newNames[0];
+
+ // Then wait for title update, but on slow test runner, the title may already
+ // be set to the expected value
+ if (newTabTarget.textContent != "foo") {
+ yield waitForContentMutation(newTabTarget);
+ }
+
+ // Check that the new tab appears in the UI
+ is(newTabTarget.textContent, "foo", "The tab title got updated");
+ is(newTabTarget.title, TAB_URL, "The tab tooltip is the url");
+
+ // Finally, close the tab
+ let onTabsUpdate = waitForMutation(tabsElement, { childList: true });
+ yield removeTab(newTab);
+ yield onTabsUpdate;
+
+ // Check that the tab disappeared from the UI
+ names = [...tabsElement.querySelectorAll("#tabs .target-name")];
+ is(names.length, initialTabCount, "The tab disappeared from the UI");
+
+ yield closeAboutDebugging(tab);
+});
diff --git a/devtools/client/aboutdebugging/test/head.js b/devtools/client/aboutdebugging/test/head.js
new file mode 100644
index 000000000..001d36e34
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/head.js
@@ -0,0 +1,367 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env browser */
+/* exported openAboutDebugging, changeAboutDebuggingHash, closeAboutDebugging,
+ installAddon, uninstallAddon, waitForMutation, waitForContentMutation, assertHasTarget,
+ getServiceWorkerList, getTabList, openPanel, waitForInitialAddonList,
+ waitForServiceWorkerRegistered, unregisterServiceWorker,
+ waitForDelayedStartupFinished, setupTestAboutDebuggingWebExtension,
+ waitForServiceWorkerActivation */
+/* import-globals-from ../../framework/test/shared-head.js */
+
+"use strict";
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+ this);
+
+const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
+const { Management } = Cu.import("resource://gre/modules/Extension.jsm", {});
+
+flags.testing = true;
+registerCleanupFunction(() => {
+ flags.testing = false;
+});
+
+function* openAboutDebugging(page, win) {
+ info("opening about:debugging");
+ let url = "about:debugging";
+ if (page) {
+ url += "#" + page;
+ }
+
+ let tab = yield addTab(url, { window: win });
+ let browser = tab.linkedBrowser;
+ let document = browser.contentDocument;
+
+ if (!document.querySelector(".app")) {
+ yield waitForMutation(document.body, { childList: true });
+ }
+
+ return { tab, document };
+}
+
+/**
+ * Change url hash for current about:debugging tab, return a promise after
+ * new content is loaded.
+ * @param {DOMDocument} document container document from current tab
+ * @param {String} hash hash for about:debugging
+ * @return {Promise}
+ */
+function changeAboutDebuggingHash(document, hash) {
+ info(`Opening about:debugging#${hash}`);
+ window.openUILinkIn(`about:debugging#${hash}`, "current");
+ return waitForMutation(
+ document.querySelector(".main-content"), {childList: true});
+}
+
+function openPanel(document, panelId) {
+ info(`Opening ${panelId} panel`);
+ document.querySelector(`[aria-controls="${panelId}"]`).click();
+ return waitForMutation(
+ document.querySelector(".main-content"), {childList: true});
+}
+
+function closeAboutDebugging(tab) {
+ info("Closing about:debugging");
+ return removeTab(tab);
+}
+
+function getSupportsFile(path) {
+ let cr = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIChromeRegistry);
+ let uri = Services.io.newURI(CHROME_URL_ROOT + path, null, null);
+ let fileurl = cr.convertChromeURL(uri);
+ return fileurl.QueryInterface(Ci.nsIFileURL);
+}
+
+/**
+ * Depending on whether there are addons installed, return either a target list
+ * element or its container.
+ * @param {DOMDocument} document #addons section container document
+ * @return {DOMNode} target list or container element
+ */
+function getAddonList(document) {
+ return document.querySelector("#addons .target-list") ||
+ document.querySelector("#addons .targets");
+}
+
+/**
+ * Depending on whether there are service workers installed, return either a
+ * target list element or its container.
+ * @param {DOMDocument} document #service-workers section container document
+ * @return {DOMNode} target list or container element
+ */
+function getServiceWorkerList(document) {
+ return document.querySelector("#service-workers .target-list") ||
+ document.querySelector("#service-workers.targets");
+}
+
+/**
+ * Depending on whether there are tabs opened, return either a
+ * target list element or its container.
+ * @param {DOMDocument} document #tabs section container document
+ * @return {DOMNode} target list or container element
+ */
+function getTabList(document) {
+ return document.querySelector("#tabs .target-list") ||
+ document.querySelector("#tabs.targets");
+}
+
+function* installAddon({document, path, name, isWebExtension}) {
+ // Mock the file picker to select a test addon
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(null);
+ let file = getSupportsFile(path);
+ MockFilePicker.returnFiles = [file.file];
+
+ let addonList = getAddonList(document);
+ let addonListMutation = waitForMutation(addonList, { childList: true });
+
+ let onAddonInstalled;
+
+ if (isWebExtension) {
+ onAddonInstalled = new Promise(done => {
+ Management.on("startup", function listener(event, extension) {
+ if (extension.name != name) {
+ return;
+ }
+
+ Management.off("startup", listener);
+ done();
+ });
+ });
+ } else {
+ // Wait for a "test-devtools" message sent by the addon's bootstrap.js file
+ onAddonInstalled = new Promise(done => {
+ Services.obs.addObserver(function listener() {
+ Services.obs.removeObserver(listener, "test-devtools");
+
+ done();
+ }, "test-devtools", false);
+ });
+ }
+ // Trigger the file picker by clicking on the button
+ document.getElementById("load-addon-from-file").click();
+
+ yield onAddonInstalled;
+ ok(true, "Addon installed and running its bootstrap.js file");
+
+ // Check that the addon appears in the UI
+ yield addonListMutation;
+ let names = [...addonList.querySelectorAll(".target-name")];
+ names = names.map(element => element.textContent);
+ ok(names.includes(name),
+ "The addon name appears in the list of addons: " + names);
+}
+
+function* uninstallAddon({document, id, name}) {
+ let addonList = getAddonList(document);
+ let addonListMutation = waitForMutation(addonList, { childList: true });
+
+ // Now uninstall this addon
+ yield new Promise(done => {
+ AddonManager.getAddonByID(id, addon => {
+ let listener = {
+ onUninstalled: function (uninstalledAddon) {
+ if (uninstalledAddon != addon) {
+ return;
+ }
+ AddonManager.removeAddonListener(listener);
+
+ done();
+ }
+ };
+ AddonManager.addAddonListener(listener);
+ addon.uninstall();
+ });
+ });
+
+ // Ensure that the UI removes the addon from the list
+ yield addonListMutation;
+ let names = [...addonList.querySelectorAll(".target-name")];
+ names = names.map(element => element.textContent);
+ ok(!names.includes(name),
+ "After uninstall, the addon name disappears from the list of addons: "
+ + names);
+}
+
+/**
+ * Returns a promise that will resolve when the add-on list has been updated.
+ *
+ * @param {Node} document
+ * @return {Promise}
+ */
+function waitForInitialAddonList(document) {
+ const addonListContainer = getAddonList(document);
+ let addonCount = addonListContainer.querySelectorAll(".target");
+ addonCount = addonCount ? [...addonCount].length : -1;
+ info("Waiting for add-ons to load. Current add-on count: " + addonCount);
+
+ // This relies on the network speed of the actor responding to the
+ // listAddons() request and also the speed of openAboutDebugging().
+ let result;
+ if (addonCount > 0) {
+ info("Actually, the add-ons have already loaded");
+ result = Promise.resolve();
+ } else {
+ result = waitForMutation(addonListContainer, { childList: true });
+ }
+ return result;
+}
+
+/**
+ * Returns a promise that will resolve after receiving a mutation matching the
+ * provided mutation options on the provided target.
+ * @param {Node} target
+ * @param {Object} mutationOptions
+ * @return {Promise}
+ */
+function waitForMutation(target, mutationOptions) {
+ return new Promise(resolve => {
+ let observer = new MutationObserver(() => {
+ observer.disconnect();
+ resolve();
+ });
+ observer.observe(target, mutationOptions);
+ });
+}
+
+/**
+ * Returns a promise that will resolve after receiving a mutation in the subtree of the
+ * provided target. Depending on the current React implementation, a text change might be
+ * observable as a childList mutation or a characterData mutation.
+ *
+ * @param {Node} target
+ * @return {Promise}
+ */
+function waitForContentMutation(target) {
+ return waitForMutation(target, {
+ characterData: true,
+ childList: true,
+ subtree: true
+ });
+}
+
+/**
+ * Checks if an about:debugging TargetList element contains a Target element
+ * corresponding to the specified name.
+ * @param {Boolean} expected
+ * @param {Document} document
+ * @param {String} type
+ * @param {String} name
+ */
+function assertHasTarget(expected, document, type, name) {
+ let names = [...document.querySelectorAll("#" + type + " .target-name")];
+ names = names.map(element => element.textContent);
+ is(names.includes(name), expected,
+ "The " + type + " url appears in the list: " + names);
+}
+
+/**
+ * Returns a promise that will resolve after the service worker in the page
+ * has successfully registered itself.
+ * @param {Tab} tab
+ * @return {Promise} Resolves when the service worker is registered.
+ */
+function waitForServiceWorkerRegistered(tab) {
+ return ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ // Retrieve the `sw` promise created in the html page.
+ let { sw } = content.wrappedJSObject;
+ yield sw;
+ });
+}
+
+/**
+ * Asks the service worker within the test page to unregister, and returns a
+ * promise that will resolve when it has successfully unregistered itself and the
+ * about:debugging UI has fully processed this update.
+ *
+ * @param {Tab} tab
+ * @param {Node} serviceWorkersElement
+ * @return {Promise} Resolves when the service worker is unregistered.
+ */
+function* unregisterServiceWorker(tab, serviceWorkersElement) {
+ let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ // Retrieve the `sw` promise created in the html page
+ let { sw } = content.wrappedJSObject;
+ let registration = yield sw;
+ yield registration.unregister();
+ });
+ return onMutation;
+}
+
+/**
+ * Waits for the creation of a new window, usually used with create private
+ * browsing window.
+ * Returns a promise that will resolve when the window is successfully created.
+ * @param {window} win
+ */
+function waitForDelayedStartupFinished(win) {
+ return new Promise(function (resolve) {
+ Services.obs.addObserver(function observer(subject, topic) {
+ if (win == subject) {
+ Services.obs.removeObserver(observer, topic);
+ resolve();
+ }
+ }, "browser-delayed-startup-finished", false);
+ });
+}
+
+/**
+ * open the about:debugging page and install an addon
+ */
+function* setupTestAboutDebuggingWebExtension(name, path) {
+ yield new Promise(resolve => {
+ let options = {"set": [
+ // Force enabling of addons debugging
+ ["devtools.chrome.enabled", true],
+ ["devtools.debugger.remote-enabled", true],
+ // Disable security prompt
+ ["devtools.debugger.prompt-connection", false],
+ // Enable Browser toolbox test script execution via env variable
+ ["devtools.browser-toolbox.allow-unsafe-script", true],
+ ]};
+ SpecialPowers.pushPrefEnv(options, resolve);
+ });
+
+ let { tab, document } = yield openAboutDebugging("addons");
+ yield waitForInitialAddonList(document);
+
+ yield installAddon({
+ document,
+ path,
+ name,
+ isWebExtension: true,
+ });
+
+ // Retrieve the DEBUG button for the addon
+ let names = [...document.querySelectorAll("#addons .target-name")];
+ let nameEl = names.filter(element => element.textContent === name)[0];
+ ok(name, "Found the addon in the list");
+ let targetElement = nameEl.parentNode.parentNode;
+ let debugBtn = targetElement.querySelector(".debug-button");
+ ok(debugBtn, "Found its debug button");
+
+ return { tab, document, debugBtn };
+}
+
+/**
+ * Wait for aboutdebugging to be notified about the activation of the service worker
+ * corresponding to the provided service worker url.
+ */
+function* waitForServiceWorkerActivation(swUrl, document) {
+ let serviceWorkersElement = getServiceWorkerList(document);
+ let names = serviceWorkersElement.querySelectorAll(".target-name");
+ let name = [...names].filter(element => element.textContent === swUrl)[0];
+
+ let targetElement = name.parentNode.parentNode;
+ let targetStatus = targetElement.querySelector(".target-status");
+ while (targetStatus.textContent === "Registering") {
+ // Wait for the status to leave the "registering" stage.
+ yield waitForMutation(serviceWorkersElement, { childList: true, subtree: true });
+ }
+}
diff --git a/devtools/client/aboutdebugging/test/service-workers/delay-sw.html b/devtools/client/aboutdebugging/test/service-workers/delay-sw.html
new file mode 100644
index 000000000..545830eba
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/delay-sw.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+var sw = navigator.serviceWorker.register("delay-sw.js");
+sw.then(
+ function () {
+ dump("SW registered\n");
+ },
+ function (e) {
+ dump("SW not registered: " + e + "\n");
+ }
+);
+</script>
+</body>
+</html>
diff --git a/devtools/client/aboutdebugging/test/service-workers/delay-sw.js b/devtools/client/aboutdebugging/test/service-workers/delay-sw.js
new file mode 100644
index 000000000..3f16c5058
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/delay-sw.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env worker */
+
+"use strict";
+
+function wait(ms) {
+ return new Promise(resolve => {
+ setTimeout(resolve, ms);
+ });
+}
+
+// Wait for one second to switch from installing to installed.
+self.addEventListener("install", function (event) {
+ event.waitUntil(wait(1000));
+});
diff --git a/devtools/client/aboutdebugging/test/service-workers/empty-sw.html b/devtools/client/aboutdebugging/test/service-workers/empty-sw.html
new file mode 100644
index 000000000..a94c2b9ff
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/empty-sw.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+var sw = navigator.serviceWorker.register("empty-sw.js");
+sw.then(
+ function () {
+ dump("SW registered\n");
+ },
+ function (e) {
+ dump("SW not registered: " + e + "\n");
+ }
+);
+</script>
+</body>
+</html>
diff --git a/devtools/client/aboutdebugging/test/service-workers/empty-sw.js b/devtools/client/aboutdebugging/test/service-workers/empty-sw.js
new file mode 100644
index 000000000..1e7226402
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/empty-sw.js
@@ -0,0 +1 @@
+// Empty, just test registering.
diff --git a/devtools/client/aboutdebugging/test/service-workers/push-sw.html b/devtools/client/aboutdebugging/test/service-workers/push-sw.html
new file mode 100644
index 000000000..7db01f091
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/push-sw.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Service worker push test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+SpecialPowers.addPermission("desktop-notification", true, document);
+var sw = navigator.serviceWorker.register("push-sw.js");
+var sub = null;
+sw.then(
+ function (registration) {
+ dump("SW registered\n");
+ registration.pushManager.subscribe().then(
+ function (subscription) {
+ sub = subscription;
+ dump("SW subscribed to push: " + sub.endpoint + "\n");
+ },
+ function (error) {
+ dump("SW not subscribed to push: " + error + "\n");
+ }
+ );
+ },
+ function (error) {
+ dump("SW not registered: " + error + "\n");
+ }
+);
+</script>
+</body>
+</html>
diff --git a/devtools/client/aboutdebugging/test/service-workers/push-sw.js b/devtools/client/aboutdebugging/test/service-workers/push-sw.js
new file mode 100644
index 000000000..b5006eedb
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/push-sw.js
@@ -0,0 +1,33 @@
+/* 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"));
+});