diff options
Diffstat (limited to 'comm/mail/base/test/browser/head.js')
-rw-r--r-- | comm/mail/base/test/browser/head.js | 371 |
1 files changed, 371 insertions, 0 deletions
diff --git a/comm/mail/base/test/browser/head.js b/comm/mail/base/test/browser/head.js new file mode 100644 index 0000000000..4ee9845d89 --- /dev/null +++ b/comm/mail/base/test/browser/head.js @@ -0,0 +1,371 @@ +/* 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 { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +async function focusWindow(win) { + win.focus(); + await TestUtils.waitForCondition( + () => Services.focus.focusedWindow?.browsingContext.topChromeWindow == win, + "waiting for window to be focused" + ); +} + +async function openExtensionPopup(win, buttonId) { + await focusWindow(win.top); + + let actionButton = await TestUtils.waitForCondition( + () => + win.document.querySelector( + `#${buttonId}, [item-id="${buttonId}"] button` + ), + "waiting for the action button to exist" + ); + await TestUtils.waitForCondition( + () => BrowserTestUtils.is_visible(actionButton), + "waiting for action button to be visible" + ); + EventUtils.synthesizeMouseAtCenter(actionButton, {}, win); + + let panel = win.top.document.getElementById( + "webextension-remote-preload-panel" + ); + let browser = panel.querySelector("browser"); + await TestUtils.waitForCondition( + () => browser.clientWidth > 100, + "waiting for browser to resize" + ); + + return { actionButton, panel, browser }; +} + +function getSmartServer() { + return MailServices.accounts.findServer("nobody", "smart mailboxes", "none"); +} + +function resetSmartMailboxes() { + let oldServer = getSmartServer(); + // Clean up any leftover server from an earlier test. + if (oldServer) { + let oldAccount = MailServices.accounts.FindAccountForServer(oldServer); + MailServices.accounts.removeAccount(oldAccount, false); + } +} + +class MenuTestHelper { + /** @type {XULMenuElement} */ + menu; + + /** + * An object describing the state of a <menu> or <menuitem>. + * + * @typedef {Object} MenuItemData + * @property {boolean|string[]} [hidden] - true if the item should be hidden + * in all modes, or a list of modes in which it should be hidden. + * @property {boolean|string[]} [disabled] - true if the item should be + * disabled in all modes, or a list of modes in which it should be + * disabled. If the item should be hidden this property is ignored. + * @property {boolean|string[]} [checked] - true if the item should be + * checked in all modes, or a list of modes in which it should be + * checked. If the item should be hidden this property is ignored. + * @property {string} [l10nID] - the ID of the Fluent string this item + * should be displaying. If specified, `l10nArgs` will be checked. + * @property {object} [l10nArgs] - the arguments for the Fluent string this + * item should be displaying. If not specified, the string should not have + * arguments. + */ + /** + * An object describing the possible states of a menu's items. Object keys + * are the item's ID, values describe the item's state. + * + * @typedef {Object.<string, MenuItemData>} MenuData + */ + + /** @type {MenuData} */ + baseData; + + constructor(menuID, baseData) { + this.menu = document.getElementById(menuID); + this.baseData = baseData; + } + + /** + * Clicks on the menu and waits for it to open. + */ + async openMenu() { + let shownPromise = BrowserTestUtils.waitForEvent( + this.menu.menupopup, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(this.menu, {}); + await shownPromise; + } + + /** + * Check that an item matches the expected state. + * + * @param {XULElement} actual - A <menu> or <menuitem>. + * @param {MenuItemData} expected + */ + checkItem(actual, expected) { + Assert.equal( + BrowserTestUtils.is_hidden(actual), + !!expected.hidden, + `${actual.id} hidden` + ); + if (!expected.hidden) { + Assert.equal( + actual.disabled, + !!expected.disabled, + `${actual.id} disabled` + ); + } + if (expected.checked) { + Assert.equal( + actual.getAttribute("checked"), + "true", + `${actual.id} checked` + ); + } else if (["checkbox", "radio"].includes(actual.getAttribute("type"))) { + Assert.ok( + !actual.hasAttribute("checked") || + actual.getAttribute("checked") == "false", + `${actual.id} not checked` + ); + } + if (expected.l10nID) { + let attributes = actual.ownerDocument.l10n.getAttributes(actual); + Assert.equal(attributes.id, expected.l10nID, `${actual.id} L10n string`); + Assert.deepEqual( + attributes.args, + expected.l10nArgs ?? null, + `${actual.id} L10n args` + ); + } + } + + /** + * Recurses through submenus performing checks on items. + * + * @param {XULPopupElement} popup - The current pop-up to check. + * @param {MenuData} data - The expected values to test against. + * @param {boolean} [itemsMustBeInData=false] - If true, all menu items and + * menus within `popup` must be specified in `data`. If false, items not + * in `data` will be ignored. + */ + async iterate(popup, data, itemsMustBeInData = false) { + if (popup.state != "open") { + await BrowserTestUtils.waitForEvent(popup, "popupshown"); + } + + for (let item of popup.children) { + if (!item.id || item.localName == "menuseparator") { + continue; + } + + if (!(item.id in data)) { + if (itemsMustBeInData) { + Assert.report(true, undefined, undefined, `${item.id} in data`); + } + continue; + } + let itemData = data[item.id]; + this.checkItem(item, itemData); + delete data[item.id]; + + if (item.localName == "menu") { + if (BrowserTestUtils.is_visible(item) && !item.disabled) { + item.openMenu(true); + await this.iterate(item.menupopup, data, itemsMustBeInData); + } else { + for (let hiddenItem of item.querySelectorAll("menu, menuitem")) { + delete data[hiddenItem.id]; + } + } + } + } + + popup.hidePopup(); + await new Promise(resolve => setTimeout(resolve)); + } + + /** + * Checks every item in the menu and submenus against the expected states. + * + * @param {string} mode - The current mode, used to select the right expected + * values from `baseData`. + */ + async testAllItems(mode) { + // Get the data for just this mode. + let data = {}; + for (let [id, itemData] of Object.entries(this.baseData)) { + data[id] = { + ...itemData, + hidden: itemData.hidden === true || itemData.hidden?.includes(mode), + disabled: + itemData.disabled === true || itemData.disabled?.includes(mode), + checked: itemData.checked === true || itemData.checked?.includes(mode), + }; + } + + // Open the menu and all submenus and check the items. + await this.openMenu(); + await this.iterate(this.menu.menupopup, data, true); + + // Report any unexpected items. + for (let id of Object.keys(data)) { + Assert.report(true, undefined, undefined, `extra item ${id} in data`); + } + } + + /** + * Checks specific items in the menu. + * + * @param {MenuData} data - The expected values to test against. + */ + async testItems(data) { + await this.openMenu(); + await this.iterate(this.menu.menupopup, data); + + for (let id of Object.keys(data)) { + Assert.report(true, undefined, undefined, `extra item ${id} in data`); + } + + if (this.menu.menupopup.state != "closed") { + let hiddenPromise = BrowserTestUtils.waitForEvent( + this.menu.menupopup, + "popuphidden" + ); + this.menu.menupopup.hidePopup(); + await hiddenPromise; + } + await new Promise(resolve => setTimeout(resolve)); + } + + /** + * Activates the item in the menu. + * + * @note This currently only works on top-level items. + * @param {string} menuItemID - The item to activate. + * @param {MenuData} [data] - If given, the expected state of the menu item + * before activation. + */ + async activateItem(menuItemID, data) { + await this.openMenu(); + let hiddenPromise = BrowserTestUtils.waitForEvent( + this.menu.menupopup, + "popuphidden" + ); + let item = document.getElementById(menuItemID); + if (data) { + this.checkItem(item, data); + } + this.menu.menupopup.activateItem(item); + await hiddenPromise; + await new Promise(resolve => setTimeout(resolve)); + } +} + +/** + * 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" + ); +} + +// Report and remove any remaining accounts/servers. If we register a cleanup +// function here, it will run before any other cleanup function has had a +// chance to run. Instead, when it runs register another cleanup function +// which will run last. +registerCleanupFunction(function () { + registerCleanupFunction(function () { + Services.prefs.clearUserPref("mail.pane_config.dynamic"); + Services.xulStore.removeValue( + "chrome://messenger/content/messenger.xhtml", + "threadPane", + "view" + ); + + let tabmail = document.getElementById("tabmail"); + if (tabmail.tabInfo.length > 1) { + Assert.report( + true, + undefined, + undefined, + "Unexpected tab(s) open at the end of the test run" + ); + tabmail.closeOtherTabs(0); + } + + for (let server of MailServices.accounts.allServers) { + Assert.report( + true, + undefined, + undefined, + `Found ${server} at the end of the test run` + ); + MailServices.accounts.removeIncomingServer(server, false); + } + for (let account of MailServices.accounts.accounts) { + Assert.report( + true, + undefined, + undefined, + `Found ${account} at the end of the test run` + ); + MailServices.accounts.removeAccount(account, false); + } + + resetSmartMailboxes(); + ensure_cards_view(); + + // 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(); + }); +}); |