Bug 1302702 - Add devtools webextension actor mochitest-chrome unit tests.
MozReview-Commit-ID: 9pAbT89SlJJ
--- a/devtools/server/tests/mochitest/chrome.ini
+++ b/devtools/server/tests/mochitest/chrome.ini
@@ -19,17 +19,17 @@ support-files =
inspector-traversal-data.html
large-image.jpg
memory-helpers.js
nonchrome_unsafeDereference.html
small-image.gif
setup-in-child.js
setup-in-parent.js
webconsole-helpers.js
-
+ webextension-helpers.js
[test_animation_actor-lifetime.html]
[test_connection-manager.html]
[test_connectToChild.html]
[test_css-logic.html]
[test_css-logic-media-queries.html]
[test_css-logic-specificity.html]
[test_css-properties.html]
[test_Debugger.Source.prototype.introductionScript.html]
@@ -94,10 +94,12 @@ support-files =
[test_styles-applied.html]
[test_styles-computed.html]
[test_styles-layout.html]
[test_styles-matched.html]
[test_styles-modify.html]
[test_styles-svg.html]
[test_unsafeDereference.html]
[test_webconsole-node-grip.html]
+[test_webextension-addon-debugging-connect.html]
+[test_webextension-addon-debugging-reload.html]
[test_websocket-server.html]
skip-if = false
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/mochitest/test_webextension-addon-debugging-connect.html
@@ -0,0 +1,102 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1302702 - Test connect to a webextension addon
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Mozilla Bug</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="webextension-helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+"use strict";
+
+async function test_connect_addon(oopMode) {
+ // Install and start a test webextension.
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ background: function () {
+ browser.test.log("background script executed");
+ browser.test.sendMessage("background page ready");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("background page ready");
+
+ // Connect a DebuggerClient.
+ const transport = DebuggerServer.connectPipe();
+ const client = new DebuggerClient(transport);
+ await client.connect();
+
+ // List addons and assertions on the expected addon actor.
+ const {addons} = await client.mainRoot.listAddons();
+ const addonActor = addons.filter(actor => actor.id === extension.id).pop();
+ ok(addonActor, "The expected webextension addon actor has been found");
+
+ // Connect to the target addon actor and wait for the updated list of frames.
+ const waitFramesUpdated = waitForFramesUpdated({client});
+ const addonTarget = await TargetFactory.forRemoteTab({
+ form: addonActor,
+ client,
+ chrome: true,
+ isTabActor: true,
+ });
+ is(addonTarget.form.isOOP, oopMode,
+ "Got the expected oop mode in the webextension actor form");
+ const frames = await waitFramesUpdated;
+ const backgroundPageFrame = frames.filter((frame) => {
+ return frame.url && frame.url.endsWith("/_generated_background_page.html");
+ }).pop();
+ is(backgroundPageFrame.addonID, extension.id, "Got an extension frame");
+ ok(addonTarget.activeTab, "The addon target has an activeTab");
+
+ // When running in oop mode we can explicitly attach the thread without locking
+ // the main process.
+ if (oopMode) {
+ const [, threadFront] = await addonTarget.activeTab
+ .attachThread(addonTarget.form.threadActor);
+
+ ok(threadFront, "Got a threadFront for the target addon");
+ is(threadFront.paused, true, "The addon threadActor is paused");
+ await threadFront.resume();
+ is(threadFront.paused, false, "The addon threadActor has been resumed");
+
+ await threadFront.detach();
+ }
+
+ const waitTransportClosed = new Promise(resolve => {
+ client._transport.once("close", resolve);
+ });
+
+ await addonTarget.destroy();
+ await client.close();
+
+ // Check that if we close the debugging client without uninstalling the addon,
+ // the webextension debugging actor should release the debug browser.
+ await waitTransportClosed;
+ is(ExtensionParent.DebugUtils.debugBrowserPromises.size, 0,
+ "The debug browser has been released when the RDP connection has been closed");
+
+ await extension.unload();
+}
+
+add_task(async function test_webextension_addon_debugging_connect_inprocess() {
+ await setWebExtensionOOPMode(false);
+ await test_connect_addon(false);
+});
+
+add_task(async function test_webextension_addon_debugging_connect_oop() {
+ await setWebExtensionOOPMode(true);
+ await test_connect_addon(true);
+});
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/mochitest/test_webextension-addon-debugging-reload.html
@@ -0,0 +1,133 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1302702 - Test connect to a webextension addon
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Mozilla Bug</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="webextension-helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+"use strict";
+
+// NOTE: This test installs the webextension addon using the addon manager, so that
+// it can be reloaded using the same actor RDP method used by the developer tools.
+async function test_reload_addon() {
+ const addonID = "test-webext-debugging-reload@test.mozilla.com";
+ const addonFile = generateWebExtensionXPI({
+ manifest: {
+ applications: {
+ gecko: {id: addonID},
+ },
+ background: { scripts: ["background.js"] },
+ },
+ files: {
+ "background.js": function () {
+ console.log("background page loaded");
+ },
+ },
+ });
+
+ const {addon} = await promiseInstallFile(addonFile);
+ await promiseWebExtensionStartup();
+
+ let addonTarget = await attachAddon(addonID);
+ ok(addonTarget, "Got an addon target");
+
+ const matchBackgroundPageFrame = (data) => {
+ if (data.frames) {
+ let frameFound = data.frames.filter((frame) => {
+ return frame.url && frame.url.endsWith("_generated_background_page.html");
+ }).pop();
+
+ return !!frameFound;
+ }
+
+ return false;
+ };
+
+ const matchFrameSelected = (data) => {
+ return "selected" in data;
+ };
+
+ // Test the target addon actor reload behavior.
+
+ let waitFramesUpdated = waitForFramesUpdated(addonTarget, matchBackgroundPageFrame);
+ let collectFrameSelected = collectFrameUpdates(addonTarget, matchFrameSelected);
+
+ is(ExtensionParent.DebugUtils.debugBrowserPromises.size, 1,
+ "The expected number of debug browser has been created by the addon actor");
+
+ info("Reloading the target addon");
+ reloadAddon(addonTarget, addonID);
+ await promiseWebExtensionStartup();
+
+ is(ExtensionParent.DebugUtils.debugBrowserPromises.size, 1,
+ "The number of debug browser has not been changed after an addon reload");
+
+ let frames = await waitFramesUpdated;
+ const selectedFrame = collectFrameSelected().pop();
+
+ is(selectedFrame.selected, frames[0].id, "The new background page has been selected");
+ is(addonTarget.url, frames[0].url,
+ "The addon target has the url of the selected frame");
+
+ // Test the target addon actor once reloaded twice.
+
+ waitFramesUpdated = waitForFramesUpdated(addonTarget, matchBackgroundPageFrame);
+ collectFrameSelected = collectFrameUpdates(addonTarget, matchFrameSelected);
+
+ info("Reloading the target addon again");
+ reloadAddon(addonTarget, addonID);
+ await promiseWebExtensionStartup();
+
+ frames = await waitFramesUpdated;
+ const newSelectedFrame = collectFrameSelected().pop();
+
+ ok(newSelectedFrame !== selectedFrame,
+ "The new selected frame is different from the previous on");
+ is(newSelectedFrame.selected, frames[0].id,
+ "The new background page has been selected on the second reload");
+ is(addonTarget.url, frames[0].url,
+ "The addon target has the url of the selected frame");
+
+ // Expect the target to be automatically closed when the addon
+ // is finally uninstalled.
+
+ let {client} = addonTarget;
+ let waitDebuggingClientClosed = new Promise(resolve => {
+ addonTarget.once("close", resolve);
+ });
+
+ let waitShutdown = promiseWebExtensionShutdown();
+ addon.uninstall();
+ await waitShutdown;
+
+ info("Waiting the addon target to be closed on addon uninstall");
+ await waitDebuggingClientClosed;
+
+ // Debugging client has to be closed explicitly when
+ // the target has been created as remote.
+ await client.close();
+}
+
+add_task(async function test_webextension_addon_debugging_reload_inprocess() {
+ await setWebExtensionOOPMode(false);
+ await test_reload_addon(false);
+});
+
+add_task(async function test_webextension_addon_debugging_reload_oop() {
+ await setWebExtensionOOPMode(true);
+ await test_reload_addon(true);
+});
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/mochitest/webextension-helpers.js
@@ -0,0 +1,212 @@
+/* exported attachAddon, setWebExtensionOOPMode, waitForFramesUpdated, reloadAddon,
+ collectFrameUpdates, generateWebExtensionXPI, promiseInstallFile,
+ promiseAddonByID, promiseWebExtensionStartup, promiseWebExtensionShutdown
+ */
+
+"use strict";
+
+const Cu = Components.utils;
+const {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {DebuggerClient} = require("devtools/shared/client/main");
+const {DebuggerServer} = require("devtools/server/main");
+const {TargetFactory} = require("devtools/client/framework/target");
+
+const {AddonManager} = require("resource://gre/modules/AddonManager.jsm");
+const {Extension, Management} = require("resource://gre/modules/Extension.jsm");
+const {flushJarCache} = require("resource://gre/modules/ExtensionUtils.jsm");
+const {Services} = require("resource://gre/modules/Services.jsm");
+
+loader.lazyImporter(this, "ExtensionParent", "resource://gre/modules/ExtensionParent.jsm");
+loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm");
+
+// Initialize a minimal DebuggerServer and connect to the webextension addon actor.
+if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ SimpleTest.registerCleanupFunction(function () {
+ DebuggerServer.destroy();
+ });
+}
+
+SimpleTest.registerCleanupFunction(function () {
+ const {hiddenXULWindow} = ExtensionParent.DebugUtils;
+ const debugBrowserMapSize = ExtensionParent.DebugUtils.debugBrowserPromises.size;
+
+ if (debugBrowserMapSize > 0) {
+ is(debugBrowserMapSize, 0,
+ "ExtensionParent DebugUtils debug browsers have not been released");
+ }
+
+ if (hiddenXULWindow) {
+ ok(false, "ExtensionParent DebugUtils hiddenXULWindow has not been destroyed");
+ }
+});
+
+// Test helpers related to the webextensions debugging RDP actors.
+
+function setWebExtensionOOPMode(oopMode) {
+ return SpecialPowers.pushPrefEnv({
+ "set": [
+ ["extensions.webextensions.remote", oopMode],
+ ]
+ });
+}
+
+function waitForFramesUpdated({client}, matchFn) {
+ return new Promise(resolve => {
+ const listener = (evt, data) => {
+ if (typeof matchFn === "function" && !matchFn(data)) {
+ return;
+ } else if (!data.frames) {
+ return;
+ }
+
+ client.removeListener("frameUpdate", listener);
+ resolve(data.frames);
+ };
+ client.addListener("frameUpdate", listener);
+ });
+}
+
+function collectFrameUpdates({client}, matchFn) {
+ let collected = [];
+
+ const listener = (evt, data) => {
+ if (matchFn(data)) {
+ collected.push(data);
+ }
+ };
+
+ client.addListener("frameUpdate", listener);
+ let unsubscribe = () => {
+ unsubscribe = null;
+ client.removeListener("frameUpdate", listener);
+ return collected;
+ };
+
+ SimpleTest.registerCleanupFunction(function () {
+ if (unsubscribe) {
+ unsubscribe();
+ }
+ });
+
+ return unsubscribe;
+}
+
+async function attachAddon(addonId) {
+ const transport = DebuggerServer.connectPipe();
+ const client = new DebuggerClient(transport);
+
+ await client.connect();
+
+ const {addons} = await client.mainRoot.listAddons();
+ const addonActor = addons.filter(actor => actor.id === addonId).pop();
+
+ if (!addonActor) {
+ client.close();
+ throw new Error(`No WebExtension Actor found for ${addonId}`);
+ }
+
+ const addonTarget = await TargetFactory.forRemoteTab({
+ form: addonActor,
+ client,
+ chrome: true,
+ isTabActor: true,
+ });
+
+ return addonTarget;
+}
+
+async function reloadAddon({client}, addonId) {
+ const {addons} = await client.mainRoot.listAddons();
+ const addonActor = addons.filter(actor => actor.id === addonId).pop();
+
+ if (!addonActor) {
+ client.close();
+ throw new Error(`No WebExtension Actor found for ${addonId}`);
+ }
+
+ await client.request({
+ to: addonActor.actor,
+ type: "reload",
+ });
+}
+
+// Test helpers related to the AddonManager.
+
+function generateWebExtensionXPI(extDetails) {
+ const addonFile = Extension.generateXPI(extDetails);
+
+ flushJarCache(addonFile.path);
+ Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache",
+ {path: addonFile.path});
+
+ // Remove the file on cleanup if needed.
+ SimpleTest.registerCleanupFunction(() => {
+ flushJarCache(addonFile.path);
+ Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache",
+ {path: addonFile.path});
+
+ if (addonFile.exists()) {
+ OS.File.remove(addonFile.path);
+ }
+ });
+
+ return addonFile;
+}
+
+function promiseCompleteInstall(install) {
+ let listener;
+ return new Promise((resolve, reject) => {
+ listener = {
+ onDownloadFailed: reject,
+ onDownloadCancelled: reject,
+ onInstallFailed: reject,
+ onInstallCancelled: reject,
+ onInstallEnded: resolve,
+ onInstallPostponed: reject,
+ };
+
+ install.addListener(listener);
+ install.install();
+ }).then(() => {
+ install.removeListener(listener);
+ return install;
+ });
+}
+
+function promiseInstallFile(file) {
+ return AddonManager.getInstallForFile(file).then(install => {
+ if (!install) {
+ throw new Error(`No AddonInstall created for ${file.path}`);
+ }
+
+ if (install.state != AddonManager.STATE_DOWNLOADED) {
+ throw new Error(`Expected file to be downloaded for install of ${file.path}`);
+ }
+
+ return promiseCompleteInstall(install);
+ });
+}
+
+function promiseWebExtensionStartup() {
+ return new Promise(resolve => {
+ let listener = (evt, extension) => {
+ Management.off("ready", listener);
+ resolve(extension);
+ };
+
+ Management.on("ready", listener);
+ });
+}
+
+function promiseWebExtensionShutdown() {
+ return new Promise(resolve => {
+ let listener = (event, extension) => {
+ Management.off("shutdown", listener);
+ resolve(extension);
+ };
+
+ Management.on("shutdown", listener);
+ });
+}