summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/unifiedtoolbar/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mail/components/unifiedtoolbar/test
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mail/components/unifiedtoolbar/test')
-rw-r--r--comm/mail/components/unifiedtoolbar/test/browser/browser.ini16
-rw-r--r--comm/mail/components/unifiedtoolbar/test/browser/browser_customizableItems.js173
-rw-r--r--comm/mail/components/unifiedtoolbar/test/browser/browser_searchBar.js263
-rw-r--r--comm/mail/components/unifiedtoolbar/test/browser/browser_toolbarMigration.js99
-rw-r--r--comm/mail/components/unifiedtoolbar/test/browser/browser_unifiedToolbarTab.js285
-rw-r--r--comm/mail/components/unifiedtoolbar/test/browser/files/searchBar.xhtml21
-rw-r--r--comm/mail/components/unifiedtoolbar/test/browser/files/unifiedToolbarTab.xhtml22
-rw-r--r--comm/mail/components/unifiedtoolbar/test/unit/test_buttonStyle.js40
-rw-r--r--comm/mail/components/unifiedtoolbar/test/unit/test_customizableItems.js123
-rw-r--r--comm/mail/components/unifiedtoolbar/test/unit/test_customizableItemsDetails.js103
-rw-r--r--comm/mail/components/unifiedtoolbar/test/unit/test_customizationState.js64
-rw-r--r--comm/mail/components/unifiedtoolbar/test/unit/test_toolbarMigration.js431
-rw-r--r--comm/mail/components/unifiedtoolbar/test/unit/xpcshell.ini8
13 files changed, 1648 insertions, 0 deletions
diff --git a/comm/mail/components/unifiedtoolbar/test/browser/browser.ini b/comm/mail/components/unifiedtoolbar/test/browser/browser.ini
new file mode 100644
index 0000000000..072a4571ef
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/test/browser/browser.ini
@@ -0,0 +1,16 @@
+[DEFAULT]
+prefs =
+ mail.provider.suppress_dialog_on_startup=true
+ mail.spellcheck.inline=false
+ mail.spotlight.firstRunDone=true
+ mail.winsearch.firstRunDone=true
+ mailnews.database.global.indexer.enabled=false
+ mailnews.start_page.override_url=about:blank
+ mailnews.start_page.url=about:blank
+subsuite = thunderbird
+support-files = files/**
+
+[browser_customizableItems.js]
+[browser_searchBar.js]
+[browser_toolbarMigration.js]
+[browser_unifiedToolbarTab.js]
diff --git a/comm/mail/components/unifiedtoolbar/test/browser/browser_customizableItems.js b/comm/mail/components/unifiedtoolbar/test/browser/browser_customizableItems.js
new file mode 100644
index 0000000000..152ade47f3
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/test/browser/browser_customizableItems.js
@@ -0,0 +1,173 @@
+/* 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";
+
+const {
+ getAvailableItemIdsForSpace,
+ getDefaultItemIdsForSpace,
+ registerExtension,
+ unregisterExtension,
+} = ChromeUtils.importESModule("resource:///modules/CustomizableItems.sys.mjs");
+
+add_task(async function test_extensionRegisterUnregisterDefault() {
+ const extensionId = "thunderbird-compact-light@mozilla.org";
+ await registerExtension(extensionId);
+
+ const itemId = `ext-${extensionId}`;
+ ok(
+ getAvailableItemIdsForSpace("mail").includes(itemId),
+ "Extension item available in mail space"
+ );
+ ok(
+ getDefaultItemIdsForSpace("mail").includes(itemId),
+ "Extension item in mail space by default"
+ );
+ ok(
+ !getAvailableItemIdsForSpace().includes(itemId),
+ "Extension item not available in all spaces"
+ );
+
+ unregisterExtension(extensionId);
+
+ ok(
+ !getAvailableItemIdsForSpace("mail").includes(itemId),
+ "Extension item no longer available in mail space"
+ );
+ ok(
+ !getDefaultItemIdsForSpace("mail").includes(itemId),
+ "Extension item not in mail space by default"
+ );
+});
+
+add_task(async function test_extensionRegisterAllSpaces() {
+ const extensionId = "thunderbird-compact-light@mozilla.org";
+ await registerExtension(extensionId, []);
+
+ const itemId = `ext-${extensionId}`;
+ ok(
+ getAvailableItemIdsForSpace().includes(itemId),
+ "Extension item available in all spaces"
+ );
+ ok(
+ getDefaultItemIdsForSpace("default").includes(itemId),
+ "Extension item in all spaces by default"
+ );
+ ok(
+ !getAvailableItemIdsForSpace("mail").includes(itemId),
+ "Extension item not available in mail space"
+ );
+ ok(
+ getDefaultItemIdsForSpace("mail").includes(itemId),
+ "Extension item in mail space by default"
+ );
+
+ unregisterExtension(extensionId);
+
+ ok(
+ !getAvailableItemIdsForSpace().includes(itemId),
+ "Extension item no longer available in all spaces"
+ );
+ ok(
+ !getDefaultItemIdsForSpace("default").includes(itemId),
+ "Extension item not in any space by default"
+ );
+});
+
+add_task(async function test_extensionRegisterMultipleSpaces() {
+ const extensionId = "thunderbird-compact-light@mozilla.org";
+ await registerExtension(extensionId, ["mail", "calendar", "default"]);
+
+ const itemId = `ext-${extensionId}`;
+ ok(
+ getAvailableItemIdsForSpace("calendar").includes(itemId),
+ "Extension item available in calendar space"
+ );
+ ok(
+ getDefaultItemIdsForSpace("calendar").includes(itemId),
+ "Extension item in calendar space by default"
+ );
+ ok(
+ getAvailableItemIdsForSpace("mail").includes(itemId),
+ "Extension item available in mail space"
+ );
+ ok(
+ getDefaultItemIdsForSpace("mail").includes(itemId),
+ "Extension item in mail space by default"
+ );
+ ok(
+ !getAvailableItemIdsForSpace().includes(itemId),
+ "Extension item not available in all spaces"
+ );
+ ok(
+ getAvailableItemIdsForSpace("default").includes(itemId),
+ "Extension item available in default space"
+ );
+ ok(
+ getDefaultItemIdsForSpace("default").includes(itemId),
+ "Extension item in default space"
+ );
+
+ unregisterExtension(extensionId);
+
+ ok(
+ !getAvailableItemIdsForSpace("mail").includes(itemId),
+ "Extension item no longer available in mail space"
+ );
+ ok(
+ !getDefaultItemIdsForSpace("mail").includes(itemId),
+ "Extension item not in mail space by default"
+ );
+ ok(
+ !getAvailableItemIdsForSpace("calendar").includes(itemId),
+ "Extension item no longer available in calendar space"
+ );
+ ok(
+ !getDefaultItemIdsForSpace("calendar").includes(itemId),
+ "Extension item not in calendar space by default"
+ );
+ ok(
+ !getAvailableItemIdsForSpace().includes(itemId),
+ "Extension item not available in all spaces"
+ );
+ ok(
+ !getAvailableItemIdsForSpace("default").includes(itemId),
+ "Extension item not available in default space"
+ );
+ ok(
+ !getDefaultItemIdsForSpace("default").includes(itemId),
+ "Extension item not in default space"
+ );
+});
+
+add_task(async function test_extensionRegisterStableOrder() {
+ const extension1Id = "thunderbird-compact-light@mozilla.org";
+ const extension2Id = "thunderbird-compact-dark@mozilla.org";
+ await registerExtension(extension1Id);
+ await registerExtension(extension2Id);
+
+ const defaultItems = getDefaultItemIdsForSpace("mail");
+
+ const firstExtensionId = defaultItems
+ .find(itemId => itemId.startsWith("ext-"))
+ .slice(4);
+
+ unregisterExtension(firstExtensionId);
+
+ ok(
+ !getDefaultItemIdsForSpace("mail").includes(`ext-${firstExtensionId}`),
+ "Extension that was the first in the default set not in default set"
+ );
+
+ await registerExtension(firstExtensionId);
+
+ Assert.deepEqual(
+ getDefaultItemIdsForSpace("mail"),
+ defaultItems,
+ "Default items order stable for extensions"
+ );
+
+ unregisterExtension(extension1Id);
+ unregisterExtension(extension2Id);
+});
diff --git a/comm/mail/components/unifiedtoolbar/test/browser/browser_searchBar.js b/comm/mail/components/unifiedtoolbar/test/browser/browser_searchBar.js
new file mode 100644
index 0000000000..b88c16f684
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/test/browser/browser_searchBar.js
@@ -0,0 +1,263 @@
+/* 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 tabmail = document.getElementById("tabmail");
+registerCleanupFunction(() => {
+ tabmail.closeOtherTabs(tabmail.tabInfo[0]);
+});
+let browser;
+let searchBar;
+
+const waitForRender = () => {
+ return new Promise(resolve => {
+ window.requestAnimationFrame(resolve);
+ });
+};
+
+/* These are shadow-root safe variants of the methods in BrowserTestUtils. */
+
+/**
+ * Checks if a DOM element is hidden.
+ *
+ * @param {Element} element
+ * The element which is to be checked.
+ *
+ * @return {boolean}
+ */
+function is_hidden(element) {
+ var style = element.ownerGlobal.getComputedStyle(element);
+ if (style.display == "none") {
+ return true;
+ }
+ if (style.visibility != "visible") {
+ return true;
+ }
+ if (style.display == "-moz-popup") {
+ return ["hiding", "closed"].includes(element.state);
+ }
+
+ // Hiding a parent element will hide all its children
+ if (element.parentNode != element.ownerDocument && element.parentElement) {
+ return is_hidden(element.parentElement);
+ }
+
+ return false;
+}
+
+/**
+ * Checks if a DOM element is visible.
+ *
+ * @param {Element} element
+ * The element which is to be checked.
+ *
+ * @return {boolean}
+ */
+function is_visible(element) {
+ var style = element.ownerGlobal.getComputedStyle(element);
+ if (style.display == "none") {
+ return false;
+ }
+ if (style.visibility != "visible") {
+ return false;
+ }
+ if (style.display == "-moz-popup" && element.state != "open") {
+ return false;
+ }
+
+ // Hiding a parent element will hide all its children
+ if (element.parentNode != element.ownerDocument && element.parentElement) {
+ return is_visible(element.parentElement);
+ }
+
+ return true;
+}
+
+add_setup(async function () {
+ let tab = tabmail.openTab("contentTab", {
+ url: "chrome://mochitests/content/browser/comm/mail/components/unifiedtoolbar/test/browser/files/searchBar.xhtml",
+ });
+
+ await BrowserTestUtils.browserLoaded(tab.browser);
+ tab.browser.focus();
+ browser = tab.browser;
+ searchBar = tab.browser.contentWindow.document.querySelector("search-bar");
+});
+
+add_task(async function test_initialState() {
+ const input = searchBar.shadowRoot.querySelector("input");
+ is(
+ input.getAttribute("aria-label"),
+ searchBar.getAttribute("label"),
+ "Label forwarded to aria-label on input"
+ );
+});
+
+add_task(async function test_labelUpdate() {
+ const input = searchBar.shadowRoot.querySelector("input");
+ searchBar.setAttribute("label", "foo");
+ await waitForRender();
+ is(
+ input.getAttribute("aria-label"),
+ "foo",
+ "Updated label applied to content"
+ );
+});
+
+add_task(async function test_focus() {
+ const input = searchBar.shadowRoot.querySelector("input");
+ searchBar.focus();
+ is(
+ searchBar.shadowRoot.activeElement,
+ input,
+ "Input is focused when search bar is focused"
+ );
+});
+
+add_task(async function test_autocompleteEvent() {
+ const typeAndWaitForAutocomplete = async key => {
+ const eventPromise = BrowserTestUtils.waitForEvent(
+ searchBar,
+ "autocomplete"
+ );
+ await BrowserTestUtils.synthesizeKey(key, {}, browser);
+ return eventPromise;
+ };
+ searchBar.focus();
+ let event = await typeAndWaitForAutocomplete("T");
+ is(event.detail, "T", "Autocomplete for T");
+
+ event = await typeAndWaitForAutocomplete("e");
+ is(event.detail, "Te", "Autocomplete for e");
+
+ event = await typeAndWaitForAutocomplete("KEY_Backspace");
+ is(event.detail, "T", "Autocomplete for backspace");
+
+ await BrowserTestUtils.synthesizeKey("KEY_Backspace", {}, browser);
+});
+
+add_task(async function test_searchEventFromEnter() {
+ const input = searchBar.shadowRoot.querySelector("input");
+ input.value = "Lorem ipsum";
+ searchBar.focus();
+
+ const eventPromise = BrowserTestUtils.waitForEvent(searchBar, "search");
+ await BrowserTestUtils.synthesizeKey("KEY_Enter", {}, browser);
+ const event = await eventPromise;
+
+ is(event.detail, "Lorem ipsum", "Event contains search query");
+ await waitForRender();
+ is(input.value, "", "Input was cleared");
+});
+
+add_task(async function test_searchEventFromButton() {
+ const input = searchBar.shadowRoot.querySelector("input");
+ input.value = "Lorem ipsum";
+
+ const eventPromise = BrowserTestUtils.waitForEvent(searchBar, "search");
+ searchBar.shadowRoot.querySelector("button").click();
+ const event = await eventPromise;
+
+ is(event.detail, "Lorem ipsum", "Event contains search query");
+ await waitForRender();
+ is(input.value, "", "Input was cleared");
+});
+
+add_task(async function test_searchEventPreventDefault() {
+ const input = searchBar.shadowRoot.querySelector("input");
+ input.value = "Lorem ipsum";
+
+ searchBar.addEventListener(
+ "search",
+ event => {
+ event.preventDefault();
+ },
+ {
+ once: true,
+ passive: false,
+ }
+ );
+
+ const eventPromise = BrowserTestUtils.waitForEvent(searchBar, "search");
+ searchBar.shadowRoot.querySelector("button").click();
+ await eventPromise;
+ await waitForRender();
+
+ is(input.value, "Lorem ipsum");
+
+ input.value = "";
+});
+
+add_task(async function test_placeholderVisibility() {
+ const placeholder = searchBar.shadowRoot.querySelector("div");
+ const input = searchBar.shadowRoot.querySelector("input");
+
+ input.value = "";
+ await waitForRender();
+ ok(is_visible(placeholder), "Placeholder is visible initially");
+
+ input.value = "some input";
+ await waitForRender();
+ ok(is_hidden(placeholder), "Placeholder is hidden after text is entered");
+
+ input.value = "";
+ await waitForRender();
+ ok(
+ is_visible(placeholder),
+ "Placeholder is visible again after input is cleared"
+ );
+});
+
+add_task(async function test_placeholderFallbackToLabel() {
+ const placeholder = searchBar.querySelector("span");
+ placeholder.remove();
+
+ const shadowedPlaceholder = searchBar.shadowRoot.querySelector("div");
+ const label = searchBar.getAttribute("label");
+
+ is(
+ shadowedPlaceholder.textContent,
+ label,
+ "Falls back to label if no placeholder slot contents provided"
+ );
+
+ searchBar.setAttribute("label", "Foo bar");
+ is(
+ shadowedPlaceholder.textContent,
+ "Foo bar",
+ "Placeholder contents get updated with label attribute"
+ );
+
+ searchBar.prepend(placeholder);
+ searchBar.setAttribute("label", label);
+});
+
+add_task(async function test_reset() {
+ const input = searchBar.shadowRoot.querySelector("input");
+ const placeholder = searchBar.shadowRoot.querySelector("div");
+ input.value = "Lorem ipsum";
+
+ searchBar.reset();
+
+ is(input.value, "", "Input empty after reset");
+ await waitForRender();
+ ok(is_visible(placeholder), "Placeholder visible");
+});
+
+add_task(async function test_disabled() {
+ const input = searchBar.shadowRoot.querySelector("input");
+ const button = searchBar.shadowRoot.querySelector("button");
+
+ ok(!input.disabled, "Input enabled");
+ ok(!button.disabled, "Button enabled");
+
+ searchBar.setAttribute("disabled", true);
+
+ ok(input.disabled, "Disabled propagated to input");
+ ok(button.disabled, "Disabled propagated to button");
+
+ searchBar.removeAttribute("disabled");
+
+ ok(!input.disabled, "Input enabled again");
+ ok(!button.disabled, "Button enabled again");
+});
diff --git a/comm/mail/components/unifiedtoolbar/test/browser/browser_toolbarMigration.js b/comm/mail/components/unifiedtoolbar/test/browser/browser_toolbarMigration.js
new file mode 100644
index 0000000000..c2ca1147fd
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/test/browser/browser_toolbarMigration.js
@@ -0,0 +1,99 @@
+/* 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";
+
+const { migrateToolbarForSpace } = ChromeUtils.importESModule(
+ "resource:///modules/ToolbarMigration.sys.mjs"
+);
+const { getState, storeState } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizationState.mjs"
+);
+const { EXTENSION_PREFIX } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizableItems.sys.mjs"
+);
+const { getCachedAllowedSpaces, setCachedAllowedSpaces } = ChromeUtils.import(
+ "resource:///modules/ExtensionToolbarButtons.jsm"
+);
+
+const MESSENGER_WINDOW = "chrome://messenger/content/messenger.xhtml";
+const EXTENSION_ID = "thunderbird-compact-light@mozilla.org";
+
+add_setup(() => {
+ storeState({});
+});
+
+add_task(async function test_migrate_extension() {
+ Services.xulStore.setValue(MESSENGER_WINDOW, "mail-bar3", "currentset", "");
+ Services.xulStore.setValue(
+ MESSENGER_WINDOW,
+ "mail-bar3",
+ "defaultset",
+ "button-getmsg,button-newmsg,separator,button-tag,qfb-show-filter-bar,spring,gloda-search,thunderbird-compact-light_mozilla_org-browserAction-toolbarbutton,button-appmenu"
+ );
+ Services.xulStore.setValue(
+ MESSENGER_WINDOW,
+ "mail-bar3",
+ "extensionset",
+ "thunderbird-compact-light_mozilla_org-browserAction-toolbarbutton"
+ );
+ const extensionPref = Services.prefs.getStringPref(
+ "extensions.webextensions.uuids",
+ ""
+ );
+ const parsedPref = JSON.parse(extensionPref || "{}");
+ if (!parsedPref.hasOwnProperty(EXTENSION_ID)) {
+ parsedPref[EXTENSION_ID] = "foo";
+ Services.prefs.setStringPref(
+ "extensions.webextensions.uuids",
+ JSON.stringify(parsedPref)
+ );
+ }
+
+ migrateToolbarForSpace("mail");
+
+ const newState = getState();
+
+ Assert.deepEqual(
+ newState.mail,
+ [
+ "get-messages",
+ "write-message",
+ "tag-message",
+ "quick-filter-bar",
+ "spacer",
+ `${EXTENSION_PREFIX}${EXTENSION_ID}`,
+ "spacer",
+ ],
+ "Extension button was converted to new ID format"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(MESSENGER_WINDOW, "mail-bar3", "currentset"),
+ "Old toolbar state is cleared"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(MESSENGER_WINDOW, "mail-bar3", "defaultset"),
+ "Old toolbar default state is cleared"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(MESSENGER_WINDOW, "mail-bar3", "extensionset"),
+ "Old toolbar extension state is cleared"
+ );
+ Assert.deepEqual(
+ Object.fromEntries(getCachedAllowedSpaces()),
+ { [EXTENSION_ID]: ["mail"] },
+ "Extension set migrated to new persistent extension state"
+ );
+
+ storeState({});
+ setCachedAllowedSpaces(new Map());
+ if (extensionPref) {
+ Services.prefs.setStringPref(
+ "extensions.webextensions.uuids",
+ extensionPref
+ );
+ } else {
+ Services.prefs.clearUserPref("extensions.webextensions.uuids");
+ }
+});
diff --git a/comm/mail/components/unifiedtoolbar/test/browser/browser_unifiedToolbarTab.js b/comm/mail/components/unifiedtoolbar/test/browser/browser_unifiedToolbarTab.js
new file mode 100644
index 0000000000..336199ee51
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/test/browser/browser_unifiedToolbarTab.js
@@ -0,0 +1,285 @@
+/* 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 tabmail = document.getElementById("tabmail");
+registerCleanupFunction(() => {
+ tabmail.closeOtherTabs(tabmail.tabInfo[0]);
+});
+let browser;
+let testDocument;
+
+const waitForRender = () => {
+ return new Promise(resolve => {
+ window.requestAnimationFrame(resolve);
+ });
+};
+const getTabButton = tab => tab.shadowRoot.querySelector("button");
+/**
+ * Get the relevant elements for the tab at the given index.
+ *
+ * @param {number} tabIndex
+ * @returns {{tab: UnifiedToolbarTab, button: HTMLButtonElement, pane: HTMLElement}}
+ */
+const getTabElements = tabIndex => {
+ const tab = testDocument.querySelector(
+ `unified-toolbar-tab:nth-child(${tabIndex})`
+ );
+ const button = getTabButton(tab);
+ const pane = tab.pane;
+ return { tab, button, pane };
+};
+
+add_setup(async function () {
+ let tab = tabmail.openTab("contentTab", {
+ url: "chrome://mochitests/content/browser/comm/mail/components/unifiedtoolbar/test/browser/files/unifiedToolbarTab.xhtml",
+ });
+
+ await BrowserTestUtils.browserLoaded(tab.browser);
+ tab.browser.focus();
+ browser = tab.browser;
+ testDocument = tab.browser.contentWindow.document;
+});
+
+add_task(function test_tabElementInitialization() {
+ const activeTab = testDocument.querySelector("unified-toolbar-tab[selected]");
+ is(
+ activeTab.getAttribute("role"),
+ "presentation",
+ "The custom element is just for show"
+ );
+ ok(
+ !activeTab.hasAttribute("aria-controls"),
+ "aria-controls removed from custom element"
+ );
+ ok(activeTab.hasAttribute("selected"), "Active tab kept itself selected");
+ const tabButton = getTabButton(activeTab);
+ is(tabButton.getAttribute("role"), "tab", "Active tab is marked as tab");
+ is(tabButton.tabIndex, 0, "Active tab is in the focus ring");
+ is(
+ tabButton.getAttribute("aria-selected"),
+ "true",
+ "Tab is marked as selected"
+ );
+ ok(
+ tabButton.hasAttribute("aria-controls"),
+ "aria-controls got given to button"
+ );
+
+ const otherTab = testDocument.querySelector(
+ "unified-toolbar-tab:not([selected])"
+ );
+ is(
+ otherTab.getAttribute("role"),
+ "presentation",
+ "The custom element is just for show on the other tab"
+ );
+ ok(
+ !otherTab.hasAttribute("aria-controls"),
+ "aria-controls removed from the other tab"
+ );
+ ok(!otherTab.hasAttribute("selected"), "Other tab didn't select itself");
+ const otherButton = getTabButton(otherTab);
+ is(otherButton.getAttribute("role"), "tab", "Other tab is marked as tab");
+ is(otherButton.tabIndex, -1, "Other tab is not in the focus ring");
+ ok(
+ !otherButton.hasAttribute("aria-selected"),
+ "Other tab isn't marked as selected"
+ );
+ ok(
+ otherButton.hasAttribute("aria-controls"),
+ "aria-controls got given to other button"
+ );
+});
+
+add_task(async function test_paneGetter() {
+ const tab1 = getTabElements(1);
+ const tabPane = testDocument.getElementById("tabPane");
+ const tab2 = getTabElements(2);
+ const otherTabPane = testDocument.getElementById("otherTabPane");
+
+ is(
+ tab1.button.getAttribute("aria-controls"),
+ tabPane.id,
+ "Tab 1 controls tab 1 pane"
+ );
+ is(
+ tab2.button.getAttribute("aria-controls"),
+ otherTabPane.id,
+ "Tab 2 controls tab 2 pane"
+ );
+
+ Assert.strictEqual(
+ tab1.tab.pane,
+ tabPane,
+ "Tab 1 pane getter returns #tabPane"
+ );
+ Assert.strictEqual(
+ tab2.tab.pane,
+ otherTabPane,
+ "Tab 2 pane getter returns #otherTabPane"
+ );
+});
+
+add_task(async function test_unselect() {
+ const tab = getTabElements(1);
+
+ tab.tab.unselect();
+
+ ok(!tab.button.hasAttribute("aria-selected"), "Tab not marked as selected");
+ is(tab.button.tabIndex, -1, "Tab not in focus ring");
+ ok(!tab.tab.hasAttribute("selected"), "Tab not marked selected");
+ ok(tab.pane.hidden, "Tab pane hidden");
+});
+
+add_task(async function test_select() {
+ const tab1 = getTabElements(1);
+ const tab2 = getTabElements(2);
+
+ let tabswitchPromise = BrowserTestUtils.waitForEvent(
+ testDocument.body,
+ "tabswitch"
+ );
+ tab1.tab.select();
+
+ await tabswitchPromise;
+ ok(tab1.tab.hasAttribute("selected"), "Tab 1 selected");
+ is(
+ tab1.button.getAttribute("aria-selected"),
+ "true",
+ "Tab 1 marked as selected"
+ );
+ is(tab1.button.tabIndex, 0, "Tab 1 keyboard selectable");
+ ok(!tab1.pane.hidden, "Tab pane for tab 1 visible");
+
+ tabswitchPromise = BrowserTestUtils.waitForEvent(tab2.tab, "tabswitch");
+ tab2.tab.select();
+
+ await tabswitchPromise;
+ ok(tab2.tab.hasAttribute("selected"), "Tab 2 selected");
+ is(
+ tab2.button.getAttribute("aria-selected"),
+ "true",
+ "Tab 2 has a11y selection"
+ );
+ is(tab2.button.tabIndex, 0, "Tab 2 keyboard selectable");
+ ok(!tab2.pane.hidden, "Tab pane for tab 2 visible");
+
+ ok(!tab1.tab.hasAttribute("selected"), "Tab 1 unselected");
+ ok(!tab1.button.hasAttribute("aria-selected"), "Tab 1 marked as unselected");
+ is(tab1.button.tabIndex, -1, "Tab 1 not in focus ring");
+ ok(tab1.pane.hidden, "Tab pane for tab 1 hidden");
+});
+
+add_task(async function test_switchingTabWithMouse() {
+ const tab1 = getTabElements(1);
+ const tab2 = getTabElements(2);
+
+ tab2.button.click();
+ ok(tab2.tab.hasAttribute("selected"), "Other tab is selected");
+ is(tab2.button.tabIndex, 0, "Other tab is in focus ring");
+ ok(!tab1.tab.hasAttribute("selected"), "First tab is not selected");
+ is(tab1.button.tabIndex, -1, "First tab is not in focus ring");
+ ok(
+ BrowserTestUtils.is_visible(tab2.pane),
+ "Tab pane for selected tab is visible"
+ );
+ ok(BrowserTestUtils.is_hidden(tab1.pane), "Tab pane for first tab is hidden");
+
+ tab1.button.click();
+ ok(tab1.tab.hasAttribute("selected"), "First tab is selected");
+ is(tab1.button.tabIndex, 0, "First tab is in focus ring");
+ ok(!tab2.tab.hasAttribute("selected"), "Other tab is not selected");
+ is(tab2.button.tabIndex, -1, "Other tab is not in focus ring");
+ ok(
+ BrowserTestUtils.is_visible(tab1.pane),
+ "Tab pane for first tab is visible"
+ );
+ ok(BrowserTestUtils.is_hidden(tab2.pane), "Tab pane for other tab is hidden");
+});
+
+add_task(async function test_switchingTabWithKeyboard() {
+ const tab1 = getTabElements(1);
+ const tab2 = getTabElements(2);
+
+ tab1.tab.focus();
+ is(testDocument.activeElement, tab1.tab, "Initially first tab is active");
+
+ await BrowserTestUtils.synthesizeKey("KEY_ArrowRight", {}, browser);
+ is(testDocument.activeElement, tab2.tab, "Second tab is focused");
+ is(
+ tab2.tab.shadowRoot.activeElement,
+ tab2.button,
+ "Button within tab is focused"
+ );
+ await BrowserTestUtils.synthesizeKey(" ", {}, browser);
+ ok(tab2.tab.hasAttribute("selected"), "Other tab is selected");
+ is(tab2.button.tabIndex, 0, "Other tab is in focus ring");
+ ok(!tab1.tab.hasAttribute("selected"), "First tab is not selected");
+ is(tab1.button.tabIndex, -1, "First tab is not in focus ring");
+ ok(
+ BrowserTestUtils.is_visible(tab2.pane),
+ "Tab pane for selected tab is visible"
+ );
+ ok(BrowserTestUtils.is_hidden(tab1.pane), "Tab pane for first tab is hidden");
+
+ await BrowserTestUtils.synthesizeKey("KEY_ArrowLeft", {}, browser);
+ is(testDocument.activeElement, tab1.tab, "Previous tab is selected");
+ await BrowserTestUtils.synthesizeKey("KEY_End", {}, browser);
+ is(testDocument.activeElement, tab2.tab, "Last tab is selected");
+ await BrowserTestUtils.synthesizeKey("KEY_Home", {}, browser);
+ is(testDocument.activeElement, tab1.tab, "First tab is selected");
+ await BrowserTestUtils.synthesizeKey("KEY_Enter", {}, browser);
+ ok(tab1.tab.hasAttribute("selected"), "First tab is selected");
+ is(tab1.button.tabIndex, 0, "First tab is in focus ring");
+ ok(!tab2.tab.hasAttribute("selected"), "Other tab is not selected");
+ is(tab2.button.tabIndex, -1, "Other tab is not in focus ring");
+ ok(
+ BrowserTestUtils.is_visible(tab1.pane),
+ "Tab pane for first tab is visible"
+ );
+ ok(BrowserTestUtils.is_hidden(tab2.pane), "Tab pane for other tab is hidden");
+});
+
+add_task(async function test_switchingTabWithKeyboardRTL() {
+ testDocument.dir = "rtl";
+ await waitForRender();
+ const tab1 = getTabElements(1);
+ const tab2 = getTabElements(2);
+
+ tab1.tab.focus();
+ is(testDocument.activeElement, tab1.tab, "Initially first tab is active");
+
+ await BrowserTestUtils.synthesizeKey("KEY_ArrowLeft", {}, browser);
+ is(testDocument.activeElement, tab2.tab, "Second tab is selected");
+ is(
+ tab2.tab.shadowRoot.activeElement,
+ tab2.button,
+ "Button within tab is focused"
+ );
+ await BrowserTestUtils.synthesizeKey(" ", {}, browser);
+ ok(tab2.tab.hasAttribute("selected"), "Other tab is selected");
+ is(tab2.button.tabIndex, 0, "Other tab is in focus ring");
+ ok(!tab1.tab.hasAttribute("selected"), "First tab is not selected");
+ is(tab1.button.tabIndex, -1, "First tab is not in focus ring");
+ ok(
+ BrowserTestUtils.is_visible(tab2.pane),
+ "Tab pane for selected tab is visible"
+ );
+ ok(BrowserTestUtils.is_hidden(tab1.pane), "Tab pane for first tab is hidden");
+
+ await BrowserTestUtils.synthesizeKey("KEY_ArrowRight", {}, browser);
+ is(testDocument.activeElement, tab1.tab, "Previous tab is selected");
+ await BrowserTestUtils.synthesizeKey("KEY_Enter", {}, browser);
+ ok(tab1.tab.hasAttribute("selected"), "First tab is selected");
+ is(tab1.button.tabIndex, 0, "First tab is in focus ring");
+ ok(!tab2.tab.hasAttribute("selected"), "Other tab is not selected");
+ is(tab2.button.tabIndex, -1, "Other tab is not in focus ring");
+ ok(
+ BrowserTestUtils.is_visible(tab1.pane),
+ "Tab pane for first tab is visible"
+ );
+ ok(BrowserTestUtils.is_hidden(tab2.pane), "Tab pane for other tab is hidden");
+
+ testDocument.dir = "ltr";
+});
diff --git a/comm/mail/components/unifiedtoolbar/test/browser/files/searchBar.xhtml b/comm/mail/components/unifiedtoolbar/test/browser/files/searchBar.xhtml
new file mode 100644
index 0000000000..33000135b4
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/test/browser/files/searchBar.xhtml
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf-8" />
+ <title>Search bar element test</title>
+ <script type="module" src="chrome://messenger/content/unifiedtoolbar/search-bar.mjs"></script>
+ </head>
+ <body>
+ <template id="searchBarTemplate">
+ <form>
+ <input type="search" placeholder="" required="required"/>
+ <div aria-hidden="true"><slot name="placeholder"></slot></div>
+ <button class="button button-flat icon-button"><slot name="button"></slot></button>
+ </form>
+ </template>
+ <search-bar label="Search">
+ <span slot="placeholder">Placeholder</span>
+ <img slot="button" src="chrome://messenger/skin/icons/new/compact/search.svg" alt="Search"/>
+ </search-bar>
+ </body>
+</html>
diff --git a/comm/mail/components/unifiedtoolbar/test/browser/files/unifiedToolbarTab.xhtml b/comm/mail/components/unifiedtoolbar/test/browser/files/unifiedToolbarTab.xhtml
new file mode 100644
index 0000000000..f30f2b7d8b
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/test/browser/files/unifiedToolbarTab.xhtml
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr">
+ <head>
+ <meta charset="utf-8" />
+ <title>Search bar element test</title>
+ <script type="module" src="chrome://messenger/content/unifiedtoolbar/unified-toolbar-tab.mjs"></script>
+ </head>
+ <body>
+ <template id="unifiedToolbarTabTemplate">
+ <button role="tab">
+ <img alt="" src="" />
+ <slot></slot>
+ </button>
+ </template>
+ <div role="tablist">
+ <unified-toolbar-tab selected="true" aria-controls="tabPane">Tab Title</unified-toolbar-tab>
+ <unified-toolbar-tab aria-controls="otherTabPane">Other Tab</unified-toolbar-tab>
+ </div>
+ <div id="tabPane" role="tabpanel">Panel 1</div>
+ <div id="otherTabPane" role="tabpanel" hidden="hidden">Panel 2</div>
+ </body>
+</html>
diff --git a/comm/mail/components/unifiedtoolbar/test/unit/test_buttonStyle.js b/comm/mail/components/unifiedtoolbar/test/unit/test_buttonStyle.js
new file mode 100644
index 0000000000..0ffeefa00b
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/test/unit/test_buttonStyle.js
@@ -0,0 +1,40 @@
+/* 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 { BUTTON_STYLE_MAP, BUTTON_STYLE_PREF } = ChromeUtils.importESModule(
+ "resource:///modules/ButtonStyle.mjs"
+);
+
+add_task(function test_buttonStyleMap() {
+ Assert.ok(Array.isArray(BUTTON_STYLE_MAP), "BUTTON_STYLE_MAP is an array");
+ Assert.ok(
+ BUTTON_STYLE_MAP.every(style => typeof style === "string"),
+ "All entries in the style map should be strings"
+ );
+ for (const style of BUTTON_STYLE_MAP) {
+ Assert.stringMatches(
+ style,
+ /[a-z-]/,
+ "Button style class should be formatted in kebab case"
+ );
+ }
+});
+
+add_task(function test_buttonStylePref() {
+ Assert.equal(
+ typeof BUTTON_STYLE_PREF,
+ "string",
+ "BUTTON_STYLE_PREF is a string"
+ );
+ const prefValue = Services.prefs.getIntPref(BUTTON_STYLE_PREF, 0);
+ Assert.ok(
+ Number.isInteger(prefValue),
+ "BUTTON_STYLE_PREF pref should hold an integer"
+ );
+ Assert.less(
+ prefValue,
+ BUTTON_STYLE_MAP.length,
+ "Value of BUTTON_STYLE_PREF should be within map"
+ );
+});
diff --git a/comm/mail/components/unifiedtoolbar/test/unit/test_customizableItems.js b/comm/mail/components/unifiedtoolbar/test/unit/test_customizableItems.js
new file mode 100644
index 0000000000..55d4f5ba91
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/test/unit/test_customizableItems.js
@@ -0,0 +1,123 @@
+/* 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 {
+ getAvailableItemIdsForSpace,
+ getDefaultItemIdsForSpace,
+ MULTIPLE_ALLOWED_ITEM_IDS,
+ SKIP_FOCUS_ITEM_IDS,
+} = ChromeUtils.importESModule("resource:///modules/CustomizableItems.sys.mjs");
+
+const { default: CUSTOMIZABLE_ITEMS } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizableItemsDetails.mjs"
+);
+
+add_task(function test_getAvailableItemIdsForSpace_anySpace() {
+ const itemsForAnySpace = getAvailableItemIdsForSpace();
+ Assert.ok(Array.isArray(itemsForAnySpace), "returns an array");
+ for (const itemId of itemsForAnySpace) {
+ Assert.equal(typeof itemId, "string", `item ID "${itemId}" is string`);
+ Assert.greater(itemId.length, 0, `item ID is not empty`);
+ }
+});
+
+add_task(function test_getAvailableItemIdsForSpace_emptySpace() {
+ const itemsForEmptySpace = getAvailableItemIdsForSpace("test");
+ Assert.deepEqual(itemsForEmptySpace, [], "Empty array for empty space");
+});
+
+add_task(function test_getAvailableItemIdsForSpace_includingAgnostic() {
+ const items = getAvailableItemIdsForSpace("mail", true);
+ const itemsForAnySpace = getAvailableItemIdsForSpace();
+ const itemsForMailSpace = getAvailableItemIdsForSpace("mail");
+
+ Assert.ok(
+ itemsForAnySpace.every(itemId => items.includes(itemId)),
+ "All space agnostic items are included"
+ );
+
+ Assert.ok(
+ itemsForMailSpace.every(itemId => items.includes(itemId)),
+ "All mail space items are included"
+ );
+});
+
+add_task(function test_getDefaultItemIdsForSpace_default() {
+ const items = getDefaultItemIdsForSpace("default");
+
+ Assert.ok(Array.isArray(items), "Should return an array");
+ Assert.deepEqual(
+ items,
+ ["spacer", "search-bar", "spacer"],
+ "Default space should contain the default item set"
+ );
+});
+
+add_task(function test_getDefaultItemIdsForSpace_cloningArray() {
+ const items1 = getDefaultItemIdsForSpace("default");
+ const items2 = getDefaultItemIdsForSpace("default");
+ const items3 = getDefaultItemIdsForSpace("mail");
+
+ Assert.notStrictEqual(
+ items1,
+ items2,
+ "The default sets should be different array instances"
+ );
+ Assert.notStrictEqual(
+ items2,
+ items3,
+ "The second default set an mail space should be different array instances"
+ );
+ Assert.notStrictEqual(
+ items3,
+ items1,
+ "The mail space and first default set should be different array instances"
+ );
+
+ Assert.deepEqual(
+ items1,
+ items2,
+ "The two default pseudospace sets should contain the same items"
+ );
+});
+
+add_task(function test_multipleAllowedItemIds() {
+ Assert.equal(
+ typeof MULTIPLE_ALLOWED_ITEM_IDS.has,
+ "function",
+ "Multiple allowed item IDs should be set-like"
+ );
+ Assert.ok(
+ Array.from(MULTIPLE_ALLOWED_ITEM_IDS).every(
+ itemId => typeof itemId === "string"
+ ),
+ "Every item in the set should be a string"
+ );
+ for (const item of CUSTOMIZABLE_ITEMS) {
+ Assert.equal(
+ MULTIPLE_ALLOWED_ITEM_IDS.has(item.id),
+ Boolean(item.allowMultiple),
+ `Set's state should matche the allowMultiple value of ${item.allowMultiple} for ${item.id}`
+ );
+ }
+});
+
+add_task(function test_skipFocusItemIds() {
+ Assert.equal(
+ typeof SKIP_FOCUS_ITEM_IDS.has,
+ "function",
+ "Skip focus item IDs should be set-like"
+ );
+ Assert.ok(
+ Array.from(SKIP_FOCUS_ITEM_IDS).every(itemId => typeof itemId === "string"),
+ "Every item in the set should be a string"
+ );
+ for (const item of CUSTOMIZABLE_ITEMS) {
+ Assert.equal(
+ SKIP_FOCUS_ITEM_IDS.has(item.id),
+ Boolean(item.skipFocus),
+ `Set's state should match the skipFocus value of ${item.skipFocus} for ${item.id}`
+ );
+ }
+});
diff --git a/comm/mail/components/unifiedtoolbar/test/unit/test_customizableItemsDetails.js b/comm/mail/components/unifiedtoolbar/test/unit/test_customizableItemsDetails.js
new file mode 100644
index 0000000000..474e5483ce
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/test/unit/test_customizableItemsDetails.js
@@ -0,0 +1,103 @@
+/* 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 { default: CUSTOMIZABLE_ITEMS } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizableItemsDetails.mjs"
+);
+
+add_task(function test_format() {
+ for (const item of CUSTOMIZABLE_ITEMS) {
+ Assert.equal(typeof item, "object", "Customizable item is an object");
+ Assert.equal(typeof item.id, "string", `id "${item.id}" is a string`);
+ Assert.ok(!item.id.includes(","), `id "${item.id}" may not contain commas`);
+ Assert.greater(item.id.length, 0, `id "${item.id}" is not empty`);
+ Assert.equal(
+ typeof item.labelId,
+ "string",
+ `labelId is a string for ${item.id}`
+ );
+ Assert.greater(
+ item.labelId.length,
+ 0,
+ `labelId is not empty for ${item.id}`
+ );
+ Assert.ok(
+ !item.allowMultiple || item.allowMultiple === true,
+ `allowMultiple is falsy or boolean for ${item.id}`
+ );
+ Assert.ok(
+ item.spaces === undefined || Array.isArray(item.spaces),
+ `spaces is undefined or an array for ${item.id}`
+ );
+ if (item.spaces) {
+ for (const space of item.spaces) {
+ Assert.equal(
+ typeof space,
+ "string",
+ `space "${space}" expected to be string for ${item.id}`
+ );
+ Assert.greater(
+ space.length,
+ 0,
+ `space is not empty in ${item.id} spaces`
+ );
+ }
+ }
+ Assert.ok(
+ item.templateId === undefined || typeof item.templateId === "string",
+ `templateId must be undefined or a string for ${item.id}`
+ );
+ if (item.templateId !== undefined) {
+ Assert.greater(
+ item.templateId.length,
+ 0,
+ `templateId is not empty for ${item.id}`
+ );
+ Assert.ok(
+ item.requiredModules === undefined ||
+ Array.isArray(item.requiredModules),
+ `requiredModules is undefined or an array for ${item.id}`
+ );
+ if (item.requiredModules) {
+ for (const module of item.requiredModules) {
+ Assert.equal(
+ typeof module,
+ "string",
+ `module "${module}" expected to be string for ${item.id}`
+ );
+ Assert.greater(
+ module.length,
+ 0,
+ `module is not empty in ${item.id} requiredModules`
+ );
+ }
+ }
+ } else {
+ Assert.strictEqual(
+ item.requiredModules,
+ undefined,
+ `requiredModules must not be set because there is no template for item ${item.id}`
+ );
+ }
+ Assert.ok(
+ item.hasContextMenu === undefined ||
+ typeof item.hasContextMenu === "boolean",
+ `hasContextMenu must be undefined or a boolean for ${item.id}`
+ );
+ Assert.ok(
+ item.skipFocus === undefined || typeof item.skipFocus === "boolean",
+ `skipFocus must be undefined or a boolean for ${item.id}`
+ );
+ }
+});
+
+add_task(function test_idsUnique() {
+ const allIds = CUSTOMIZABLE_ITEMS.map(item => item.id);
+ const idCounts = allIds.reduce((counts, id) => {
+ counts[id] = counts[id] ? counts[id] + 1 : 1;
+ return counts;
+ }, {});
+ const duplicateIds = Object.keys(idCounts).filter(id => idCounts[id] > 1);
+ Assert.deepEqual(duplicateIds, [], "All IDs should only be used once");
+});
diff --git a/comm/mail/components/unifiedtoolbar/test/unit/test_customizationState.js b/comm/mail/components/unifiedtoolbar/test/unit/test_customizationState.js
new file mode 100644
index 0000000000..048b5c5cde
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/test/unit/test_customizationState.js
@@ -0,0 +1,64 @@
+/* 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 { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+const { storeState, getState } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizationState.mjs"
+);
+
+add_setup(function () {
+ // Ensure xulStore has a profile to refer to.
+ do_get_profile();
+});
+
+add_task(function test_getState_empty() {
+ const state = getState();
+ Assert.equal(typeof state, "object", "State should be an object");
+ Assert.deepEqual(state, {}, "Empty state should be an empty object");
+});
+
+add_task(async function test_storeState_observer() {
+ const stateChangeObserved = TestUtils.topicObserved(
+ "unified-toolbar-state-change"
+ );
+ storeState({
+ mail: ["write-message", "spacer", "search-bar", "spacer"],
+ });
+ await stateChangeObserved;
+});
+
+add_task(function test_storeState_getState() {
+ const state = {
+ mail: ["write-message", "spacer", "search-bar", "spacer"],
+ calendar: [],
+ };
+ const previousState = getState();
+ Assert.notDeepEqual(
+ previousState,
+ state,
+ "Current state should be different from the state to write"
+ );
+ storeState(state);
+ const newState = getState();
+ Assert.deepEqual(
+ newState,
+ state,
+ "State loaded should matche the stored state"
+ );
+ Assert.notStrictEqual(
+ newState,
+ state,
+ "State loaded should not be the same object as what was saved"
+ );
+});
+
+registerCleanupFunction(() => {
+ Services.xulStore.removeValue(
+ "chrome://messenger/content/messenger.xhtml",
+ "unifiedToolbar",
+ "state"
+ );
+});
diff --git a/comm/mail/components/unifiedtoolbar/test/unit/test_toolbarMigration.js b/comm/mail/components/unifiedtoolbar/test/unit/test_toolbarMigration.js
new file mode 100644
index 0000000000..637d40e066
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/test/unit/test_toolbarMigration.js
@@ -0,0 +1,431 @@
+/* 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";
+
+const { migrateToolbarForSpace, clearXULToolbarState } =
+ ChromeUtils.importESModule("resource:///modules/ToolbarMigration.sys.mjs");
+const { getState, storeState } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizationState.mjs"
+);
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const MESSENGER_WINDOW = "chrome://messenger/content/messenger.xhtml";
+
+function setXULToolbarState(
+ currentSet = "",
+ defaultSet = "",
+ toolbarId = "mail-bar3"
+) {
+ Services.xulStore.setValue(
+ MESSENGER_WINDOW,
+ toolbarId,
+ "currentset",
+ currentSet
+ );
+ Services.xulStore.setValue(
+ MESSENGER_WINDOW,
+ toolbarId,
+ "defaultset",
+ defaultSet
+ );
+}
+
+add_setup(() => {
+ do_get_profile();
+ storeState({});
+});
+
+add_task(function test_migration_customized() {
+ setXULToolbarState(
+ "button-getmsg,button-newmsg,button-reply,spacer,qfb-show-filter-bar,button-file,folder-location-container,spring,gloda-search,button-appmenu"
+ );
+ setXULToolbarState(
+ "menubar-items,spring,button-addons",
+ "",
+ "toolbar-menubar"
+ );
+ setXULToolbarState("button-delete", "", "tabbar-toolbar");
+
+ migrateToolbarForSpace("mail");
+
+ const newState = getState();
+
+ Assert.deepEqual(
+ newState.mail,
+ [
+ "get-messages",
+ "write-message",
+ "reply",
+ "spacer",
+ "quick-filter-bar",
+ "move-to",
+ "folder-location",
+ "spacer",
+ "search-bar",
+ "spacer",
+ "add-ons-and-themes",
+ "delete",
+ ],
+ "Items were combined and migrated"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(MESSENGER_WINDOW, "mail-bar3", "currentset"),
+ "Old toolbar state is cleared"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(MESSENGER_WINDOW, "mail-bar3", "defaultset"),
+ "Old toolbar default state is cleared"
+ );
+
+ storeState({});
+});
+
+add_task(function test_migration_defaults() {
+ setXULToolbarState();
+ setXULToolbarState("", "", "toolbar-menubar");
+ setXULToolbarState("", "", "tabbar-toolbar");
+
+ migrateToolbarForSpace("mail");
+
+ const newState = getState();
+
+ Assert.ok(!newState.mail, "New default state was preserved");
+ Assert.ok(
+ !Services.xulStore.hasValue(MESSENGER_WINDOW, "mail-bar3", "currentset"),
+ "Old toolbar state is cleared"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(MESSENGER_WINDOW, "mail-bar3", "defaultset"),
+ "Old toolbar default state is cleared"
+ );
+
+ storeState({});
+});
+
+add_task(function test_migration_empty() {
+ setXULToolbarState("__empty");
+ setXULToolbarState("__empty", "menubar-items,spring", "toolbar-menubar");
+ setXULToolbarState("__empty", "", "tabbar-toolbar");
+
+ migrateToolbarForSpace("mail");
+
+ const newState = getState();
+
+ Assert.deepEqual(newState.mail, [], "The toolbar contents were emptied");
+ Assert.ok(
+ !Services.xulStore.hasValue(MESSENGER_WINDOW, "mail-bar3", "currentset"),
+ "Old toolbar state is cleared"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(MESSENGER_WINDOW, "mail-bar3", "defaultset"),
+ "Old toolbar default state is cleared"
+ );
+
+ storeState({});
+});
+
+add_task(function test_migration_noop() {
+ const state = { mail: ["spacer", "search-bar", "spacer"] };
+ storeState(state);
+
+ migrateToolbarForSpace("mail");
+
+ const newState = getState();
+
+ Assert.deepEqual(newState, state, "Customization state is not modified");
+ Assert.ok(
+ !Services.xulStore.hasValue(MESSENGER_WINDOW, "mail-bar3", "currentset"),
+ "Old toolbar state is cleared"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(MESSENGER_WINDOW, "mail-bar3", "defaultset"),
+ "Old toolbar default state is cleared"
+ );
+
+ storeState({});
+});
+
+add_task(function test_calendar_migration() {
+ setXULToolbarState(
+ "calendar-synchronize-button,calendar-newevent-button,separator,calendar-edit-button,calendar-delete-button,spring,calendar-unifinder-button,calendar-appmenu-button",
+ "",
+ "calendar-toolbar2"
+ );
+ setXULToolbarState(
+ "menubar-items,spring,button-addons",
+ "",
+ "toolbar-menubar"
+ );
+ setXULToolbarState("button-delete", "", "tabbar-toolbar");
+
+ migrateToolbarForSpace("calendar");
+
+ const newState = getState();
+
+ Assert.deepEqual(
+ newState.calendar,
+ [
+ "synchronize",
+ "new-event",
+ "edit-event",
+ "delete-event",
+ "spacer",
+ "unifinder",
+ "spacer",
+ "add-ons-and-themes",
+ ],
+ "Items were combined and migrated"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(
+ MESSENGER_WINDOW,
+ "calendar-toolbar2",
+ "currentset"
+ ),
+ "Old toolbar state is cleared"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(
+ MESSENGER_WINDOW,
+ "calendar-toolbar2",
+ "defaultset"
+ ),
+ "Old toolbar default state is cleared"
+ );
+
+ storeState({});
+});
+
+add_task(function test_calendar_migration_defaults() {
+ setXULToolbarState("", "", "calendar-toolbar2");
+ setXULToolbarState("", "", "toolbar-menubar");
+ setXULToolbarState("", "", "tabbar-toolbar");
+
+ migrateToolbarForSpace("calendar");
+
+ const newState = getState();
+
+ Assert.deepEqual(
+ newState.calendar,
+ [
+ "synchronize",
+ "new-event",
+ "new-task",
+ "edit-event",
+ "delete-event",
+ "spacer",
+ ],
+ "Default states were combined and migrated"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(
+ MESSENGER_WINDOW,
+ "calendar-toolbar2",
+ "currentset"
+ ),
+ "Old toolbar state is cleared"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(
+ MESSENGER_WINDOW,
+ "calendar-toolbar2",
+ "defaultset"
+ ),
+ "Old toolbar default state is cleared"
+ );
+
+ storeState({});
+});
+
+add_task(function test_tasks_migration() {
+ setXULToolbarState(
+ "task-synchronize-button,task-newtask-button,task-edit-button,task-delete-button,task-print-button,spring,task-appmenu-button",
+ "",
+ "task-toolbar2"
+ );
+ setXULToolbarState(
+ "menubar-items,spring,button-addons",
+ "",
+ "toolbar-menubar"
+ );
+ setXULToolbarState("button-delete", "", "tabbar-toolbar");
+
+ migrateToolbarForSpace("tasks");
+
+ const newState = getState();
+
+ Assert.deepEqual(
+ newState.tasks,
+ [
+ "synchronize",
+ "new-task",
+ "edit-event",
+ "delete-event",
+ "print-event",
+ "spacer",
+ "add-ons-and-themes",
+ ],
+ "Items were combined and migrated"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(
+ MESSENGER_WINDOW,
+ "task-toolbar2",
+ "currentset"
+ ),
+ "Old toolbar state is cleared"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(
+ MESSENGER_WINDOW,
+ "task-toolbar2",
+ "defaultset"
+ ),
+ "Old toolbar default state is cleared"
+ );
+
+ storeState({});
+});
+
+add_task(function test_tasks_migration_defaults() {
+ setXULToolbarState("", "", "task-toolbar2");
+ setXULToolbarState("", "", "toolbar-menubar");
+ setXULToolbarState("", "", "tabbar-toolbar");
+
+ migrateToolbarForSpace("tasks");
+
+ const newState = getState();
+
+ Assert.deepEqual(
+ newState.tasks,
+ [
+ "synchronize",
+ "new-event",
+ "new-task",
+ "edit-event",
+ "delete-event",
+ "spacer",
+ ],
+ "Default states were combined and migrated"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(
+ MESSENGER_WINDOW,
+ "task-toolbar2",
+ "currentset"
+ ),
+ "Old toolbar state is cleared"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(
+ MESSENGER_WINDOW,
+ "task-toolbar2",
+ "defaultset"
+ ),
+ "Old toolbar default state is cleared"
+ );
+
+ storeState({});
+});
+
+add_task(function test_global_items_migration() {
+ setXULToolbarState(
+ "menubar-items,spring,button-addons",
+ "",
+ "toolbar-menubar"
+ );
+ setXULToolbarState("button-delete", "", "tabbar-toolbar");
+
+ migrateToolbarForSpace("settings");
+
+ const newState = getState();
+
+ Assert.deepEqual(newState.settings, [
+ "spacer",
+ "search-bar",
+ "spacer",
+ "add-ons-and-themes",
+ ]);
+
+ storeState({});
+});
+
+add_task(function test_global_items_migration_defaults() {
+ setXULToolbarState("", "", "toolbar-menubar");
+ setXULToolbarState("", "", "tabbar-toolbar");
+
+ migrateToolbarForSpace("settings");
+
+ const newState = getState();
+
+ Assert.deepEqual(newState.settings, ["spacer", "search-bar", "spacer"]);
+
+ storeState({});
+});
+
+add_task(function test_clear_xul_toolbar_state() {
+ setXULToolbarState(
+ "menubar-items,spring,button-addons",
+ "menubar-items,spring",
+ "toolbar-menubar"
+ );
+
+ clearXULToolbarState("toolbar-menubar");
+
+ Assert.ok(
+ !Services.xulStore.hasValue(
+ MESSENGER_WINDOW,
+ "toolbar-menubar",
+ "currentset"
+ ),
+ "Old toolbar state is cleared"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(
+ MESSENGER_WINDOW,
+ "toolbar-menubar",
+ "defaultset"
+ ),
+ "Old toolbar default state is cleared"
+ );
+});
+
+add_task(function test_migration_defaults_with_extension() {
+ setXULToolbarState(
+ AppConstants.platform == "macosx"
+ ? "button-getmsg,button-newmsg,button-tag,qfb-show-filter-bar,spring,gloda-search,extension1,extension2,button-appmenu"
+ : "button-getmsg,button-newmsg,separator,button-tag,qfb-show-filter-bar,spring,gloda-search,extension1,extension2,button-appmenu"
+ );
+ setXULToolbarState("", "", "toolbar-menubar");
+ setXULToolbarState("", "", "tabbar-toolbar");
+ Services.xulStore.setValue(
+ MESSENGER_WINDOW,
+ "mail-bar3",
+ "extensionset",
+ "extension1,extension2"
+ );
+
+ migrateToolbarForSpace("mail");
+
+ const newState = getState();
+
+ Assert.ok(!newState.mail, "New default state was preserved");
+ Assert.ok(
+ !Services.xulStore.hasValue(MESSENGER_WINDOW, "mail-bar3", "currentset"),
+ "Old toolbar state is cleared"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(MESSENGER_WINDOW, "mail-bar3", "defaultset"),
+ "Old toolbar default state is cleared"
+ );
+ Assert.ok(
+ !Services.xulStore.hasValue(MESSENGER_WINDOW, "mail-bar3", "extensionset"),
+ "Old toolbar extension state is cleared"
+ );
+
+ storeState({});
+});
diff --git a/comm/mail/components/unifiedtoolbar/test/unit/xpcshell.ini b/comm/mail/components/unifiedtoolbar/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..ec9807b399
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/test/unit/xpcshell.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+head =
+
+[test_buttonStyle.js]
+[test_customizableItems.js]
+[test_customizableItemsDetails.js]
+[test_customizationState.js]
+[test_toolbarMigration.js]