Bug 1509246 - Create mochitests for existing WebExtension APIs; fix some nits found; r=mkmelin
authorGeoff Lankow <geoff@darktrojan.net>
Sun, 30 Dec 2018 20:32:25 +1300
changeset 33224 4b4bc20aa6de
parent 33223 dde486474a5e
child 33225 71c8bc1307b8
push id2368
push userclokep@gmail.com
push dateMon, 28 Jan 2019 21:12:50 +0000
treeherdercomm-beta@56d23c07d815 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs1509246
Bug 1509246 - Create mochitests for existing WebExtension APIs; fix some nits found; r=mkmelin
mail/components/extensions/.eslintrc.js
mail/components/extensions/ExtensionToolbarButtons.jsm
mail/components/extensions/child/.eslintrc.js
mail/components/extensions/moz.build
mail/components/extensions/parent/.eslintrc.js
mail/components/extensions/parent/ext-addressBook.js
mail/components/extensions/parent/ext-browserAction.js
mail/components/extensions/schemas/addressBook.json
mail/components/extensions/test/browser/.eslintrc.js
mail/components/extensions/test/browser/browser.ini
mail/components/extensions/test/browser/browser_ext_addressBooksUI.js
mail/components/extensions/test/browser/browser_ext_browserAction.js
mail/components/extensions/test/browser/browser_ext_composeAction.js
mail/components/extensions/test/browser/head.js
mail/components/extensions/test/xpcshell/head.js
mail/components/extensions/test/xpcshell/test_ext_addressBook.js
deleted file mode 100644
--- a/mail/components/extensions/.eslintrc.js
+++ /dev/null
@@ -1,21 +0,0 @@
-"use strict";
-
-module.exports = {
-  "globals": {
-    // These are defined in the WebExtension script scopes by ExtensionCommon.jsm.
-    // From toolkit/components/extensions/.eslintrc.js.
-    "Cc": true,
-    "Ci": true,
-    "Cr": true,
-    "Cu": true,
-    "AppConstants": true,
-    "ExtensionAPI": true,
-    "ExtensionCommon": true,
-    "ExtensionUtils": true,
-    "extensions": true,
-    "global": true,
-    "require": false,
-    "Services": true,
-    "XPCOMUtils": true,
-  },
-};
--- a/mail/components/extensions/ExtensionToolbarButtons.jsm
+++ b/mail/components/extensions/ExtensionToolbarButtons.jsm
@@ -1,14 +1,15 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["ToolbarButtonAPI"];
 
+ChromeUtils.defineModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
 ChromeUtils.defineModuleGetter(this, "ViewPopup", "resource:///modules/ExtensionPopups.jsm");
 ChromeUtils.defineModuleGetter(this, "ExtensionSupport", "resource:///modules/extensionSupport.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
 
 const {
   EventManager,
@@ -77,28 +78,24 @@ this.ToolbarButtonAPI = class extends Ex
         () => this.getIconData(this.defaults.icon)));
 
     ExtensionSupport.registerWindowListener(this.id, {
       chromeURLs: this.windowURLs,
       onLoadWindow: window => {
         this.paint(window);
       },
     });
+
+    extension.callOnClose(this);
   }
 
   /**
    * Called when the extension is disabled or removed.
-   *
-   * @param reason
    */
-  onShutdown(reason) {
-    if (reason === "APP_SHUTDOWN") {
-      return;
-    }
-
+  close() {
     ExtensionSupport.unregisterWindowListener(this.id);
     for (let window of ExtensionSupport.openWindows) {
       if (this.windowURLs.includes(window.location.href)) {
         this.unpaint(window);
       }
     }
   }
 
@@ -347,44 +344,47 @@ this.ToolbarButtonAPI = class extends Ex
   }
 
   /**
    * Update the toolbar button for a given window.
    *
    * @param {ChromeWindow} window
    *        Browser chrome window.
    */
-  updateWindow(window) {
+  async updateWindow(window) {
     let button = window.document.getElementById(this.id);
     if (button) {
       this.updateButton(button, this.globals);
     }
+    await new Promise(window.requestAnimationFrame);
   }
 
   /**
    * Update the toolbar button when the extension changes the icon, title, url, etc.
    * If it only changes a parameter for a single tab, `target` will be that tab.
    * If it only changes a parameter for a single window, `target` will be that window.
    * Otherwise `target` will be null.
    *
    * @param {XULElement|ChromeWindow|null} target
    *        Browser tab or browser chrome window, may be null.
    */
-  updateOnChange(target) {
+  async updateOnChange(target) {
     if (target) {
       let window = target.ownerGlobal;
       if (target === window || target.selected) {
-        this.updateWindow(window);
+        await this.updateWindow(window);
       }
     } else {
+      let promises = [];
       for (let window of ExtensionSupport.openWindows) {
         if (this.windowURLs.includes(window.location.href)) {
-          this.updateWindow(window);
+          promises.push(this.updateWindow(window));
         }
       }
+      await Promise.all(promises);
     }
   }
 
   /**
    * Gets the target object and its associated values corresponding to
    * the `details` parameter of the various get* and set* API methods.
    *
    * @param {Object} details
@@ -422,25 +422,25 @@ this.ToolbarButtonAPI = class extends Ex
    * @param {Object} details
    *        An object with optional `tabId` or `windowId` properties.
    * @param {string} prop
    *        String property to set. Should should be one of "icon", "title",
    *        "badgeText", "popup", "badgeBackgroundColor" or "enabled".
    * @param {string} value
    *        Value for prop.
    */
-  setProperty(details, prop, value) {
+  async setProperty(details, prop, value) {
     let {target, values} = this.getContextData(details);
     if (value === null) {
       delete values[prop];
     } else {
       values[prop] = value;
     }
 
-    this.updateOnChange(target);
+    await this.updateOnChange(target);
   }
 
   /**
    * Retrieve the value of a global, window specific or tab specific property.
    *
    * @param {Object} details
    *        An object with optional `tabId` or `windowId` properties.
    * @param {string} prop
@@ -475,82 +475,82 @@ this.ToolbarButtonAPI = class extends Ex
             };
             action.on("click", listener);
             return () => {
               action.off("click", listener);
             };
           },
         }).api(),
 
-        enable(tabId) {
-          action.setProperty({tabId}, "enabled", true);
+        async enable(tabId) {
+          await action.setProperty({tabId}, "enabled", true);
         },
 
-        disable(tabId) {
-          action.setProperty({tabId}, "enabled", false);
+        async disable(tabId) {
+          await action.setProperty({tabId}, "enabled", false);
         },
 
         isEnabled(details) {
           return action.getProperty(details, "enabled");
         },
 
-        setTitle(details) {
-          action.setProperty(details, "title", details.title);
+        async setTitle(details) {
+          await action.setProperty(details, "title", details.title);
         },
 
         getTitle(details) {
           return action.getProperty(details, "title");
         },
 
-        setIcon(details) {
+        async setIcon(details) {
           details.iconType = this.manifestName;
 
           let icon = IconDetails.normalize(details, extension, context);
           if (!Object.keys(icon).length) {
             icon = null;
           }
-          action.setProperty(details, "icon", icon);
+          await action.setProperty(details, "icon", icon);
         },
 
-        setBadgeText(details) {
-          action.setProperty(details, "badgeText", details.text);
+        async setBadgeText(details) {
+          await action.setProperty(details, "badgeText", details.text);
         },
 
         getBadgeText(details) {
           return action.getProperty(details, "badgeText");
         },
 
-        setPopup(details) {
+        async setPopup(details) {
           // Note: Chrome resolves arguments to setIcon relative to the calling
           // context, but resolves arguments to setPopup relative to the extension
           // root.
           // For internal consistency, we currently resolve both relative to the
           // calling context.
           let url = details.popup && context.uri.resolve(details.popup);
           if (url && !context.checkLoadURL(url)) {
             return Promise.reject({message: `Access denied for URL ${url}`});
           }
-          action.setProperty(details, "popup", url);
+          await action.setProperty(details, "popup", url);
           return Promise.resolve(null);
         },
 
         getPopup(details) {
           return action.getProperty(details, "popup");
         },
 
-        setBadgeBackgroundColor(details) {
+        async setBadgeBackgroundColor(details) {
           let color = details.color;
           if (typeof color == "string") {
             let col = InspectorUtils.colorToRGBA(color);
             if (!col) {
               throw new ExtensionError(`Invalid badge background color: "${color}"`);
             }
             color = col && [col.r, col.g, col.b, Math.round(col.a * 255)];
           }
-          action.setProperty(details, "badgeBackgroundColor", color);
+          await action.setProperty(details, "badgeBackgroundColor", color);
         },
 
         getBadgeBackgroundColor(details, callback) {
           let color = action.getProperty(details, "badgeBackgroundColor");
           return color || [0xd9, 0, 0, 255];
         },
 
         openPopup() {
--- a/mail/components/extensions/child/.eslintrc.js
+++ b/mail/components/extensions/child/.eslintrc.js
@@ -1,8 +1,13 @@
 "use strict";
 
 module.exports = {
   "globals": {
+    // These are defined in the WebExtension script scopes by ExtensionCommon.jsm.
+    // From toolkit/components/extensions/.eslintrc.js.
+    "ExtensionAPI": true,
+    "extensions": true,
+
     // From toolkit/components/extensions/child/.eslintrc.js.
     "EventManager": true,
   },
 };
--- a/mail/components/extensions/moz.build
+++ b/mail/components/extensions/moz.build
@@ -8,11 +8,14 @@ EXTRA_COMPONENTS += [
 
 EXTRA_JS_MODULES += [
     'ExtensionPopups.jsm',
     'ExtensionToolbarButtons.jsm',
 ]
 
 JAR_MANIFESTS += ['jar.mn']
 
+BROWSER_CHROME_MANIFESTS += [
+    'test/browser/browser.ini'
+]
 XPCSHELL_TESTS_MANIFESTS += [
     'test/xpcshell/xpcshell.ini',
 ]
--- a/mail/components/extensions/parent/.eslintrc.js
+++ b/mail/components/extensions/parent/.eslintrc.js
@@ -1,12 +1,21 @@
 "use strict";
 
 module.exports = {
   "globals": {
+    // These are defined in the WebExtension script scopes by ExtensionCommon.jsm.
+    // From toolkit/components/extensions/.eslintrc.js.
+    "ExtensionAPI": true,
+    "ExtensionCommon": true,
+    "ExtensionUtils": true,
+    "extensions": true,
+    "global": true,
+    "Services": true,
+
     // From toolkit/components/extensions/parent/.eslintrc.js.
     "CONTAINER_STORE": true,
     "DEFAULT_STORE": true,
     "EventEmitter": true,
     "EventManager": true,
     "InputEventManager": true,
     "PRIVATE_STORE": true,
     "TabBase": true,
--- a/mail/components/extensions/parent/ext-addressBook.js
+++ b/mail/components/extensions/parent/ext-addressBook.js
@@ -1,14 +1,12 @@
 /* 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/. */
 
-const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
-
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource:///modules/MailServices.jsm");
 
 const AB_WINDOW_TYPE = "mail:addressbook";
 const AB_WINDOW_URI = "chrome://messenger/content/addressbook/addressbook.xul";
 
 const kPABDirectory = 2; // defined in nsDirPrefs.h
 
@@ -281,25 +279,25 @@ var cache = new class extends EventEmitt
     }
   }
 };
 
 this.addressBook = class extends ExtensionAPI {
   getAPI(context) {
     return {
       addressBooks: {
-        openUI() {
+        async openUI() {
           let topWindow = Services.wm.getMostRecentWindow(AB_WINDOW_TYPE);
           if (!topWindow) {
             // TODO: wait until window is loaded before resolving
             topWindow = Services.ww.openWindow(null, AB_WINDOW_URI, "_blank", "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar", null);
           }
           topWindow.focus();
         },
-        closeUI() {
+        async closeUI() {
           for (let win of Services.wm.getEnumerator(AB_WINDOW_TYPE)) {
             win.close();
           }
         },
 
         list(complete = false) {
           return cache.convert(cache.tree, complete);
         },
--- a/mail/components/extensions/parent/ext-browserAction.js
+++ b/mail/components/extensions/parent/ext-browserAction.js
@@ -11,18 +11,18 @@ this.browserAction = class extends Toolb
     return browserActionMap.get(extension);
   }
 
   async onManifestEntry(entryName) {
     await super.onManifestEntry(entryName);
     browserActionMap.set(this.extension, this);
   }
 
-  onShutdown(reason) {
-    super.onShutdown(reason);
+  close() {
+    super.close();
     browserActionMap.delete(this.extension);
   }
 
   constructor(extension) {
     super(extension);
     this.manifest_name = "browser_action";
     this.manifestName = "browserAction";
     this.windowURLs = ["chrome://messenger/content/messenger.xul"];
--- a/mail/components/extensions/schemas/addressBook.json
+++ b/mail/components/extensions/schemas/addressBook.json
@@ -75,22 +75,24 @@
           }
         }
       }
     ],
     "functions": [
       {
         "name": "openUI",
         "type": "function",
+        "async": true,
         "description": "Opens the address book user interface.",
         "parameters": []
       },
       {
         "name": "closeUI",
         "type": "function",
+        "async": true,
         "description": "Closes the address book user interface.",
         "parameters": []
       },
       {
         "name": "list",
         "type": "function",
         "async": true,
         "parameters": [
new file mode 100644
--- /dev/null
+++ b/mail/components/extensions/test/browser/.eslintrc.js
@@ -0,0 +1,13 @@
+"use strict";
+
+module.exports = {
+  "extends": "plugin:mozilla/browser-test",
+
+  "env": {
+    "webextensions": true,
+  },
+
+  "rules": {
+    "func-names": "off",
+  },
+};
new file mode 100644
--- /dev/null
+++ b/mail/components/extensions/test/browser/browser.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+head = head.js
+subsuite = thunderbird
+tags = webextensions
+
+[browser_ext_addressBooksUI.js]
+[browser_ext_browserAction.js]
+[browser_ext_composeAction.js]
new file mode 100644
--- /dev/null
+++ b/mail/components/extensions/test/browser/browser_ext_addressBooksUI.js
@@ -0,0 +1,46 @@
+/* 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/. */
+
+add_task(async () => {
+  async function background() {
+    let awaitMessage = function(messageToSend, ...sendArgs) {
+      return new Promise(resolve => {
+        browser.test.onMessage.addListener(function listener(...args) {
+          browser.test.onMessage.removeListener(listener);
+          resolve(args);
+        });
+        if (messageToSend) {
+          browser.test.sendMessage(messageToSend, ...sendArgs);
+        }
+      });
+    };
+
+    await awaitMessage("checkNumberOfAddressBookWindows", 0);
+
+    await browser.addressBooks.openUI();
+    await awaitMessage("checkNumberOfAddressBookWindows", 1);
+
+    await browser.addressBooks.openUI();
+    await awaitMessage("checkNumberOfAddressBookWindows", 1);
+
+    await browser.addressBooks.closeUI();
+    await awaitMessage("checkNumberOfAddressBookWindows", 0);
+
+    browser.test.notifyPass("addressBooks");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: { permissions: ["addressBooks"] },
+  });
+
+  extension.onMessage("checkNumberOfAddressBookWindows", (count) => {
+    is([...Services.wm.getEnumerator("mail:addressbook")].length, count, "Right number of address books open");
+    extension.sendMessage();
+  });
+
+  await extension.startup();
+  await extension.awaitFinish("addressBooks");
+  await extension.unload();
+});
new file mode 100644
--- /dev/null
+++ b/mail/components/extensions/test/browser/browser_ext_browserAction.js
@@ -0,0 +1,96 @@
+/* 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/. */
+
+add_task(async () => {
+  async function test_it(extension) {
+    await extension.startup();
+
+    let buttonId = "test1_mochi_test-browserAction-toolbarbutton";
+    let toolbar = document.getElementById("mail-bar3");
+    ok(!toolbar.getAttribute("currentset"), "No toolbar current set");
+
+    let button = document.getElementById(buttonId);
+    ok(button, "Button created");
+    is(toolbar.id, button.parentNode.id, "Button added to toolbar");
+    ok(toolbar.currentSet.split(",").includes(buttonId), "Button added to toolbar current set");
+
+    let icon = document.getAnonymousElementByAttribute(
+      button, "class", "toolbarbutton-icon"
+    );
+    is(getComputedStyle(icon).listStyleImage,
+       `url("chrome://messenger/content/extension.svg")`, "Default icon");
+    let label = document.getAnonymousElementByAttribute(
+      button, "class", "toolbarbutton-text"
+    );
+    is(label.value, "This is a test", "Correct label");
+
+    EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 });
+    await extension.awaitFinish("browserAction");
+    await promiseAnimationFrame();
+
+    is(document.getElementById(buttonId), button);
+    label = document.getAnonymousElementByAttribute(
+      button, "class", "toolbarbutton-text"
+    );
+    is(label.value, "New title", "Correct label");
+
+    await extension.unload();
+    await promiseAnimationFrame();
+
+    ok(!document.getElementById(buttonId), "Button destroyed");
+  }
+
+  async function background_nopopup() {
+    browser.browserAction.onClicked.addListener(async () => {
+      await browser.browserAction.setTitle({ title: "New title" });
+      await new Promise(setTimeout);
+      browser.test.notifyPass("browserAction");
+    });
+  }
+
+  async function background_popup() {
+    browser.runtime.onMessage.addListener(async (msg) => {
+      browser.test.assertEq("popup.html", msg);
+      await browser.browserAction.setTitle({ title: "New title" });
+      await new Promise(setTimeout);
+      browser.test.notifyPass("browserAction");
+    });
+  }
+
+  let extensionDetails = {
+    background: background_nopopup,
+    files: {
+      "popup.html": `<html>
+          <head>
+            <meta charset="utf-8">
+            <script src="popup.js"></script>
+          </head>
+          <body>popup.js</body>
+        </html>`,
+      "popup.js": function() {
+        window.onload = async () => {
+          await browser.runtime.sendMessage("popup.html");
+          window.close();
+        };
+      },
+    },
+    manifest: {
+      applications: {
+        gecko: {
+          id: "test1@mochi.test",
+        },
+      },
+      browser_action: {
+        default_title: "This is a test",
+      },
+    },
+  };
+  let extension = ExtensionTestUtils.loadExtension(extensionDetails);
+  await test_it(extension);
+
+  extensionDetails.background = background_popup;
+  extensionDetails.manifest.browser_action.default_popup = "popup.html";
+  extension = ExtensionTestUtils.loadExtension(extensionDetails);
+  await test_it(extension);
+});
new file mode 100644
--- /dev/null
+++ b/mail/components/extensions/test/browser/browser_ext_composeAction.js
@@ -0,0 +1,152 @@
+/* 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/. */
+
+let gAccount;
+
+async function openComposeWindow() {
+  let params = Cc["@mozilla.org/messengercompose/composeparams;1"]
+                 .createInstance(Ci.nsIMsgComposeParams);
+  let composeFields = Cc["@mozilla.org/messengercompose/composefields;1"]
+                        .createInstance(Ci.nsIMsgCompFields);
+
+  params.identity = gAccount.defaultIdentity;
+  params.composeFields = composeFields;
+
+  await new Promise(resolve => {
+    let observer = {
+      observe(subject, topic, data) {
+        Services.ww.unregisterNotification(observer);
+        subject.addEventListener("load", () => {
+          promiseAnimationFrame().then(resolve);
+        }, { once: true });
+      },
+    };
+    Services.ww.registerNotification(observer);
+    MailServices.compose.OpenComposeWindowWithParams(null, params);
+  });
+  return Services.wm.getMostRecentWindow("msgcompose");
+}
+
+async function test_it(extensionDetails, toolbarId) {
+  let extension = ExtensionTestUtils.loadExtension(extensionDetails);
+  let buttonId = "test1_mochi_test-composeAction-toolbarbutton";
+
+  await extension.startup();
+  await extension.awaitMessage();
+
+  let composeWindow = await openComposeWindow();
+  let composeDocument = composeWindow.document;
+  await promiseAnimationFrame(composeWindow);
+
+  try {
+    let toolbar = composeDocument.getElementById(toolbarId);
+    ok(!toolbar.getAttribute("currentset"), "No toolbar current set");
+
+    let button = composeDocument.getElementById(buttonId);
+    ok(button, "Button created");
+    is(getComputedStyle(button).MozBinding,
+      `url("chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton-badged")`);
+    is(toolbar.id, button.parentNode.id, "Button added to toolbar");
+    ok(toolbar.currentSet.split(",").includes(buttonId), "Button added to toolbar current set");
+
+    let icon = composeDocument.getAnonymousElementByAttribute(button, "class", "toolbarbutton-icon");
+    is(getComputedStyle(icon).listStyleImage,
+       `url("chrome://messenger/content/extension.svg")`, "Default icon");
+    let label = composeDocument.getAnonymousElementByAttribute(
+      button, "class", "toolbarbutton-text"
+    );
+    is(label.value, "This is a test", "Correct label");
+
+    EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, composeWindow);
+    await extension.awaitFinish("composeAction");
+    await promiseAnimationFrame(composeWindow);
+
+    is(composeDocument.getElementById(buttonId), button);
+    label = composeDocument.getAnonymousElementByAttribute(
+      button, "class", "toolbarbutton-text"
+    );
+    is(label.value, "New title", "Correct label");
+  } finally {
+    await extension.unload();
+    await promiseAnimationFrame(composeWindow);
+    ok(!composeDocument.getElementById(buttonId), "Button destroyed");
+    composeWindow.close();
+  }
+}
+
+add_task(async function setup() {
+  gAccount = createAccount();
+  addIdentity(gAccount);
+  let rootFolder = gAccount.incomingServer.rootFolder;
+
+  window.gFolderTreeView.selectFolder(rootFolder);
+  await new Promise(executeSoon);
+});
+
+add_task(async function the_test() {
+  async function background_nopopup() {
+    browser.test.log("nopopup background script ran");
+    browser.composeAction.onClicked.addListener(async () => {
+      await browser.composeAction.setTitle({ title: "New title" });
+      await new Promise(setTimeout);
+      browser.test.notifyPass("composeAction");
+    });
+    browser.test.sendMessage();
+  }
+
+  async function background_popup() {
+    browser.test.log("popup background script ran");
+    browser.runtime.onMessage.addListener(async (msg) => {
+      browser.test.assertEq("popup.html", msg);
+      await browser.composeAction.setTitle({ title: "New title" });
+      await new Promise(setTimeout);
+      browser.test.notifyPass("composeAction");
+    });
+    browser.test.sendMessage();
+  }
+
+  let extensionDetails = {
+    background: background_nopopup,
+    files: {
+      "popup.html": `<html>
+          <head>
+            <meta charset="utf-8">
+            <script src="popup.js"></script>
+          </head>
+          <body>popup.js</body>
+        </html>`,
+      "popup.js": function() {
+        window.onload = async () => {
+          await browser.runtime.sendMessage("popup.html");
+          window.close();
+        };
+      },
+    },
+    manifest: {
+      applications: {
+        gecko: {
+          id: "test1@mochi.test",
+        },
+      },
+      compose_action: {
+        default_title: "This is a test",
+      },
+    },
+  };
+
+  await test_it(extensionDetails, "composeToolbar2");
+
+  extensionDetails.background = background_popup;
+  extensionDetails.manifest.compose_action.default_popup = "popup.html";
+  await test_it(extensionDetails, "composeToolbar2");
+
+  extensionDetails.background = background_nopopup;
+  extensionDetails.manifest.compose_action.default_area = "formattoolbar";
+  delete extensionDetails.manifest.compose_action.default_popup;
+  await test_it(extensionDetails, "FormatToolbar");
+
+  extensionDetails.background = background_popup;
+  extensionDetails.manifest.compose_action.default_popup = "popup.html";
+  await test_it(extensionDetails, "FormatToolbar");
+});
new file mode 100644
--- /dev/null
+++ b/mail/components/extensions/test/browser/head.js
@@ -0,0 +1,38 @@
+/* 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/. */
+
+ChromeUtils.defineModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+ChromeUtils.defineModuleGetter(this, "MailServices", "resource:///modules/MailServices.jsm");
+
+function createAccount() {
+  registerCleanupFunction(() => {
+    [...MailServices.accounts.accounts.enumerate()].forEach(cleanUpAccount);
+  });
+
+  MailServices.accounts.createLocalMailAccount();
+  let account = MailServices.accounts.accounts.enumerate().getNext();
+  info(`Created account ${account.toString()}`);
+
+  return account;
+}
+
+function cleanUpAccount(account) {
+  info(`Cleaning up account ${account.toString()}`);
+  MailServices.accounts.removeIncomingServer(account.incomingServer, true);
+  MailServices.accounts.removeAccount(account, true);
+}
+
+function addIdentity(account) {
+  let identity = MailServices.accounts.createIdentity();
+  identity.email = "mochitest@localhost";
+  account.addIdentity(identity);
+  account.defaultIdentity = identity;
+  info(`Created identity ${identity.toString()}`);
+}
+
+async function promiseAnimationFrame(win = window) {
+  await new Promise(win.requestAnimationFrame);
+  // dispatchToMainThread throws if used as the first argument of Promise.
+  return new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+}
--- a/mail/components/extensions/test/xpcshell/head.js
+++ b/mail/components/extensions/test/xpcshell/head.js
@@ -1,13 +1,14 @@
 /* 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/. */
 
-ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.defineModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+ChromeUtils.defineModuleGetter(this, "MailServices", "resource:///modules/MailServices.jsm");
 
 // Ensure the profile directory is set up
 do_get_profile();
 
 // Windows (Outlook Express) Address Book deactivation. (Bug 448859)
 Services.prefs.deleteBranch("ldap_2.servers.oe.");
 
 // OSX Address Book deactivation (Bug 955842)
--- a/mail/components/extensions/test/xpcshell/test_ext_addressBook.js
+++ b/mail/components/extensions/test/xpcshell/test_ext_addressBook.js
@@ -1,18 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-ChromeUtils.import("resource:///modules/MailServices.jsm");
 ChromeUtils.import("resource://testing-common/ExtensionXPCShellUtils.jsm");
-
 ExtensionTestUtils.init(this);
 
 add_task(async function test_addressBooks() {
   async function background() {
     let firstBookId, secondBookId, newContactId;
 
     let events = [];
     for (let eventNamespace of ["addressBooks", "contacts", "mailingLists"]) {
@@ -53,22 +50,25 @@ add_task(async function test_addressBook
         if (expectedEvents.length == 1) {
           return event.args;
         }
       }
 
       return null;
     };
 
-    let awaitMessage = function() {
+    let awaitMessage = function(messageToSend, ...sendArgs) {
       return new Promise(resolve => {
         browser.test.onMessage.addListener(function listener(...args) {
           browser.test.onMessage.removeListener(listener);
           resolve(args);
         });
+        if (messageToSend) {
+          browser.test.sendMessage(messageToSend, ...sendArgs);
+        }
       });
     };
 
     async function addressBookTest() {
       browser.test.log("Starting addressBookTest");
       let list = await browser.addressBooks.list();
       browser.test.assertEq(2, list.length);
       for (let b of list) {
@@ -332,80 +332,69 @@ add_task(async function test_addressBook
 
       browser.test.assertEq(0, events.length, "No events left unconsumed");
       browser.test.log("Completed contactRemovalTest");
     }
 
     async function outsideEventsTest() {
       browser.test.log("Starting outsideEventsTest");
 
-      browser.test.sendMessage("outsideEventsTest", "createAddressBook");
-      let [bookId, newBookPrefId] = await awaitMessage();
+      let [bookId, newBookPrefId] = await awaitMessage("outsideEventsTest", "createAddressBook");
       let [newBook] = checkEvents(["addressBooks", "onCreated", { type: "addressBook", id: bookId }]);
       browser.test.assertEq("external add", newBook.name);
 
-      browser.test.sendMessage("outsideEventsTest", "updateAddressBook", newBookPrefId);
-      await awaitMessage();
+      await awaitMessage("outsideEventsTest", "updateAddressBook", newBookPrefId);
       let [updatedBook] = checkEvents(["addressBooks", "onUpdated", { type: "addressBook", id: bookId }]);
       browser.test.assertEq("external edit", updatedBook.name);
 
-      browser.test.sendMessage("outsideEventsTest", "deleteAddressBook", newBookPrefId);
-      await awaitMessage();
+      await awaitMessage("outsideEventsTest", "deleteAddressBook", newBookPrefId);
       checkEvents(["addressBooks", "onDeleted", bookId]);
 
-      browser.test.sendMessage("outsideEventsTest", "createContact");
-      let [parentId1, contactId] = await awaitMessage();
+      let [parentId1, contactId] = await awaitMessage("outsideEventsTest", "createContact");
       let [newContact] = checkEvents(
         ["contacts", "onCreated", { type: "contact", parentId: parentId1, id: contactId }]
       );
       browser.test.assertEq("external", newContact.properties.FirstName);
       browser.test.assertEq("add", newContact.properties.LastName);
 
-      browser.test.sendMessage("outsideEventsTest", "updateContact", contactId);
-      await awaitMessage();
+      await awaitMessage("outsideEventsTest", "updateContact", contactId);
       let [updatedContact] = checkEvents(
         ["contacts", "onUpdated", { type: "contact", parentId: parentId1, id: contactId }]
       );
       browser.test.assertEq("external", updatedContact.properties.FirstName);
       browser.test.assertEq("edit", updatedContact.properties.LastName);
 
-      browser.test.sendMessage("outsideEventsTest", "createMailingList");
-      let [parentId2, listId] = await awaitMessage();
+      let [parentId2, listId] = await awaitMessage("outsideEventsTest", "createMailingList");
       let [newList] = checkEvents(
         ["mailingLists", "onCreated", { type: "mailingList", parentId: parentId2, id: listId }]
       );
       browser.test.assertEq("external add", newList.name);
 
-      browser.test.sendMessage("outsideEventsTest", "updateMailingList", listId);
-      await awaitMessage();
+      await awaitMessage("outsideEventsTest", "updateMailingList", listId);
       let [updatedList] = checkEvents(
         ["mailingLists", "onUpdated", { type: "mailingList", parentId: parentId2, id: listId }]
       );
       browser.test.assertEq("external edit", updatedList.name);
 
-      browser.test.sendMessage("outsideEventsTest", "addMailingListMember", listId, contactId);
-      await awaitMessage();
+      await awaitMessage("outsideEventsTest", "addMailingListMember", listId, contactId);
       checkEvents(
         ["mailingLists", "onMemberAdded", { type: "contact", parentId: listId, id: contactId }]
       );
       let listMembers = await browser.mailingLists.listMembers(listId);
       browser.test.assertEq(1, listMembers.length);
 
-      browser.test.sendMessage("outsideEventsTest", "removeMailingListMember", listId, contactId);
-      await awaitMessage();
+      await awaitMessage("outsideEventsTest", "removeMailingListMember", listId, contactId);
       checkEvents(
         ["mailingLists", "onMemberRemoved", listId, contactId]
       );
 
-      browser.test.sendMessage("outsideEventsTest", "deleteMailingList", listId);
-      await awaitMessage();
+      await awaitMessage("outsideEventsTest", "deleteMailingList", listId);
       checkEvents(["mailingLists", "onDeleted", parentId2, listId]);
 
-      browser.test.sendMessage("outsideEventsTest", "deleteContact", contactId);
-      await awaitMessage();
+      await awaitMessage("outsideEventsTest", "deleteContact", contactId);
       checkEvents(["contacts", "onDeleted", parentId1, contactId]);
 
       browser.test.log("Completed outsideEventsTest");
     }
 
     await addressBookTest();
     await contactsTest();
     await mailingListsTest();