diff options
Diffstat (limited to 'devtools/client/aboutdebugging')
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 Binary files differnew file mode 100644 index 000000000..e1c42376e --- /dev/null +++ b/devtools/client/aboutdebugging/test/addons/bug1273184.xpi 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")); +}); |