Bug 1302702 - Add devtools webextension actor mochitest-chrome unit tests. draft
authorLuca Greco <lgreco@mozilla.com>
Wed, 10 May 2017 12:54:18 +0200
changeset 579751 d0a8c18bd65f342f90280617557968509a4f00fc
parent 579750 a3e1180c480e2e6e764747e6bb3a25a0c0def884
child 579752 f593fc6df43e62384fc44561e377aa99d444115b
push id59363
push userluca.greco@alcacoop.it
push dateWed, 17 May 2017 19:17:20 +0000
bugs1302702
milestone55.0a1
Bug 1302702 - Add devtools webextension actor mochitest-chrome unit tests. MozReview-Commit-ID: 9pAbT89SlJJ
devtools/server/tests/mochitest/chrome.ini
devtools/server/tests/mochitest/test_webextension-addon-debugging-connect.html
devtools/server/tests/mochitest/test_webextension-addon-debugging-reload.html
devtools/server/tests/mochitest/webextension-helpers.js
--- 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);
+  });
+}