diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /devtools/client/aboutdebugging/test | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/aboutdebugging/test')
37 files changed, 2363 insertions, 0 deletions
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")); +}); |