summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/extensions/test/browser/head.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/extensions/test/browser/head.js')
-rw-r--r--comm/mail/components/extensions/test/browser/head.js1533
1 files changed, 1533 insertions, 0 deletions
diff --git a/comm/mail/components/extensions/test/browser/head.js b/comm/mail/components/extensions/test/browser/head.js
new file mode 100644
index 0000000000..ed25bde87f
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/head.js
@@ -0,0 +1,1533 @@
+/* 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/. */
+
+var { MailConsts } = ChromeUtils.import("resource:///modules/MailConsts.jsm");
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { getCachedAllowedSpaces, setCachedAllowedSpaces } = ChromeUtils.import(
+ "resource:///modules/ExtensionToolbarButtons.jsm"
+);
+const { storeState, getState } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizationState.mjs"
+);
+const { getDefaultItemIdsForSpace, getAvailableItemIdsForSpace } =
+ ChromeUtils.importESModule("resource:///modules/CustomizableItems.sys.mjs");
+
+var { ExtensionCommon } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionCommon.sys.mjs"
+);
+var { makeWidgetId } = ExtensionCommon;
+
+// Persistent Listener test functionality
+var { assertPersistentListeners } = ExtensionTestUtils.testAssertions;
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// This bug should be fixed, but for the moment this directory is whitelisted.
+//
+// NOTE: Entire directory whitelisting should be kept to a minimum. Normally you
+// should use "expectUncaughtRejection" to flag individual failures.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Message manager disconnected/
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/No matching message handler/);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Receiving end does not exist/
+);
+
+// Adjust timeout to take care of code coverage runs and fission runs to be a
+// lot slower.
+let originalRequestLongerTimeout = requestLongerTimeout;
+// eslint-disable-next-line no-global-assign
+requestLongerTimeout = factor => {
+ let ccovMultiplier = AppConstants.MOZ_CODE_COVERAGE ? 2 : 1;
+ let fissionMultiplier = SpecialPowers.useRemoteSubframes ? 2 : 1;
+ originalRequestLongerTimeout(ccovMultiplier * fissionMultiplier * factor);
+};
+requestLongerTimeout(1);
+
+add_setup(async () => {
+ await check3PaneState(true, true);
+ let tabmail = document.getElementById("tabmail");
+ if (tabmail.tabInfo.length > 1) {
+ info(`Will close ${tabmail.tabInfo.length - 1} tabs left over from others`);
+ for (let i = tabmail.tabInfo.length - 1; i > 0; i--) {
+ tabmail.closeTab(i);
+ }
+ is(tabmail.tabInfo.length, 1, "One tab open from start");
+ }
+});
+registerCleanupFunction(() => {
+ let tabmail = document.getElementById("tabmail");
+ is(tabmail.tabInfo.length, 1, "Only one tab open at end of test");
+
+ while (tabmail.tabInfo.length > 1) {
+ tabmail.closeTab(tabmail.tabInfo[1]);
+ }
+
+ // Some tests that open new windows don't return focus to the main window
+ // in a way that satisfies mochitest, and the test times out.
+ Services.focus.focusedWindow = window;
+ // Focus an element in the main window, then blur it again to avoid it
+ // hijacking keypresses.
+ let mainWindowElement = document.getElementById("button-appmenu");
+ mainWindowElement.focus();
+ mainWindowElement.blur();
+
+ MailServices.accounts.accounts.forEach(cleanUpAccount);
+ check3PaneState(true, true);
+
+ // The unified toolbar must have been cleaned up. If this fails, check if a
+ // test loaded an extension with a browser_action without setting "useAddonManager"
+ // to either "temporary" or "permanent", which triggers onUninstalled to be
+ // called on extension unload.
+ let cachedAllowedSpaces = getCachedAllowedSpaces();
+ is(
+ cachedAllowedSpaces.size,
+ 0,
+ `Stored known extension spaces should be cleared: ${JSON.stringify(
+ Object.fromEntries(cachedAllowedSpaces)
+ )}`
+ );
+ setCachedAllowedSpaces(new Map());
+ Services.prefs.clearUserPref("mail.pane_config.dynamic");
+ Services.xulStore.removeValue(
+ "chrome://messenger/content/messenger.xhtml",
+ "threadPane",
+ "view"
+ );
+});
+
+/**
+ * Enforce a certain state in the unified toolbar.
+ * @param {Object} state - A dictionary with arrays of buttons assigned to a space
+ */
+async function enforceState(state) {
+ const stateChangeObserved = TestUtils.topicObserved(
+ "unified-toolbar-state-change"
+ );
+ storeState(state);
+ await stateChangeObserved;
+}
+
+async function check3PaneState(folderPaneOpen = null, messagePaneOpen = null) {
+ let tabmail = document.getElementById("tabmail");
+ let tab = tabmail.currentTabInfo;
+ if (tab.chromeBrowser.contentDocument.readyState != "complete") {
+ await BrowserTestUtils.waitForEvent(
+ tab.chromeBrowser.contentWindow,
+ "load"
+ );
+ }
+
+ let { paneLayout } = tabmail.currentAbout3Pane;
+ if (folderPaneOpen !== null) {
+ Assert.equal(
+ paneLayout.folderPaneVisible,
+ folderPaneOpen,
+ "State of folder pane splitter is correct"
+ );
+ paneLayout.folderPaneVisible = folderPaneOpen;
+ }
+
+ if (messagePaneOpen !== null) {
+ Assert.equal(
+ paneLayout.messagePaneVisible,
+ messagePaneOpen,
+ "State of message pane splitter is correct"
+ );
+ paneLayout.messagePaneVisible = messagePaneOpen;
+ }
+}
+
+function createAccount(type = "none") {
+ let account;
+
+ if (type == "local") {
+ MailServices.accounts.createLocalMailAccount();
+ account = MailServices.accounts.FindAccountForServer(
+ MailServices.accounts.localFoldersServer
+ );
+ } else {
+ account = MailServices.accounts.createAccount();
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ `${account.key}user`,
+ "localhost",
+ type
+ );
+ }
+
+ info(`Created account ${account.toString()}`);
+ return account;
+}
+
+function cleanUpAccount(account) {
+ // If the current displayed message/folder belongs to the account to be removed,
+ // select the root folder, otherwise the removal of this account will trigger
+ // a "shouldn't have any listeners left" assertion in nsMsgDatabase.cpp.
+ let [folder] = window.GetSelectedMsgFolders();
+ if (folder && folder.server && folder.server == account.incomingServer) {
+ let tabmail = document.getElementById("tabmail");
+ tabmail.currentAbout3Pane.displayFolder(folder.server.rootFolder.URI);
+ }
+
+ let serverKey = account.incomingServer.key;
+ let serverType = account.incomingServer.type;
+ info(
+ `Cleaning up ${serverType} account ${account.key} and server ${serverKey}`
+ );
+ MailServices.accounts.removeAccount(account, true);
+
+ try {
+ let server = MailServices.accounts.getIncomingServer(serverKey);
+ if (server) {
+ info(`Cleaning up leftover ${serverType} server ${serverKey}`);
+ MailServices.accounts.removeIncomingServer(server, false);
+ }
+ } catch (e) {}
+}
+
+function addIdentity(account, email = "mochitest@localhost") {
+ let identity = MailServices.accounts.createIdentity();
+ identity.email = email;
+ account.addIdentity(identity);
+ if (!account.defaultIdentity) {
+ account.defaultIdentity = identity;
+ }
+ info(`Created identity ${identity.toString()}`);
+ return identity;
+}
+
+async function createSubfolder(parent, name) {
+ parent.createSubfolder(name, null);
+ return parent.getChildNamed(name);
+}
+
+function createMessages(folder, makeMessagesArg) {
+ if (typeof makeMessagesArg == "number") {
+ makeMessagesArg = { count: makeMessagesArg };
+ }
+ if (!createMessages.messageGenerator) {
+ createMessages.messageGenerator = new MessageGenerator();
+ }
+
+ let messages = createMessages.messageGenerator.makeMessages(makeMessagesArg);
+ let messageStrings = messages.map(message => message.toMboxString());
+ folder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ folder.addMessageBatch(messageStrings);
+}
+
+async function createMessageFromFile(folder, path) {
+ let message = await IOUtils.readUTF8(path);
+
+ // A cheap hack to make this acceptable to addMessageBatch. It works for
+ // existing uses but may not work for future uses.
+ let fromAddress = message.match(/From: .* <(.*@.*)>/)[0];
+ message = `From ${fromAddress}\r\n${message}`;
+
+ folder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ folder.addMessageBatch([message]);
+ folder.callFilterPlugins(null);
+}
+
+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));
+}
+
+async function focusWindow(win) {
+ if (Services.focus.activeWindow == win) {
+ return;
+ }
+
+ let promise = new Promise(resolve => {
+ win.addEventListener(
+ "focus",
+ function () {
+ resolve();
+ },
+ { capture: true, once: true }
+ );
+ });
+
+ win.focus();
+ await promise;
+}
+
+function promisePopupShown(popup) {
+ return new Promise(resolve => {
+ if (popup.state == "open") {
+ resolve();
+ } else {
+ let onPopupShown = event => {
+ popup.removeEventListener("popupshown", onPopupShown);
+ resolve();
+ };
+ popup.addEventListener("popupshown", onPopupShown);
+ }
+ });
+}
+
+function getPanelForNode(node) {
+ while (node.localName != "panel") {
+ node = node.parentNode;
+ }
+ return node;
+}
+
+/**
+ * Wait until the browser is fully loaded.
+ *
+ * @param {xul:browser} browser - A xul:browser.
+ * @param {string|function} [wantLoad = null] - If a function, takes a URL and
+ * returns true if that's the load we're interested in. If a string, gives the
+ * URL of the load we're interested in. If not present, the first load resolves
+ * the promise.
+ *
+ * @returns {Promise} When a load event is triggered for the browser or the browser
+ * is already fully loaded.
+ */
+function awaitBrowserLoaded(browser, wantLoad) {
+ let testFn = () => true;
+ if (wantLoad) {
+ testFn = typeof wantLoad === "function" ? wantLoad : url => url == wantLoad;
+ }
+
+ return TestUtils.waitForCondition(
+ () =>
+ browser.ownerGlobal.document.readyState === "complete" &&
+ (browser.webProgress?.isLoadingDocument === false ||
+ browser.contentDocument?.readyState === "complete") &&
+ browser.currentURI &&
+ testFn(browser.currentURI.spec),
+ "Browser should be loaded"
+ );
+}
+
+var awaitExtensionPanel = async function (
+ extension,
+ win = window,
+ awaitLoad = true
+) {
+ let { originalTarget: browser } = await BrowserTestUtils.waitForEvent(
+ win.document,
+ "WebExtPopupLoaded",
+ true,
+ event => event.detail.extension.id === extension.id
+ );
+
+ if (awaitLoad) {
+ await awaitBrowserLoaded(browser, url => url != "about:blank");
+ }
+ await promisePopupShown(getPanelForNode(browser));
+
+ return browser;
+};
+
+function getBrowserActionPopup(extension, win = window) {
+ return win.top.document.getElementById("webextension-remote-preload-panel");
+}
+
+function closeBrowserAction(extension, win = window) {
+ let popup = getBrowserActionPopup(extension, win);
+ let hidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ popup.hidePopup();
+
+ return hidden;
+}
+
+async function openNewMailWindow(options = {}) {
+ if (!options.newAccountWizard) {
+ Services.prefs.setBoolPref(
+ "mail.provider.suppress_dialog_on_startup",
+ true
+ );
+ }
+
+ let win = window.openDialog(
+ "chrome://messenger/content/messenger.xhtml",
+ "_blank",
+ "chrome,all,dialog=no"
+ );
+ await Promise.all([
+ BrowserTestUtils.waitForEvent(win, "focus", true),
+ BrowserTestUtils.waitForEvent(win, "activate", true),
+ ]);
+
+ return win;
+}
+
+async function openComposeWindow(account) {
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ let composeFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ params.identity = account.defaultIdentity;
+ params.composeFields = composeFields;
+
+ let composeWindowPromise = BrowserTestUtils.domWindowOpened(
+ undefined,
+ async win => {
+ await BrowserTestUtils.waitForEvent(win, "load");
+ if (
+ win.document.documentURI !=
+ "chrome://messenger/content/messengercompose/messengercompose.xhtml"
+ ) {
+ return false;
+ }
+ await BrowserTestUtils.waitForEvent(win, "compose-editor-ready");
+ return true;
+ }
+ );
+ MailServices.compose.OpenComposeWindowWithParams(null, params);
+ return composeWindowPromise;
+}
+
+async function openMessageInTab(msgHdr) {
+ if (!msgHdr.QueryInterface(Ci.nsIMsgDBHdr)) {
+ throw new Error("No message passed to openMessageInTab");
+ }
+
+ // Ensure the behaviour pref is set to open a new tab. It is the default,
+ // but you never know.
+ let oldPrefValue = Services.prefs.getIntPref("mail.openMessageBehavior");
+ Services.prefs.setIntPref(
+ "mail.openMessageBehavior",
+ MailConsts.OpenMessageBehavior.NEW_TAB
+ );
+ MailUtils.displayMessages([msgHdr]);
+ Services.prefs.setIntPref("mail.openMessageBehavior", oldPrefValue);
+
+ let win = Services.wm.getMostRecentWindow("mail:3pane");
+ let tab = win.document.getElementById("tabmail").currentTabInfo;
+ await BrowserTestUtils.waitForEvent(tab.chromeBrowser, "MsgLoaded");
+ return tab;
+}
+
+async function openMessageInWindow(msgHdr) {
+ if (!msgHdr.QueryInterface(Ci.nsIMsgDBHdr)) {
+ throw new Error("No message passed to openMessageInWindow");
+ }
+
+ let messageWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(
+ undefined,
+ async win =>
+ win.document.documentURI ==
+ "chrome://messenger/content/messageWindow.xhtml"
+ );
+ MailUtils.openMessageInNewWindow(msgHdr);
+
+ let messageWindow = await messageWindowPromise;
+ await BrowserTestUtils.waitForEvent(messageWindow, "MsgLoaded");
+ return messageWindow;
+}
+
+async function promiseMessageLoaded(browser, msgHdr) {
+ let messageURI = msgHdr.folder.getUriForMsg(msgHdr);
+ messageURI = MailServices.messageServiceFromURI(messageURI).getUrlForUri(
+ messageURI,
+ null
+ );
+
+ await awaitBrowserLoaded(browser, uri => uri == messageURI.spec);
+}
+
+/**
+ * Check the headers of an open compose window against expected values.
+ *
+ * @param {object} expected - A dictionary of expected headers.
+ * Omit headers that should have no value.
+ * @param {string[]} [fields.to]
+ * @param {string[]} [fields.cc]
+ * @param {string[]} [fields.bcc]
+ * @param {string[]} [fields.replyTo]
+ * @param {string[]} [fields.followupTo]
+ * @param {string[]} [fields.newsgroups]
+ * @param {string} [fields.subject]
+ */
+async function checkComposeHeaders(expected) {
+ let composeWindows = [...Services.wm.getEnumerator("msgcompose")];
+ is(composeWindows.length, 1);
+ let composeDocument = composeWindows[0].document;
+ let composeFields = composeWindows[0].gMsgCompose.compFields;
+
+ await new Promise(resolve => composeWindows[0].setTimeout(resolve));
+
+ if ("identityId" in expected) {
+ is(composeWindows[0].getCurrentIdentityKey(), expected.identityId);
+ }
+
+ if (expected.attachVCard) {
+ is(
+ expected.attachVCard,
+ composeFields.attachVCard,
+ "attachVCard in window should be correct"
+ );
+ }
+
+ let checkField = (fieldName, elementId) => {
+ let pills = composeDocument
+ .getElementById(elementId)
+ .getElementsByTagName("mail-address-pill");
+
+ if (fieldName in expected) {
+ is(
+ pills.length,
+ expected[fieldName].length,
+ `${fieldName} has the right number of pills`
+ );
+ for (let i = 0; i < expected[fieldName].length; i++) {
+ is(pills[i].label, expected[fieldName][i]);
+ }
+ } else {
+ is(pills.length, 0, `${fieldName} is empty`);
+ }
+ };
+
+ checkField("to", "addressRowTo");
+ checkField("cc", "addressRowCc");
+ checkField("bcc", "addressRowBcc");
+ checkField("replyTo", "addressRowReply");
+ checkField("followupTo", "addressRowFollowup");
+ checkField("newsgroups", "addressRowNewsgroups");
+
+ let subject = composeDocument.getElementById("msgSubject").value;
+ if ("subject" in expected) {
+ is(subject, expected.subject, "subject is correct");
+ } else {
+ is(subject, "", "subject is empty");
+ }
+
+ if (expected.overrideDefaultFcc) {
+ if (expected.overrideDefaultFccFolder) {
+ let server = MailServices.accounts.getAccount(
+ expected.overrideDefaultFccFolder.accountId
+ ).incomingServer;
+ let rootURI = server.rootFolder.URI;
+ is(
+ rootURI + expected.overrideDefaultFccFolder.path,
+ composeFields.fcc,
+ "fcc should be correct"
+ );
+ } else {
+ ok(
+ composeFields.fcc.startsWith("nocopy://"),
+ "fcc should start with nocopy://"
+ );
+ }
+ } else {
+ is("", composeFields.fcc, "fcc should be empty");
+ }
+
+ if (expected.additionalFccFolder) {
+ let server = MailServices.accounts.getAccount(
+ expected.additionalFccFolder.accountId
+ ).incomingServer;
+ let rootURI = server.rootFolder.URI;
+ is(
+ rootURI + expected.additionalFccFolder.path,
+ composeFields.fcc2,
+ "fcc2 should be correct"
+ );
+ } else {
+ ok(
+ composeFields.fcc2 == "" || composeFields.fcc2.startsWith("nocopy://"),
+ "fcc2 should not contain a folder uri"
+ );
+ }
+
+ if (expected.hasOwnProperty("priority")) {
+ is(
+ composeFields.priority.toLowerCase(),
+ expected.priority == "normal" ? "" : expected.priority,
+ "priority in composeFields should be correct"
+ );
+ }
+
+ if (expected.hasOwnProperty("returnReceipt")) {
+ is(
+ composeFields.returnReceipt,
+ expected.returnReceipt,
+ "returnReceipt in composeFields should be correct"
+ );
+ for (let item of composeDocument.querySelectorAll(`menuitem[command="cmd_toggleReturnReceipt"],
+ toolbarbutton[command="cmd_toggleReturnReceipt"]`)) {
+ is(
+ item.getAttribute("checked") == "true",
+ expected.returnReceipt,
+ "returnReceipt in window should be correct"
+ );
+ }
+ }
+
+ if (expected.hasOwnProperty("deliveryStatusNotification")) {
+ is(
+ composeFields.DSN,
+ !!expected.deliveryStatusNotification,
+ "deliveryStatusNotification in composeFields should be correct"
+ );
+ is(
+ composeDocument.getElementById("dsnMenu").getAttribute("checked") ==
+ "true",
+ !!expected.deliveryStatusNotification,
+ "deliveryStatusNotification in window should be correct"
+ );
+ }
+
+ if (expected.hasOwnProperty("deliveryFormat")) {
+ const deliveryFormats = {
+ auto: Ci.nsIMsgCompSendFormat.Auto,
+ plaintext: Ci.nsIMsgCompSendFormat.PlainText,
+ html: Ci.nsIMsgCompSendFormat.HTML,
+ both: Ci.nsIMsgCompSendFormat.Both,
+ };
+ const formatToId = new Map([
+ [Ci.nsIMsgCompSendFormat.PlainText, "format_plain"],
+ [Ci.nsIMsgCompSendFormat.HTML, "format_html"],
+ [Ci.nsIMsgCompSendFormat.Both, "format_both"],
+ [Ci.nsIMsgCompSendFormat.Auto, "format_auto"],
+ ]);
+ let expectedFormat = deliveryFormats[expected.deliveryFormat || "auto"];
+ is(
+ expectedFormat,
+ composeFields.deliveryFormat,
+ "deliveryFormat in composeFields should be correct"
+ );
+ for (let [format, id] of formatToId.entries()) {
+ let menuitem = composeDocument.getElementById(id);
+ is(
+ format == expectedFormat,
+ menuitem.getAttribute("checked") == "true",
+ "checked state of the deliveryFormat menu item <${id}> in window should be correct"
+ );
+ }
+ }
+}
+
+async function synthesizeMouseAtCenterAndRetry(selector, event, browser) {
+ let success = false;
+ let type = event.type || "click";
+ for (let retries = 0; !success && retries < 2; retries++) {
+ let clickPromise = BrowserTestUtils.waitForContentEvent(browser, type).then(
+ () => true
+ );
+ // Linux: Sometimes the actor used to simulate the mouse event in the content process does not
+ // react, even though the content page signals to be fully loaded. There is no status signal
+ // we could wait for, the loaded page *should* be ready at this point. To mitigate, we wait
+ // for the click event and if we do not see it within a certain time, we click again.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ let failPromise = new Promise(r =>
+ browser.ownerGlobal.setTimeout(r, 500)
+ ).then(() => false);
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(selector, event, browser);
+ success = await Promise.race([clickPromise, failPromise]);
+ }
+ Assert.ok(success, `Should have received ${type} event.`);
+}
+
+async function openContextMenu(selector = "#img1", win = window) {
+ let contentAreaContextMenu = win.document.getElementById("browserContext");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown"
+ );
+ let tabmail = document.getElementById("tabmail");
+ await synthesizeMouseAtCenterAndRetry(
+ selector,
+ { type: "mousedown", button: 2 },
+ tabmail.selectedBrowser
+ );
+ await synthesizeMouseAtCenterAndRetry(
+ selector,
+ { type: "contextmenu" },
+ tabmail.selectedBrowser
+ );
+ await popupShownPromise;
+ return contentAreaContextMenu;
+}
+
+async function openContextMenuInPopup(extension, selector, win = window) {
+ let contentAreaContextMenu =
+ win.top.document.getElementById("browserContext");
+ let stack = getBrowserActionPopup(extension, win);
+ let browser = stack.querySelector("browser");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown"
+ );
+ await synthesizeMouseAtCenterAndRetry(
+ selector,
+ { type: "mousedown", button: 2 },
+ browser
+ );
+ await synthesizeMouseAtCenterAndRetry(
+ selector,
+ { type: "contextmenu" },
+ browser
+ );
+ await popupShownPromise;
+ return contentAreaContextMenu;
+}
+
+async function closeExtensionContextMenu(
+ itemToSelect,
+ modifiers = {},
+ win = window
+) {
+ let contentAreaContextMenu =
+ win.top.document.getElementById("browserContext");
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popuphidden"
+ );
+ if (itemToSelect) {
+ itemToSelect.closest("menupopup").activateItem(itemToSelect, modifiers);
+ } else {
+ contentAreaContextMenu.hidePopup();
+ }
+ await popupHiddenPromise;
+
+ // Bug 1351638: parent menu fails to close intermittently, make sure it does.
+ contentAreaContextMenu.hidePopup();
+}
+
+async function openSubmenu(submenuItem, win = window) {
+ const submenu = submenuItem.menupopup;
+ const shown = BrowserTestUtils.waitForEvent(submenu, "popupshown");
+ submenuItem.openMenu(true);
+ await shown;
+ return submenu;
+}
+
+async function closeContextMenu(contextMenu) {
+ let contentAreaContextMenu =
+ contextMenu || document.getElementById("browserContext");
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popuphidden"
+ );
+ contentAreaContextMenu.hidePopup();
+ await popupHiddenPromise;
+}
+
+async function getUtilsJS() {
+ let response = await fetch(getRootDirectory(gTestPath) + "utils.js");
+ return response.text();
+}
+
+async function checkContent(browser, expected) {
+ await SpecialPowers.spawn(browser, [expected], expected => {
+ let body = content.document.body;
+ Assert.ok(body, "body");
+ let computedStyle = content.getComputedStyle(body);
+
+ if ("backgroundColor" in expected) {
+ Assert.equal(
+ computedStyle.backgroundColor,
+ expected.backgroundColor,
+ "backgroundColor"
+ );
+ }
+ if ("color" in expected) {
+ Assert.equal(computedStyle.color, expected.color, "color");
+ }
+ if ("foo" in expected) {
+ Assert.equal(body.getAttribute("foo"), expected.foo, "foo");
+ }
+ if ("textContent" in expected) {
+ // In message display, we only really want the message body, but the
+ // document body also has headers. For the purposes of these tests,
+ // we can just select an descendant node, since what really matters is
+ // whether (or not) a script ran, not the exact result.
+ body = body.querySelector(".moz-text-flowed") ?? body;
+ Assert.equal(body.textContent, expected.textContent, "textContent");
+ }
+ });
+}
+
+function contentTabOpenPromise(tabmail, url) {
+ return new Promise(resolve => {
+ let tabMonitor = {
+ onTabTitleChanged(aTab) {},
+ onTabClosing(aTab) {},
+ onTabPersist(aTab) {},
+ onTabRestored(aTab) {},
+ onTabSwitched(aNewTab, aOldTab) {},
+ async onTabOpened(aTab) {
+ let result = awaitBrowserLoaded(
+ aTab.linkedBrowser,
+ urlToMatch => urlToMatch == url
+ ).then(() => aTab);
+
+ let reporterListener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ onStateChange() {},
+ onProgressChange() {},
+ onLocationChange(
+ /* in nsIWebProgress*/ aWebProgress,
+ /* in nsIRequest*/ aRequest,
+ /* in nsIURI*/ aLocation
+ ) {
+ if (aLocation.spec == url) {
+ aTab.browser.removeProgressListener(reporterListener);
+ tabmail.unregisterTabMonitor(tabMonitor);
+ TestUtils.executeSoon(() => resolve(result));
+ }
+ },
+ onStatusChange() {},
+ onSecurityChange() {},
+ onContentBlockingEvent() {},
+ };
+ aTab.browser.addProgressListener(reporterListener);
+ },
+ };
+ tabmail.registerTabMonitor(tabMonitor);
+ });
+}
+
+/**
+ * @typedef ConfigData
+ * @property {string} actionType - type of action button in underscore notation
+ * @property {string} window - the window to perform the test in
+ * @property {string} [testType] - supported tests are "open-with-mouse-click" and
+ * "open-with-menu-command"
+ * @property {string} [default_area] - area to be used for the test
+ * @property {boolean} [use_default_popup] - select if the default_popup should be
+ * used for the test
+ * @property {boolean} [disable_button] - select if the button should be disabled
+ * @property {Function} [backend_script] - custom backend script to be used for the
+ * test, will override the default backend_script of the selected test
+ * @property {Function} [background_script] - custom background script to be used for the
+ * test, will override the default background_script of the selected test
+ * @property {[string]} [permissions] - custom permissions to be used for the test,
+ * must not be specified together with testType
+ */
+
+/**
+ * Creates an extension with an action button and either runs one of the default
+ * tests, or loads a custom background script and a custom backend scripts to run
+ * an arbitrary test.
+ *
+ * @param {ConfigData} configData - test configuration
+ */
+async function run_popup_test(configData) {
+ if (!configData.actionType) {
+ throw new Error("Mandatory configData.actionType is missing");
+ }
+ if (!configData.window) {
+ throw new Error("Mandatory configData.window is missing");
+ }
+
+ // Get camelCase API names from action type.
+ configData.apiName = configData.actionType.replace(/_([a-z])/g, function (g) {
+ return g[1].toUpperCase();
+ });
+ configData.moduleName =
+ configData.actionType == "action" ? "browserAction" : configData.apiName;
+
+ let backend_script = configData.backend_script;
+
+ let extensionDetails = {
+ files: {
+ "popup.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <title>Popup</title>
+ </head>
+ <body>
+ <p>Hello</p>
+ <script src="popup.js"></script>
+ </body>
+ </html>`,
+ "popup.js": async function () {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => window.setTimeout(resolve, 1000));
+ await browser.runtime.sendMessage("popup opened");
+ await new Promise(resolve => window.setTimeout(resolve));
+ window.close();
+ },
+ "utils.js": await getUtilsJS(),
+ "helper.js": function () {
+ window.actionType = browser.runtime.getManifest().description;
+ // Get camelCase API names from action type.
+ window.apiName = window.actionType.replace(/_([a-z])/g, function (g) {
+ return g[1].toUpperCase();
+ });
+ window.getPopupOpenedPromise = function () {
+ return new Promise(resolve => {
+ const handleMessage = async (message, sender, sendResponse) => {
+ if (message && message == "popup opened") {
+ sendResponse();
+ window.setTimeout(resolve);
+ browser.runtime.onMessage.removeListener(handleMessage);
+ }
+ };
+ browser.runtime.onMessage.addListener(handleMessage);
+ });
+ };
+ },
+ },
+ manifest: {
+ manifest_version: configData.manifest_version || 2,
+ browser_specific_settings: {
+ gecko: {
+ id: `${configData.actionType}@mochi.test`,
+ },
+ },
+ description: configData.actionType,
+ background: { scripts: ["utils.js", "helper.js", "background.js"] },
+ },
+ useAddonManager: "temporary",
+ };
+
+ switch (configData.testType) {
+ case "open-with-mouse-click":
+ backend_script = async function (extension, configData) {
+ let win = configData.window;
+
+ await extension.startup();
+ await promiseAnimationFrame(win);
+ await new Promise(resolve => win.setTimeout(resolve));
+ await extension.awaitMessage("ready");
+
+ let buttonId = `${configData.actionType}_mochi_test-${configData.moduleName}-toolbarbutton`;
+ let toolbarId;
+ switch (configData.actionType) {
+ case "compose_action":
+ toolbarId = "composeToolbar2";
+ if (configData.default_area == "formattoolbar") {
+ toolbarId = "FormatToolbar";
+ }
+ break;
+ case "action":
+ case "browser_action":
+ if (configData.default_windows?.join(",") === "messageDisplay") {
+ toolbarId = "mail-bar3";
+ } else {
+ toolbarId = "unified-toolbar";
+ }
+ break;
+ case "message_display_action":
+ toolbarId = "header-view-toolbar";
+ break;
+ default:
+ throw new Error(
+ `Unsupported configData.actionType: ${configData.actionType}`
+ );
+ }
+
+ let toolbar, button;
+ if (toolbarId === "unified-toolbar") {
+ toolbar = win.document.querySelector("unified-toolbar");
+ button = win.document.querySelector(
+ `#unifiedToolbarContent [extension="${configData.actionType}@mochi.test"]`
+ );
+ } else {
+ toolbar = win.document.getElementById(toolbarId);
+ button = win.document.getElementById(buttonId);
+ }
+ ok(button, "Button created");
+ ok(toolbar.contains(button), "Button added to toolbar");
+ let label;
+ if (toolbarId === "unified-toolbar") {
+ const state = getState();
+ const itemId = `ext-${configData.actionType}@mochi.test`;
+ if (state.mail) {
+ ok(
+ state.mail.includes(itemId),
+ "Button should be in unified toolbar mail space"
+ );
+ }
+ ok(
+ getDefaultItemIdsForSpace("mail").includes(itemId),
+ "Button should be in default set for unified toolbar mail space"
+ );
+ ok(
+ getAvailableItemIdsForSpace("mail").includes(itemId),
+ "Button should be available in unified toolbar mail space"
+ );
+
+ let icon = button.querySelector(".button-icon");
+ is(
+ getComputedStyle(icon).content,
+ `url("chrome://messenger/content/extension.svg")`,
+ "Default icon"
+ );
+ label = button.querySelector(".button-label");
+ is(label.textContent, "This is a test", "Correct label");
+ } else {
+ if (toolbar.hasAttribute("customizable")) {
+ ok(
+ toolbar.currentSet.split(",").includes(buttonId),
+ `Button should have been added to currentSet property of toolbar ${toolbarId}`
+ );
+ ok(
+ toolbar.getAttribute("currentset").split(",").includes(buttonId),
+ `Button should have been added to currentset attribute of toolbar ${toolbarId}`
+ );
+ }
+ ok(
+ Services.xulStore
+ .getValue(win.location.href, toolbarId, "currentset")
+ .split(",")
+ .includes(buttonId),
+ `Button should have been added to currentset xulStore of toolbar ${toolbarId}`
+ );
+
+ let icon = button.querySelector(".toolbarbutton-icon");
+ is(
+ getComputedStyle(icon).listStyleImage,
+ `url("chrome://messenger/content/extension.svg")`,
+ "Default icon"
+ );
+ label = button.querySelector(".toolbarbutton-text");
+ is(label.value, "This is a test", "Correct label");
+ }
+
+ if (
+ !configData.use_default_popup &&
+ configData?.manifest_version == 3
+ ) {
+ assertPersistentListeners(
+ extension,
+ configData.moduleName,
+ "onClicked",
+ {
+ primed: false,
+ }
+ );
+ }
+ if (configData.terminateBackground) {
+ await extension.terminateBackground({
+ disableResetIdleForTest: true,
+ });
+ if (
+ !configData.use_default_popup &&
+ configData?.manifest_version == 3
+ ) {
+ assertPersistentListeners(
+ extension,
+ configData.moduleName,
+ "onClicked",
+ {
+ primed: true,
+ }
+ );
+ }
+ }
+
+ let clickedPromise;
+ if (!configData.disable_button) {
+ clickedPromise = extension.awaitMessage("actionButtonClicked");
+ }
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, win);
+ if (configData.disable_button) {
+ // We're testing that nothing happens. Give it time to potentially happen.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => win.setTimeout(resolve, 500));
+ // In case the background was terminated, it should not restart.
+ // If it does, we will get an extra "ready" message and fail.
+ // Listeners should still be primed.
+ if (
+ configData.terminateBackground &&
+ configData?.manifest_version == 3
+ ) {
+ assertPersistentListeners(
+ extension,
+ configData.moduleName,
+ "onClicked",
+ {
+ primed: true,
+ }
+ );
+ }
+ } else {
+ let hasFiredBefore = await clickedPromise;
+ await promiseAnimationFrame(win);
+ await new Promise(resolve => win.setTimeout(resolve));
+ if (toolbarId === "unified-toolbar") {
+ is(
+ win.document.querySelector(
+ `#unifiedToolbarContent [extension="${configData.actionType}@mochi.test"]`
+ ),
+ button
+ );
+ label = button.querySelector(".button-label");
+ is(label.textContent, "New title", "Correct label");
+ } else {
+ is(win.document.getElementById(buttonId), button);
+ label = button.querySelector(".toolbarbutton-text");
+ is(label.value, "New title", "Correct label");
+ }
+
+ if (configData.terminateBackground) {
+ // The onClicked event should have restarted the background script.
+ await extension.awaitMessage("ready");
+ // Could be undefined, but it must not be true
+ is(false, !!hasFiredBefore);
+ }
+ if (
+ !configData.use_default_popup &&
+ configData?.manifest_version == 3
+ ) {
+ assertPersistentListeners(
+ extension,
+ configData.moduleName,
+ "onClicked",
+ {
+ primed: false,
+ }
+ );
+ }
+ }
+
+ // Check the open state of the action button.
+ await TestUtils.waitForCondition(
+ () => button.getAttribute("open") != "true",
+ "Button should not have open state after the popup closed."
+ );
+
+ await extension.unload();
+ await promiseAnimationFrame(win);
+ await new Promise(resolve => win.setTimeout(resolve));
+
+ ok(!win.document.getElementById(buttonId), "Button destroyed");
+
+ if (toolbarId === "unified-toolbar") {
+ const state = getState();
+ const itemId = `ext-${configData.actionType}@mochi.test`;
+ if (state.mail) {
+ ok(
+ !state.mail.includes(itemId),
+ "Button should have been removed from unified toolbar mail space"
+ );
+ }
+ ok(
+ !getDefaultItemIdsForSpace("mail").includes(itemId),
+ "Button should have been removed from default set for unified toolbar mail space"
+ );
+ ok(
+ !getAvailableItemIdsForSpace("mail").includes(itemId),
+ "Button should have no longer be available in unified toolbar mail space"
+ );
+ } else {
+ ok(
+ !Services.xulStore
+ .getValue(win.top.location.href, toolbarId, "currentset")
+ .split(",")
+ .includes(buttonId),
+ `Button should have been removed from currentset xulStore of toolbar ${toolbarId}`
+ );
+ }
+ };
+ if (configData.use_default_popup) {
+ // With popup.
+ extensionDetails.files["background.js"] = async function () {
+ browser.test.log("popup background script ran");
+ let popupPromise = window.getPopupOpenedPromise();
+ browser.test.sendMessage("ready");
+ await popupPromise;
+ await browser[window.apiName].setTitle({ title: "New title" });
+ browser.test.sendMessage("actionButtonClicked");
+ };
+ } else if (configData.disable_button) {
+ // Without popup and disabled button.
+ extensionDetails.files["background.js"] = async function () {
+ browser.test.log("nopopup & button disabled background script ran");
+ browser[window.apiName].onClicked.addListener(async (tab, info) => {
+ browser.test.fail(
+ "Should not have seen the onClicked event for a disabled button"
+ );
+ });
+ browser[window.apiName].disable();
+ browser.test.sendMessage("ready");
+ };
+ } else {
+ // Without popup.
+ extensionDetails.files["background.js"] = async function () {
+ let hasFiredBefore = false;
+ browser.test.log("nopopup background script ran");
+ browser[window.apiName].onClicked.addListener(async (tab, info) => {
+ browser.test.assertEq("object", typeof tab);
+ browser.test.assertEq("object", typeof info);
+ browser.test.assertEq(0, info.button);
+ browser.test.assertTrue(Array.isArray(info.modifiers));
+ browser.test.assertEq(0, info.modifiers.length);
+ let [currentTab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ browser.test.assertEq(
+ currentTab.id,
+ tab.id,
+ "Should find the correct tab"
+ );
+ await browser[window.apiName].setTitle({ title: "New title" });
+ await new Promise(resolve => window.setTimeout(resolve));
+ browser.test.sendMessage("actionButtonClicked", hasFiredBefore);
+ hasFiredBefore = true;
+ });
+ browser.test.sendMessage("ready");
+ };
+ }
+ break;
+
+ case "open-with-menu-command":
+ extensionDetails.manifest.permissions = ["menus"];
+ backend_script = async function (extension, configData) {
+ let win = configData.window;
+ let buttonId = `${configData.actionType}_mochi_test-${configData.moduleName}-toolbarbutton`;
+ let menuId = "toolbar-context-menu";
+ let isUnifiedToolbar = false;
+ if (
+ configData.actionType == "compose_action" &&
+ configData.default_area == "formattoolbar"
+ ) {
+ menuId = "format-toolbar-context-menu";
+ }
+ if (configData.actionType == "message_display_action") {
+ menuId = "header-toolbar-context-menu";
+ }
+ if (
+ (configData.actionType == "browser_action" ||
+ configData.actionType == "action") &&
+ configData.default_windows?.join(",") !== "messageDisplay"
+ ) {
+ menuId = "unifiedToolbarMenu";
+ isUnifiedToolbar = true;
+ }
+ const getButton = windowContent => {
+ if (isUnifiedToolbar) {
+ return windowContent.document.querySelector(
+ `#unifiedToolbarContent [extension="${configData.actionType}@mochi.test"]`
+ );
+ }
+ return windowContent.document.getElementById(buttonId);
+ };
+
+ extension.onMessage("triggerClick", async () => {
+ let button = getButton(win);
+ let menu = win.document.getElementById(menuId);
+ let onShownPromise = extension.awaitMessage("onShown");
+ let shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ button,
+ { type: "contextmenu" },
+ win
+ );
+ await shownPromise;
+ await onShownPromise;
+ await new Promise(resolve => win.setTimeout(resolve));
+
+ let menuitem = win.document.getElementById(
+ `${configData.actionType}_mochi_test-menuitem-_testmenu`
+ );
+ Assert.ok(menuitem);
+ menuitem.parentNode.activateItem(menuitem);
+
+ // Sometimes, the popup will open then instantly disappear. It seems to
+ // still be hiding after the previous appearance. If we wait a little bit,
+ // this doesn't happen.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => win.setTimeout(r, 250));
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+
+ // Check the open state of the action button.
+ let button = getButton(win);
+ await TestUtils.waitForCondition(
+ () => button.getAttribute("open") != "true",
+ "Button should not have open state after the popup closed."
+ );
+
+ await extension.unload();
+ };
+ if (configData.use_default_popup) {
+ // With popup.
+ extensionDetails.files["background.js"] = async function () {
+ browser.test.log("popup background script ran");
+ await new Promise(resolve => {
+ browser.menus.create(
+ {
+ id: "testmenu",
+ title: `Open ${window.actionType}`,
+ contexts: [window.actionType],
+ command: `_execute_${window.actionType}`,
+ },
+ resolve
+ );
+ });
+
+ await browser.menus.onShown.addListener((...args) => {
+ browser.test.sendMessage("onShown", args);
+ });
+
+ let popupPromise = window.getPopupOpenedPromise();
+ await window.sendMessage("triggerClick");
+ await popupPromise;
+
+ browser.test.notifyPass();
+ };
+ } else if (configData.disable_button) {
+ // Without popup and disabled button.
+ extensionDetails.files["background.js"] = async function () {
+ browser.test.log("nopopup & button disabled background script ran");
+ await new Promise(resolve => {
+ browser.menus.create(
+ {
+ id: "testmenu",
+ title: `Open ${window.actionType}`,
+ contexts: [window.actionType],
+ command: `_execute_${window.actionType}`,
+ },
+ resolve
+ );
+ });
+
+ await browser.menus.onShown.addListener((...args) => {
+ browser.test.sendMessage("onShown", args);
+ });
+
+ browser[window.apiName].onClicked.addListener(async (tab, info) => {
+ browser.test.fail(
+ "Should not have seen the onClicked event for a disabled button"
+ );
+ });
+
+ await browser[window.apiName].disable();
+ await window.sendMessage("triggerClick");
+ browser.test.notifyPass();
+ };
+ } else {
+ // Without popup.
+ extensionDetails.files["background.js"] = async function () {
+ browser.test.log("nopopup background script ran");
+ await new Promise(resolve => {
+ browser.menus.create(
+ {
+ id: "testmenu",
+ title: `Open ${window.actionType}`,
+ contexts: [window.actionType],
+ command: `_execute_${window.actionType}`,
+ },
+ resolve
+ );
+ });
+
+ await browser.menus.onShown.addListener((...args) => {
+ browser.test.sendMessage("onShown", args);
+ });
+
+ let clickPromise = new Promise(resolve => {
+ let listener = async (tab, info) => {
+ browser[window.apiName].onClicked.removeListener(listener);
+ browser.test.assertEq("object", typeof tab);
+ browser.test.assertEq("object", typeof info);
+ browser.test.assertEq(0, info.button);
+ browser.test.assertTrue(Array.isArray(info.modifiers));
+ browser.test.assertEq(0, info.modifiers.length);
+ browser.test.log(`Tab ID is ${tab.id}`);
+ resolve();
+ };
+ browser[window.apiName].onClicked.addListener(listener);
+ });
+ await window.sendMessage("triggerClick");
+ await clickPromise;
+
+ browser.test.notifyPass();
+ };
+ }
+ break;
+ }
+
+ extensionDetails.manifest[configData.actionType] = {
+ default_title: "This is a test",
+ };
+ if (configData.use_default_popup) {
+ extensionDetails.manifest[configData.actionType].default_popup =
+ "popup.html";
+ }
+ if (configData.default_area) {
+ extensionDetails.manifest[configData.actionType].default_area =
+ configData.default_area;
+ }
+ if (configData.hasOwnProperty("background")) {
+ extensionDetails.files["background.js"] = configData.background_script;
+ }
+ if (configData.hasOwnProperty("permissions")) {
+ extensionDetails.manifest.permissions = configData.permissions;
+ }
+ if (configData.default_windows) {
+ extensionDetails.manifest[configData.actionType].default_windows =
+ configData.default_windows;
+ }
+
+ let extension = ExtensionTestUtils.loadExtension(extensionDetails);
+ await backend_script(extension, configData);
+}
+
+async function run_action_button_order_test(configs, window, actionType) {
+ // Get camelCase API names from action type.
+ let apiName = actionType.replace(/_([a-z])/g, function (g) {
+ return g[1].toUpperCase();
+ });
+
+ function get_id(name) {
+ return `${name}_mochi_test-${apiName}-toolbarbutton`;
+ }
+
+ function test_buttons(configs, window, toolbars) {
+ for (let toolbarId of toolbars) {
+ let expected = configs.filter(e => e.toolbar == toolbarId);
+ let selector =
+ toolbarId === "unified-toolbar"
+ ? `#unifiedToolbarContent [extension$="@mochi.test"]`
+ : `#${toolbarId} toolbarbutton[id$="${get_id("")}"]`;
+ let buttons = window.document.querySelectorAll(selector);
+ Assert.equal(
+ expected.length,
+ buttons.length,
+ `Should find the correct number of buttons in ${toolbarId} toolbar`
+ );
+ for (let i = 0; i < buttons.length; i++) {
+ if (toolbarId === "unified-toolbar") {
+ Assert.equal(
+ `${expected[i].name}@mochi.test`,
+ buttons[i].getAttribute("extension"),
+ `Should find the correct button at location #${i}`
+ );
+ } else {
+ Assert.equal(
+ get_id(expected[i].name),
+ buttons[i].id,
+ `Should find the correct button at location #${i}`
+ );
+ }
+ }
+ }
+ }
+
+ // Create extension data.
+ let toolbars = new Set();
+ for (let config of configs) {
+ toolbars.add(config.toolbar);
+ config.extensionData = {
+ useAddonManager: "permanent",
+ manifest: {
+ applications: {
+ gecko: {
+ id: `${config.name}@mochi.test`,
+ },
+ },
+ [actionType]: {
+ default_title: config.name,
+ },
+ },
+ };
+ if (config.area) {
+ config.extensionData.manifest[actionType].default_area = config.area;
+ }
+ if (config.default_windows) {
+ config.extensionData.manifest[actionType].default_windows =
+ config.default_windows;
+ }
+ }
+
+ // Test order of buttons after first install.
+ for (let config of configs) {
+ config.extension = ExtensionTestUtils.loadExtension(config.extensionData);
+ await config.extension.startup();
+ }
+ test_buttons(configs, window, toolbars);
+
+ // Disable all buttons.
+ for (let config of configs) {
+ let addon = await AddonManager.getAddonByID(config.extension.id);
+ await addon.disable();
+ }
+ test_buttons([], window, toolbars);
+
+ // Re-enable all buttons in reversed order, displayed order should not change.
+ for (let config of [...configs].reverse()) {
+ let addon = await AddonManager.getAddonByID(config.extension.id);
+ await addon.enable();
+ }
+ test_buttons(configs, window, toolbars);
+
+ // Re-install all extensions in reversed order, displayed order should not change.
+ for (let config of [...configs].reverse()) {
+ config.extension2 = ExtensionTestUtils.loadExtension(config.extensionData);
+ await config.extension2.startup();
+ }
+ test_buttons(configs, window, toolbars);
+
+ // Remove all extensions.
+ for (let config of [...configs].reverse()) {
+ await config.extension.unload();
+ await config.extension2.unload();
+ }
+ test_buttons([], window, toolbars);
+}
+
+/**
+ * Helper method to switch to a cards view with vertical layout.
+ */
+async function ensure_cards_view() {
+ const { threadTree, threadPane } =
+ document.getElementById("tabmail").currentAbout3Pane;
+
+ Services.prefs.setIntPref("mail.pane_config.dynamic", 2);
+ Services.xulStore.setValue(
+ "chrome://messenger/content/messenger.xhtml",
+ "threadPane",
+ "view",
+ "cards"
+ );
+ threadPane.updateThreadView("cards");
+
+ await BrowserTestUtils.waitForCondition(
+ () => threadTree.getAttribute("rows") == "thread-card",
+ "The tree view switched to a cards layout"
+ );
+}
+
+/**
+ * Helper method to switch to a table view with classic layout.
+ */
+async function ensure_table_view() {
+ const { threadTree, threadPane } =
+ document.getElementById("tabmail").currentAbout3Pane;
+
+ Services.prefs.setIntPref("mail.pane_config.dynamic", 0);
+ Services.xulStore.setValue(
+ "chrome://messenger/content/messenger.xhtml",
+ "threadPane",
+ "view",
+ "table"
+ );
+ threadPane.updateThreadView("table");
+
+ await BrowserTestUtils.waitForCondition(
+ () => threadTree.getAttribute("rows") == "thread-row",
+ "The tree view switched to a table layout"
+ );
+}