summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/extensions/test/browser
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/extensions/test/browser')
-rw-r--r--comm/mail/components/extensions/test/browser/.eslintrc.js7
-rw-r--r--comm/mail/components/extensions/test/browser/browser.ini135
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_addressBooksUI.js116
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_browserAction_customized.js29
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_browserAction_not_customized.js17
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_browserAction_popup_click.js399
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_browserAction_popup_click_mv3_event_pages.js82
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_browserAction_properties.js348
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_bug1812530.js200
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_clickHandler.js614
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_cloudFile.js1444
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js226
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_commands_execute_compose_action.js138
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_commands_execute_message_display_action.js168
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_commands_getAll.js142
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_commands_onChanged.js59
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_commands_onCommand.js577
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_commands_onCommand_bug1845236.js74
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_commands_update.js357
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_composeAction.js268
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_composeAction_popup_click.js266
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_composeAction_popup_click_mv3_event_pages.js52
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_composeAction_properties.js125
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_composeScripts.js531
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_attachments.js2268
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_begin_attachments.js116
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_begin_body.js397
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_begin_bug1691254.js141
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_begin_forward.js339
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_begin_headers.js178
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_begin_identity.js102
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_begin_new.js136
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_begin_reply.js146
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_bug1692439.js160
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_bug1804796.js80
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_details.js725
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_details_body.js469
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_details_headers.js727
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_dictionaries.js214
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_onBeforeSend.js1010
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_saveDraft.js416
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_saveTemplate.js432
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_compose_sendMessage.js733
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_contentScripts.js438
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_content_handler.js334
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_content_tabs_navigation_menu.js250
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_mailTabs.js898
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_mailTabs_mv3.js162
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_menus_context_action.js424
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_menus_context_compose.js179
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_menus_context_content.js253
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_menus_context_folder_pane.js97
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_menus_context_message_panes.js180
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_menus_context_tabs.js77
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_menus_context_tools_main_menu.js156
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_menus_message_one_attachment.js395
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_menus_message_two_attachments.js397
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_menus_popup_action.js405
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_menus_replace_menu.js582
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_menus_replace_menu_context.js375
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_messageDisplay.js1016
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction.js337
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_popup_click.js294
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_popup_click_mv3_event_pages.js113
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_properties.js184
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_messageDisplayScripts.js636
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_bug1827032.js38
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_bug1828056.js212
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_file.js221
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_headerMessageId.js221
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_messageId.js221
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_message_external.js427
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_messages_open_attachment.js107
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_quickFilter.js132
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_sessions.js90
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_spaces.js1047
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_spacesToolbar.js755
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_tabs_content.js336
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js275
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_tabs_events.js591
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_tabs_move.js306
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_tabs_onCreated_bug1817872.js226
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_tabs_query.js113
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_tabs_update_reload.js578
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_themes_onUpdated.js150
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_tooltip_in_extension_pages.js685
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_windows.js439
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_windows_bug1732559.js94
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_windows_create_normal_cookieStoreId.js116
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_windows_create_popup_cookieStoreId.js255
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_windows_events.js405
-rw-r--r--comm/mail/components/extensions/test/browser/browser_ext_windows_types.js121
-rw-r--r--comm/mail/components/extensions/test/browser/data/cloudFile1.txt1
-rw-r--r--comm/mail/components/extensions/test/browser/data/cloudFile2.txt1
-rw-r--r--comm/mail/components/extensions/test/browser/data/content.html12
-rw-r--r--comm/mail/components/extensions/test/browser/data/content_body.html1
-rw-r--r--comm/mail/components/extensions/test/browser/data/linktest.html11
-rw-r--r--comm/mail/components/extensions/test/browser/data/tb-logo.pngbin0 -> 6462 bytes
-rw-r--r--comm/mail/components/extensions/test/browser/head.js1533
-rw-r--r--comm/mail/components/extensions/test/browser/head_menus.js733
-rw-r--r--comm/mail/components/extensions/test/browser/messages/attachedMessageSample.eml186
-rw-r--r--comm/mail/components/extensions/test/browser/messages/messageWithLink.eml26
-rw-r--r--comm/mail/components/extensions/test/browser/test_browserAction.js845
103 files changed, 34855 insertions, 0 deletions
diff --git a/comm/mail/components/extensions/test/browser/.eslintrc.js b/comm/mail/components/extensions/test/browser/.eslintrc.js
new file mode 100644
index 0000000000..e57058ecb1
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ env: {
+ webextensions: true,
+ },
+};
diff --git a/comm/mail/components/extensions/test/browser/browser.ini b/comm/mail/components/extensions/test/browser/browser.ini
new file mode 100644
index 0000000000..1bd2925968
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser.ini
@@ -0,0 +1,135 @@
+[DEFAULT]
+head = head.js
+prefs =
+ mail.provider.suppress_dialog_on_startup=true
+ mail.spellcheck.inline=false
+ mail.spotlight.firstRunDone=true
+ mail.winsearch.firstRunDone=true
+ mailnews.message_display.disable_remote_image=false
+ mailnews.start_page.override_url=about:blank
+ mailnews.start_page.url=about:blank
+subsuite = thunderbird
+support-files =
+ head_menus.js
+ test_browserAction.js
+ ../xpcshell/data/utils.js
+tags = webextensions
+
+[browser_ext_addressBooksUI.js]
+tags = addrbook
+[browser_ext_bug1812530.js]
+support-files = data/content.html
+tags = contextmenu
+[browser_ext_browserAction_customized.js]
+[browser_ext_browserAction_not_customized.js]
+[browser_ext_browserAction_popup_click.js]
+[browser_ext_browserAction_popup_click_mv3_event_pages.js]
+[browser_ext_browserAction_properties.js]
+[browser_ext_clickHandler.js]
+support-files = data/content.html data/linktest.html messages/messageWithLink.eml
+[browser_ext_cloudFile.js]
+support-files = data/cloudFile1.txt data/cloudFile2.txt
+[browser_ext_commands_execute_browser_action.js]
+[browser_ext_commands_execute_compose_action.js]
+[browser_ext_commands_execute_message_display_action.js]
+[browser_ext_commands_getAll.js]
+[browser_ext_commands_onChanged.js]
+[browser_ext_commands_onCommand.js]
+[browser_ext_commands_onCommand_bug1845236.js]
+[browser_ext_commands_update.js]
+[browser_ext_compose_attachments.js]
+[browser_ext_compose_begin_attachments.js]
+[browser_ext_compose_begin_body.js]
+[browser_ext_compose_begin_bug1691254.js]
+[browser_ext_compose_begin_forward.js]
+[browser_ext_compose_begin_headers.js]
+[browser_ext_compose_begin_identity.js]
+[browser_ext_compose_begin_new.js]
+[browser_ext_compose_begin_reply.js]
+[browser_ext_compose_details.js]
+[browser_ext_compose_details_headers.js]
+[browser_ext_compose_details_body.js]
+[browser_ext_compose_bug1692439.js]
+[browser_ext_compose_bug1804796.js]
+[browser_ext_compose_dictionaries.js]
+[browser_ext_compose_onBeforeSend.js]
+[browser_ext_compose_saveDraft.js]
+[browser_ext_compose_saveTemplate.js]
+[browser_ext_compose_sendMessage.js]
+[browser_ext_composeAction.js]
+[browser_ext_composeAction_popup_click.js]
+[browser_ext_composeAction_popup_click_mv3_event_pages.js]
+[browser_ext_composeAction_properties.js]
+[browser_ext_composeScripts.js]
+[browser_ext_content_handler.js]
+[browser_ext_content_tabs_navigation_menu.js]
+support-files = data/content.html
+tags = contextmenu
+[browser_ext_contentScripts.js]
+[browser_ext_mailTabs_mv3.js]
+[browser_ext_mailTabs.js]
+[browser_ext_menus_context_action.js]
+support-files = data/content.html data/content_body.html data/tb-logo.png
+tags = contextmenu
+[browser_ext_menus_context_compose.js]
+support-files = data/content.html data/content_body.html data/tb-logo.png
+tags = contextmenu
+[browser_ext_menus_context_content.js]
+support-files = data/content.html data/content_body.html data/tb-logo.png
+tags = contextmenu
+[browser_ext_menus_context_folder_pane.js]
+support-files = data/content.html data/content_body.html data/tb-logo.png
+tags = contextmenu
+[browser_ext_menus_context_message_panes.js]
+support-files = data/content.html data/content_body.html data/tb-logo.png
+tags = contextmenu
+[browser_ext_menus_context_tabs.js]
+support-files = data/content.html data/content_body.html data/tb-logo.png
+tags = contextmenu
+[browser_ext_menus_context_tools_main_menu.js]
+support-files = data/content.html data/content_body.html data/tb-logo.png
+tags = contextmenu
+[browser_ext_menus_message_one_attachment.js]
+support-files = data/content.html data/content_body.html data/tb-logo.png
+[browser_ext_menus_message_two_attachments.js]
+support-files = data/content.html data/content_body.html data/tb-logo.png
+tags = contextmenu
+[browser_ext_menus_popup_action.js]
+[browser_ext_menus_replace_menu.js]
+tags = contextmenu
+[browser_ext_menus_replace_menu_context.js]
+tags = contextmenu
+[browser_ext_message_external.js]
+support-files = messages/attachedMessageSample.eml
+[browser_ext_messageDisplay.js]
+[browser_ext_messageDisplay_bug1827032.js]
+[browser_ext_messageDisplay_bug1828056.js]
+[browser_ext_messageDisplay_open_file.js]
+[browser_ext_messageDisplay_open_headerMessageId.js]
+[browser_ext_messageDisplay_open_messageId.js]
+[browser_ext_messageDisplayAction.js]
+[browser_ext_messageDisplayAction_popup_click.js]
+[browser_ext_messageDisplayAction_popup_click_mv3_event_pages.js]
+[browser_ext_messageDisplayAction_properties.js]
+[browser_ext_messageDisplayScripts.js]
+[browser_ext_messages_open_attachment.js]
+[browser_ext_quickFilter.js]
+[browser_ext_sessions.js]
+[browser_ext_spaces.js]
+[browser_ext_spacesToolbar.js]
+[browser_ext_tabs_content.js]
+[browser_ext_tabs_cookieStoreId.js]
+[browser_ext_tabs_events.js]
+[browser_ext_tabs_onCreated_bug1817872.js]
+[browser_ext_tabs_move.js]
+[browser_ext_tabs_query.js]
+[browser_ext_tabs_update_reload.js]
+[browser_ext_themes_onUpdated.js]
+[browser_ext_tooltip_in_extension_pages.js]
+[browser_ext_windows.js]
+[browser_ext_windows_bug1732559.js]
+[browser_ext_windows_create_normal_cookieStoreId.js]
+[browser_ext_windows_create_popup_cookieStoreId.js]
+[browser_ext_windows_events.js]
+[browser_ext_windows_types.js]
+
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_addressBooksUI.js b/comm/mail/components/extensions/test/browser/browser_ext_addressBooksUI.js
new file mode 100644
index 0000000000..4171bf47bf
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_addressBooksUI.js
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async function testUI() {
+ async function background() {
+ async function checkNumberOfAddressBookTabs(expectedNumberOfTabs) {
+ let addressBookTabs = await browser.tabs.query({ type: "addressBook" });
+ browser.test.assertEq(
+ expectedNumberOfTabs,
+ addressBookTabs.length,
+ "Should find the correct number of open address book tabs"
+ );
+ }
+
+ let addedTabs = new Set();
+ let removedTabs = 0;
+ function tabCreateListener(tab) {
+ if (tab.type == "addressBook") {
+ addedTabs.add(tab.id);
+ } else {
+ browser.test.fail(
+ "Should not receive a onTabCreated event for a non address book tab"
+ );
+ }
+ }
+
+ function tabRemoveListener(tabId) {
+ console.log("Remove: " + tabId);
+ if (addedTabs.has(tabId)) {
+ removedTabs++;
+ } else {
+ browser.test.fail(
+ "Should not receive a onTabRemoved event for a non address book tab"
+ );
+ }
+ }
+
+ browser.tabs.onCreated.addListener(tabCreateListener);
+ browser.tabs.onRemoved.addListener(tabRemoveListener);
+
+ await window.sendMessage("checkNumberOfAddressBookTabs", 0);
+ await checkNumberOfAddressBookTabs(0);
+
+ let abTab1 = await browser.addressBooks.openUI();
+ browser.test.log(JSON.stringify(abTab1));
+ browser.test.assertEq(
+ "addressBook",
+ abTab1.type,
+ "Should have found an addressBook tab"
+ );
+ await window.sendMessage("checkNumberOfAddressBookTabs", 1);
+ await checkNumberOfAddressBookTabs(1);
+
+ await browser.addressBooks.openUI();
+ let abTab2 = await browser.addressBooks.openUI();
+ browser.test.log(JSON.stringify(abTab2));
+ browser.test.assertEq(
+ "addressBook",
+ abTab2.type,
+ "Should have found an addressBook tab"
+ );
+ await window.sendMessage("checkNumberOfAddressBookTabs", 1);
+ await checkNumberOfAddressBookTabs(1);
+
+ browser.test.assertEq(
+ abTab1.id,
+ abTab2.id,
+ "addressBook tabs should be identical"
+ );
+
+ await browser.addressBooks.closeUI();
+ await window.sendMessage("checkNumberOfAddressBookTabs", 0);
+ await checkNumberOfAddressBookTabs(0);
+
+ browser.tabs.onCreated.removeListener(tabCreateListener);
+ browser.tabs.onRemoved.removeListener(tabRemoveListener);
+
+ browser.test.assertEq(
+ 1,
+ removedTabs,
+ "Should have seen the correct number of address book tabs being removed"
+ );
+
+ browser.test.assertEq(
+ 1,
+ addedTabs.size,
+ "Should have seen the correct number of address book tabs being added"
+ );
+
+ browser.test.notifyPass("addressBooks");
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["addressBooks"],
+ },
+ });
+
+ extension.onMessage("checkNumberOfAddressBookTabs", count => {
+ let tabmail = document.getElementById("tabmail");
+ let tabs = tabmail.tabInfo.filter(
+ tab => tab.browser?.currentURI.spec == "about:addressbook"
+ );
+ Assert.equal(tabs.length, count, "Right number of address books open");
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("addressBooks");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_browserAction_customized.js b/comm/mail/components/extensions/test/browser/browser_ext_browserAction_customized.js
new file mode 100644
index 0000000000..056fec372e
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_browserAction_customized.js
@@ -0,0 +1,29 @@
+/* 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/. */
+
+async function enforceState(state) {
+ const stateChangeObserved = TestUtils.topicObserved(
+ "unified-toolbar-state-change"
+ );
+ storeState(state);
+ await stateChangeObserved;
+}
+
+add_setup(async () => {
+ // Set a customized state for the spaces we are working with in this test.
+ await enforceState({
+ mail: ["spacer", "search-bar", "spacer"],
+ calendar: ["spacer", "search-bar", "spacer"],
+ });
+
+ registerCleanupFunction(async () => {
+ await enforceState({});
+ });
+});
+
+// Load browserAction tests.
+Services.scriptloader.loadSubScript(
+ new URL("test_browserAction.js", gTestPath).href,
+ this
+);
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_browserAction_not_customized.js b/comm/mail/components/extensions/test/browser/browser_ext_browserAction_not_customized.js
new file mode 100644
index 0000000000..755e950a84
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_browserAction_not_customized.js
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_setup(async () => {
+ Assert.equal(
+ 0,
+ Object.keys(getState()).length,
+ "Unified toolbar should not be customized"
+ );
+});
+
+// Load browserAction tests.
+Services.scriptloader.loadSubScript(
+ new URL("test_browserAction.js", gTestPath).href,
+ this
+);
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_browserAction_popup_click.js b/comm/mail/components/extensions/test/browser/browser_ext_browserAction_popup_click.js
new file mode 100644
index 0000000000..9b985a2c7a
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_browserAction_popup_click.js
@@ -0,0 +1,399 @@
+/* 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 account;
+let messages;
+
+add_setup(async () => {
+ account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ let subFolders = rootFolder.subFolders;
+ createMessages(subFolders[0], 10);
+ messages = subFolders[0].messages;
+});
+
+// This test clicks on the action button to open the popup.
+add_task(async function test_popup_open_with_click() {
+ info("3-pane tab");
+ {
+ let testConfig = {
+ actionType: "browser_action",
+ testType: "open-with-mouse-click",
+ window,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+ }
+
+ info("Message window");
+ {
+ let messageWindow = await openMessageInWindow(messages.getNext());
+ let testConfig = {
+ actionType: "browser_action",
+ testType: "open-with-mouse-click",
+ default_windows: ["messageDisplay"],
+ window: messageWindow,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+
+ messageWindow.close();
+ }
+});
+
+// This test uses openPopup() to open the popup in a normal window.
+add_task(async function test_popup_open_with_openPopup_in_normal_window() {
+ let files = {
+ "background.js": async () => {
+ let windows = await browser.windows.getAll();
+ let mailWindow = windows.find(window => window.type == "normal");
+ let messageWindow = windows.find(
+ window => window.type == "messageDisplay"
+ );
+ browser.test.assertTrue(!!mailWindow, "should have found a mailWindow");
+ browser.test.assertTrue(
+ !!messageWindow,
+ "should have found a messageWindow"
+ );
+
+ // The test starts with an opened messageWindow, the browser_action is not
+ // allowed there and should not be visible, openPopup() should fail.
+ browser.test.assertTrue(
+ (await browser.windows.get(messageWindow.id)).focused,
+ "messageWindow should be focused"
+ );
+ browser.test.assertFalse(
+ await browser.browserAction.openPopup(),
+ "openPopup() should have failed while the messageWindow is active"
+ );
+
+ // Specifically open the browser_action of the mailWindow, should become
+ // focused and openPopup() should succeed.
+ browser.test.assertTrue(
+ await browser.browserAction.openPopup({ windowId: mailWindow.id }),
+ "openPopup() should have succeeded when explicitly requesting the mailWindow"
+ );
+ await window.waitForMessage();
+ browser.test.assertTrue(
+ (await browser.windows.get(mailWindow.id)).focused,
+ "mailWindow should be focused"
+ );
+
+ // mailWindow is the topmost window now, openPopup() should succeed.
+ browser.test.assertTrue(
+ await browser.browserAction.openPopup(),
+ "openPopup() should have succeeded after the mailWindow has become active"
+ );
+ await window.waitForMessage();
+
+ // Create content tab, the browser_action is not allowed in that space and
+ // should not be visible, openPopup() should fail.
+ let contentTab = await browser.tabs.create({
+ url: "https://www.example.com",
+ });
+ browser.test.assertFalse(
+ await browser.browserAction.openPopup(),
+ "openPopup() should have failed while the content tab is active"
+ );
+
+ // Close the content tab and return to the mail space, the browser_action
+ // should be visible again, openPopup() should succeed.
+ await browser.tabs.remove(contentTab.id);
+ browser.test.assertTrue(
+ await browser.browserAction.openPopup(),
+ "openPopup() should have succeeded after the content tab was closed"
+ );
+ await window.waitForMessage();
+
+ // Disable the browser_action, openPopup() should fail.
+ await browser.browserAction.disable();
+ browser.test.assertFalse(
+ await browser.browserAction.openPopup(),
+ "openPopup() should have failed after the action_button was disabled"
+ );
+
+ // Enable the browser_action, openPopup() should succeed.
+ await browser.browserAction.enable();
+ browser.test.assertTrue(
+ await browser.browserAction.openPopup(),
+ "openPopup() should have succeeded after the action_button was enabled again"
+ );
+ await window.waitForMessage();
+
+ // Create a popup window, which does not have a browser_action, openPopup()
+ // should fail.
+ let popupWindow = await browser.windows.create({
+ type: "popup",
+ url: "https://www.example.com",
+ });
+ browser.test.assertTrue(
+ (await browser.windows.get(popupWindow.id)).focused,
+ "popupWindow should be focused"
+ );
+ browser.test.assertFalse(
+ await browser.browserAction.openPopup(),
+ "openPopup() should have failed while the popup window is active"
+ );
+
+ // Specifically open the browser_action of the mailWindow, should become
+ // focused and openPopup() should succeed.
+ browser.test.assertTrue(
+ await browser.browserAction.openPopup({ windowId: mailWindow.id }),
+ "openPopup() should have succeeded when explicitly requesting the mailWindow"
+ );
+ await window.waitForMessage();
+ browser.test.assertTrue(
+ (await browser.windows.get(mailWindow.id)).focused,
+ "mailWindow should be focused"
+ );
+
+ // Close the popup window
+ await browser.windows.remove(popupWindow.id);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ "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 () {
+ browser.test.sendMessage("popup opened");
+ window.close();
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: {
+ id: "browser_action_openPopup@mochi.test",
+ },
+ },
+ background: { scripts: ["utils.js", "background.js"] },
+ browser_action: {
+ default_title: "default",
+ default_popup: "popup.html",
+ },
+ },
+ });
+
+ extension.onMessage("popup opened", async () => {
+ // Wait a moment to make sure the popup has closed.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => window.setTimeout(r, 150));
+ extension.sendMessage();
+ });
+
+ let messageWindow = await openMessageInWindow(messages.getNext());
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ messageWindow.close();
+});
+
+// This test adds the action button to the message window and not to the mail
+// window (the default_windows manifest property is set to ["messageDisplay"].
+// the test then uses openPopup() to open the popup in a message window.
+add_task(async function test_popup_open_with_openPopup_in_message_window() {
+ let files = {
+ "background.js": async () => {
+ let windows = await browser.windows.getAll();
+ let mailWindow = windows.find(window => window.type == "normal");
+ let messageWindow = windows.find(
+ window => window.type == "messageDisplay"
+ );
+ browser.test.assertTrue(!!mailWindow, "should have found a mailWindow");
+ browser.test.assertTrue(
+ !!messageWindow,
+ "should have found a messageWindow"
+ );
+
+ // The test starts with an opened messageWindow, the browser_action is allowed
+ // there and should be visible, openPopup() should succeed.
+ browser.test.assertTrue(
+ (await browser.windows.get(messageWindow.id)).focused,
+ "messageWindow should be focused"
+ );
+ browser.test.assertTrue(
+ await browser.browserAction.openPopup(),
+ "openPopup() should have succeeded while the messageWindow is active"
+ );
+ await window.waitForMessage();
+
+ // Collapse the toolbar, openPopup() should fail.
+ await window.sendMessage("collapseToolbar", true);
+ browser.test.assertFalse(
+ await browser.browserAction.openPopup(),
+ "openPopup() should have failed while the toolbar is collapsed"
+ );
+
+ // Restore the toolbar, openPopup() should succeed.
+ await window.sendMessage("collapseToolbar", false);
+ browser.test.assertTrue(
+ await browser.browserAction.openPopup(),
+ "openPopup() should have succeeded after the toolbar is restored"
+ );
+ await window.waitForMessage();
+
+ // Specifically open the browser_action of the mailWindow, it should not be
+ // allowed there and openPopup() should fail.
+ browser.test.assertFalse(
+ await browser.browserAction.openPopup({ windowId: mailWindow.id }),
+ "openPopup() should have failed when explicitly requesting the mailWindow"
+ );
+
+ // The messageWindow should still have focus, openPopup() should succeed.
+ browser.test.assertTrue(
+ (await browser.windows.get(messageWindow.id)).focused,
+ "messageWindow should be focused"
+ );
+ browser.test.assertTrue(
+ await browser.browserAction.openPopup(),
+ "openPopup() should still have succeeded while the messageWindow is active"
+ );
+ await window.waitForMessage();
+
+ // Disable the browser_action, openPopup() should fail.
+ await browser.browserAction.disable();
+ browser.test.assertFalse(
+ await browser.browserAction.openPopup(),
+ "openPopup() should have failed after the action_button was disabled"
+ );
+
+ // Enable the browser_action, openPopup() should succeed.
+ await browser.browserAction.enable();
+ browser.test.assertTrue(
+ await browser.browserAction.openPopup(),
+ "openPopup() should have succeeded after the action_button was enabled again"
+ );
+ await window.waitForMessage();
+
+ // Create a popup window, which does not have a browser_action, openPopup()
+ // should fail.
+ let popupWindow = await browser.windows.create({
+ type: "popup",
+ url: "https://www.example.com",
+ });
+ browser.test.assertTrue(
+ await browser.windows.get(popupWindow.id),
+ "popupWindow should be focused"
+ );
+ browser.test.assertFalse(
+ await browser.browserAction.openPopup(),
+ "openPopup() should have failed while the popup window is active"
+ );
+
+ // Specifically open the browser_action of the messageWindow, should become
+ // focused and openPopup() should succeed.
+ browser.test.assertTrue(
+ await browser.browserAction.openPopup({ windowId: messageWindow.id }),
+ "openPopup() should have succeeded when explicitly requesting the messageWindow"
+ );
+ await window.waitForMessage();
+ browser.test.assertTrue(
+ (await browser.windows.get(messageWindow.id)).focused,
+ "messageWindow should be focused"
+ );
+
+ // The messageWindow is focused now, openPopup() should succeed.
+ browser.test.assertTrue(
+ await browser.browserAction.openPopup(),
+ "openPopup() should have succeeded while the messageWindow is active"
+ );
+ await window.waitForMessage();
+
+ // Close the popup window and finish
+ await browser.windows.remove(popupWindow.id);
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ "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 () {
+ browser.test.sendMessage("popup opened");
+ window.close();
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: {
+ id: "browser_action_openPopup@mochi.test",
+ },
+ },
+ background: { scripts: ["utils.js", "background.js"] },
+ browser_action: {
+ default_title: "default",
+ default_popup: "popup.html",
+ default_windows: ["messageDisplay"],
+ },
+ },
+ });
+
+ extension.onMessage("popup opened", async () => {
+ // Wait a moment to make sure the popup has closed.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => window.setTimeout(r, 150));
+ extension.sendMessage();
+ });
+
+ extension.onMessage("collapseToolbar", state => {
+ let window = Services.wm.getMostRecentWindow("mail:messageWindow");
+ let toolbar = window.document.getElementById("mail-bar3");
+ if (state) {
+ toolbar.setAttribute("collapsed", "true");
+ } else {
+ toolbar.removeAttribute("collapsed");
+ }
+ extension.sendMessage();
+ });
+
+ let messageWindow = await openMessageInWindow(messages.getNext());
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ messageWindow.close();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_browserAction_popup_click_mv3_event_pages.js b/comm/mail/components/extensions/test/browser/browser_ext_browserAction_popup_click_mv3_event_pages.js
new file mode 100644
index 0000000000..a58e0077ef
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_browserAction_popup_click_mv3_event_pages.js
@@ -0,0 +1,82 @@
+/* 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 account;
+let subFolders;
+
+add_setup(async () => {
+ account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ subFolders = rootFolder.subFolders;
+ createMessages(subFolders[0], 10);
+ await TestUtils.waitForCondition(
+ () => subFolders[0].messages.hasMoreElements(),
+ "Messages should be added to folder"
+ );
+});
+
+function getMessage() {
+ let messages = subFolders[0].messages;
+ ok(messages.hasMoreElements(), "Should have messages to iterate to");
+ return messages.getNext();
+}
+
+async function subtest_popup_open_with_click_MV3_event_pages(
+ terminateBackground
+) {
+ info("3-pane tab");
+ let testConfig = {
+ actionType: "action",
+ manifest_version: 3,
+ terminateBackground,
+ testType: "open-with-mouse-click",
+ window,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+
+ info("Message window");
+ {
+ let messageWindow = await openMessageInWindow(getMessage());
+ let testConfig = {
+ actionType: "action",
+ manifest_version: 3,
+ terminateBackground,
+ testType: "open-with-mouse-click",
+ default_windows: ["messageDisplay"],
+ window: messageWindow,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+ messageWindow.close();
+ }
+}
+// This MV3 test clicks on the action button to open the popup.
+add_task(async function test_event_pages_without_background_termination() {
+ await subtest_popup_open_with_click_MV3_event_pages(false);
+});
+// This MV3 test clicks on the action button to open the popup (background termination).
+add_task(async function test_event_pages_with_background_termination() {
+ await subtest_popup_open_with_click_MV3_event_pages(true);
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_browserAction_properties.js b/comm/mail/components/extensions/test/browser/browser_ext_browserAction_properties.js
new file mode 100644
index 0000000000..18633c5715
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_browserAction_properties.js
@@ -0,0 +1,348 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async () => {
+ let account = createAccount();
+ addIdentity(account);
+ let rootFolder = account.incomingServer.rootFolder;
+
+ let files = {
+ "background.js": async () => {
+ async function checkProperty(property, expectedDefault, ...expected) {
+ browser.test.log(
+ `${property}: ${expectedDefault}, ${expected.join(", ")}`
+ );
+
+ browser.test.assertEq(
+ expectedDefault,
+ await browser.browserAction[property]({}),
+ `Default value for ${property} should be correct`
+ );
+ for (let i = 0; i < 3; i++) {
+ browser.test.assertEq(
+ expected[i],
+ await browser.browserAction[property]({ tabId: tabIDs[i] }),
+ `Specific value for ${property} of tab #${i} should be correct`
+ );
+ }
+ }
+
+ async function checkRealState(property, ...expected) {
+ await window.sendMessage(whichTest, property, expected);
+ }
+
+ let tabs = await browser.mailTabs.query({});
+ browser.test.assertEq(3, tabs.length);
+ let tabIDs = tabs.map(t => t.id);
+
+ let whichTest = "checkProperty";
+
+ // Test enable property.
+ await checkProperty("isEnabled", true, true, true, true);
+ await checkRealState("enabled", true, true, true);
+ await browser.browserAction.disable();
+ await checkProperty("isEnabled", false, false, false, false);
+ await checkRealState("enabled", false, false, false);
+ await browser.browserAction.enable(tabIDs[0]);
+ await checkProperty("isEnabled", false, true, false, false);
+ await checkRealState("enabled", true, false, false);
+ await browser.browserAction.enable();
+ await checkProperty("isEnabled", true, true, true, true);
+ await checkRealState("enabled", true, true, true);
+ await browser.browserAction.disable();
+ await checkProperty("isEnabled", false, true, false, false);
+ await checkRealState("enabled", true, false, false);
+ await browser.browserAction.disable(tabIDs[0]);
+ await checkProperty("isEnabled", false, false, false, false);
+ await checkRealState("enabled", false, false, false);
+ await browser.browserAction.enable();
+ await checkProperty("isEnabled", true, false, true, true);
+ await checkRealState("enabled", false, true, true);
+
+ // Test title property (since a label has not been set, this sets the
+ // tooltip and the actual label of the button).
+ await checkProperty(
+ "getTitle",
+ "default",
+ "default",
+ "default",
+ "default"
+ );
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "default", "default", "default");
+ await browser.browserAction.setTitle({ tabId: tabIDs[2], title: "tab2" });
+ await checkProperty("getTitle", "default", "default", "default", "tab2");
+ await checkRealState("tooltip", "default", "default", "tab2");
+ await checkRealState("label", "default", "default", "tab2");
+ await browser.browserAction.setTitle({ title: "new" });
+ await checkProperty("getTitle", "new", "new", "new", "tab2");
+ await checkRealState("tooltip", "new", "new", "tab2");
+ await checkRealState("label", "new", "new", "tab2");
+ await browser.browserAction.setTitle({ tabId: tabIDs[1], title: "tab1" });
+ await checkProperty("getTitle", "new", "new", "tab1", "tab2");
+ await checkRealState("tooltip", "new", "tab1", "tab2");
+ await checkRealState("label", "new", "tab1", "tab2");
+ await browser.browserAction.setTitle({ tabId: tabIDs[2], title: null });
+ await checkProperty("getTitle", "new", "new", "tab1", "new");
+ await checkRealState("tooltip", "new", "tab1", "new");
+ await checkRealState("label", "new", "tab1", "new");
+ await browser.browserAction.setTitle({ title: null });
+ await checkProperty("getTitle", "default", "default", "tab1", "default");
+ await checkRealState("tooltip", "default", "tab1", "default");
+ await checkRealState("label", "default", "tab1", "default");
+ await browser.browserAction.setTitle({ tabId: tabIDs[1], title: null });
+ await checkProperty(
+ "getTitle",
+ "default",
+ "default",
+ "default",
+ "default"
+ );
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "default", "default", "default");
+
+ // Test label property (tooltip should not change).
+ await checkProperty("getLabel", null, null, null, null);
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "default", "default", "default");
+ await browser.browserAction.setLabel({ tabId: tabIDs[2], label: "" });
+ await checkProperty("getLabel", null, null, null, "");
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "default", "default", "");
+ await browser.browserAction.setLabel({ tabId: tabIDs[2], label: "tab2" });
+ await checkProperty("getLabel", null, null, null, "tab2");
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "default", "default", "tab2");
+ await browser.browserAction.setLabel({ label: "new" });
+ await checkProperty("getLabel", "new", "new", "new", "tab2");
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "new", "new", "tab2");
+ await browser.browserAction.setLabel({ tabId: tabIDs[1], label: "tab1" });
+ await checkProperty("getLabel", "new", "new", "tab1", "tab2");
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "new", "tab1", "tab2");
+ await browser.browserAction.setLabel({ tabId: tabIDs[2], label: null });
+ await checkProperty("getLabel", "new", "new", "tab1", "new");
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "new", "tab1", "new");
+ await browser.browserAction.setLabel({ label: null });
+ await checkProperty("getLabel", null, null, "tab1", null);
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "default", "tab1", "default");
+ await browser.browserAction.setLabel({ tabId: tabIDs[1], label: null });
+ await checkProperty("getLabel", null, null, null, null);
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "default", "default", "default");
+
+ // Check that properties are updated without switching tabs. We might be
+ // relying on the tab switch to update the properties.
+
+ // Tab 0's enabled state doesn't reflect the default any more, so we
+ // can't just run the code above again.
+
+ browser.test.log("checkPropertyCurrent");
+ whichTest = "checkPropertyCurrent";
+
+ // Test enable property.
+ await checkProperty("isEnabled", true, false, true, true);
+ await checkRealState("enabled", false, true, true);
+ await browser.browserAction.disable();
+ await checkProperty("isEnabled", false, false, false, false);
+ await checkRealState("enabled", false, false, false);
+ await browser.browserAction.enable(tabIDs[0]);
+ await checkProperty("isEnabled", false, true, false, false);
+ await checkRealState("enabled", true, false, false);
+ await browser.browserAction.enable();
+ await checkProperty("isEnabled", true, true, true, true);
+ await checkRealState("enabled", true, true, true);
+ await browser.browserAction.disable();
+ await checkProperty("isEnabled", false, true, false, false);
+ await checkRealState("enabled", true, false, false);
+ await browser.browserAction.disable(tabIDs[0]);
+ await checkProperty("isEnabled", false, false, false, false);
+ await checkRealState("enabled", false, false, false);
+ await browser.browserAction.enable();
+ await checkProperty("isEnabled", true, false, true, true);
+ await checkRealState("enabled", false, true, true);
+
+ // Test title property (since a label has not been set, this sets the
+ // tooltip and the actual label of the button).
+ await checkProperty(
+ "getTitle",
+ "default",
+ "default",
+ "default",
+ "default"
+ );
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "default", "default", "default");
+ await browser.browserAction.setTitle({ tabId: tabIDs[0], title: "tab0" });
+ await checkProperty("getTitle", "default", "tab0", "default", "default");
+ await checkRealState("tooltip", "tab0", "default", "default");
+ await checkRealState("label", "tab0", "default", "default");
+ await browser.browserAction.setTitle({ title: "new" });
+ await checkProperty("getTitle", "new", "tab0", "new", "new");
+ await checkRealState("tooltip", "tab0", "new", "new");
+ await checkRealState("label", "tab0", "new", "new");
+ await browser.browserAction.setTitle({ tabId: tabIDs[1], title: "tab1" });
+ await checkProperty("getTitle", "new", "tab0", "tab1", "new");
+ await checkRealState("tooltip", "tab0", "tab1", "new");
+ await checkRealState("label", "tab0", "tab1", "new");
+ await browser.browserAction.setTitle({ tabId: tabIDs[0], title: null });
+ await checkProperty("getTitle", "new", "new", "tab1", "new");
+ await checkRealState("tooltip", "new", "tab1", "new");
+ await checkRealState("label", "new", "tab1", "new");
+ await browser.browserAction.setTitle({ title: null });
+ await checkProperty("getTitle", "default", "default", "tab1", "default");
+ await checkRealState("tooltip", "default", "tab1", "default");
+ await checkRealState("label", "default", "tab1", "default");
+ await browser.browserAction.setTitle({ tabId: tabIDs[1], title: null });
+ await checkProperty(
+ "getTitle",
+ "default",
+ "default",
+ "default",
+ "default"
+ );
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "default", "default", "default");
+
+ // Test label property (tooltip should not change).
+ await checkProperty("getLabel", null, null, null, null);
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "default", "default", "default");
+ await browser.browserAction.setLabel({ tabId: tabIDs[0], label: "" });
+ await checkProperty("getLabel", null, "", null, null);
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "", "default", "default");
+ await browser.browserAction.setLabel({ tabId: tabIDs[0], label: "tab0" });
+ await checkProperty("getLabel", null, "tab0", null, null);
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "tab0", "default", "default");
+ await browser.browserAction.setLabel({ label: "new" });
+ await checkProperty("getLabel", "new", "tab0", "new", "new");
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "tab0", "new", "new");
+ await browser.browserAction.setLabel({ tabId: tabIDs[1], label: "tab1" });
+ await checkProperty("getLabel", "new", "tab0", "tab1", "new");
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "tab0", "tab1", "new");
+ await browser.browserAction.setLabel({ tabId: tabIDs[0], label: null });
+ await checkProperty("getLabel", "new", "new", "tab1", "new");
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "new", "tab1", "new");
+ await browser.browserAction.setLabel({ label: null });
+ await checkProperty("getLabel", null, null, "tab1", null);
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "default", "tab1", "default");
+ await browser.browserAction.setLabel({ tabId: tabIDs[1], label: null });
+ await checkProperty("getLabel", null, null, null, null);
+ await checkRealState("tooltip", "default", "default", "default");
+ await checkRealState("label", "default", "default", "default");
+
+ await browser.tabs.remove(tabIDs[1]);
+ await browser.tabs.remove(tabIDs[2]);
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: {
+ id: "browser_action_properties@mochi.test",
+ },
+ },
+ background: { scripts: ["utils.js", "background.js"] },
+ browser_action: {
+ default_title: "default",
+ },
+ },
+ });
+
+ let tabmail = document.getElementById("tabmail");
+ tabmail.openTab("mail3PaneTab", {
+ folderURI: rootFolder.URI,
+ background: false,
+ });
+ tabmail.openTab("mail3PaneTab", {
+ folderURI: rootFolder.URI,
+ background: false,
+ });
+
+ let mailTabs = tabmail.tabInfo;
+ is(mailTabs.length, 3, "Expect 3 tabs");
+ tabmail.switchToTab(mailTabs[0]);
+
+ await extension.startup();
+
+ let button = document.querySelector(
+ `.unified-toolbar [extension="browser_action_properties@mochi.test"]`
+ );
+
+ extension.onMessage("checkProperty", async (property, expected) => {
+ for (let i = 0; i < 3; i++) {
+ tabmail.switchToTab(mailTabs[i]);
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ switch (property) {
+ case "enabled":
+ is(button.disabled, !expected[i], `button ${i} enabled state`);
+ break;
+ case "tooltip":
+ is(
+ button.getAttribute("title"),
+ expected[i],
+ `button ${i} tooltip title`
+ );
+ break;
+ case "label":
+ if (expected[i] == "") {
+ ok(
+ button.classList.contains("prefer-icon-only"),
+ `button ${i} has hidden label`
+ );
+ } else {
+ is(button.getAttribute("label"), expected[i], `button ${i} label`);
+ }
+ break;
+ }
+ }
+
+ tabmail.switchToTab(mailTabs[0]);
+ extension.sendMessage();
+ });
+
+ extension.onMessage("checkPropertyCurrent", async (property, expected) => {
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ switch (property) {
+ case "enabled":
+ is(button.disabled, !expected[0], `button 0 enabled state`);
+ break;
+ case "tooltip":
+ is(button.getAttribute("title"), expected[0], `button 0 tooltip title`);
+ break;
+ case "label":
+ if (expected[0] == "") {
+ ok(
+ button.classList.contains("prefer-icon-only"),
+ `button 0 has hidden label`
+ );
+ } else {
+ is(button.getAttribute("label"), expected[0], `button 0 label`);
+ }
+ break;
+ }
+
+ extension.sendMessage();
+ });
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ tabmail.closeTab(mailTabs[2]);
+ tabmail.closeTab(mailTabs[1]);
+ is(tabmail.tabInfo.length, 1);
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_bug1812530.js b/comm/mail/components/extensions/test/browser/browser_ext_bug1812530.js
new file mode 100644
index 0000000000..1042ae5bbf
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_bug1812530.js
@@ -0,0 +1,200 @@
+/* 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/. *
+ */
+
+// Load subscript shared with all menu tests.
+Services.scriptloader.loadSubScript(
+ new URL("head_menus.js", gTestPath).href,
+ this
+);
+
+let { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+/** @implements {nsIExternalProtocolService} */
+let mockExternalProtocolService = {
+ _loadedURLs: [],
+ externalProtocolHandlerExists(protocolScheme) {},
+ getApplicationDescription(scheme) {},
+ getProtocolHandlerInfo(protocolScheme) {},
+ getProtocolHandlerInfoFromOS(protocolScheme, found) {},
+ isExposedProtocol(protocolScheme) {},
+ loadURI(uri, windowContext) {
+ this._loadedURLs.push(uri.spec);
+ },
+ setProtocolHandlerDefaults(handlerInfo, osHandlerExists) {},
+ urlLoaded(url) {
+ return this._loadedURLs.includes(url);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIExternalProtocolService"]),
+};
+
+let mockExternalProtocolServiceCID = MockRegistrar.register(
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ mockExternalProtocolService
+);
+
+add_setup(async () => {
+ let account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ let subFolders = rootFolder.subFolders;
+ createMessages(subFolders[0], 10);
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: subFolders[0],
+ messagePaneVisible: true,
+ });
+ about3Pane.threadTree.selectedIndex = 0;
+ await awaitBrowserLoaded(
+ about3Pane.messageBrowser.contentWindow.getMessagePaneBrowser()
+ );
+});
+
+registerCleanupFunction(() => {
+ MockRegistrar.unregister(mockExternalProtocolServiceCID);
+});
+
+const subtest_clickOpenInBrowserContextMenu = async (extension, getBrowser) => {
+ async function contextClick(elementSelector, browser) {
+ await awaitBrowserLoaded(browser, url => url != "about:blank");
+
+ let menuId = browser.getAttribute("context");
+ let menu = browser.ownerGlobal.top.document.getElementById(menuId);
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ await rightClickOnContent(menu, elementSelector, browser);
+ Assert.ok(
+ menu.querySelector("#browserContext-openInBrowser"),
+ "menu item should exist"
+ );
+ menu.activateItem(menu.querySelector("#browserContext-openInBrowser"));
+ await hiddenPromise;
+ }
+
+ await extension.startup();
+
+ // Wait for click on #description
+ {
+ let { elementSelector, url } = await extension.awaitMessage("contextClick");
+ Assert.equal(
+ "#description",
+ elementSelector,
+ `Test should click on the correct element.`
+ );
+ Assert.equal(
+ "https://example.org/browser/comm/mail/components/extensions/test/browser/data/content.html",
+ url,
+ `Test should open the correct page.`
+ );
+ await contextClick(elementSelector, getBrowser());
+ Assert.ok(
+ mockExternalProtocolService.urlLoaded(url),
+ `Page should have correctly been opened in external browser.`
+ );
+ await extension.sendMessage();
+ }
+
+ await extension.awaitFinish();
+ await extension.unload();
+};
+
+add_task(async function test_tabs() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "utils.js": await getUtilsJS(),
+ "background.js": async () => {
+ // Open remote file and re-open it in the browser.
+ const url =
+ "https://example.org/browser/comm/mail/components/extensions/test/browser/data/content.html";
+ const elementSelector = "#description";
+
+ let testTab = await browser.tabs.create({ url });
+ await window.sendMessage("contextClick", { elementSelector, url });
+ await browser.tabs.remove(testTab.id);
+
+ browser.test.notifyPass();
+ },
+ },
+ manifest: {
+ background: {
+ scripts: ["utils.js", "background.js"],
+ },
+ permissions: ["tabs"],
+ },
+ });
+
+ await subtest_clickOpenInBrowserContextMenu(
+ extension,
+ () => document.getElementById("tabmail").currentTabInfo.browser
+ );
+});
+
+add_task(async function test_windows() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "utils.js": await getUtilsJS(),
+ "background.js": async () => {
+ // Open remote file and re-open it in the browser.
+ const url =
+ "https://example.org/browser/comm/mail/components/extensions/test/browser/data/content.html";
+ const elementSelector = "#description";
+
+ let testWindow = await browser.windows.create({ type: "popup", url });
+ await window.sendMessage("contextClick", { elementSelector, url });
+ await browser.windows.remove(testWindow.id);
+
+ browser.test.notifyPass();
+ },
+ },
+ manifest: {
+ background: {
+ scripts: ["utils.js", "background.js"],
+ },
+ permissions: ["tabs"],
+ },
+ });
+
+ await subtest_clickOpenInBrowserContextMenu(
+ extension,
+ () => Services.wm.getMostRecentWindow("mail:extensionPopup").browser
+ );
+});
+
+add_task(async function test_mail3pane() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "utils.js": await getUtilsJS(),
+ "background.js": async () => {
+ // Open remote file and re-open it in the browser.
+ const url =
+ "https://example.org/browser/comm/mail/components/extensions/test/browser/data/content.html";
+ const elementSelector = "#description";
+
+ let mailTabs = await browser.tabs.query({ type: "mail" });
+ browser.test.assertEq(
+ 1,
+ mailTabs.length,
+ "Should find a single mailTab"
+ );
+ await browser.tabs.update(mailTabs[0].id, { url });
+ await window.sendMessage("contextClick", { elementSelector, url });
+
+ browser.test.notifyPass();
+ },
+ },
+ manifest: {
+ background: {
+ scripts: ["utils.js", "background.js"],
+ },
+ permissions: ["tabs"],
+ },
+ });
+
+ await subtest_clickOpenInBrowserContextMenu(
+ extension,
+ () => document.getElementById("tabmail").currentTabInfo.browser
+ );
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_clickHandler.js b/comm/mail/components/extensions/test/browser/browser_ext_clickHandler.js
new file mode 100644
index 0000000000..504de75218
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_clickHandler.js
@@ -0,0 +1,614 @@
+/* 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 { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+/** @implements {nsIExternalProtocolService} */
+let mockExternalProtocolService = {
+ _loadedURLs: [],
+ externalProtocolHandlerExists(protocolScheme) {},
+ getApplicationDescription(scheme) {},
+ getProtocolHandlerInfo(protocolScheme) {},
+ getProtocolHandlerInfoFromOS(protocolScheme, found) {},
+ isExposedProtocol(protocolScheme) {},
+ loadURI(uri, windowContext) {
+ this._loadedURLs.push(uri.spec);
+ },
+ setProtocolHandlerDefaults(handlerInfo, osHandlerExists) {},
+ urlLoaded(url) {
+ let rv = this._loadedURLs.length == 1 && this._loadedURLs[0] == url;
+ this._loadedURLs = [];
+ return rv;
+ },
+ hasAnyUrlLoaded() {
+ let rv = this._loadedURLs.length > 0;
+ this._loadedURLs = [];
+ return rv;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIExternalProtocolService"]),
+};
+let mockExternalProtocolServiceCID = MockRegistrar.register(
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ mockExternalProtocolService
+);
+
+registerCleanupFunction(() => {
+ MockRegistrar.unregister(mockExternalProtocolServiceCID);
+});
+
+const getCommonFiles = async () => {
+ return {
+ "utils.js": await getUtilsJS(),
+ "common.js": () => {
+ window.CreateTabPromise = class {
+ constructor() {
+ this.promise = new Promise(resolve => {
+ let createListener = tab => {
+ browser.tabs.onCreated.removeListener(createListener);
+ resolve(tab);
+ };
+ browser.tabs.onCreated.addListener(createListener);
+ });
+ }
+ async done() {
+ return this.promise;
+ }
+ };
+
+ window.UpdateTabPromise = class {
+ constructor() {
+ this.promise = new Promise(resolve => {
+ let log = {};
+ let updateListener = (tabId, changes, tab) => {
+ if (changes.url == "about:blank") {
+ // Reset whatever we have seen so far.
+ log = {};
+ } else {
+ if (changes.url) {
+ log.url = changes.url;
+ }
+ if (changes.status == "loading") {
+ log.loading = true;
+ }
+ // The complete is only valid, if we seen a url (which was not
+ // "about:blank")
+ if (log.url && changes.status == "complete") {
+ log.complete = true;
+ }
+ }
+ if (log.id && log.id != tabId) {
+ browser.test.fail(
+ "Should not receive update events for multiple tabs"
+ );
+ }
+ log.id = tabId;
+
+ if (log.url && log.loading && log.complete) {
+ browser.tabs.onUpdated.removeListener(updateListener);
+ resolve(log);
+ }
+ };
+ browser.tabs.onUpdated.addListener(updateListener);
+ });
+ }
+ async verify(id, url) {
+ // The updatePromise resolves after we have seen both states (loading
+ // and complete) and a url.
+ let updateLog = await this.promise;
+ browser.test.assertEq(
+ id,
+ updateLog.id,
+ "Updates must belong to the current tab"
+ );
+ browser.test.assertEq(
+ url,
+ updateLog.url,
+ "Should have seen the correct url loaded."
+ );
+ }
+ };
+ },
+ "background.js": async () => {
+ let expectedLinkHandler = await window.sendMessage("expectedLinkHandler");
+
+ // Open local file and click link to a different site.
+ await window.expectLinkOpenInExternalBrowser(
+ browser.runtime.getURL("test.html"),
+ "#link1",
+ "https://www.example.de/"
+ );
+
+ // Open local file and click same site link (no target).
+ await window.expectLinkOpenInSameTab(
+ browser.runtime.getURL("test.html"),
+ "#link2",
+ browser.runtime.getURL("example.html")
+ );
+
+ // Open local file and click same site link ("_self" target).
+ await window.expectLinkOpenInSameTab(
+ browser.runtime.getURL("test.html"),
+ "#link3",
+ browser.runtime.getURL("example.html#self")
+ );
+
+ // Open local file and click same site link ("_blank" target).
+ await window.expectLinkOpenInNewTab(
+ browser.runtime.getURL("test.html"),
+ "#link4",
+ browser.runtime.getURL("example.html#blank")
+ );
+
+ // Open local file and click same site link ("_other" target).
+ await window.expectLinkOpenInNewTab(
+ browser.runtime.getURL("test.html"),
+ "#link5",
+ browser.runtime.getURL("example.html#other")
+ );
+
+ // Open a remote page and click link on same site.
+ if (expectedLinkHandler == "single-page") {
+ await window.expectLinkOpenInExternalBrowser(
+ "https://example.org/browser/comm/mail/components/extensions/test/browser/data/linktest.html",
+ "#linkExt1",
+ "https://example.org/browser/comm/mail/components/extensions/test/browser/data/content.html"
+ );
+ } else {
+ await window.expectLinkOpenInSameTab(
+ "https://example.org/browser/comm/mail/components/extensions/test/browser/data/linktest.html",
+ "#linkExt1",
+ "https://example.org/browser/comm/mail/components/extensions/test/browser/data/content.html"
+ );
+ }
+
+ // Open a remote page and click link to a different site.
+ await window.expectLinkOpenInExternalBrowser(
+ "https://example.org/browser/comm/mail/components/extensions/test/browser/data/linktest.html",
+ "#linkExt2",
+ "https://mozilla.org/"
+ );
+
+ browser.test.notifyPass();
+ },
+ "example.html": `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <title>EXAMPLE</title>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ </head>
+ <body>
+ <p>This is an example page</p>
+ </body>
+ </html>`,
+ "test.html": `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <title>TEST</title>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ </head>
+ <body>
+ <ul>
+ <li><a id="link1" href="https://www.example.de/">external</a>
+ <li><a id="link2" href="example.html">no target</a>
+ <li><a id="link3" href="example.html#self" target = "_self">_self target</a>
+ <li><a id="link4" href="example.html#blank" target = "_blank">_blank target</a>
+ <li><a id="link5" href="example.html#other" target = "_other">_other target</a>
+ </ul>
+ </body>
+ </html>`,
+ };
+};
+
+const subtest_clickInBrowser = async (
+ extension,
+ expectedLinkHandler,
+ getBrowser
+) => {
+ async function clickLink(linkId, browser) {
+ await awaitBrowserLoaded(browser, url => url != "about:blank");
+ await synthesizeMouseAtCenterAndRetry(linkId, {}, browser);
+ }
+
+ await extension.startup();
+
+ await extension.awaitMessage("expectedLinkHandler");
+ extension.sendMessage(expectedLinkHandler);
+
+ // Wait for click on #link1 (external)
+ {
+ let { linkId, expectedUrl } = await extension.awaitMessage("click");
+ Assert.equal("#link1", linkId, `Test should click on the correct link.`);
+ Assert.equal(
+ "https://www.example.de/",
+ expectedUrl,
+ `Test should open the correct link.`
+ );
+ await clickLink(linkId, getBrowser());
+ Assert.ok(
+ mockExternalProtocolService.urlLoaded(expectedUrl),
+ `Link should have correctly been opened in external browser.`
+ );
+ await extension.sendMessage();
+ }
+
+ // Wait for click on #link2 (same tab)
+ {
+ let { linkId } = await extension.awaitMessage("click");
+ Assert.equal("#link2", linkId, `Test should click on the correct link.`);
+ await clickLink(linkId, getBrowser());
+ Assert.ok(
+ !mockExternalProtocolService.hasAnyUrlLoaded(),
+ `Link should not have been opened in external browser.`
+ );
+ await extension.sendMessage();
+ }
+
+ // Wait for click on #link3 (same tab)
+ {
+ let { linkId } = await extension.awaitMessage("click");
+ Assert.equal("#link3", linkId, `Test should click on the correct link.`);
+ await clickLink(linkId, getBrowser());
+ Assert.ok(
+ !mockExternalProtocolService.hasAnyUrlLoaded(),
+ `Link should not have been opened in external browser.`
+ );
+ await extension.sendMessage();
+ }
+
+ // Wait for click on #link4 (new tab)
+ {
+ let { linkId } = await extension.awaitMessage("click");
+ Assert.equal("#link4", linkId, `Test should click on the correct link.`);
+ await clickLink(linkId, getBrowser());
+ Assert.ok(
+ !mockExternalProtocolService.hasAnyUrlLoaded(),
+ `Link should not have been opened in external browser.`
+ );
+ await extension.sendMessage();
+ }
+
+ // Wait for click on #link5 (new tab)
+ {
+ let { linkId } = await extension.awaitMessage("click");
+ Assert.equal("#link5", linkId, `Test should click on the correct link.`);
+ await clickLink(linkId, getBrowser());
+ Assert.ok(
+ !mockExternalProtocolService.hasAnyUrlLoaded(),
+ `Link should not have been opened in external browser.`
+ );
+ await extension.sendMessage();
+ }
+
+ // Wait for click on #linkExt1
+ if (expectedLinkHandler == "single-page") {
+ // Should open extern with single-page link handler.
+ let { linkId, expectedUrl } = await extension.awaitMessage("click");
+ Assert.equal("#linkExt1", linkId, `Test should click on the correct link.`);
+ Assert.equal(
+ "https://example.org/browser/comm/mail/components/extensions/test/browser/data/content.html",
+ expectedUrl,
+ `Test should open the correct link.`
+ );
+ await clickLink(linkId, getBrowser());
+ Assert.ok(
+ mockExternalProtocolService.urlLoaded(expectedUrl),
+ `Link should have correctly been opened in external browser.`
+ );
+ await extension.sendMessage();
+ } else {
+ // Should open in same tab with single-site link handler.
+ let { linkId } = await extension.awaitMessage("click");
+ Assert.equal("#linkExt1", linkId, `Test should click on the correct link.`);
+ await clickLink(linkId, getBrowser());
+ Assert.ok(
+ !mockExternalProtocolService.hasAnyUrlLoaded(),
+ `Link should not have been opened in external browser.`
+ );
+ await extension.sendMessage();
+ }
+
+ // Wait for click on #linkExt2 (external)
+ {
+ let { linkId, expectedUrl } = await extension.awaitMessage("click");
+ Assert.equal("#linkExt2", linkId, `Test should click on the correct link.`);
+ Assert.equal(
+ "https://mozilla.org/",
+ expectedUrl,
+ `Test should open the correct link.`
+ );
+ await clickLink(linkId, getBrowser());
+ Assert.ok(
+ mockExternalProtocolService.urlLoaded(expectedUrl),
+ `Link should have correctly been opened in external browser.`
+ );
+ await extension.sendMessage();
+ }
+
+ await extension.awaitFinish();
+ await extension.unload();
+};
+
+add_task(async function test_tabs() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "tabFunctions.js": async () => {
+ let openTestTab = async url => {
+ let createdTestTab = new window.CreateTabPromise();
+ let updatedTestTab = new window.UpdateTabPromise();
+ let testTab = await browser.tabs.create({ url });
+ await createdTestTab.done();
+ await updatedTestTab.verify(testTab.id, url);
+ return testTab;
+ };
+
+ window.expectLinkOpenInNewTab = async (
+ testUrl,
+ linkId,
+ expectedUrl
+ ) => {
+ let testTab = await openTestTab(testUrl);
+
+ // Click a link in testTab to open a new tab.
+ let createdNewTab = new window.CreateTabPromise();
+ let updatedNewTab = new window.UpdateTabPromise();
+ await window.sendMessage("click", { linkId });
+ let createdTab = await createdNewTab.done();
+ await updatedNewTab.verify(createdTab.id, expectedUrl);
+
+ await browser.tabs.remove(createdTab.id);
+ await browser.tabs.remove(testTab.id);
+ };
+
+ window.expectLinkOpenInSameTab = async (
+ testUrl,
+ linkId,
+ expectedUrl
+ ) => {
+ let testTab = await openTestTab(testUrl);
+
+ // Click a link in testTab to open in self.
+ let updatedTab = new window.UpdateTabPromise();
+ await window.sendMessage("click", { linkId });
+ await updatedTab.verify(testTab.id, expectedUrl);
+
+ await browser.tabs.remove(testTab.id);
+ };
+
+ window.expectLinkOpenInExternalBrowser = async (
+ testUrl,
+ linkId,
+ expectedUrl
+ ) => {
+ let testTab = await openTestTab(testUrl);
+ await window.sendMessage("click", { linkId, expectedUrl });
+ await browser.tabs.remove(testTab.id);
+ };
+ },
+ ...(await getCommonFiles()),
+ },
+ manifest: {
+ background: {
+ scripts: ["utils.js", "common.js", "tabFunctions.js", "background.js"],
+ },
+ permissions: ["tabs"],
+ },
+ });
+
+ await subtest_clickInBrowser(
+ extension,
+ "single-site",
+ () => document.getElementById("tabmail").currentTabInfo.browser
+ );
+}).skip(AppConstants.DEBUG); // Disabled until Bug 1770105 is fully fixed.
+
+add_task(async function test_windows() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "windowFunctions.js": async () => {
+ let openTestTab = async url => {
+ let createdTestTab = new window.CreateTabPromise();
+ let updatedTestTab = new window.UpdateTabPromise();
+ let testWindow = await browser.windows.create({ type: "popup", url });
+ await createdTestTab.done();
+
+ let [testTab] = await browser.tabs.query({ windowId: testWindow.id });
+ await updatedTestTab.verify(testTab.id, url);
+ return testTab;
+ };
+
+ window.expectLinkOpenInNewTab = async (
+ testUrl,
+ linkId,
+ expectedUrl
+ ) => {
+ let testTab = await openTestTab(testUrl);
+
+ // Click a link in testWindow to open a new tab.
+ let createdNewTab = new window.CreateTabPromise();
+ let updatedNewTab = new window.UpdateTabPromise();
+ await window.sendMessage("click", { linkId });
+ let createdTab = await createdNewTab.done();
+ await updatedNewTab.verify(createdTab.id, expectedUrl);
+
+ await browser.tabs.remove(createdTab.id);
+ await browser.tabs.remove(testTab.id);
+ };
+
+ window.expectLinkOpenInSameTab = async (
+ testUrl,
+ linkId,
+ expectedUrl
+ ) => {
+ let testTab = await openTestTab(testUrl);
+
+ // Click a link in testWindow to open in self.
+ let updatedTab = new window.UpdateTabPromise();
+ await window.sendMessage("click", { linkId });
+ await updatedTab.verify(testTab.id, expectedUrl);
+ await browser.tabs.remove(testTab.id);
+ };
+
+ window.expectLinkOpenInExternalBrowser = async (
+ testUrl,
+ linkId,
+ expectedUrl
+ ) => {
+ let testTab = await openTestTab(testUrl);
+ await window.sendMessage("click", { linkId, expectedUrl });
+ await browser.tabs.remove(testTab.id);
+ };
+ },
+ ...(await getCommonFiles()),
+ },
+ manifest: {
+ background: {
+ scripts: [
+ "utils.js",
+ "common.js",
+ "windowFunctions.js",
+ "background.js",
+ ],
+ },
+ permissions: ["tabs"],
+ },
+ });
+
+ await subtest_clickInBrowser(
+ extension,
+ "single-site",
+ () => Services.wm.getMostRecentWindow("mail:extensionPopup").browser
+ );
+}).skip(AppConstants.DEBUG); // Disabled until Bug 1770105 is fully fixed.
+
+add_task(async function test_mail3pane() {
+ let account = createAccount();
+ let subFolders = account.incomingServer.rootFolder.subFolders;
+ createMessages(subFolders[0], 1);
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ Assert.ok(Boolean(about3Pane), "about:3pane should be the current tab");
+ about3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: subFolders[0],
+ messagePaneVisible: true,
+ });
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ about3Pane.messageBrowser.contentWindow.getMessagePaneBrowser()
+ );
+ about3Pane.threadTree.selectedIndex = 0;
+ await loadedPromise;
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "mail3paneFunctions.js": async () => {
+ let updateTestTab = async url => {
+ let updatedTestTab = new window.UpdateTabPromise();
+ let mailTabs = await browser.tabs.query({ type: "mail" });
+ browser.test.assertEq(
+ 1,
+ mailTabs.length,
+ "Should find a single mailTab"
+ );
+ await browser.tabs.update(mailTabs[0].id, { url });
+ await updatedTestTab.verify(mailTabs[0].id, url);
+ return mailTabs[0];
+ };
+
+ window.expectLinkOpenInNewTab = async (
+ testUrl,
+ linkId,
+ expectedUrl
+ ) => {
+ await updateTestTab(testUrl);
+
+ // Click a link in testTab to open a new tab.
+ let createdNewTab = new window.CreateTabPromise();
+ let updatedNewTab = new window.UpdateTabPromise();
+ await window.sendMessage("click", { linkId });
+ let createdTab = await createdNewTab.done();
+ await updatedNewTab.verify(createdTab.id, expectedUrl);
+
+ await browser.tabs.remove(createdTab.id);
+ };
+
+ window.expectLinkOpenInSameTab = async (
+ testUrl,
+ linkId,
+ expectedUrl
+ ) => {
+ let testTab = await updateTestTab(testUrl);
+
+ // Click a link in testTab to open in self.
+ let updatedTab = new window.UpdateTabPromise();
+ await window.sendMessage("click", { linkId });
+ await updatedTab.verify(testTab.id, expectedUrl);
+ };
+
+ window.expectLinkOpenInExternalBrowser = async (
+ testUrl,
+ linkId,
+ expectedUrl
+ ) => {
+ await updateTestTab(testUrl);
+ await window.sendMessage("click", { linkId, expectedUrl });
+ };
+ },
+ ...(await getCommonFiles()),
+ },
+ manifest: {
+ background: {
+ scripts: [
+ "utils.js",
+ "common.js",
+ "mail3paneFunctions.js",
+ "background.js",
+ ],
+ },
+ permissions: ["tabs"],
+ },
+ });
+
+ await subtest_clickInBrowser(
+ extension,
+ "single-page",
+ () => document.getElementById("tabmail").currentTabInfo.browser
+ );
+}).skip(AppConstants.DEBUG); // Disabled until Bug 1770105 is fully fixed.
+
+// This is actually not an extension test, but everything we need is here already
+// and we only want to simulate a click on a link in a message.
+add_task(async function test_message() {
+ let gAccount = createAccount();
+ let gRootFolder = gAccount.incomingServer.rootFolder;
+ gRootFolder.createSubfolder("test0", null);
+
+ let subFolders = {};
+ for (let folder of gRootFolder.subFolders) {
+ subFolders[folder.name] = folder;
+ }
+ await createMessageFromFile(
+ subFolders.test0,
+ getTestFilePath("messages/messageWithLink.eml")
+ );
+
+ // Select the message which has a link.
+ let gFolder = subFolders.test0;
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(gFolder.URI);
+ let messagePane =
+ about3Pane.messageBrowser.contentWindow.getMessagePaneBrowser();
+ let loadedPromise = BrowserTestUtils.browserLoaded(messagePane);
+ about3Pane.threadTree.selectedIndex = 0;
+ await loadedPromise;
+
+ // Click the link.
+ await synthesizeMouseAtCenterAndRetry("#link", {}, messagePane);
+ Assert.ok(
+ mockExternalProtocolService.urlLoaded(
+ "https://www.example.de/messageLink.html"
+ ),
+ `Link should have correctly been opened in external browser.`
+ );
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_cloudFile.js b/comm/mail/components/extensions/test/browser/browser_ext_cloudFile.js
new file mode 100644
index 0000000000..2e9b53916c
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_cloudFile.js
@@ -0,0 +1,1444 @@
+/* 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";
+
+var { cloudFileAccounts } = ChromeUtils.import(
+ "resource:///modules/cloudFileAccounts.jsm"
+);
+
+/**
+ * Test cloudfile methods (getAccount, getAllAccounts, updateAccount) and
+ * events (onAccountAdded, onAccountDeleted, onFileUpload, onFileUploadAbort,
+ * onFileDeleted, onFileRename) without UI interaction.
+ */
+add_task(async function test_without_UI() {
+ async function background() {
+ function createCloudfileAccount() {
+ let addListener = window.waitForEvent("cloudFile.onAccountAdded");
+ browser.test.sendMessage("createAccount");
+ return addListener;
+ }
+
+ function removeCloudfileAccount(id) {
+ let deleteListener = window.waitForEvent("cloudFile.onAccountDeleted");
+ browser.test.sendMessage("removeAccount", id);
+ return deleteListener;
+ }
+
+ function assertAccountsMatch(b, a) {
+ browser.test.assertEq(a.id, b.id);
+ browser.test.assertEq(a.name, b.name);
+ browser.test.assertEq(a.configured, b.configured);
+ browser.test.assertEq(a.uploadSizeLimit, b.uploadSizeLimit);
+ browser.test.assertEq(a.spaceRemaining, b.spaceRemaining);
+ browser.test.assertEq(a.spaceUsed, b.spaceUsed);
+ browser.test.assertEq(a.managementUrl, b.managementUrl);
+ }
+
+ async function test_account_creation_removal() {
+ browser.test.log("test_account_creation_removal");
+ // Account creation
+ let [createdAccount] = await createCloudfileAccount();
+ assertAccountsMatch(createdAccount, {
+ id: "account1",
+ name: "mochitest",
+ configured: false,
+ uploadSizeLimit: -1,
+ spaceRemaining: -1,
+ spaceUsed: -1,
+ managementUrl: browser.runtime.getURL("/content/management.html"),
+ });
+
+ // Other account creation
+ await new Promise((resolve, reject) => {
+ function accountListener(account) {
+ browser.cloudFile.onAccountAdded.removeListener(accountListener);
+ browser.test.fail("Got onAccountAdded for account from other addon");
+ reject();
+ }
+
+ browser.cloudFile.onAccountAdded.addListener(accountListener);
+ browser.test.sendMessage("createAccount", "ext-other-addon");
+
+ // Resolve in the next tick
+ setTimeout(() => {
+ browser.cloudFile.onAccountAdded.removeListener(accountListener);
+ resolve();
+ });
+ });
+
+ // Account removal
+ let [removedAccountId] = await removeCloudfileAccount(createdAccount.id);
+ browser.test.assertEq(createdAccount.id, removedAccountId);
+ }
+
+ async function test_getters_update() {
+ browser.test.log("test_getters_update");
+ browser.test.sendMessage("createAccount", "ext-other-addon");
+
+ let [createdAccount] = await createCloudfileAccount();
+
+ // getAccount and getAllAccounts
+ let retrievedAccount = await browser.cloudFile.getAccount(
+ createdAccount.id
+ );
+ assertAccountsMatch(createdAccount, retrievedAccount);
+
+ let retrievedAccounts = await browser.cloudFile.getAllAccounts();
+ browser.test.assertEq(retrievedAccounts.length, 1);
+ assertAccountsMatch(createdAccount, retrievedAccounts[0]);
+
+ // update()
+ let changes = {
+ configured: true,
+ // uploadSizeLimit intentionally left unset
+ spaceRemaining: 456,
+ spaceUsed: 789,
+ managementUrl: "/account.html",
+ };
+
+ let changedAccount = await browser.cloudFile.updateAccount(
+ retrievedAccount.id,
+ changes
+ );
+ retrievedAccount = await browser.cloudFile.getAccount(createdAccount.id);
+
+ let expected = {
+ id: createdAccount.id,
+ name: "mochitest",
+ configured: true,
+ uploadSizeLimit: -1,
+ spaceRemaining: 456,
+ spaceUsed: 789,
+ managementUrl: browser.runtime.getURL("/account.html"),
+ };
+
+ assertAccountsMatch(changedAccount, expected);
+ assertAccountsMatch(retrievedAccount, expected);
+
+ await removeCloudfileAccount(createdAccount.id);
+ }
+
+ async function test_upload_rename_delete() {
+ browser.test.log("test_upload_rename_delete");
+ let [createdAccount] = await createCloudfileAccount();
+
+ let fileId = await new Promise(resolve => {
+ async function fileListener(
+ account,
+ { id, name, data },
+ tab,
+ relatedFileInfo
+ ) {
+ browser.cloudFile.onFileUpload.removeListener(fileListener);
+ browser.test.assertEq(account.id, createdAccount.id);
+ browser.test.assertEq(name, "cloudFile1.txt");
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(data instanceof File);
+ let content = await data.text();
+ browser.test.assertEq(content, "you got the moves!\n");
+ browser.test.assertEq(undefined, relatedFileInfo);
+ setTimeout(() => resolve(id));
+ return { url: "https://example.com/" + name };
+ }
+
+ browser.cloudFile.onFileUpload.addListener(fileListener);
+ browser.test.sendMessage("uploadFile", createdAccount.id, "cloudFile1");
+ });
+
+ browser.test.log("test upload error");
+ await new Promise(resolve => {
+ function fileListener(
+ account,
+ { id, name, data },
+ tab,
+ relatedFileInfo
+ ) {
+ browser.cloudFile.onFileUpload.removeListener(fileListener);
+ browser.test.assertEq(undefined, relatedFileInfo);
+ setTimeout(() => resolve(id));
+ return { error: true };
+ }
+
+ browser.cloudFile.onFileUpload.addListener(fileListener);
+ browser.test.sendMessage(
+ "uploadFile",
+ createdAccount.id,
+ "cloudFile2",
+ "uploadErr",
+ "Upload error."
+ );
+ });
+
+ browser.test.log("test upload error with message");
+ await new Promise(resolve => {
+ function fileListener(
+ account,
+ { id, name, data },
+ tab,
+ relatedFileInfo
+ ) {
+ browser.cloudFile.onFileUpload.removeListener(fileListener);
+ browser.test.assertEq(undefined, relatedFileInfo);
+ setTimeout(() => resolve(id));
+ return { error: "Service currently unavailable." };
+ }
+
+ browser.cloudFile.onFileUpload.addListener(fileListener);
+ browser.test.sendMessage(
+ "uploadFile",
+ createdAccount.id,
+ "cloudFile2",
+ "uploadErrWithCustomMessage",
+ "Service currently unavailable."
+ );
+ });
+
+ browser.test.log("test upload quota error");
+ await browser.cloudFile.updateAccount(createdAccount.id, {
+ spaceRemaining: 1,
+ });
+ await window.sendMessage(
+ "uploadFileError",
+ createdAccount.id,
+ "cloudFile2",
+ "uploadWouldExceedQuota",
+ "Quota error: Can't upload file. Only 1KB left of quota."
+ );
+ await browser.cloudFile.updateAccount(createdAccount.id, {
+ spaceRemaining: -1,
+ });
+
+ browser.test.log("test upload file size limit error");
+ await browser.cloudFile.updateAccount(createdAccount.id, {
+ uploadSizeLimit: 1,
+ });
+ await window.sendMessage(
+ "uploadFileError",
+ createdAccount.id,
+ "cloudFile2",
+ "uploadExceedsFileLimit",
+ "Upload error: File size is 19KB and exceeds the file size limit of 1KB"
+ );
+ await browser.cloudFile.updateAccount(createdAccount.id, {
+ uploadSizeLimit: -1,
+ });
+
+ browser.test.log("test rename with url update");
+ await new Promise(resolve => {
+ function fileListener(account, id, newName) {
+ browser.cloudFile.onFileRename.removeListener(fileListener);
+ browser.test.assertEq(account.id, createdAccount.id);
+ browser.test.assertEq(newName, "cloudFile3.txt");
+ setTimeout(() => resolve(id));
+ return { url: "https://example.com/" + newName };
+ }
+
+ browser.cloudFile.onFileRename.addListener(fileListener);
+ browser.test.sendMessage("renameFile", createdAccount.id, fileId, {
+ newName: "cloudFile3.txt",
+ newUrl: "https://example.com/cloudFile3.txt",
+ });
+ });
+
+ browser.test.log("test rename without url update");
+ await new Promise(resolve => {
+ function fileListener(account, id, newName) {
+ browser.cloudFile.onFileRename.removeListener(fileListener);
+ browser.test.assertEq(account.id, createdAccount.id);
+ browser.test.assertEq(newName, "cloudFile4.txt");
+ setTimeout(() => resolve(id));
+ }
+
+ browser.cloudFile.onFileRename.addListener(fileListener);
+ browser.test.sendMessage("renameFile", createdAccount.id, fileId, {
+ newName: "cloudFile4.txt",
+ newUrl: "https://example.com/cloudFile3.txt",
+ });
+ });
+
+ browser.test.log("test rename error");
+ await new Promise(resolve => {
+ function fileListener(account, id, newName) {
+ browser.cloudFile.onFileRename.removeListener(fileListener);
+ browser.test.assertEq(account.id, createdAccount.id);
+ browser.test.assertEq(newName, "cloudFile5.txt");
+ setTimeout(() => resolve(id));
+ return { error: true };
+ }
+
+ browser.cloudFile.onFileRename.addListener(fileListener);
+ browser.test.sendMessage(
+ "renameFile",
+ createdAccount.id,
+ fileId,
+ { newName: "cloudFile5.txt" },
+ "renameErr",
+ "Rename error."
+ );
+ });
+
+ browser.test.log("test rename error with message");
+ await new Promise(resolve => {
+ function fileListener(account, id, newName) {
+ browser.cloudFile.onFileRename.removeListener(fileListener);
+ browser.test.assertEq(account.id, createdAccount.id);
+ browser.test.assertEq(newName, "cloudFile5.txt");
+ setTimeout(() => resolve(id));
+ return { error: "Service currently unavailable." };
+ }
+
+ browser.cloudFile.onFileRename.addListener(fileListener);
+ browser.test.sendMessage(
+ "renameFile",
+ createdAccount.id,
+ fileId,
+ { newName: "cloudFile5.txt" },
+ "renameErrWithCustomMessage",
+ "Service currently unavailable."
+ );
+ });
+
+ browser.test.log("test upload aborted");
+ await new Promise(resolve => {
+ async function fileListener(
+ account,
+ { id, name, data },
+ tab,
+ relatedFileInfo
+ ) {
+ browser.cloudFile.onFileUpload.removeListener(fileListener);
+
+ // The listener won't return until onFileUploadAbort fires. When that happens,
+ // we return an aborted message, which completes the abort cycle.
+ await new Promise(resolveAbort => {
+ function abortListener(accountAccount, abortId) {
+ browser.cloudFile.onFileUploadAbort.removeListener(abortListener);
+ browser.test.assertEq(account.id, accountAccount.id);
+ browser.test.assertEq(id, abortId);
+ resolveAbort();
+ }
+ browser.cloudFile.onFileUploadAbort.addListener(abortListener);
+ browser.test.sendMessage("cancelUpload", createdAccount.id);
+ });
+
+ browser.test.assertEq(undefined, relatedFileInfo);
+ setTimeout(resolve);
+ return { aborted: true };
+ }
+
+ browser.cloudFile.onFileUpload.addListener(fileListener);
+ browser.test.sendMessage(
+ "uploadFile",
+ createdAccount.id,
+ "cloudFile2",
+ "uploadCancelled",
+ "Upload cancelled."
+ );
+ });
+
+ browser.test.log("test delete");
+ await new Promise(resolve => {
+ function fileListener(account, id) {
+ browser.cloudFile.onFileDeleted.removeListener(fileListener);
+ browser.test.assertEq(account.id, createdAccount.id);
+ browser.test.assertEq(id, fileId);
+ setTimeout(resolve);
+ }
+
+ browser.cloudFile.onFileDeleted.addListener(fileListener);
+ browser.test.sendMessage("deleteFile", createdAccount.id);
+ });
+
+ await removeCloudfileAccount(createdAccount.id);
+ await new Promise(resolve => setTimeout(resolve));
+ }
+
+ // Tests to run
+ await test_account_creation_removal();
+ await test_getters_update();
+ await test_upload_rename_delete();
+
+ browser.test.notifyPass("cloudFile");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ cloud_file: {
+ name: "mochitest",
+ management_url: "/content/management.html",
+ },
+ applications: { gecko: { id: "cloudfile@mochi.test" } },
+ background: { scripts: ["utils.js", "background.js"] },
+ },
+ });
+
+ let testFiles = {
+ cloudFile1: new FileUtils.File(getTestFilePath("data/cloudFile1.txt")),
+ cloudFile2: new FileUtils.File(getTestFilePath("data/cloudFile2.txt")),
+ };
+
+ let uploads = {};
+
+ extension.onMessage("createAccount", (id = "ext-cloudfile@mochi.test") => {
+ cloudFileAccounts.createAccount(id);
+ });
+
+ extension.onMessage("removeAccount", id => {
+ cloudFileAccounts.removeAccount(id);
+ });
+
+ extension.onMessage(
+ "uploadFileError",
+ async (id, filename, expectedErrorStatus, expectedErrorMessage) => {
+ let account = cloudFileAccounts.getAccount(id);
+
+ let status;
+ try {
+ await account.uploadFile(null, testFiles[filename]);
+ } catch (ex) {
+ status = ex;
+ }
+
+ Assert.ok(
+ !!status,
+ `Upload should have failed for ${testFiles[filename].leafName}`
+ );
+ Assert.equal(
+ status.result,
+ cloudFileAccounts.constants[expectedErrorStatus],
+ `Error status should be correct for ${testFiles[filename].leafName}`
+ );
+ Assert.equal(
+ status.message,
+ expectedErrorMessage,
+ `Error message should be correct for ${testFiles[filename].leafName}`
+ );
+ extension.sendMessage();
+ }
+ );
+
+ extension.onMessage(
+ "uploadFile",
+ (id, filename, expectedErrorStatus = Cr.NS_OK, expectedErrorMessage) => {
+ let account = cloudFileAccounts.getAccount(id);
+
+ if (typeof expectedErrorStatus == "string") {
+ expectedErrorStatus = cloudFileAccounts.constants[expectedErrorStatus];
+ }
+
+ account.uploadFile(null, testFiles[filename]).then(
+ upload => {
+ Assert.equal(Cr.NS_OK, expectedErrorStatus);
+ uploads[filename] = upload;
+ },
+ status => {
+ Assert.equal(
+ status.result,
+ expectedErrorStatus,
+ `Error status should be correct for ${testFiles[filename].leafName}`
+ );
+ Assert.equal(
+ status.message,
+ expectedErrorMessage,
+ `Error message should be correct for ${testFiles[filename].leafName}`
+ );
+ }
+ );
+ }
+ );
+
+ extension.onMessage(
+ "renameFile",
+ (
+ id,
+ uploadId,
+ { newName, newUrl },
+ expectedErrorStatus = Cr.NS_OK,
+ expectedErrorMessage
+ ) => {
+ let account = cloudFileAccounts.getAccount(id);
+
+ if (typeof expectedErrorStatus == "string") {
+ expectedErrorStatus = cloudFileAccounts.constants[expectedErrorStatus];
+ }
+
+ account.renameFile(null, uploadId, newName).then(
+ upload => {
+ Assert.equal(Cr.NS_OK, expectedErrorStatus);
+ Assert.equal(upload.name, newName, "New name should match.");
+ Assert.equal(upload.url, newUrl, "New url should match.");
+ },
+ status => {
+ Assert.equal(status.result, expectedErrorStatus);
+ Assert.equal(
+ status.message,
+ expectedErrorMessage,
+ `Error message should be correct.`
+ );
+ }
+ );
+ }
+ );
+
+ extension.onMessage("cancelUpload", id => {
+ let account = cloudFileAccounts.getAccount(id);
+ account.cancelFileUpload(null, testFiles.cloudFile2);
+ });
+
+ extension.onMessage("deleteFile", id => {
+ let account = cloudFileAccounts.getAccount(id);
+ account.deleteFile(null, uploads.cloudFile1.id);
+ });
+
+ Assert.ok(!cloudFileAccounts.getProviderForType("ext-cloudfile@mochi.test"));
+ await extension.startup();
+ Assert.ok(cloudFileAccounts.getProviderForType("ext-cloudfile@mochi.test"));
+ Assert.equal(cloudFileAccounts.accounts.length, 1);
+
+ await extension.awaitFinish("cloudFile");
+ await extension.unload();
+
+ Assert.ok(!cloudFileAccounts.getProviderForType("ext-cloudfile@mochi.test"));
+ Assert.equal(cloudFileAccounts.accounts.length, 0);
+});
+
+/**
+ * Test the tab parameter in cloudFile.onFileUpload, cloudFile.onFileDeleted,
+ * cloudFile.onFileRename and cloudFile.onFileUploadAbort listeners with UI
+ * interaction.
+ */
+add_task(async function test_compose_window_MV2() {
+ let testFiles = {
+ cloudFile1: new FileUtils.File(getTestFilePath("data/cloudFile1.txt")),
+ cloudFile2: new FileUtils.File(getTestFilePath("data/cloudFile2.txt")),
+ };
+ let uploads = {};
+ let composeWindow;
+
+ async function background() {
+ function createCloudfileAccount(id) {
+ let addListener = window.waitForEvent("cloudFile.onAccountAdded");
+ browser.test.sendMessage("createAccount", id);
+ return addListener;
+ }
+
+ function removeCloudfileAccount(id) {
+ let deleteListener = window.waitForEvent("cloudFile.onAccountDeleted");
+ browser.test.sendMessage("removeAccount", id);
+ return deleteListener;
+ }
+
+ async function test_tab_in_upload_rename_abort_delete_listener(composeTab) {
+ browser.test.log("test_upload_delete");
+ let [createdAccount] = await createCloudfileAccount(
+ "ext-cloudfile@mochi.test"
+ );
+
+ let fileId = await new Promise(resolve => {
+ async function fileListener(
+ uploadAccount,
+ { id, name, data },
+ tab,
+ relatedFileInfo
+ ) {
+ browser.cloudFile.onFileUpload.removeListener(fileListener);
+
+ browser.test.assertEq(tab.id, composeTab.id);
+ browser.test.assertEq(uploadAccount.id, createdAccount.id);
+ browser.test.assertEq(name, "cloudFile1.txt");
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(data instanceof File);
+ let content = await data.text();
+ browser.test.assertEq(content, "you got the moves!\n");
+
+ browser.test.assertEq(undefined, relatedFileInfo);
+ setTimeout(() => resolve(id));
+ return { url: "https://example.com/" + name };
+ }
+
+ browser.cloudFile.onFileUpload.addListener(fileListener);
+ browser.test.sendMessage("uploadFile", createdAccount.id, "cloudFile1");
+ });
+
+ browser.test.log("test rename with Url update");
+ await new Promise(resolve => {
+ function fileListener(account, id, newName, tab) {
+ browser.cloudFile.onFileRename.removeListener(fileListener);
+ browser.test.assertEq(tab.id, composeTab.id);
+ browser.test.assertEq(account.id, createdAccount.id);
+ browser.test.assertEq(newName, "cloudFile3.txt");
+ setTimeout(() => resolve(id));
+ return { url: "https://example.com/" + newName };
+ }
+
+ browser.cloudFile.onFileRename.addListener(fileListener);
+ browser.test.sendMessage("renameFile", createdAccount.id, fileId, {
+ newName: "cloudFile3.txt",
+ newUrl: "https://example.com/cloudFile3.txt",
+ });
+ });
+
+ browser.test.log("test upload aborted");
+ await new Promise(resolve => {
+ async function fileListener(
+ uploadAccount,
+ { id, name, data },
+ tab,
+ relatedFileInfo
+ ) {
+ browser.cloudFile.onFileUpload.removeListener(fileListener);
+ browser.test.assertEq(tab.id, composeTab.id);
+
+ // The listener won't return until onFileUploadAbort fires. When that happens,
+ // we return an aborted message, which completes the abort cycle.
+ await new Promise(resolveAbort => {
+ function abortListener(abortAccount, abortId, tab) {
+ browser.cloudFile.onFileUploadAbort.removeListener(abortListener);
+ browser.test.assertEq(tab.id, composeTab.id);
+ browser.test.assertEq(uploadAccount.id, abortAccount.id);
+ browser.test.assertEq(id, abortId);
+ resolveAbort();
+ }
+ browser.cloudFile.onFileUploadAbort.addListener(abortListener);
+ browser.test.sendMessage("cancelUpload", createdAccount.id);
+ });
+
+ browser.test.assertEq(undefined, relatedFileInfo);
+ setTimeout(resolve);
+ return { aborted: true };
+ }
+
+ browser.cloudFile.onFileUpload.addListener(fileListener);
+ browser.test.sendMessage(
+ "uploadFile",
+ createdAccount.id,
+ "cloudFile2",
+ "uploadCancelled",
+ "Upload cancelled."
+ );
+ });
+
+ browser.test.log("test delete");
+ await new Promise(resolve => {
+ function fileListener(deleteAccount, id, tab) {
+ browser.cloudFile.onFileDeleted.removeListener(fileListener);
+ browser.test.assertEq(tab.id, composeTab.id);
+ browser.test.assertEq(deleteAccount.id, createdAccount.id);
+ browser.test.assertEq(id, fileId);
+ setTimeout(resolve);
+ }
+
+ browser.cloudFile.onFileDeleted.addListener(fileListener);
+ browser.test.sendMessage("deleteFile", createdAccount.id);
+ });
+
+ await removeCloudfileAccount(createdAccount.id);
+ await new Promise(resolve => setTimeout(resolve));
+ }
+
+ let [composerTab] = await browser.tabs.query({
+ windowType: "messageCompose",
+ });
+ await test_tab_in_upload_rename_abort_delete_listener(composerTab);
+
+ browser.test.notifyPass("finished");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ cloud_file: {
+ name: "mochitest",
+ management_url: "/content/management.html",
+ },
+ applications: { gecko: { id: "cloudfile@mochi.test" } },
+ background: { scripts: ["utils.js", "background.js"] },
+ },
+ });
+
+ extension.onMessage("createAccount", id => {
+ cloudFileAccounts.createAccount(id);
+ });
+
+ extension.onMessage("removeAccount", id => {
+ cloudFileAccounts.removeAccount(id);
+ });
+
+ extension.onMessage(
+ "uploadFile",
+ (id, filename, expectedErrorStatus = Cr.NS_OK, expectedErrorMessage) => {
+ let cloudFileAccount = cloudFileAccounts.getAccount(id);
+
+ if (typeof expectedErrorStatus == "string") {
+ expectedErrorStatus = cloudFileAccounts.constants[expectedErrorStatus];
+ }
+
+ cloudFileAccount.uploadFile(composeWindow, testFiles[filename]).then(
+ upload => {
+ Assert.equal(Cr.NS_OK, expectedErrorStatus);
+ uploads[filename] = upload;
+ },
+ status => {
+ Assert.equal(
+ status.result,
+ expectedErrorStatus,
+ `Error status should be correct for ${testFiles[filename].leafName}`
+ );
+ Assert.equal(
+ status.message,
+ expectedErrorMessage,
+ `Error message should be correct for ${testFiles[filename].leafName}`
+ );
+ }
+ );
+ }
+ );
+
+ extension.onMessage(
+ "renameFile",
+ (
+ id,
+ uploadId,
+ { newName, newUrl },
+ expectedErrorStatus = Cr.NS_OK,
+ expectedErrorMessage
+ ) => {
+ let cloudFileAccount = cloudFileAccounts.getAccount(id);
+
+ if (typeof expectedErrorStatus == "string") {
+ expectedErrorStatus = cloudFileAccounts.constants[expectedErrorStatus];
+ }
+
+ cloudFileAccount.renameFile(composeWindow, uploadId, newName).then(
+ upload => {
+ Assert.equal(Cr.NS_OK, expectedErrorStatus);
+ Assert.equal(upload.name, newName, "New name should match.");
+ Assert.equal(upload.url, newUrl, "New url should match.");
+ },
+ status => {
+ Assert.equal(status.result, expectedErrorStatus);
+ Assert.equal(
+ status.message,
+ expectedErrorMessage,
+ `Error message should be correct.`
+ );
+ }
+ );
+ }
+ );
+
+ extension.onMessage("cancelUpload", id => {
+ let cloudFileAccount = cloudFileAccounts.getAccount(id);
+ cloudFileAccount.cancelFileUpload(composeWindow, testFiles.cloudFile2);
+ });
+
+ extension.onMessage("deleteFile", id => {
+ let cloudFileAccount = cloudFileAccounts.getAccount(id);
+ cloudFileAccount.deleteFile(composeWindow, uploads.cloudFile1.id);
+ });
+
+ let account = createAccount();
+ addIdentity(account);
+
+ composeWindow = await openComposeWindow(account);
+ await focusWindow(composeWindow);
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ composeWindow.close();
+});
+
+/**
+ * Test persistent cloudFile.* events (onFileUpload, onFileDeleted, onFileRename,
+ * onFileUploadAbort, onAccountAdded, onAccountDeleted) with UI interaction and
+ * background terminations and background restarts.
+ */
+add_task(async function test_compose_window_MV3_event_page() {
+ let testFiles = {
+ cloudFile1: new FileUtils.File(getTestFilePath("data/cloudFile1.txt")),
+ cloudFile2: new FileUtils.File(getTestFilePath("data/cloudFile2.txt")),
+ };
+ let uploads = {};
+ let composeWindow;
+
+ async function background() {
+ let abortResolveCallback;
+ // Whenever the extension starts or wakes up, the eventCounter is reset and
+ // allows to observe the order of events fired. In case of a wake-up, the
+ // first observed event is the one that woke up the background.
+ let eventCounter = 0;
+
+ browser.cloudFile.onFileUpload.addListener(
+ async (uploadAccount, { id, name, data }, tab, relatedFileInfo) => {
+ eventCounter++;
+ browser.test.assertEq(
+ eventCounter,
+ 1,
+ "onFileUpload should be the wake up event"
+ );
+ let [{ cloudAccountId, composeTabId, aborting }] =
+ await window.sendMessage("getEnvironment");
+ browser.test.assertEq(tab.id, composeTabId);
+ browser.test.assertEq(uploadAccount.id, cloudAccountId);
+ browser.test.assertEq(name, "cloudFile1.txt");
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(data instanceof File);
+ let content = await data.text();
+ browser.test.assertEq(content, "you got the moves!\n");
+ browser.test.assertEq(undefined, relatedFileInfo);
+
+ if (aborting) {
+ let abortPromise = new Promise(resolve => {
+ abortResolveCallback = resolve;
+ });
+ browser.test.sendMessage("uploadStarted", id);
+ await abortPromise;
+ setTimeout(() => {
+ browser.test.sendMessage("uploadAborted");
+ });
+ return { aborted: true };
+ }
+
+ setTimeout(() => {
+ browser.test.sendMessage("uploadFinished", id);
+ });
+ return { url: "https://example.com/" + name };
+ }
+ );
+
+ browser.cloudFile.onFileRename.addListener(
+ async (account, id, newName, tab) => {
+ eventCounter++;
+ browser.test.assertEq(
+ eventCounter,
+ 1,
+ "onFileRename should be the wake up event"
+ );
+ let [{ cloudAccountId, fileId, composeTabId }] =
+ await window.sendMessage("getEnvironment");
+ browser.test.assertEq(tab.id, composeTabId);
+ browser.test.assertEq(account.id, cloudAccountId);
+ browser.test.assertEq(id, fileId);
+ browser.test.assertEq(newName, "cloudFile3.txt");
+ setTimeout(() => {
+ browser.test.sendMessage("renameFinished", id);
+ });
+ return { url: "https://example.com/" + newName };
+ }
+ );
+
+ browser.cloudFile.onFileDeleted.addListener(async (account, id, tab) => {
+ eventCounter++;
+ browser.test.assertEq(
+ eventCounter,
+ 1,
+ "onFileDeleted should be the wake up event"
+ );
+ let [{ cloudAccountId, fileId, composeTabId }] = await window.sendMessage(
+ "getEnvironment"
+ );
+ browser.test.assertEq(tab.id, composeTabId);
+ browser.test.assertEq(account.id, cloudAccountId);
+ browser.test.assertEq(id, fileId);
+ setTimeout(() => {
+ browser.test.sendMessage("deleteFinished");
+ });
+ });
+
+ browser.cloudFile.onFileUploadAbort.addListener(
+ async (account, id, tab) => {
+ eventCounter++;
+ browser.test.assertEq(
+ eventCounter,
+ 2,
+ "onFileUploadAbort should not be the wake up event"
+ );
+ let [{ cloudAccountId, fileId, composeTabId }] =
+ await window.sendMessage("getEnvironment");
+ browser.test.assertEq(tab.id, composeTabId);
+ browser.test.assertEq(account.id, cloudAccountId);
+ browser.test.assertEq(id, fileId);
+ abortResolveCallback();
+ }
+ );
+
+ browser.cloudFile.onAccountAdded.addListener(account => {
+ eventCounter++;
+ browser.test.assertEq(
+ eventCounter,
+ 1,
+ "onAccountAdded should be the wake up event"
+ );
+ browser.test.sendMessage("accountCreated", account.id);
+ });
+
+ browser.cloudFile.onAccountDeleted.addListener(async accountId => {
+ eventCounter++;
+ browser.test.assertEq(
+ eventCounter,
+ 1,
+ "onAccountDeleted should be the wake up event"
+ );
+ let [{ cloudAccountId }] = await window.sendMessage("getEnvironment");
+ browser.test.assertEq(accountId, cloudAccountId);
+ browser.test.notifyPass("finished");
+ });
+
+ browser.runtime.onInstalled.addListener(async () => {
+ eventCounter++;
+ let [composeTab] = await browser.tabs.query({
+ windowType: "messageCompose",
+ });
+ await window.sendMessage("setEnvironment", {
+ composeTabId: composeTab.id,
+ });
+ browser.test.sendMessage("installed");
+ });
+
+ browser.test.sendMessage("background started");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ useAddonManager: "permanent",
+ manifest: {
+ manifest_version: 3,
+ cloud_file: {
+ name: "mochitest",
+ management_url: "/content/management.html",
+ },
+ browser_specific_settings: { gecko: { id: "cloudfile@mochi.test" } },
+ background: { scripts: ["utils.js", "background.js"] },
+ },
+ });
+
+ function uploadFile(
+ id,
+ filename,
+ expectedErrorStatus = Cr.NS_OK,
+ expectedErrorMessage
+ ) {
+ let cloudFileAccount = cloudFileAccounts.getAccount(id);
+
+ if (typeof expectedErrorStatus == "string") {
+ expectedErrorStatus = cloudFileAccounts.constants[expectedErrorStatus];
+ }
+
+ return cloudFileAccount.uploadFile(composeWindow, testFiles[filename]).then(
+ upload => {
+ Assert.equal(Cr.NS_OK, expectedErrorStatus);
+ uploads[filename] = upload;
+ },
+ status => {
+ Assert.equal(
+ status.result,
+ expectedErrorStatus,
+ `Error status should be correct for ${testFiles[filename].leafName}`
+ );
+ Assert.equal(
+ status.message,
+ expectedErrorMessage,
+ `Error message should be correct for ${testFiles[filename].leafName}`
+ );
+ }
+ );
+ }
+ function startUpload(id, filename) {
+ let cloudFileAccount = cloudFileAccounts.getAccount(id);
+ return cloudFileAccount
+ .uploadFile(composeWindow, testFiles[filename])
+ .catch(() => {});
+ }
+ function cancelUpload(id, filename) {
+ let cloudFileAccount = cloudFileAccounts.getAccount(id);
+ return cloudFileAccount.cancelFileUpload(
+ composeWindow,
+ testFiles[filename]
+ );
+ }
+ function renameFile(
+ id,
+ uploadId,
+ { newName, newUrl },
+ expectedErrorStatus = Cr.NS_OK,
+ expectedErrorMessage
+ ) {
+ let cloudFileAccount = cloudFileAccounts.getAccount(id);
+
+ if (typeof expectedErrorStatus == "string") {
+ expectedErrorStatus = cloudFileAccounts.constants[expectedErrorStatus];
+ }
+
+ return cloudFileAccount.renameFile(composeWindow, uploadId, newName).then(
+ upload => {
+ Assert.equal(Cr.NS_OK, expectedErrorStatus);
+ Assert.equal(upload.name, newName, "New name should match.");
+ Assert.equal(upload.url, newUrl, "New url should match.");
+ },
+ status => {
+ Assert.equal(status.result, expectedErrorStatus);
+ Assert.equal(
+ status.message,
+ expectedErrorMessage,
+ `Error message should be correct.`
+ );
+ }
+ );
+ }
+ function deleteFile(id, uploadId) {
+ let cloudFileAccount = cloudFileAccounts.getAccount(id);
+ return cloudFileAccount.deleteFile(composeWindow, uploadId);
+ }
+
+ let environment = {};
+ extension.onMessage("setEnvironment", data => {
+ if (data.composeTabId) {
+ environment.composeTabId = data.composeTabId;
+ }
+ extension.sendMessage();
+ });
+ extension.onMessage("getEnvironment", () => {
+ extension.sendMessage(environment);
+ });
+
+ let account = createAccount();
+ addIdentity(account);
+
+ composeWindow = await openComposeWindow(account);
+ await focusWindow(composeWindow);
+
+ await extension.startup();
+ await extension.awaitMessage("installed");
+ await extension.awaitMessage("background started");
+
+ function checkPersistentListeners({ primed }) {
+ // A persistent event is referenced by its moduleName as defined in
+ // ext-mails.json, not by its actual namespace.
+ const persistent_events = [
+ "onFileUpload",
+ "onFileRename",
+ "onFileDeleted",
+ "onFileUploadAbort",
+ "onAccountAdded",
+ "onAccountDeleted",
+ ];
+ for (let eventName of persistent_events) {
+ assertPersistentListeners(extension, "cloudFile", eventName, {
+ primed,
+ });
+ }
+ }
+
+ // Verify persistent listener, not yet primed.
+ checkPersistentListeners({ primed: false });
+
+ // Create account.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ cloudFileAccounts.createAccount("ext-cloudfile@mochi.test");
+ await extension.awaitMessage("background started");
+ environment.cloudAccountId = await extension.awaitMessage("accountCreated");
+ checkPersistentListeners({ primed: false });
+
+ // Upload.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ uploadFile(environment.cloudAccountId, "cloudFile1");
+ await extension.awaitMessage("background started");
+ environment.fileId = await extension.awaitMessage("uploadFinished");
+ checkPersistentListeners({ primed: false });
+
+ // Rename.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ renameFile(environment.cloudAccountId, environment.fileId, {
+ newName: "cloudFile3.txt",
+ newUrl: "https://example.com/cloudFile3.txt",
+ });
+ await extension.awaitMessage("background started");
+ await extension.awaitMessage("renameFinished");
+ checkPersistentListeners({ primed: false });
+
+ // Delete.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ deleteFile(environment.cloudAccountId, environment.fileId);
+ await extension.awaitMessage("background started");
+ await extension.awaitMessage("deleteFinished");
+ checkPersistentListeners({ primed: false });
+
+ // Aborted upload.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ environment.aborting = true;
+ startUpload(environment.cloudAccountId, "cloudFile1");
+ await extension.awaitMessage("background started");
+ environment.fileId = await extension.awaitMessage("uploadStarted");
+ cancelUpload(environment.cloudAccountId, "cloudFile1");
+ await extension.awaitMessage("uploadAborted");
+ checkPersistentListeners({ primed: false });
+
+ // Remove account.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ cloudFileAccounts.removeAccount(environment.cloudAccountId);
+ await extension.awaitMessage("background started");
+ checkPersistentListeners({ primed: false });
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ composeWindow.close();
+});
+
+/**
+ * Test cloudFiles without accounts and removed local files.
+ */
+add_task(async function test_incomplete_cloudFiles() {
+ let testFiles = {
+ cloudFile1: new FileUtils.File(getTestFilePath("data/cloudFile1.txt")),
+ cloudFile2: new FileUtils.File(getTestFilePath("data/cloudFile2.txt")),
+ };
+ let uploads = {};
+ let composeWindow;
+ let cloudFileAccount = null;
+
+ async function background() {
+ function createCloudfileAccount(id) {
+ let addListener = window.waitForEvent("cloudFile.onAccountAdded");
+ browser.test.sendMessage("createAccount", id);
+ return addListener;
+ }
+
+ function removeCloudfileAccount(id) {
+ let deleteListener = window.waitForEvent("cloudFile.onAccountDeleted");
+ browser.test.sendMessage("removeAccount", id);
+ return deleteListener;
+ }
+
+ let [composerTab] = await browser.tabs.query({
+ windowType: "messageCompose",
+ });
+
+ let [createdAccount] = await createCloudfileAccount(
+ "ext-cloudfile@mochi.test"
+ );
+
+ await new Promise(resolve => {
+ function fileListener(
+ uploadAccount,
+ { id, name, data },
+ tab,
+ relatedFileInfo
+ ) {
+ browser.cloudFile.onFileUpload.removeListener(fileListener);
+ setTimeout(() => resolve(id));
+ return { url: "https://example.com/" + name };
+ }
+
+ browser.cloudFile.onFileUpload.addListener(fileListener);
+ browser.test.sendMessage("uploadFile", createdAccount.id, "cloudFile1");
+ });
+
+ await window.sendMessage("attachAndInvalidate", "cloudFile1");
+ let attachments = await browser.compose.listAttachments(composerTab.id);
+ let [attachmentId] = attachments
+ .filter(e => e.name == "cloudFile1.txt")
+ .map(e => e.id);
+
+ await browser.test.assertRejects(
+ browser.compose.updateAttachment(composerTab.id, attachmentId, {
+ name: "cloudFile3",
+ }),
+ e => {
+ return (
+ e.message.startsWith(
+ "CloudFile Error: Attachment file not found: "
+ ) && e.message.endsWith("cloudFile1.txt_invalid")
+ );
+ },
+ "browser.compose.updateAttachment() should reject, if the local file does not exist."
+ );
+
+ await removeCloudfileAccount(createdAccount.id);
+ await browser.test.assertRejects(
+ browser.compose.updateAttachment(composerTab.id, attachmentId, {
+ name: "cloudFile3",
+ }),
+ `CloudFile Error: Account not found: ${createdAccount.id}`,
+ "browser.compose.updateAttachment() should reject, if the account does not exist."
+ );
+
+ browser.test.notifyPass("finished");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ cloud_file: {
+ name: "mochitest",
+ management_url: "/content/management.html",
+ },
+ permissions: ["compose"],
+ applications: { gecko: { id: "cloudfile@mochi.test" } },
+ background: { scripts: ["utils.js", "background.js"] },
+ },
+ });
+
+ extension.onMessage("createAccount", id => {
+ cloudFileAccount = cloudFileAccounts.createAccount(id);
+ });
+
+ extension.onMessage("removeAccount", id => {
+ cloudFileAccounts.removeAccount(id);
+ });
+
+ extension.onMessage("attachAndInvalidate", async filename => {
+ let upload = uploads[filename];
+ await composeWindow.attachToCloudRepeat(
+ uploads[filename],
+ cloudFileAccount
+ );
+
+ let bucket = composeWindow.document.getElementById("attachmentBucket");
+ let item = [...bucket.children].find(e => e.attachment.name == upload.name);
+ Assert.ok(item, "Should have found the attachment item");
+
+ // Invalidate the cloud attachment, simulating a file move/delete.
+ item.attachment.url = `${item.attachment.url}_invalid`;
+ item.cloudFileAccount.markAsImmutable(item.cloudFileUpload.id);
+ extension.sendMessage();
+ });
+
+ extension.onMessage(
+ "uploadFile",
+ (id, filename, expectedErrorStatus = Cr.NS_OK, expectedErrorMessage) => {
+ let cloudFileAccount = cloudFileAccounts.getAccount(id);
+
+ if (typeof expectedErrorStatus == "string") {
+ expectedErrorStatus = cloudFileAccounts.constants[expectedErrorStatus];
+ }
+
+ cloudFileAccount.uploadFile(composeWindow, testFiles[filename]).then(
+ upload => {
+ Assert.equal(Cr.NS_OK, expectedErrorStatus);
+ uploads[filename] = upload;
+ },
+ status => {
+ Assert.equal(
+ status.result,
+ expectedErrorStatus,
+ `Error status should be correct for ${testFiles[filename].leafName}`
+ );
+ Assert.equal(
+ status.message,
+ expectedErrorMessage,
+ `Error message should be correct for ${testFiles[filename].leafName}`
+ );
+ }
+ );
+ }
+ );
+
+ let account = createAccount();
+ addIdentity(account);
+
+ composeWindow = await openComposeWindow(account);
+ await focusWindow(composeWindow);
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ composeWindow.close();
+});
+
+/** Test data_format "File", which is the default if none is specified in the
+ * manifest. */
+add_task(async function test_file_format() {
+ async function background() {
+ function createCloudfileAccount() {
+ let addListener = window.waitForEvent("cloudFile.onAccountAdded");
+ browser.test.sendMessage("createAccount");
+ return addListener;
+ }
+
+ function removeCloudfileAccount(id) {
+ let deleteListener = window.waitForEvent("cloudFile.onAccountDeleted");
+ browser.test.sendMessage("removeAccount", id);
+ return deleteListener;
+ }
+
+ let [createdAccount] = await createCloudfileAccount();
+
+ browser.test.log("test upload");
+ await new Promise(resolve => {
+ function fileListener(account, { id, name, data }, tab, relatedFileInfo) {
+ browser.cloudFile.onFileUpload.removeListener(fileListener);
+ browser.test.assertEq(name, "cloudFile1.txt");
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(data instanceof File);
+ let reader = new FileReader();
+ reader.addEventListener("loadend", () => {
+ browser.test.assertEq(reader.result, "you got the moves!\n");
+ setTimeout(() => resolve(id));
+ });
+ reader.readAsText(data);
+ browser.test.assertEq(undefined, relatedFileInfo);
+ return { url: "https://example.com/" + name };
+ }
+
+ browser.cloudFile.onFileUpload.addListener(fileListener);
+ browser.test.sendMessage("uploadFile", createdAccount.id, "cloudFile1");
+ });
+
+ browser.test.log("test upload quota error");
+ await browser.cloudFile.updateAccount(createdAccount.id, {
+ spaceRemaining: 1,
+ });
+ await window.sendMessage(
+ "uploadFileError",
+ createdAccount.id,
+ "cloudFile2",
+ "uploadWouldExceedQuota",
+ "Quota error: Can't upload file. Only 1KB left of quota."
+ );
+ await browser.cloudFile.updateAccount(createdAccount.id, {
+ spaceRemaining: -1,
+ });
+
+ browser.test.log("test upload file size limit error");
+ await browser.cloudFile.updateAccount(createdAccount.id, {
+ uploadSizeLimit: 1,
+ });
+ await window.sendMessage(
+ "uploadFileError",
+ createdAccount.id,
+ "cloudFile2",
+ "uploadExceedsFileLimit",
+ "Upload error: File size is 19KB and exceeds the file size limit of 1KB"
+ );
+ await browser.cloudFile.updateAccount(createdAccount.id, {
+ uploadSizeLimit: -1,
+ });
+
+ await removeCloudfileAccount(createdAccount.id);
+ browser.test.notifyPass("cloudFile");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ cloud_file: {
+ name: "mochitest",
+ management_url: "/content/management.html",
+ },
+ applications: { gecko: { id: "cloudfile@mochi.test" } },
+ background: { scripts: ["utils.js", "background.js"] },
+ },
+ });
+
+ let testFiles = {
+ cloudFile1: new FileUtils.File(getTestFilePath("data/cloudFile1.txt")),
+ cloudFile2: new FileUtils.File(getTestFilePath("data/cloudFile2.txt")),
+ };
+
+ extension.onMessage("createAccount", (id = "ext-cloudfile@mochi.test") => {
+ cloudFileAccounts.createAccount(id);
+ });
+
+ extension.onMessage("removeAccount", id => {
+ cloudFileAccounts.removeAccount(id);
+ });
+
+ extension.onMessage(
+ "uploadFileError",
+ async (id, filename, expectedErrorStatus, expectedErrorMessage) => {
+ let account = cloudFileAccounts.getAccount(id);
+
+ let status;
+ try {
+ await account.uploadFile(null, testFiles[filename]);
+ } catch (ex) {
+ status = ex;
+ }
+
+ Assert.ok(
+ !!status,
+ `Upload should have failed for ${testFiles[filename].leafName}`
+ );
+ Assert.equal(
+ status.result,
+ cloudFileAccounts.constants[expectedErrorStatus],
+ `Error status should be correct for ${testFiles[filename].leafName}`
+ );
+ Assert.equal(
+ status.message,
+ expectedErrorMessage,
+ `Error message should be correct for ${testFiles[filename].leafName}`
+ );
+ extension.sendMessage();
+ }
+ );
+
+ extension.onMessage(
+ "uploadFile",
+ (id, filename, expectedErrorStatus = Cr.NS_OK, expectedErrorMessage) => {
+ let account = cloudFileAccounts.getAccount(id);
+
+ if (typeof expectedErrorStatus == "string") {
+ expectedErrorStatus = cloudFileAccounts.constants[expectedErrorStatus];
+ }
+
+ account.uploadFile(null, testFiles[filename]).then(
+ upload => {
+ Assert.equal(Cr.NS_OK, expectedErrorStatus);
+ },
+ status => {
+ Assert.equal(
+ status.result,
+ expectedErrorStatus,
+ `Error status should be correct for ${testFiles[filename].leafName}`
+ );
+ Assert.equal(
+ status.message,
+ expectedErrorMessage,
+ `Error message should be correct for ${testFiles[filename].leafName}`
+ );
+ }
+ );
+ }
+ );
+
+ await extension.startup();
+ await extension.awaitFinish("cloudFile");
+ await extension.unload();
+
+ Assert.ok(!cloudFileAccounts.getProviderForType("ext-cloudfile@mochi.test"));
+ Assert.equal(cloudFileAccounts.accounts.length, 0);
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js b/comm/mail/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js
new file mode 100644
index 0000000000..47b804a763
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js
@@ -0,0 +1,226 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function testExecuteBrowserActionWithOptions_mv2(options = {}) {
+ // Make sure the mouse isn't hovering over the browserAction widget.
+ let folderTree = document
+ .getElementById("tabmail")
+ .currentAbout3Pane.document.getElementById("folderTree");
+ EventUtils.synthesizeMouseAtCenter(folderTree, { type: "mouseover" }, window);
+
+ let extensionOptions = {
+ useAddonManager: "temporary",
+ };
+
+ extensionOptions.manifest = {
+ commands: {
+ _execute_browser_action: {
+ suggested_key: {
+ default: "Alt+Shift+J",
+ },
+ },
+ },
+ browser_action: {
+ browser_style: true,
+ },
+ };
+
+ if (options.withPopup) {
+ extensionOptions.manifest.browser_action.default_popup = "popup.html";
+
+ extensionOptions.files = {
+ "popup.html": `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="popup.js"></script>
+ </head>
+ <body>
+ Popup
+ </body>
+ </html>
+ `,
+ "popup.js": function () {
+ browser.runtime.sendMessage("from-browser-action-popup");
+ },
+ };
+ }
+
+ extensionOptions.background = () => {
+ browser.test.onMessage.addListener((message, withPopup) => {
+ browser.commands.onCommand.addListener(commandName => {
+ browser.test.fail(
+ "The onCommand listener should never fire for a valid _execute_* command."
+ );
+ });
+
+ browser.browserAction.onClicked.addListener(() => {
+ if (withPopup) {
+ browser.test.fail(
+ "The onClick listener should never fire if the browserAction has a popup."
+ );
+ browser.test.notifyFail("execute-browser-action-on-clicked-fired");
+ } else {
+ browser.test.notifyPass("execute-browser-action-on-clicked-fired");
+ }
+ });
+
+ browser.runtime.onMessage.addListener(msg => {
+ if (msg == "from-browser-action-popup") {
+ browser.test.notifyPass("execute-browser-action-popup-opened");
+ }
+ });
+
+ browser.test.sendMessage("send-keys");
+ });
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionOptions);
+
+ extension.onMessage("send-keys", () => {
+ EventUtils.synthesizeKey("j", { altKey: true, shiftKey: true });
+ });
+
+ await extension.startup();
+
+ await SimpleTest.promiseFocus(window);
+
+ // trigger setup of listeners in background and the send-keys msg
+ extension.sendMessage("withPopup", options.withPopup);
+
+ if (options.withPopup) {
+ await extension.awaitFinish("execute-browser-action-popup-opened");
+
+ if (!getBrowserActionPopup(extension)) {
+ await awaitExtensionPanel(extension);
+ }
+ await closeBrowserAction(extension);
+ } else {
+ await extension.awaitFinish("execute-browser-action-on-clicked-fired");
+ }
+ await extension.unload();
+}
+
+add_task(async function test_execute_browser_action_with_popup_mv2() {
+ await testExecuteBrowserActionWithOptions_mv2({
+ withPopup: true,
+ });
+});
+
+add_task(async function test_execute_browser_action_without_popup_mv2() {
+ await testExecuteBrowserActionWithOptions_mv2();
+});
+
+async function testExecuteActionWithOptions_mv3(options = {}) {
+ // Make sure the mouse isn't hovering over the action widget.
+ let folderTree = document
+ .getElementById("tabmail")
+ .currentAbout3Pane.document.getElementById("folderTree");
+ EventUtils.synthesizeMouseAtCenter(folderTree, { type: "mouseover" }, window);
+
+ let extensionOptions = {
+ useAddonManager: "temporary",
+ };
+
+ extensionOptions.manifest = {
+ manifest_version: 3,
+ commands: {
+ _execute_action: {
+ suggested_key: {
+ default: "Alt+Shift+J",
+ },
+ },
+ },
+ action: {
+ browser_style: true,
+ },
+ };
+
+ if (options.withPopup) {
+ extensionOptions.manifest.action.default_popup = "popup.html";
+
+ extensionOptions.files = {
+ "popup.html": `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="popup.js"></script>
+ </head>
+ <body>
+ Popup
+ </body>
+ </html>
+ `,
+ "popup.js": function () {
+ browser.runtime.sendMessage("from-action-popup");
+ },
+ };
+ }
+
+ extensionOptions.background = () => {
+ browser.test.onMessage.addListener((message, withPopup) => {
+ browser.commands.onCommand.addListener(commandName => {
+ browser.test.fail(
+ "The onCommand listener should never fire for a valid _execute_* command."
+ );
+ });
+
+ browser.action.onClicked.addListener(() => {
+ if (withPopup) {
+ browser.test.fail(
+ "The onClick listener should never fire if the action has a popup."
+ );
+ browser.test.notifyFail("execute-action-on-clicked-fired");
+ } else {
+ browser.test.notifyPass("execute-action-on-clicked-fired");
+ }
+ });
+
+ browser.runtime.onMessage.addListener(msg => {
+ if (msg == "from-action-popup") {
+ browser.test.notifyPass("execute-action-popup-opened");
+ }
+ });
+
+ browser.test.sendMessage("send-keys");
+ });
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionOptions);
+
+ extension.onMessage("send-keys", () => {
+ EventUtils.synthesizeKey("j", { altKey: true, shiftKey: true });
+ });
+
+ await extension.startup();
+
+ await SimpleTest.promiseFocus(window);
+
+ // trigger setup of listeners in background and the send-keys msg
+ extension.sendMessage("withPopup", options.withPopup);
+
+ if (options.withPopup) {
+ await extension.awaitFinish("execute-action-popup-opened");
+
+ if (!getBrowserActionPopup(extension)) {
+ await awaitExtensionPanel(extension);
+ }
+ await closeBrowserAction(extension);
+ } else {
+ await extension.awaitFinish("execute-action-on-clicked-fired");
+ }
+ await extension.unload();
+}
+
+add_task(async function test_execute_browser_action_with_popup_mv3() {
+ await testExecuteActionWithOptions_mv3({
+ withPopup: true,
+ });
+});
+
+add_task(async function test_execute_browser_action_without_popup_mv3() {
+ await testExecuteActionWithOptions_mv3();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_commands_execute_compose_action.js b/comm/mail/components/extensions/test/browser/browser_ext_commands_execute_compose_action.js
new file mode 100644
index 0000000000..a84a2cac3c
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_commands_execute_compose_action.js
@@ -0,0 +1,138 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+let gAccount;
+
+async function testExecuteComposeActionWithOptions(options = {}) {
+ info(
+ `--> Running test commands_execute_compose_action with the following options: ${JSON.stringify(
+ options
+ )}`
+ );
+
+ let extensionOptions = {};
+ extensionOptions.manifest = {
+ permissions: ["accountsRead"],
+ commands: {
+ _execute_compose_action: {
+ suggested_key: {
+ default: "Alt+Shift+J",
+ mac: "Ctrl+Shift+J",
+ },
+ },
+ },
+ compose_action: {
+ browser_style: true,
+ },
+ };
+
+ if (options.withFormatToolbar) {
+ extensionOptions.manifest.compose_action.default_area = "formattoolbar";
+ }
+
+ if (options.withPopup) {
+ extensionOptions.manifest.compose_action.default_popup = "popup.html";
+
+ extensionOptions.files = {
+ "popup.html": `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="popup.js"></script>
+ </head>
+ <body>
+ Popup
+ </body>
+ </html>
+ `,
+ "popup.js": function () {
+ browser.test.log("sending from-compose-action-popup");
+ browser.runtime.sendMessage("from-compose-action-popup");
+ },
+ };
+ }
+
+ extensionOptions.background = async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(1, accounts.length, "number of accounts");
+
+ browser.test.onMessage.addListener((message, withPopup) => {
+ browser.commands.onCommand.addListener(commandName => {
+ browser.test.fail(
+ "The onCommand listener should never fire for a valid _execute_* command."
+ );
+ });
+
+ browser.composeAction.onClicked.addListener(() => {
+ if (withPopup) {
+ browser.test.fail(
+ "The onClick listener should never fire if the composeAction has a popup."
+ );
+ browser.test.notifyFail("execute-compose-action-on-clicked-fired");
+ } else {
+ browser.test.notifyPass("execute-compose-action-on-clicked-fired");
+ }
+ });
+
+ browser.runtime.onMessage.addListener(msg => {
+ if (msg == "from-compose-action-popup") {
+ browser.test.notifyPass("execute-compose-action-popup-opened");
+ }
+ });
+
+ browser.test.log("Sending send-keys");
+ browser.test.sendMessage("send-keys");
+ });
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionOptions);
+ await extension.startup();
+
+ let composeWindow = await openComposeWindow(gAccount);
+ await focusWindow(composeWindow);
+
+ // trigger setup of listeners in background and the send-keys msg
+ extension.sendMessage("withPopup", options.withPopup);
+
+ await extension.awaitMessage("send-keys");
+ info("Simulating ALT+SHIFT+J");
+ let modifiers =
+ AppConstants.platform == "macosx"
+ ? { metaKey: true, shiftKey: true }
+ : { altKey: true, shiftKey: true };
+ EventUtils.synthesizeKey("j", modifiers, composeWindow);
+
+ if (options.withPopup) {
+ await extension.awaitFinish("execute-compose-action-popup-opened");
+
+ if (!getBrowserActionPopup(extension, composeWindow)) {
+ await awaitExtensionPanel(extension, composeWindow);
+ }
+ await closeBrowserAction(extension, composeWindow);
+ } else {
+ await extension.awaitFinish("execute-compose-action-on-clicked-fired");
+ }
+ composeWindow.close();
+ await extension.unload();
+}
+
+add_setup(async () => {
+ gAccount = createAccount();
+ addIdentity(gAccount);
+});
+
+let popupJobs = [true, false];
+let formatToolbarJobs = [true, false];
+
+for (let popupJob of popupJobs) {
+ for (let formatToolbarJob of formatToolbarJobs) {
+ add_task(async () => {
+ await testExecuteComposeActionWithOptions({
+ withPopup: popupJob,
+ withFormatToolbar: formatToolbarJob,
+ });
+ });
+ }
+}
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_commands_execute_message_display_action.js b/comm/mail/components/extensions/test/browser/browser_ext_commands_execute_message_display_action.js
new file mode 100644
index 0000000000..2b35b791ec
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_commands_execute_message_display_action.js
@@ -0,0 +1,168 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+let gMessages;
+
+async function testExecuteMessageDisplayActionWithOptions(msg, options = {}) {
+ info(
+ `--> Running test commands_execute_message_display_action with the following options: ${JSON.stringify(
+ options
+ )}`
+ );
+
+ let extensionOptions = {};
+ extensionOptions.manifest = {
+ commands: {
+ _execute_message_display_action: {
+ suggested_key: {
+ default: "Alt+Shift+J",
+ },
+ },
+ },
+ message_display_action: {
+ browser_style: true,
+ },
+ };
+
+ if (options.withPopup) {
+ extensionOptions.manifest.message_display_action.default_popup =
+ "popup.html";
+
+ extensionOptions.files = {
+ "popup.html": `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="popup.js"></script>
+ </head>
+ <body>
+ Popup
+ </body>
+ </html>
+ `,
+ "popup.js": function () {
+ browser.test.log("sending from-message-display-action-popup");
+ browser.runtime.sendMessage("from-message-display-action-popup");
+ },
+ };
+ }
+
+ extensionOptions.background = () => {
+ browser.test.onMessage.addListener((message, withPopup) => {
+ browser.commands.onCommand.addListener(commandName => {
+ browser.test.fail(
+ "The onCommand listener should never fire for a valid _execute_* command."
+ );
+ });
+
+ browser.messageDisplayAction.onClicked.addListener(() => {
+ if (withPopup) {
+ browser.test.fail(
+ "The onClick listener should never fire if the messageDisplayAction has a popup."
+ );
+ browser.test.notifyFail(
+ "execute-message-display-action-on-clicked-fired"
+ );
+ } else {
+ browser.test.notifyPass(
+ "execute-message-display-action-on-clicked-fired"
+ );
+ }
+ });
+
+ browser.runtime.onMessage.addListener(msg => {
+ if (msg == "from-message-display-action-popup") {
+ browser.test.notifyPass(
+ "execute-message-display-action-popup-opened"
+ );
+ }
+ });
+
+ browser.test.log("Sending send-keys");
+ browser.test.sendMessage("send-keys");
+ });
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionOptions);
+
+ extension.onMessage("send-keys", () => {
+ info("Simulating ALT+SHIFT+J");
+ EventUtils.synthesizeKey(
+ "j",
+ { altKey: true, shiftKey: true },
+ messageWindow
+ );
+ });
+
+ await extension.startup();
+
+ let tabmail = document.getElementById("tabmail");
+ let messageWindow = window;
+ let aboutMessage = tabmail.currentAboutMessage;
+ switch (options.displayType) {
+ case "tab":
+ await openMessageInTab(msg);
+ aboutMessage = tabmail.currentAboutMessage;
+ break;
+ case "window":
+ messageWindow = await openMessageInWindow(msg);
+ aboutMessage = messageWindow.messageBrowser.contentWindow;
+ break;
+ }
+ await SimpleTest.promiseFocus(aboutMessage);
+
+ // trigger setup of listeners in background and the send-keys msg
+ extension.sendMessage("withPopup", options.withPopup);
+
+ if (options.withPopup) {
+ await extension.awaitFinish("execute-message-display-action-popup-opened");
+
+ if (!getBrowserActionPopup(extension, aboutMessage)) {
+ await awaitExtensionPanel(extension, aboutMessage);
+ }
+ await closeBrowserAction(extension, aboutMessage);
+ } else {
+ await extension.awaitFinish(
+ "execute-message-display-action-on-clicked-fired"
+ );
+ }
+
+ switch (options.displayType) {
+ case "tab":
+ tabmail.closeTab();
+ break;
+ case "window":
+ messageWindow.close();
+ break;
+ }
+
+ await extension.unload();
+}
+
+add_setup(async () => {
+ let account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ let subFolders = rootFolder.subFolders;
+ createMessages(subFolders[0], 10);
+ gMessages = [...subFolders[0].messages];
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(subFolders[0].URI);
+ about3Pane.threadTree.selectedIndex = 0;
+});
+
+let popupJobs = [true, false];
+let displayJobs = ["3pane", "tab", "window"];
+
+for (let popupJob of popupJobs) {
+ for (let displayJob of displayJobs) {
+ add_task(async () => {
+ await testExecuteMessageDisplayActionWithOptions(gMessages[1], {
+ withPopup: popupJob,
+ displayType: displayJob,
+ });
+ });
+ }
+}
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_commands_getAll.js b/comm/mail/components/extensions/test/browser/browser_ext_commands_getAll.js
new file mode 100644
index 0000000000..c38fdc291c
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_commands_getAll.js
@@ -0,0 +1,142 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "_locales/en/messages.json": {
+ with_translation: {
+ message: "The description",
+ description: "A description",
+ },
+ },
+ },
+ manifest: {
+ name: "Commands Extension",
+ default_locale: "en",
+ commands: {
+ "with-desciption": {
+ suggested_key: {
+ default: "Ctrl+Shift+Y",
+ },
+ description: "should have a description",
+ },
+ "without-description": {
+ suggested_key: {
+ default: "Ctrl+Shift+D",
+ },
+ },
+ "with-platform-info": {
+ suggested_key: {
+ mac: "Ctrl+Shift+M",
+ linux: "Ctrl+Shift+L",
+ windows: "Ctrl+Shift+W",
+ android: "Ctrl+Shift+A",
+ },
+ },
+ "with-translation": {
+ description: "__MSG_with_translation__",
+ },
+ "without-suggested-key": {
+ description: "has no suggested_key",
+ },
+ "without-suggested-key-nor-description": {},
+ },
+ },
+
+ background() {
+ browser.test.onMessage.addListener((message, additionalScope) => {
+ browser.commands.getAll(commands => {
+ let errorMessage = "getAll should return an array of commands";
+ browser.test.assertEq(commands.length, 6, errorMessage);
+
+ let command = commands.find(c => c.name == "with-desciption");
+
+ errorMessage =
+ "The description should match what is provided in the manifest";
+ browser.test.assertEq(
+ "should have a description",
+ command.description,
+ errorMessage
+ );
+
+ errorMessage =
+ "The shortcut should match the default shortcut provided in the manifest";
+ browser.test.assertEq("Ctrl+Shift+Y", command.shortcut, errorMessage);
+
+ command = commands.find(c => c.name == "without-description");
+
+ errorMessage =
+ "The description should be empty when it is not provided";
+ browser.test.assertEq(null, command.description, errorMessage);
+
+ errorMessage =
+ "The shortcut should match the default shortcut provided in the manifest";
+ browser.test.assertEq("Ctrl+Shift+D", command.shortcut, errorMessage);
+
+ let platformKeys = {
+ macosx: "M",
+ linux: "L",
+ win: "W",
+ android: "A",
+ };
+
+ command = commands.find(c => c.name == "with-platform-info");
+ let platformKey = platformKeys[additionalScope.platform];
+ let shortcut = `Ctrl+Shift+${platformKey}`;
+ errorMessage = `The shortcut should match the one provided in the manifest for OS='${additionalScope.platform}'`;
+ browser.test.assertEq(shortcut, command.shortcut, errorMessage);
+
+ command = commands.find(c => c.name == "with-translation");
+ browser.test.assertEq(
+ command.description,
+ "The description",
+ "The description can be localized"
+ );
+
+ command = commands.find(c => c.name == "without-suggested-key");
+
+ browser.test.assertEq(
+ "has no suggested_key",
+ command.description,
+ "The description should match what is provided in the manifest"
+ );
+
+ browser.test.assertEq(
+ "",
+ command.shortcut,
+ "The shortcut should be empty if not provided"
+ );
+
+ command = commands.find(
+ c => c.name == "without-suggested-key-nor-description"
+ );
+
+ browser.test.assertEq(
+ null,
+ command.description,
+ "The description should be empty when it is not provided"
+ );
+
+ browser.test.assertEq(
+ "",
+ command.shortcut,
+ "The shortcut should be empty if not provided"
+ );
+
+ browser.test.notifyPass("commands");
+ });
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ extension.sendMessage("additional-scope", {
+ platform: AppConstants.platform,
+ });
+ await extension.awaitFinish("commands");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_commands_onChanged.js b/comm/mail/components/extensions/test/browser/browser_ext_commands_onChanged.js
new file mode 100644
index 0000000000..db90d71f00
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_commands_onChanged.js
@@ -0,0 +1,59 @@
+"use strict";
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ browser_specific_settings: { gecko: { id: "commands@mochi.test" } },
+ commands: {
+ foo: {
+ suggested_key: {
+ default: "Ctrl+Shift+V",
+ },
+ description: "The foo command",
+ },
+ },
+ },
+ async background() {
+ const { commands } = browser.runtime.getManifest();
+
+ const originalFoo = commands.foo;
+
+ let resolver = {};
+ resolver.promise = new Promise(resolve => (resolver.resolve = resolve));
+
+ browser.commands.onChanged.addListener(update => {
+ browser.test.assertDeepEq(
+ update,
+ {
+ name: "foo",
+ newShortcut: "Ctrl+Shift+L",
+ oldShortcut: originalFoo.suggested_key.default,
+ },
+ `The name should match what was provided in the manifest.
+ The new shortcut should match what was provided in the update.
+ The old shortcut should match what was provided in the manifest
+ `
+ );
+ browser.test.assertFalse(
+ resolver.hasResolvedAlready,
+ `resolver was not resolved yet`
+ );
+ resolver.resolve();
+ resolver.hasResolvedAlready = true;
+ });
+
+ await browser.commands.update({ name: "foo", shortcut: "Ctrl+Shift+L" });
+ // We're checking that nothing emits when
+ // the new shortcut is identical to the old one
+ await browser.commands.update({ name: "foo", shortcut: "Ctrl+Shift+L" });
+
+ await resolver.promise;
+
+ browser.test.notifyPass("commands");
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("commands");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_commands_onCommand.js b/comm/mail/components/extensions/test/browser/browser_ext_commands_onCommand.js
new file mode 100644
index 0000000000..82928957f4
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_commands_onCommand.js
@@ -0,0 +1,577 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+var testCommands = [
+ // Ctrl Shortcuts
+ {
+ name: "toggle-ctrl-a",
+ shortcut: "Ctrl+A",
+ key: "A",
+ // Does not work in compose window on Linux.
+ skip: ["messageCompose", "content"],
+ modifiers: {
+ accelKey: true,
+ },
+ },
+ {
+ name: "toggle-ctrl-up",
+ shortcut: "Ctrl+Up",
+ key: "VK_UP",
+ modifiers: {
+ accelKey: true,
+ },
+ },
+ // Alt Shortcuts
+ {
+ name: "toggle-alt-a",
+ shortcut: "Alt+A",
+ key: "A",
+ // Does not work in compose window on Mac.
+ skip: ["messageCompose"],
+ modifiers: {
+ altKey: true,
+ },
+ },
+ {
+ name: "toggle-alt-down",
+ shortcut: "Alt+Down",
+ key: "VK_DOWN",
+ modifiers: {
+ altKey: true,
+ },
+ },
+ // Mac Shortcuts
+ {
+ name: "toggle-command-shift-page-up",
+ shortcutMac: "Command+Shift+PageUp",
+ key: "VK_PAGE_UP",
+ modifiers: {
+ accelKey: true,
+ shiftKey: true,
+ },
+ },
+ {
+ name: "toggle-mac-control-shift+period",
+ shortcut: "Ctrl+Shift+Period",
+ shortcutMac: "MacCtrl+Shift+Period",
+ key: "VK_PERIOD",
+ modifiers: {
+ ctrlKey: true,
+ shiftKey: true,
+ },
+ },
+ // Ctrl+Shift Shortcuts
+ {
+ name: "toggle-ctrl-shift-left",
+ shortcut: "Ctrl+Shift+Left",
+ key: "VK_LEFT",
+ modifiers: {
+ accelKey: true,
+ shiftKey: true,
+ },
+ },
+ {
+ name: "toggle-ctrl-shift-1",
+ shortcut: "Ctrl+Shift+1",
+ key: "1",
+ modifiers: {
+ accelKey: true,
+ shiftKey: true,
+ },
+ },
+ // Alt+Shift Shortcuts
+ {
+ name: "toggle-alt-shift-1",
+ shortcut: "Alt+Shift+1",
+ key: "1",
+ modifiers: {
+ altKey: true,
+ shiftKey: true,
+ },
+ },
+ // TODO: This results in multiple events fired. See bug 1805375.
+ /*
+ {
+ name: "toggle-alt-shift-a",
+ shortcut: "Alt+Shift+A",
+ key: "A",
+ // Does not work in compose window on Mac.
+ skip: ["messageCompose"],
+ modifiers: {
+ altKey: true,
+ shiftKey: true,
+ },
+ },
+ */
+ {
+ name: "toggle-alt-shift-right",
+ shortcut: "Alt+Shift+Right",
+ key: "VK_RIGHT",
+ modifiers: {
+ altKey: true,
+ shiftKey: true,
+ },
+ },
+ // Function keys
+ {
+ name: "function-keys-Alt+Shift+F3",
+ shortcut: "Alt+Shift+F3",
+ key: "VK_F3",
+ modifiers: {
+ altKey: true,
+ shiftKey: true,
+ },
+ },
+ {
+ name: "function-keys-F2",
+ shortcut: "F2",
+ key: "VK_F2",
+ modifiers: {
+ altKey: false,
+ shiftKey: false,
+ },
+ },
+ // Misc Shortcuts
+ {
+ name: "valid-command-with-unrecognized-property-name",
+ shortcut: "Alt+Shift+3",
+ key: "3",
+ modifiers: {
+ altKey: true,
+ shiftKey: true,
+ },
+ unrecognized_property: "with-a-random-value",
+ },
+ {
+ name: "spaces-in-shortcut-name",
+ shortcut: " Alt + Shift + 2 ",
+ key: "2",
+ modifiers: {
+ altKey: true,
+ shiftKey: true,
+ },
+ },
+ {
+ name: "toggle-ctrl-space",
+ shortcut: "Ctrl+Space",
+ key: "VK_SPACE",
+ modifiers: {
+ accelKey: true,
+ },
+ },
+ {
+ name: "toggle-ctrl-comma",
+ shortcut: "Ctrl+Comma",
+ key: "VK_COMMA",
+ modifiers: {
+ accelKey: true,
+ },
+ },
+ {
+ name: "toggle-ctrl-period",
+ shortcut: "Ctrl+Period",
+ key: "VK_PERIOD",
+ modifiers: {
+ accelKey: true,
+ },
+ },
+ {
+ name: "toggle-ctrl-alt-v",
+ shortcut: "Ctrl+Alt+V",
+ key: "V",
+ modifiers: {
+ accelKey: true,
+ altKey: true,
+ },
+ },
+];
+
+requestLongerTimeout(2);
+
+add_task(async function test_user_defined_commands() {
+ let win1 = await openNewMailWindow();
+
+ let commands = {};
+ let isMac = AppConstants.platform == "macosx";
+ let totalMacOnlyCommands = 0;
+ let numberNumericCommands = 4;
+
+ for (let testCommand of testCommands) {
+ let command = {
+ suggested_key: {},
+ };
+
+ if (testCommand.shortcut) {
+ command.suggested_key.default = testCommand.shortcut;
+ }
+
+ if (testCommand.shortcutMac) {
+ command.suggested_key.mac = testCommand.shortcutMac;
+ }
+
+ if (testCommand.shortcutMac && !testCommand.shortcut) {
+ totalMacOnlyCommands++;
+ }
+
+ if (testCommand.unrecognized_property) {
+ command.unrecognized_property = testCommand.unrecognized_property;
+ }
+
+ commands[testCommand.name] = command;
+ }
+
+ function background() {
+ browser.commands.onCommand.addListener((commandName, activeTab) => {
+ browser.test.sendMessage("oncommand event received", {
+ commandName,
+ activeTab,
+ });
+ });
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ commands,
+ },
+ background,
+ });
+
+ SimpleTest.waitForExplicitFinish();
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [
+ {
+ message:
+ /Reading manifest: Warning processing commands.*.unrecognized_property: An unexpected property was found/,
+ },
+ ]);
+ });
+
+ // Unrecognized_property in manifest triggers warning.
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ await extension.startup();
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+ await extension.awaitMessage("ready");
+
+ async function runTest(window, expectedTabType) {
+ for (let testCommand of testCommands) {
+ if (testCommand.skip && testCommand.skip.includes(expectedTabType)) {
+ continue;
+ }
+ if (testCommand.shortcutMac && !testCommand.shortcut && !isMac) {
+ continue;
+ }
+ await BrowserTestUtils.synthesizeKey(
+ testCommand.key,
+ testCommand.modifiers,
+ window.browsingContext
+ );
+ let message = await extension.awaitMessage("oncommand event received");
+ is(
+ message.commandName,
+ testCommand.name,
+ `Expected onCommand listener to fire with the correct name: ${testCommand.name}`
+ );
+ is(
+ message.activeTab.type,
+ expectedTabType,
+ `Expected onCommand listener to fire with the correct tab type: ${expectedTabType}`
+ );
+ }
+ }
+
+ // Create another window after the extension is loaded.
+ let win2 = await openNewMailWindow();
+
+ let totalTestCommands =
+ Object.keys(testCommands).length + numberNumericCommands;
+ let expectedCommandsRegistered = isMac
+ ? totalTestCommands
+ : totalTestCommands - totalMacOnlyCommands;
+
+ let account = createAccount();
+ addIdentity(account);
+ let win3 = await openComposeWindow(account);
+ // Some key combinations do not work if the TO field has focus.
+ win3.document.querySelector("editor").focus();
+
+ // Confirm the keysets have been added to all windows.
+ let keysetID = `ext-keyset-id-${makeWidgetId(extension.id)}`;
+
+ let keyset = win1.document.getElementById(keysetID);
+ ok(keyset != null, "Expected keyset to exist");
+ is(
+ keyset.children.length,
+ expectedCommandsRegistered,
+ "Expected keyset of window #1 to have the correct number of children"
+ );
+
+ keyset = win2.document.getElementById(keysetID);
+ ok(keyset != null, "Expected keyset to exist");
+ is(
+ keyset.children.length,
+ expectedCommandsRegistered,
+ "Expected keyset of window #2 to have the correct number of children"
+ );
+
+ keyset = win3.document.getElementById(keysetID);
+ ok(keyset != null, "Expected keyset to exist");
+ is(
+ keyset.children.length,
+ expectedCommandsRegistered,
+ "Expected keyset of window #3 to have the correct number of children"
+ );
+
+ // Confirm that the commands are registered to all windows.
+ await focusWindow(win1);
+ await runTest(win1, "mail");
+
+ await focusWindow(win2);
+ await runTest(win2, "mail");
+
+ await focusWindow(win3);
+ await runTest(win3, "messageCompose");
+
+ // Unload the extension and confirm that the keysets have been removed from all windows.
+ await extension.unload();
+
+ keyset = win1.document.getElementById(keysetID);
+ is(keyset, null, "Expected keyset to be removed from the window #1");
+
+ keyset = win2.document.getElementById(keysetID);
+ is(keyset, null, "Expected keyset to be removed from the window #2");
+
+ keyset = win3.document.getElementById(keysetID);
+ is(keyset, null, "Expected keyset to be removed from the window #3");
+
+ await BrowserTestUtils.closeWindow(win1);
+ await BrowserTestUtils.closeWindow(win2);
+ await BrowserTestUtils.closeWindow(win3);
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+});
+
+add_task(async function test_commands_MV3_event_page() {
+ let win1 = await openNewMailWindow();
+
+ let commands = {};
+ let isMac = AppConstants.platform == "macosx";
+ let totalMacOnlyCommands = 0;
+ let numberNumericCommands = 4;
+
+ for (let testCommand of testCommands) {
+ let command = {
+ suggested_key: {},
+ };
+
+ if (testCommand.shortcut) {
+ command.suggested_key.default = testCommand.shortcut;
+ }
+
+ if (testCommand.shortcutMac) {
+ command.suggested_key.mac = testCommand.shortcutMac;
+ }
+
+ if (testCommand.shortcutMac && !testCommand.shortcut) {
+ totalMacOnlyCommands++;
+ }
+
+ if (testCommand.unrecognized_property) {
+ command.unrecognized_property = testCommand.unrecognized_property;
+ }
+
+ commands[testCommand.name] = command;
+ }
+
+ function background() {
+ // Whenever the extension starts or wakes up, the eventCounter is reset and
+ // allows to observe the order of events fired. In case of a wake-up, the
+ // first observed event is the one that woke up the background.
+ let eventCounter = 0;
+
+ browser.test.onMessage.addListener(async message => {
+ if (message == "createPopup") {
+ let popup = await browser.windows.create({
+ type: "popup",
+ url: "example.html",
+ });
+ browser.test.sendMessage("popupCreated", popup);
+ }
+ });
+
+ browser.commands.onCommand.addListener(async (commandName, activeTab) => {
+ browser.test.sendMessage("oncommand event received", {
+ eventCount: ++eventCounter,
+ commandName,
+ activeTab,
+ });
+ });
+ browser.test.sendMessage("ready");
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ "example.html": `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <title>EXAMPLE</title>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ </head>
+ <body>
+ <p>This is an example page</p>
+ </body>
+ </html>`,
+ },
+ manifest: {
+ manifest_version: 3,
+ background: { scripts: ["utils.js", "background.js"] },
+ browser_specific_settings: { gecko: { id: "commands@mochi.test" } },
+ commands,
+ },
+ });
+
+ SimpleTest.waitForExplicitFinish();
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [
+ {
+ message:
+ /Reading manifest: Warning processing commands.*.unrecognized_property: An unexpected property was found/,
+ },
+ ]);
+ });
+
+ // Unrecognized_property in manifest triggers warning.
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ await extension.startup();
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+ await extension.awaitMessage("ready");
+
+ // Check for persistent listener.
+ assertPersistentListeners(extension, "commands", "onCommand", {
+ primed: false,
+ });
+
+ let gEventCounter = 0;
+ async function runTest(window, expectedTabType) {
+ // The second run will terminate the background script before each keypress,
+ // verifying that the background script is waking up correctly.
+ for (let terminateBackground of [false, true]) {
+ for (let testCommand of testCommands) {
+ if (testCommand.skip && testCommand.skip.includes(expectedTabType)) {
+ continue;
+ }
+ if (testCommand.shortcutMac && !testCommand.shortcut && !isMac) {
+ continue;
+ }
+
+ if (terminateBackground) {
+ gEventCounter = 0;
+ }
+
+ if (terminateBackground) {
+ // Terminate the background and verify the primed persistent listener.
+ await extension.terminateBackground({
+ disableResetIdleForTest: true,
+ });
+ assertPersistentListeners(extension, "commands", "onCommand", {
+ primed: true,
+ });
+ await BrowserTestUtils.synthesizeKey(
+ testCommand.key,
+ testCommand.modifiers,
+ window.browsingContext
+ );
+ // Wait for background restart.
+ await extension.awaitMessage("ready");
+ } else {
+ await BrowserTestUtils.synthesizeKey(
+ testCommand.key,
+ testCommand.modifiers,
+ window.browsingContext
+ );
+ }
+
+ let message = await extension.awaitMessage("oncommand event received");
+ is(
+ message.commandName,
+ testCommand.name,
+ `onCommand listener should fire with the correct command name`
+ );
+ is(
+ message.activeTab.type,
+ expectedTabType,
+ `onCommand listener should fire with the correct tab type`
+ );
+ is(
+ message.eventCount,
+ ++gEventCounter,
+ `Event counter should be correct`
+ );
+ }
+ }
+ }
+
+ // Create another window after the extension is loaded.
+ let win2 = await openNewMailWindow();
+
+ let totalTestCommands =
+ Object.keys(testCommands).length + numberNumericCommands;
+ let expectedCommandsRegistered = isMac
+ ? totalTestCommands
+ : totalTestCommands - totalMacOnlyCommands;
+
+ let account = createAccount();
+ addIdentity(account);
+ let win3 = await openComposeWindow(account);
+ // Some key combinations do not work if the TO field has focus.
+ win3.document.querySelector("editor").focus();
+
+ // Open a popup window.
+ let popupPromise = extension.awaitMessage("popupCreated");
+ extension.sendMessage("createPopup");
+ let popup = await popupPromise;
+ let win4 = Services.wm.getOuterWindowWithId(popup.id);
+
+ // Confirm the keysets have been added to all windows.
+ let keysetID = `ext-keyset-id-${makeWidgetId(extension.id)}`;
+
+ let windows = [
+ { window: win1, autoRemove: false, type: "mail" },
+ { window: win2, autoRemove: false, type: "mail" },
+ { window: win3, autoRemove: false, type: "messageCompose" },
+ { window: win4, autoRemove: true, type: "content" },
+ ];
+ for (let i in windows) {
+ let keyset = windows[i].window.document.getElementById(keysetID);
+ ok(keyset != null, "Expected keyset to exist");
+ is(
+ keyset.children.length,
+ expectedCommandsRegistered,
+ `Expected keyset of window #${i} to have the correct number of children`
+ );
+
+ // Confirm that the commands are registered to all windows.
+ await focusWindow(windows[i].window);
+ await runTest(windows[i].window, windows[i].type);
+ }
+
+ // Unload the extension and confirm that the keysets have been removed from
+ // all windows.
+ await extension.unload();
+ for (let i in windows) {
+ // Extension popup windows are removed/closed on extension unload, so they
+ // have to skip this part of the test.
+ if (windows[i].autoRemove) {
+ continue;
+ }
+ let keyset = windows[i].window.document.getElementById(keysetID);
+ is(keyset, null, `Expected keyset to be removed from the window #${i}`);
+ await BrowserTestUtils.closeWindow(windows[i].window);
+ }
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_commands_onCommand_bug1845236.js b/comm/mail/components/extensions/test/browser/browser_ext_commands_onCommand_bug1845236.js
new file mode 100644
index 0000000000..3a240cc1ce
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_commands_onCommand_bug1845236.js
@@ -0,0 +1,74 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_multiple_messages_selected() {
+ let account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ let subFolders = rootFolder.subFolders;
+ createMessages(subFolders[0], 2);
+ await TestUtils.waitForCondition(
+ () => subFolders[0].messages.hasMoreElements(),
+ "Messages should be added to folder"
+ );
+
+ async function background() {
+ browser.commands.onCommand.addListener((commandName, activeTab) => {
+ browser.test.sendMessage("oncommand event received", {
+ commandName,
+ activeTab,
+ });
+ });
+
+ let { messages } = await browser.messages.query({});
+ await browser.mailTabs.setSelectedMessages(messages.map(m => m.id));
+ let { messages: selectedMessages } =
+ await browser.mailTabs.getSelectedMessages();
+ browser.test.assertEq(
+ selectedMessages.length,
+ 2,
+ "Should have two messages selected"
+ );
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["accountsRead", "messagesRead"],
+ commands: {
+ "test-multi-message": {
+ suggested_key: {
+ default: "Ctrl+Up",
+ },
+ },
+ },
+ },
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ // Trigger the registered command.
+ await BrowserTestUtils.synthesizeKey(
+ "VK_UP",
+ {
+ accelKey: true,
+ },
+ window.browsingContext
+ );
+ let message = await extension.awaitMessage("oncommand event received");
+ is(
+ message.commandName,
+ "test-multi-message",
+ `Expected onCommand listener to fire with the correct name: test-multi-message`
+ );
+ is(
+ message.activeTab.type,
+ "mail",
+ `Expected onCommand listener to fire with the correct tab type: mail`
+ );
+
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_commands_update.js b/comm/mail/components/extensions/test/browser/browser_ext_commands_update.js
new file mode 100644
index 0000000000..1d57585ca6
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_commands_update.js
@@ -0,0 +1,357 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ ExtensionSettingsStore:
+ "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
+});
+
+function enableAddon(addon) {
+ return new Promise(resolve => {
+ AddonManager.addAddonListener({
+ onEnabled(enabledAddon) {
+ if (enabledAddon.id == addon.id) {
+ resolve();
+ AddonManager.removeAddonListener(this);
+ }
+ },
+ });
+ addon.enable();
+ });
+}
+
+function disableAddon(addon) {
+ return new Promise(resolve => {
+ AddonManager.addAddonListener({
+ onDisabled(disabledAddon) {
+ if (disabledAddon.id == addon.id) {
+ resolve();
+ AddonManager.removeAddonListener(this);
+ }
+ },
+ });
+ addon.disable();
+ });
+}
+
+add_task(async function test_update_defined_command() {
+ let extension;
+ let updatedExtension;
+
+ registerCleanupFunction(async () => {
+ await extension.unload();
+
+ // updatedExtension might not have started up if we didn't make it that far.
+ if (updatedExtension) {
+ await updatedExtension.unload();
+ }
+
+ // Check that ESS is cleaned up on uninstall.
+ let storedCommands = ExtensionSettingsStore.getAllForExtension(
+ extension.id,
+ "commands"
+ );
+ is(storedCommands.length, 0, "There are no stored commands after unload");
+ });
+
+ extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ applications: { gecko: { id: "commands_update@mochi.test" } },
+ commands: {
+ foo: {
+ suggested_key: {
+ default: "Ctrl+Shift+I",
+ },
+ description: "The foo command",
+ },
+ },
+ },
+ background() {
+ browser.test.onMessage.addListener(async (msg, data) => {
+ if (msg == "update") {
+ await browser.commands.update(data);
+ browser.test.sendMessage("updateDone");
+ return;
+ } else if (msg == "reset") {
+ await browser.commands.reset(data);
+ browser.test.sendMessage("resetDone");
+ return;
+ } else if (msg != "run") {
+ return;
+ }
+ // Test initial manifest command.
+ let commands = await browser.commands.getAll();
+ browser.test.assertEq(1, commands.length, "There is 1 command");
+ let command = commands[0];
+ browser.test.assertEq("foo", command.name, "The name is right");
+ browser.test.assertEq(
+ "The foo command",
+ command.description,
+ "The description is right"
+ );
+ browser.test.assertEq(
+ "Ctrl+Shift+I",
+ command.shortcut,
+ "The shortcut is right"
+ );
+
+ // Update the shortcut.
+ await browser.commands.update({
+ name: "foo",
+ shortcut: "Ctrl+Shift+L",
+ });
+
+ // Test the updated shortcut.
+ commands = await browser.commands.getAll();
+ browser.test.assertEq(1, commands.length, "There is still 1 command");
+ command = commands[0];
+ browser.test.assertEq("foo", command.name, "The name is unchanged");
+ browser.test.assertEq(
+ "The foo command",
+ command.description,
+ "The description is unchanged"
+ );
+ browser.test.assertEq(
+ "Ctrl+Shift+L",
+ command.shortcut,
+ "The shortcut is updated"
+ );
+
+ // Update the description.
+ await browser.commands.update({
+ name: "foo",
+ description: "The only command",
+ });
+
+ // Test the updated shortcut.
+ commands = await browser.commands.getAll();
+ browser.test.assertEq(1, commands.length, "There is still 1 command");
+ command = commands[0];
+ browser.test.assertEq("foo", command.name, "The name is unchanged");
+ browser.test.assertEq(
+ "The only command",
+ command.description,
+ "The description is updated"
+ );
+ browser.test.assertEq(
+ "Ctrl+Shift+L",
+ command.shortcut,
+ "The shortcut is unchanged"
+ );
+
+ // Clear the shortcut.
+ await browser.commands.update({
+ name: "foo",
+ shortcut: "",
+ });
+ commands = await browser.commands.getAll();
+ browser.test.assertEq(1, commands.length, "There is still 1 command");
+ command = commands[0];
+ browser.test.assertEq("foo", command.name, "The name is unchanged");
+ browser.test.assertEq(
+ "The only command",
+ command.description,
+ "The description is unchanged"
+ );
+ browser.test.assertEq("", command.shortcut, "The shortcut is empty");
+
+ // Update the description and shortcut.
+ await browser.commands.update({
+ name: "foo",
+ description: "The new command",
+ shortcut: " Alt+ Shift +9",
+ });
+
+ // Test the updated shortcut.
+ commands = await browser.commands.getAll();
+ browser.test.assertEq(1, commands.length, "There is still 1 command");
+ command = commands[0];
+ browser.test.assertEq("foo", command.name, "The name is unchanged");
+ browser.test.assertEq(
+ "The new command",
+ command.description,
+ "The description is updated"
+ );
+ browser.test.assertEq(
+ "Alt+Shift+9",
+ command.shortcut,
+ "The shortcut is updated"
+ );
+
+ // Test a bad shortcut update.
+ browser.test.assertThrows(
+ () =>
+ browser.commands.update({ name: "foo", shortcut: "Ctl+Shift+L" }),
+ /Type error for parameter detail .+ primary modifier and a key/,
+ "It rejects for a bad shortcut"
+ );
+
+ // Try to update a command that doesn't exist.
+ await browser.test.assertRejects(
+ browser.commands.update({ name: "bar", shortcut: "Ctrl+Shift+L" }),
+ 'Unknown command "bar"',
+ "It rejects for an unknown command"
+ );
+
+ browser.test.notifyPass("commands");
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+
+ function extensionKeyset(extensionId) {
+ return document.getElementById(
+ makeWidgetId(`ext-keyset-id-${extensionId}`)
+ );
+ }
+
+ function checkKey(extensionId, shortcutKey, modifiers) {
+ let keyset = extensionKeyset(extensionId);
+ is(keyset.children.length, 1, "There is 1 key in the keyset");
+ let key = keyset.children[0];
+ is(key.getAttribute("key"), shortcutKey, "The key is correct");
+ is(key.getAttribute("modifiers"), modifiers, "The modifiers are correct");
+ }
+
+ function checkNumericKey(extensionId, key, modifiers) {
+ let keyset = extensionKeyset(extensionId);
+ is(
+ keyset.children.length,
+ 2,
+ "There are 2 keys in the keyset now, 1 of which contains a keycode."
+ );
+ let numpadKey = keyset.children[0];
+ is(
+ numpadKey.getAttribute("keycode"),
+ `VK_NUMPAD${key}`,
+ "The numpad keycode is correct."
+ );
+ is(
+ numpadKey.getAttribute("modifiers"),
+ modifiers,
+ "The modifiers are correct"
+ );
+
+ let originalNumericKey = keyset.children[1];
+ is(
+ originalNumericKey.getAttribute("keycode"),
+ `VK_${key}`,
+ "The original key is correct."
+ );
+ is(
+ originalNumericKey.getAttribute("modifiers"),
+ modifiers,
+ "The modifiers are correct"
+ );
+ }
+
+ // Check that the <key> is set for the original shortcut.
+ checkKey(extension.id, "I", "accel,shift");
+
+ await extension.awaitMessage("ready");
+ extension.sendMessage("run");
+ await extension.awaitFinish("commands");
+
+ // Check that the <keycode> has been updated.
+ checkNumericKey(extension.id, "9", "alt,shift");
+
+ // Check that the updated command is stored in ExtensionSettingsStore.
+ let storedCommands = ExtensionSettingsStore.getAllForExtension(
+ extension.id,
+ "commands"
+ );
+ is(storedCommands.length, 1, "There is only one stored command");
+ let command = ExtensionSettingsStore.getSetting(
+ "commands",
+ "foo",
+ extension.id
+ ).value;
+ is(command.description, "The new command", "The description is stored");
+ is(command.shortcut, "Alt+Shift+9", "The shortcut is stored");
+
+ // Check that the key is updated immediately.
+ extension.sendMessage("update", { name: "foo", shortcut: "Ctrl+Shift+M" });
+ await extension.awaitMessage("updateDone");
+ checkKey(extension.id, "M", "accel,shift");
+
+ // Ensure all successive updates are stored.
+ // Force the command to only have a description saved.
+ await ExtensionSettingsStore.addSetting(extension.id, "commands", "foo", {
+ description: "description only",
+ });
+ // This command now only has a description set in storage, also update the shortcut.
+ extension.sendMessage("update", { name: "foo", shortcut: "Alt+Shift+9" });
+ await extension.awaitMessage("updateDone");
+ let storedCommand = await ExtensionSettingsStore.getSetting(
+ "commands",
+ "foo",
+ extension.id
+ );
+ is(
+ storedCommand.value.shortcut,
+ "Alt+Shift+9",
+ "The shortcut is saved correctly"
+ );
+ is(
+ storedCommand.value.description,
+ "description only",
+ "The description is saved correctly"
+ );
+
+ // Calling browser.commands.reset("foo") should reset to manifest version.
+ extension.sendMessage("reset", "foo");
+ await extension.awaitMessage("resetDone");
+
+ checkKey(extension.id, "I", "accel,shift");
+
+ // Check that enable/disable removes the keyset and reloads the saved command.
+ let addon = await AddonManager.getAddonByID(extension.id);
+ await disableAddon(addon);
+ let keyset = extensionKeyset(extension.id);
+ is(keyset, null, "The extension keyset is removed when disabled");
+ // Add some commands to storage, only "foo" should get loaded.
+ await ExtensionSettingsStore.addSetting(extension.id, "commands", "foo", {
+ shortcut: "Alt+Shift+9",
+ });
+ await ExtensionSettingsStore.addSetting(extension.id, "commands", "unknown", {
+ shortcut: "Ctrl+Shift+P",
+ });
+ storedCommands = ExtensionSettingsStore.getAllForExtension(
+ extension.id,
+ "commands"
+ );
+ is(storedCommands.length, 2, "There are now 2 commands stored");
+ await enableAddon(addon);
+ // Wait for the keyset to appear (it's async on enable).
+ await TestUtils.waitForCondition(() => extensionKeyset(extension.id));
+ // The keyset is back with the value from ExtensionSettingsStore.
+ checkNumericKey(extension.id, "9", "alt,shift");
+
+ // Check that an update to a shortcut in the manifest is mapped correctly.
+ updatedExtension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ applications: { gecko: { id: "commands_update@mochi.test" } },
+ commands: {
+ foo: {
+ suggested_key: {
+ default: "Ctrl+Shift+L",
+ },
+ description: "The foo command",
+ },
+ },
+ },
+ });
+ await updatedExtension.startup();
+
+ await TestUtils.waitForCondition(() => extensionKeyset(extension.id));
+ // Shortcut is unchanged since it was previously updated.
+ checkNumericKey(extension.id, "9", "alt,shift");
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_composeAction.js b/comm/mail/components/extensions/test/browser/browser_ext_composeAction.js
new file mode 100644
index 0000000000..aba352edf1
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_composeAction.js
@@ -0,0 +1,268 @@
+/* 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 account;
+
+add_setup(async () => {
+ account = createAccount();
+ addIdentity(account);
+});
+
+// This test uses a command from the menus API to open the popup.
+add_task(async function test_popup_open_with_menu_command() {
+ let composeWindow = await openComposeWindow(account);
+ await focusWindow(composeWindow);
+
+ for (let area of ["maintoolbar", "formattoolbar"]) {
+ let testConfig = {
+ actionType: "compose_action",
+ testType: "open-with-menu-command",
+ default_area: area,
+ window: composeWindow,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+ }
+
+ composeWindow.close();
+});
+
+add_task(async function test_theme_icons() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: {
+ gecko: {
+ id: "compose_action@mochi.test",
+ },
+ },
+ compose_action: {
+ default_title: "default",
+ default_icon: "default.png",
+ theme_icons: [
+ {
+ dark: "dark.png",
+ light: "light.png",
+ size: 16,
+ },
+ ],
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let composeWindow = await openComposeWindow(account);
+ await focusWindow(composeWindow);
+
+ let uuid = extension.uuid;
+ let button = composeWindow.document.getElementById(
+ "compose_action_mochi_test-composeAction-toolbarbutton"
+ );
+
+ let dark_theme = await AddonManager.getAddonByID(
+ "thunderbird-compact-dark@mozilla.org"
+ );
+ await dark_theme.enable();
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ Assert.equal(
+ composeWindow.getComputedStyle(button).listStyleImage,
+ `url("moz-extension://${uuid}/light.png")`,
+ `Dark theme should use light icon.`
+ );
+
+ let light_theme = await AddonManager.getAddonByID(
+ "thunderbird-compact-light@mozilla.org"
+ );
+ await light_theme.enable();
+ Assert.equal(
+ composeWindow.getComputedStyle(button).listStyleImage,
+ `url("moz-extension://${uuid}/dark.png")`,
+ `Light theme should use dark icon.`
+ );
+
+ // Disabling a theme will enable the default theme.
+ await light_theme.disable();
+ Assert.equal(
+ composeWindow.getComputedStyle(button).listStyleImage,
+ `url("moz-extension://${uuid}/default.png")`,
+ `Default theme should use default icon.`
+ );
+
+ composeWindow.close();
+ await extension.unload();
+});
+
+add_task(async function test_button_order() {
+ let composeWindow = await openComposeWindow(account);
+ await focusWindow(composeWindow);
+
+ await run_action_button_order_test(
+ [
+ {
+ name: "addon1",
+ area: "maintoolbar",
+ toolbar: "composeToolbar2",
+ },
+ {
+ name: "addon2",
+ area: "formattoolbar",
+ toolbar: "FormatToolbar",
+ },
+ {
+ name: "addon3",
+ area: "maintoolbar",
+ toolbar: "composeToolbar2",
+ },
+ {
+ name: "addon4",
+ area: "formattoolbar",
+ toolbar: "FormatToolbar",
+ },
+ ],
+ composeWindow,
+ "compose_action"
+ );
+
+ composeWindow.close();
+});
+
+add_task(async function test_upgrade() {
+ let composeWindow = await openComposeWindow(account);
+ await focusWindow(composeWindow);
+
+ // Add a compose_action, to make sure the currentSet has been initialized.
+ let extension1 = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ manifest_version: 2,
+ version: "1.0",
+ name: "Extension1",
+ applications: { gecko: { id: "Extension1@mochi.test" } },
+ compose_action: {
+ default_title: "Extension1",
+ },
+ },
+ background() {
+ browser.test.sendMessage("Extension1 ready");
+ },
+ });
+ await extension1.startup();
+ await extension1.awaitMessage("Extension1 ready");
+
+ // Add extension without a compose_action.
+ let extension2 = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ manifest_version: 2,
+ version: "1.0",
+ name: "Extension2",
+ applications: { gecko: { id: "Extension2@mochi.test" } },
+ },
+ background() {
+ browser.test.sendMessage("Extension2 ready");
+ },
+ });
+ await extension2.startup();
+ await extension2.awaitMessage("Extension2 ready");
+
+ // Update the extension, now including a compose_action.
+ let updatedExtension2 = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ manifest_version: 2,
+ version: "2.0",
+ name: "Extension2",
+ applications: { gecko: { id: "Extension2@mochi.test" } },
+ compose_action: {
+ default_title: "Extension2",
+ },
+ },
+ background() {
+ browser.test.sendMessage("Extension2 updated");
+ },
+ });
+ await updatedExtension2.startup();
+ await updatedExtension2.awaitMessage("Extension2 updated");
+
+ let button = composeWindow.document.getElementById(
+ "extension2_mochi_test-composeAction-toolbarbutton"
+ );
+
+ Assert.ok(button, "Button should exist");
+
+ await extension1.unload();
+ await extension2.unload();
+ await updatedExtension2.unload();
+
+ composeWindow.close();
+});
+
+add_task(async function test_iconPath() {
+ let composeWindow = await openComposeWindow(account);
+ await focusWindow(composeWindow);
+
+ // String values for the default_icon manifest entry have been tested in the
+ // theme_icons test already. Here we test imagePath objects for the manifest key
+ // and string values as well as objects for the setIcons() function.
+ let files = {
+ "background.js": async () => {
+ await window.sendMessage("checkState", "icon1.png");
+
+ await browser.composeAction.setIcon({ path: "icon2.png" });
+ await window.sendMessage("checkState", "icon2.png");
+
+ await browser.composeAction.setIcon({ path: { 16: "icon3.png" } });
+ await window.sendMessage("checkState", "icon3.png");
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ applications: {
+ gecko: {
+ id: "compose_action@mochi.test",
+ },
+ },
+ compose_action: {
+ default_title: "default",
+ default_icon: { 16: "icon1.png" },
+ },
+ background: { scripts: ["utils.js", "background.js"] },
+ },
+ });
+
+ extension.onMessage("checkState", async expected => {
+ let uuid = extension.uuid;
+ let button = composeWindow.document.getElementById(
+ "compose_action_mochi_test-composeAction-toolbarbutton"
+ );
+
+ Assert.equal(
+ window.getComputedStyle(button).listStyleImage,
+ `url("moz-extension://${uuid}/${expected}")`,
+ `Icon path should be correct.`
+ );
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ composeWindow.close();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_composeAction_popup_click.js b/comm/mail/components/extensions/test/browser/browser_ext_composeAction_popup_click.js
new file mode 100644
index 0000000000..2c858cf8ab
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_composeAction_popup_click.js
@@ -0,0 +1,266 @@
+/* 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 account;
+
+add_setup(async () => {
+ account = createAccount();
+ addIdentity(account);
+});
+
+// This test clicks on the action button to open the popup.
+add_task(async function test_popup_open_with_click() {
+ for (let area of [null, "formattoolbar"]) {
+ let composeWindow = await openComposeWindow(account);
+ await focusWindow(composeWindow);
+
+ await run_popup_test({
+ actionType: "compose_action",
+ testType: "open-with-mouse-click",
+ window: composeWindow,
+ default_area: area,
+ });
+
+ await run_popup_test({
+ actionType: "compose_action",
+ testType: "open-with-mouse-click",
+ window: composeWindow,
+ default_area: area,
+ disable_button: true,
+ });
+
+ await run_popup_test({
+ actionType: "compose_action",
+ testType: "open-with-mouse-click",
+ window: composeWindow,
+ default_area: area,
+ use_default_popup: true,
+ });
+
+ composeWindow.close();
+ Services.xulStore.removeDocument(
+ "chrome://messenger/content/messengercompose/messengercompose.xhtml"
+ );
+ }
+});
+
+let background_for_openPopup_tests = async () => {
+ let composeTab = await browser.compose.beginNew();
+ browser.test.assertTrue(!!composeTab, "should have found a compose tab");
+
+ let windows = await browser.windows.getAll();
+ let composeWindow = windows.find(window => window.type == "messageCompose");
+ browser.test.assertTrue(
+ !!composeWindow,
+ "should have found a compose window"
+ );
+
+ // The test starts with an opened composeWindow, the compose_action
+ // is allowed there and should be visible, openPopup() should succeed.
+ browser.test.assertTrue(
+ (await browser.windows.get(composeWindow.id)).focused,
+ "composeWindow should be focused"
+ );
+ browser.test.assertTrue(
+ await browser.composeAction.openPopup(),
+ "openPopup() should have succeeded while the compose window is active"
+ );
+ await window.waitForMessage();
+
+ // Disable the compose_action, openPopup() should fail.
+ await browser.composeAction.disable();
+ browser.test.assertFalse(
+ await browser.composeAction.openPopup(),
+ "openPopup() should have failed after the action button was disabled"
+ );
+
+ // Enable the compose_action, openPopup() should succeed.
+ await browser.composeAction.enable();
+ browser.test.assertTrue(
+ await browser.composeAction.openPopup(),
+ "openPopup() should have succeeded after the action button was enabled again"
+ );
+ await window.waitForMessage();
+
+ // Create a popup window, which does not have a compose_action, openPopup()
+ // should fail.
+ let popupWindow = await browser.windows.create({
+ type: "popup",
+ url: "https://www.example.com",
+ });
+ browser.test.assertTrue(
+ (await browser.windows.get(popupWindow.id)).focused,
+ "popupWindow should be focused"
+ );
+ browser.test.assertFalse(
+ await browser.composeAction.openPopup(),
+ "openPopup() should have failed while the popup window is active"
+ );
+
+ // Specifically open the compose_action of the compose window, should become
+ // focused and openPopup() should succeed.
+ browser.test.assertTrue(
+ await browser.composeAction.openPopup({
+ windowId: composeWindow.id,
+ }),
+ "openPopup() should have succeeded when explicitly requesting the compose window"
+ );
+ await window.waitForMessage();
+ browser.test.assertTrue(
+ (await browser.windows.get(composeWindow.id)).focused,
+ "composeWindow should be focused"
+ );
+
+ // The compose window is focused now, openPopup() should succeed.
+ browser.test.assertTrue(
+ await browser.composeAction.openPopup(),
+ "openPopup() should have succeeded while the compose window is active"
+ );
+ await window.waitForMessage();
+
+ // Collapse the toolbar, openPopup() should fail.
+ await window.sendMessage("collapseToolbar", true);
+ browser.test.assertFalse(
+ await browser.composeAction.openPopup(),
+ "openPopup() should have failed while the toolbar is collapsed"
+ );
+
+ // Restore the toolbar, openPopup() should succeed.
+ await window.sendMessage("collapseToolbar", false);
+ browser.test.assertTrue(
+ await browser.composeAction.openPopup(),
+ "openPopup() should have succeeded after the toolbar is restored"
+ );
+ await window.waitForMessage();
+
+ // Close the popup window and finish
+ await browser.windows.remove(popupWindow.id);
+ await browser.windows.remove(composeWindow.id);
+ browser.test.notifyPass("finished");
+};
+
+// This test uses openPopup() to open the popup in a compose window.
+add_task(
+ async function test_popup_open_with_openPopup_in_compose_maintoolbar() {
+ let files = {
+ "background.js": background_for_openPopup_tests,
+ "utils.js": await getUtilsJS(),
+ "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 () {
+ browser.test.sendMessage("popup opened");
+ window.close();
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: {
+ id: "compose_action_openPopup@mochi.test",
+ },
+ },
+ background: { scripts: ["utils.js", "background.js"] },
+ compose_action: {
+ default_title: "default",
+ default_popup: "popup.html",
+ },
+ },
+ });
+
+ extension.onMessage("popup opened", async () => {
+ // Wait a moment to make sure the popup has closed.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => window.setTimeout(r, 150));
+ extension.sendMessage();
+ });
+
+ extension.onMessage("collapseToolbar", state => {
+ let window = Services.wm.getMostRecentWindow("msgcompose");
+ let toolbar = window.document.getElementById("composeToolbar2");
+ if (state) {
+ toolbar.setAttribute("collapsed", "true");
+ } else {
+ toolbar.removeAttribute("collapsed");
+ }
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+// This test uses openPopup() to open the popup in a compose window.
+add_task(
+ async function test_popup_open_with_openPopup_in_compose_formatoolbar() {
+ let files = {
+ "background.js": background_for_openPopup_tests,
+ "utils.js": await getUtilsJS(),
+ "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 () {
+ browser.test.sendMessage("popup opened");
+ window.close();
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: {
+ id: "compose_action_openPopup@mochi.test",
+ },
+ },
+ background: { scripts: ["utils.js", "background.js"] },
+ compose_action: {
+ default_title: "default",
+ default_popup: "popup.html",
+ default_area: "formattoolbar",
+ },
+ },
+ });
+
+ extension.onMessage("popup opened", async () => {
+ // Wait a moment to make sure the popup has closed.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => window.setTimeout(r, 150));
+ extension.sendMessage();
+ });
+
+ extension.onMessage("collapseToolbar", state => {
+ let window = Services.wm.getMostRecentWindow("msgcompose");
+ let toolbar = window.document.getElementById("FormatToolbar");
+ if (state) {
+ toolbar.setAttribute("collapsed", "true");
+ } else {
+ toolbar.removeAttribute("collapsed");
+ }
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_composeAction_popup_click_mv3_event_pages.js b/comm/mail/components/extensions/test/browser/browser_ext_composeAction_popup_click_mv3_event_pages.js
new file mode 100644
index 0000000000..2a5cca1e12
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_composeAction_popup_click_mv3_event_pages.js
@@ -0,0 +1,52 @@
+/* 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 account;
+
+add_setup(async () => {
+ account = createAccount();
+ addIdentity(account);
+});
+
+async function subtest_popup_open_with_click_MV3_event_pages(
+ terminateBackground
+) {
+ for (let area of [null, "formattoolbar"]) {
+ let composeWindow = await openComposeWindow(account);
+ await focusWindow(composeWindow);
+ let testConfig = {
+ manifest_version: 3,
+ terminateBackground,
+ actionType: "compose_action",
+ testType: "open-with-mouse-click",
+ window: composeWindow,
+ default_area: area,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+
+ composeWindow.close();
+ Services.xulStore.removeDocument(
+ "chrome://messenger/content/messengercompose/messengercompose.xhtml"
+ );
+ }
+}
+// This MV3 test clicks on the action button to open the popup.
+add_task(async function test_event_pages_without_background_termination() {
+ await subtest_popup_open_with_click_MV3_event_pages(false);
+});
+// This MV3 test clicks on the action button to open the popup (background termination).
+add_task(async function test_event_pages_with_background_termination() {
+ await subtest_popup_open_with_click_MV3_event_pages(true);
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_composeAction_properties.js b/comm/mail/components/extensions/test/browser/browser_ext_composeAction_properties.js
new file mode 100644
index 0000000000..517dae8c46
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_composeAction_properties.js
@@ -0,0 +1,125 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async () => {
+ let account = createAccount();
+ addIdentity(account);
+
+ let files = {
+ "background.js": async () => {
+ async function checkProperty(property, expectedDefault, ...expected) {
+ browser.test.log(
+ `${property}: ${expectedDefault}, ${expected.join(", ")}`
+ );
+
+ browser.test.assertEq(
+ expectedDefault,
+ await browser.composeAction[property]({})
+ );
+ for (let i = 0; i < 3; i++) {
+ browser.test.assertEq(
+ expected[i],
+ await browser.composeAction[property]({ tabId: tabIDs[i] })
+ );
+ }
+
+ await window.sendMessage("checkProperty", property, expected);
+ }
+
+ await browser.compose.beginNew();
+ await browser.compose.beginNew();
+ await browser.compose.beginNew();
+ let windows = await browser.windows.getAll({
+ populate: true,
+ windowTypes: ["messageCompose"],
+ });
+ let tabIDs = windows.map(w => w.tabs[0].id);
+
+ await checkProperty("isEnabled", true, true, true, true);
+ await browser.composeAction.disable();
+ await checkProperty("isEnabled", false, false, false, false);
+ await browser.composeAction.enable(tabIDs[0]);
+ await checkProperty("isEnabled", false, true, false, false);
+ await browser.composeAction.enable();
+ await checkProperty("isEnabled", true, true, true, true);
+ await browser.composeAction.disable();
+ await checkProperty("isEnabled", false, true, false, false);
+ await browser.composeAction.disable(tabIDs[0]);
+ await checkProperty("isEnabled", false, false, false, false);
+ await browser.composeAction.enable();
+ await checkProperty("isEnabled", true, false, true, true);
+
+ await checkProperty(
+ "getTitle",
+ "default",
+ "default",
+ "default",
+ "default"
+ );
+ await browser.composeAction.setTitle({ tabId: tabIDs[2], title: "tab2" });
+ await checkProperty("getTitle", "default", "default", "default", "tab2");
+ await browser.composeAction.setTitle({ title: "new" });
+ await checkProperty("getTitle", "new", "new", "new", "tab2");
+ await browser.composeAction.setTitle({ tabId: tabIDs[1], title: "tab1" });
+ await checkProperty("getTitle", "new", "new", "tab1", "tab2");
+ await browser.composeAction.setTitle({ tabId: tabIDs[2], title: null });
+ await checkProperty("getTitle", "new", "new", "tab1", "new");
+ await browser.composeAction.setTitle({ title: null });
+ await checkProperty("getTitle", "default", "default", "tab1", "default");
+ await browser.composeAction.setTitle({ tabId: tabIDs[1], title: null });
+ await checkProperty(
+ "getTitle",
+ "default",
+ "default",
+ "default",
+ "default"
+ );
+
+ await browser.tabs.remove(tabIDs[0]);
+ await browser.tabs.remove(tabIDs[1]);
+ await browser.tabs.remove(tabIDs[2]);
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ applications: {
+ gecko: {
+ id: "compose_action_properties@mochi.test",
+ },
+ },
+ background: { scripts: ["utils.js", "background.js"] },
+ compose_action: {
+ default_title: "default",
+ },
+ },
+ });
+
+ extension.onMessage("checkProperty", async (property, expected) => {
+ let composeWindows = [...Services.wm.getEnumerator("msgcompose")];
+ is(composeWindows.length, 3);
+
+ for (let i = 0; i < 3; i++) {
+ let button = composeWindows[i].document.getElementById(
+ "compose_action_properties_mochi_test-composeAction-toolbarbutton"
+ );
+ switch (property) {
+ case "isEnabled":
+ is(button.disabled, !expected[i], `button ${i} enabled state`);
+ break;
+ case "getTitle":
+ is(button.getAttribute("label"), expected[i], `button ${i} label`);
+ break;
+ }
+ }
+
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_composeScripts.js b/comm/mail/components/extensions/test/browser/browser_ext_composeScripts.js
new file mode 100644
index 0000000000..b642a5654d
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_composeScripts.js
@@ -0,0 +1,531 @@
+/* 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/. */
+
+addIdentity(createAccount());
+
+async function checkComposeBody(expected, waitForEvent) {
+ let composeWindows = [...Services.wm.getEnumerator("msgcompose")];
+ Assert.equal(composeWindows.length, 1);
+
+ let composeWindow = composeWindows[0];
+ if (waitForEvent) {
+ await BrowserTestUtils.waitForEvent(
+ composeWindow,
+ "extension-scripts-added"
+ );
+ }
+
+ let composeEditor = composeWindow.GetCurrentEditorElement();
+
+ await checkContent(composeEditor, expected);
+}
+
+/** Tests browser.tabs.insertCSS and browser.tabs.removeCSS. */
+add_task(async function testInsertRemoveCSS() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let tab = await browser.compose.beginNew();
+ await window.sendMessage();
+
+ await browser.tabs.insertCSS(tab.id, {
+ code: "body { background-color: lime; }",
+ });
+ await window.sendMessage();
+
+ await browser.tabs.removeCSS(tab.id, {
+ code: "body { background-color: lime; }",
+ });
+ await window.sendMessage();
+
+ await browser.tabs.insertCSS(tab.id, { file: "test.css" });
+ await window.sendMessage();
+
+ await browser.tabs.removeCSS(tab.id, { file: "test.css" });
+ await window.sendMessage();
+
+ await browser.tabs.remove(tab.id);
+ browser.test.notifyPass("finished");
+ },
+ "test.css": "body { background-color: green; }",
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage();
+ await checkComposeBody({ backgroundColor: "rgba(0, 0, 0, 0)" });
+ extension.sendMessage();
+
+ await extension.awaitMessage();
+ await checkComposeBody({ backgroundColor: "rgb(0, 255, 0)" });
+ extension.sendMessage();
+
+ await extension.awaitMessage();
+ await checkComposeBody({ backgroundColor: "rgba(0, 0, 0, 0)" });
+ extension.sendMessage();
+
+ await extension.awaitMessage();
+ await checkComposeBody({ backgroundColor: "rgb(0, 128, 0)" });
+ extension.sendMessage();
+
+ await extension.awaitMessage();
+ await checkComposeBody({ backgroundColor: "rgba(0, 0, 0, 0)" });
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+/** Tests browser.tabs.insertCSS fails without the "compose" permission. */
+add_task(async function testInsertRemoveCSSNoPermissions() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let tab = await browser.compose.beginNew();
+
+ await browser.test.assertRejects(
+ browser.tabs.insertCSS(tab.id, {
+ code: "body { background-color: darkred; }",
+ }),
+ /Missing host permission for the tab/,
+ "insertCSS without permission should throw"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.insertCSS(tab.id, { file: "test.css" }),
+ /Missing host permission for the tab/,
+ "insertCSS without permission should throw"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.insertCSS(tab.id, {
+ file: "test.css",
+ matchAboutBlank: true,
+ }),
+ /Missing host permission for the tab/,
+ "insertCSS without permission should throw"
+ );
+
+ await window.sendMessage();
+
+ await browser.tabs.remove(tab.id);
+ browser.test.notifyPass("finished");
+ },
+ "test.css": "body { background-color: red; }",
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: [],
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage();
+ await checkComposeBody({
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ textContent: "",
+ });
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+/** Tests browser.tabs.executeScript. */
+add_task(async function testExecuteScript() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let tab = await browser.compose.beginNew();
+ await window.sendMessage();
+
+ await browser.tabs.executeScript(tab.id, {
+ code: `document.body.setAttribute("foo", "bar");`,
+ });
+ await window.sendMessage();
+
+ await browser.tabs.executeScript(tab.id, { file: "test.js" });
+ await window.sendMessage();
+
+ await browser.tabs.remove(tab.id);
+ browser.test.notifyPass("finished");
+ },
+ "test.js": () => {
+ document.body.textContent = "Hey look, the script ran!";
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage();
+ await checkComposeBody({ textContent: "" });
+ extension.sendMessage();
+
+ await extension.awaitMessage();
+ await checkComposeBody({ foo: "bar" });
+ extension.sendMessage();
+
+ await extension.awaitMessage();
+ await checkComposeBody({
+ foo: "bar",
+ textContent: "Hey look, the script ran!",
+ });
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+/** Tests browser.tabs.executeScript fails without the "compose" permission. */
+add_task(async function testExecuteScriptNoPermissions() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let tab = await browser.compose.beginNew();
+
+ await browser.test.assertRejects(
+ browser.tabs.executeScript(tab.id, {
+ code: `document.body.setAttribute("foo", "bar");`,
+ }),
+ /Missing host permission for the tab/,
+ "executeScript without permission should throw"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.executeScript(tab.id, { file: "test.js" }),
+ /Missing host permission for the tab/,
+ "executeScript without permission should throw"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.executeScript(tab.id, {
+ file: "test.js",
+ matchAboutBlank: true,
+ }),
+ /Missing host permission for the tab/,
+ "executeScript without permission should throw"
+ );
+
+ await window.sendMessage();
+
+ await browser.tabs.remove(tab.id);
+ browser.test.notifyPass("finished");
+ },
+ "test.js": () => {
+ document.body.textContent = "Hey look, the script ran!";
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: [],
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage();
+ await checkComposeBody({ foo: null, textContent: "" });
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+/** Tests the messenger alias is available. */
+add_task(async function testExecuteScriptAlias() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let tab = await browser.compose.beginNew();
+ await window.sendMessage();
+
+ await browser.tabs.executeScript(tab.id, {
+ code: `document.body.textContent = messenger.runtime.getManifest().applications.gecko.id;`,
+ });
+ await window.sendMessage();
+
+ await browser.tabs.remove(tab.id);
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ applications: { gecko: { id: "compose_scripts@mochitest" } },
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage();
+ await checkComposeBody({ textContent: "" });
+ extension.sendMessage();
+
+ await extension.awaitMessage();
+ await checkComposeBody({ textContent: "compose_scripts@mochitest" });
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+/**
+ * Tests browser.composeScripts.register correctly adds CSS and JavaScript to
+ * message composition windows opened after it was called. Also tests calling
+ * `unregister` on the returned object.
+ */
+add_task(async function testRegisterBeforeCompose() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let registeredScript = await browser.composeScripts.register({
+ css: [{ code: "body { color: white }" }, { file: "test.css" }],
+ js: [
+ { code: `document.body.setAttribute("foo", "bar");` },
+ { file: "test.js" },
+ ],
+ });
+
+ await browser.compose.beginNew();
+ await window.sendMessage();
+
+ await registeredScript.unregister();
+ browser.test.notifyPass("finished");
+ },
+ "test.css": "body { background-color: green; }",
+ "test.js": () => {
+ document.body.textContent = "Hey look, the script ran!";
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage();
+ await checkComposeBody(
+ {
+ backgroundColor: "rgb(0, 128, 0)",
+ color: "rgb(255, 255, 255)",
+ foo: "bar",
+ textContent: "Hey look, the script ran!",
+ },
+ true
+ );
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await checkComposeBody({
+ backgroundColor: "rgb(0, 128, 0)",
+ color: "rgb(255, 255, 255)",
+ foo: "bar",
+ textContent: "Hey look, the script ran!",
+ });
+
+ await extension.unload();
+ await checkComposeBody({
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ color: "rgb(0, 0, 0)",
+ foo: "bar",
+ textContent: "Hey look, the script ran!",
+ });
+
+ await BrowserTestUtils.closeWindow(
+ Services.wm.getMostRecentWindow("msgcompose")
+ );
+});
+
+/**
+ * Tests browser.composeScripts.register correctly adds CSS and JavaScript to
+ * message composition windows already open when it was called. Also tests
+ * calling `unregister` on the returned object.
+ */
+add_task(async function testRegisterDuringCompose() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let tab = await browser.compose.beginNew();
+ await window.sendMessage();
+
+ let registeredScript = await browser.composeScripts.register({
+ css: [{ code: "body { color: white }" }, { file: "test.css" }],
+ js: [
+ { code: `document.body.setAttribute("foo", "bar");` },
+ { file: "test.js" },
+ ],
+ });
+
+ await window.sendMessage();
+
+ await registeredScript.unregister();
+ await window.sendMessage();
+
+ await browser.tabs.remove(tab.id);
+ browser.test.notifyPass("finished");
+ },
+ "test.css": "body { background-color: green; }",
+ "test.js": () => {
+ document.body.textContent = "Hey look, the script ran!";
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage();
+ await checkComposeBody({
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ textContent: "",
+ });
+ extension.sendMessage();
+
+ await extension.awaitMessage();
+ await checkComposeBody({
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ textContent: "",
+ });
+ extension.sendMessage();
+
+ await extension.awaitMessage();
+ await checkComposeBody({
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ textContent: "",
+ });
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+/** Tests content_scripts in the manifest do not affect compose windows. */
+async function subtestContentScriptManifest(...permissions) {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let tab = await browser.compose.beginNew();
+
+ await window.sendMessage();
+
+ await browser.tabs.remove(tab.id);
+ browser.test.notifyPass("finished");
+ },
+ "test.css": "body { background-color: red; }",
+ "test.js": () => {
+ document.body.textContent = "Hey look, the script ran!";
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions,
+ content_scripts: [
+ {
+ matches: ["<all_urls>"],
+ css: ["test.css"],
+ js: ["test.js"],
+ match_about_blank: true,
+ match_origin_as_fallback: true,
+ },
+ ],
+ },
+ });
+
+ // match_origin_as_fallback is not implemented yet. Bug 1475831.
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ await extension.startup();
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+
+ await extension.awaitMessage();
+ await checkComposeBody({
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ textContent: "",
+ });
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+}
+
+add_task(async function testContentScriptManifestNoPermission() {
+ await subtestContentScriptManifest();
+});
+add_task(async function testContentScriptManifest() {
+ await subtestContentScriptManifest("compose");
+});
+
+/** Tests registered content scripts do not affect compose windows. */
+async function subtestContentScriptRegister(...permissions) {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ await browser.contentScripts.register({
+ matches: ["<all_urls>"],
+ css: [{ file: "test.css" }],
+ js: [{ file: "test.js" }],
+ matchAboutBlank: true,
+ });
+
+ let tab = await browser.compose.beginNew();
+
+ await window.sendMessage();
+
+ await browser.tabs.remove(tab.id);
+ browser.test.notifyPass("finished");
+ },
+ "test.css": "body { background-color: red; }",
+ "test.js": () => {
+ document.body.textContent = "Hey look, the script ran!";
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions,
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage();
+ await checkComposeBody({
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ textContent: "",
+ });
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+}
+
+add_task(async function testContentScriptRegisterNoPermission() {
+ await subtestContentScriptRegister("<all_urls>");
+});
+add_task(async function testContentScriptRegister() {
+ await subtestContentScriptRegister("<all_urls>", "compose");
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_attachments.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_attachments.js
new file mode 100644
index 0000000000..2b66b5a200
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_attachments.js
@@ -0,0 +1,2268 @@
+/* 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";
+
+var { cloudFileAccounts } = ChromeUtils.import(
+ "resource:///modules/cloudFileAccounts.jsm"
+);
+
+const { ExtensionUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionUtils.sys.mjs"
+);
+
+var { ExtensionSupport } = ChromeUtils.import(
+ "resource:///modules/ExtensionSupport.jsm"
+);
+
+let account = createAccount();
+let defaultIdentity = addIdentity(account);
+
+function findWindow(subject) {
+ let windows = Array.from(Services.wm.getEnumerator("msgcompose"));
+ return windows.find(win => {
+ let composeFields = win.GetComposeDetails();
+ return composeFields.subject == subject;
+ });
+}
+
+var MockCompleteGenericSendMessage = {
+ register() {
+ // For every compose window that opens, replace the function which does the
+ // actual sending with one that only records when it has been called.
+ MockCompleteGenericSendMessage._didTryToSendMessage = false;
+ ExtensionSupport.registerWindowListener("MockCompleteGenericSendMessage", {
+ chromeURLs: [
+ "chrome://messenger/content/messengercompose/messengercompose.xhtml",
+ ],
+ onLoadWindow(window) {
+ window.CompleteGenericSendMessage = function (msgType) {
+ let items = [...window.gAttachmentBucket.itemChildren];
+ for (let item of items) {
+ if (item.attachment.sendViaCloud && item.cloudFileAccount) {
+ item.cloudFileAccount.markAsImmutable(item.cloudFileUpload.id);
+ }
+ }
+ Services.obs.notifyObservers(
+ {
+ composeWindow: window,
+ },
+ "mail:composeSendProgressStop"
+ );
+ };
+ },
+ });
+ },
+
+ unregister() {
+ ExtensionSupport.unregisterWindowListener("MockCompleteGenericSendMessage");
+ },
+};
+
+add_task(async function test_file_attachments() {
+ let files = {
+ "background.js": async () => {
+ let listener = {
+ events: [],
+ currentPromise: null,
+
+ pushEvent(...args) {
+ browser.test.log(JSON.stringify(args));
+ this.events.push(args);
+ if (this.currentPromise) {
+ let p = this.currentPromise;
+ this.currentPromise = null;
+ p.resolve();
+ }
+ },
+ async checkEvent(expectedEvent, ...expectedArgs) {
+ if (this.events.length == 0) {
+ await new Promise(resolve => (this.currentPromise = { resolve }));
+ }
+ let [actualEvent, ...actualArgs] = this.events.shift();
+ browser.test.assertEq(expectedEvent, actualEvent);
+ browser.test.assertEq(expectedArgs.length, actualArgs.length);
+
+ for (let i = 0; i < expectedArgs.length; i++) {
+ browser.test.assertEq(typeof expectedArgs[i], typeof actualArgs[i]);
+ if (typeof expectedArgs[i] == "object") {
+ for (let key of Object.keys(expectedArgs[i])) {
+ browser.test.assertEq(expectedArgs[i][key], actualArgs[i][key]);
+ }
+ } else {
+ browser.test.assertEq(expectedArgs[i], actualArgs[i]);
+ }
+ }
+
+ return actualArgs;
+ },
+ };
+ browser.compose.onAttachmentAdded.addListener((...args) =>
+ listener.pushEvent("onAttachmentAdded", ...args)
+ );
+ browser.compose.onAttachmentRemoved.addListener((...args) =>
+ listener.pushEvent("onAttachmentRemoved", ...args)
+ );
+
+ let checkData = async (attachment, size) => {
+ let data = await browser.compose.getAttachmentFile(attachment.id);
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(data instanceof File);
+ browser.test.assertEq(size, data.size);
+ };
+
+ let checkUI = async (composeTab, ...expected) => {
+ let attachments = await browser.compose.listAttachments(composeTab.id);
+ browser.test.assertEq(expected.length, attachments.length);
+ for (let i = 0; i < expected.length; i++) {
+ browser.test.assertEq(expected[i].id, attachments[i].id);
+ browser.test.assertEq(expected[i].size, attachments[i].size);
+ }
+ let details = await browser.compose.getComposeDetails(composeTab.id);
+ return window.sendMessage("checkUI", details, expected);
+ };
+
+ let createCloudfileAccount = () => {
+ let addListener = window.waitForEvent("cloudFile.onAccountAdded");
+ browser.test.sendMessage("createAccount");
+ return addListener;
+ };
+
+ let removeCloudfileAccount = id => {
+ let deleteListener = window.waitForEvent("cloudFile.onAccountDeleted");
+ browser.test.sendMessage("removeAccount", id);
+ return deleteListener;
+ };
+
+ let [createdAccount] = await createCloudfileAccount();
+
+ let file1 = new File(["File number one!"], "file1.txt", {
+ type: "application/vnd.regify",
+ });
+ let file2 = new File(
+ ["File number two? Yes, this is number two."],
+ "file2.txt"
+ );
+ let file3 = new File(["I'm pretending to be file two."], "file3.txt");
+ let composeTab = await browser.compose.beginNew({
+ subject: "Message #1",
+ });
+
+ await checkUI(composeTab);
+
+ // Add an attachment.
+
+ let attachment1 = await browser.compose.addAttachment(composeTab.id, {
+ file: file1,
+ });
+ browser.test.assertEq("file1.txt", attachment1.name);
+ browser.test.assertEq(16, attachment1.size);
+ await checkData(attachment1, file1.size);
+
+ let [, added1] = await listener.checkEvent(
+ "onAttachmentAdded",
+ { id: composeTab.id },
+ { id: attachment1.id, name: "file1.txt" }
+ );
+ await checkData(added1, file1.size);
+
+ await checkUI(composeTab, {
+ id: attachment1.id,
+ name: "file1.txt",
+ size: file1.size,
+ contentType: "application/vnd.regify",
+ });
+
+ // Add another attachment.
+
+ let attachment2 = await browser.compose.addAttachment(composeTab.id, {
+ file: file2,
+ name: "this is file2.txt",
+ });
+ browser.test.assertEq("this is file2.txt", attachment2.name);
+ browser.test.assertEq(41, attachment2.size);
+ await checkData(attachment2, file2.size);
+
+ let [, added2] = await listener.checkEvent(
+ "onAttachmentAdded",
+ { id: composeTab.id },
+ { id: attachment2.id, name: "this is file2.txt" }
+ );
+ await checkData(added2, file2.size);
+
+ await checkUI(
+ composeTab,
+ {
+ id: attachment1.id,
+ name: "file1.txt",
+ size: file1.size,
+ contentType: "application/vnd.regify",
+ },
+ { id: attachment2.id, name: "this is file2.txt", size: file2.size }
+ );
+
+ // Change an attachment.
+
+ let changed2 = await browser.compose.updateAttachment(
+ composeTab.id,
+ attachment2.id,
+ {
+ name: "file2 with a new name.txt",
+ }
+ );
+ browser.test.assertEq("file2 with a new name.txt", changed2.name);
+ browser.test.assertEq(41, changed2.size);
+ await checkData(changed2, file2.size);
+
+ await checkUI(
+ composeTab,
+ {
+ id: attachment1.id,
+ name: "file1.txt",
+ size: file1.size,
+ contentType: "application/vnd.regify",
+ },
+ {
+ id: attachment2.id,
+ name: "file2 with a new name.txt",
+ size: file2.size,
+ }
+ );
+
+ let changed3 = await browser.compose.updateAttachment(
+ composeTab.id,
+ attachment2.id,
+ { file: file3 }
+ );
+ browser.test.assertEq("file2 with a new name.txt", changed3.name);
+ browser.test.assertEq(30, changed3.size);
+ await checkData(changed3, file3.size);
+
+ await checkUI(
+ composeTab,
+ {
+ id: attachment1.id,
+ name: "file1.txt",
+ size: file1.size,
+ contentType: "application/vnd.regify",
+ },
+ {
+ id: attachment2.id,
+ name: "file2 with a new name.txt",
+ size: file3.size,
+ }
+ );
+
+ // Remove the first/local attachment.
+
+ await browser.compose.removeAttachment(composeTab.id, attachment1.id);
+ await listener.checkEvent(
+ "onAttachmentRemoved",
+ { id: composeTab.id },
+ attachment1.id
+ );
+
+ await checkUI(composeTab, {
+ id: attachment2.id,
+ name: "file2 with a new name.txt",
+ size: file3.size,
+ });
+
+ // Convert the second attachment to a cloudFile attachment.
+
+ await new Promise(resolve => {
+ function fileListener(account, fileInfo, tab, relatedFileInfo) {
+ browser.cloudFile.onFileUpload.removeListener(fileListener);
+ browser.test.assertEq(1, fileInfo.id);
+ browser.test.assertEq(undefined, relatedFileInfo);
+ setTimeout(() => resolve());
+ return {
+ url: "https://cloud.provider.net/1",
+ templateInfo: {
+ download_limit: "2",
+ service_name: "Superior Mochitest Service",
+ },
+ };
+ }
+
+ browser.cloudFile.onFileUpload.addListener(fileListener);
+ // Conversion/upload is not yet supported via WebExt API.
+ browser.test.sendMessage(
+ "convertFile",
+ createdAccount.id,
+ "file2 with a new name.txt"
+ );
+ });
+
+ // File retrieved by WebExt API should still be the real file.
+ await checkData(attachment2, 30);
+
+ // UI should show both file size.
+ await checkUI(composeTab, {
+ id: attachment2.id,
+ name: "file2 with a new name.txt",
+ size: file3.size,
+ htmlSize: 4536,
+ });
+
+ // Rename the second/cloud attachment.
+
+ await browser.test.assertRejects(
+ browser.compose.updateAttachment(composeTab.id, attachment2.id, {
+ name: "cloud file2 with a new name.txt",
+ }),
+ "Rename error: Missing cloudFile.onFileRename listener for compose.attachments@mochi.test",
+ "Provider should reject for missing rename support"
+ );
+
+ function cloudFileRenameListener(account, id) {
+ browser.cloudFile.onFileRename.removeListener(cloudFileRenameListener);
+ browser.test.assertEq(1, id);
+ return { url: "https://cloud.provider.net/2" };
+ }
+ browser.cloudFile.onFileRename.addListener(cloudFileRenameListener);
+
+ let changed4 = await browser.compose.updateAttachment(
+ composeTab.id,
+ attachment2.id,
+ {
+ name: "cloud file2 with a new name.txt",
+ }
+ );
+ browser.test.assertEq("cloud file2 with a new name.txt", changed4.name);
+ browser.test.assertEq(30, changed4.size);
+
+ await checkUI(composeTab, {
+ id: attachment2.id,
+ name: "cloud file2 with a new name.txt",
+ size: file3.size,
+ htmlSize: 4554,
+ });
+
+ // File retrieved by WebExt API should still be the real file.
+ await checkData(changed4, 30);
+
+ // Update the second/cloud attachment.
+
+ await browser.test.assertRejects(
+ browser.compose.updateAttachment(composeTab.id, attachment2.id, {
+ file: file2,
+ }),
+ "Upload error: Missing cloudFile.onFileUpload listener for compose.attachments@mochi.test (or it is not returning url or aborted)",
+ "Provider should reject due to upload errors"
+ );
+
+ function cloudFileUploadListener(
+ account,
+ fileInfo,
+ tab,
+ relatedFileInfo
+ ) {
+ browser.cloudFile.onFileUpload.removeListener(cloudFileUploadListener);
+ browser.test.assertEq(3, fileInfo.id);
+ browser.test.assertEq("cloud file2 with a new name.txt", fileInfo.name);
+ browser.test.assertEq(1, relatedFileInfo.id);
+ browser.test.assertEq(
+ "cloud file2 with a new name.txt",
+ relatedFileInfo.name
+ );
+ browser.test.assertTrue(
+ relatedFileInfo.dataChanged,
+ `data should have changed`
+ );
+ browser.test.assertEq(
+ "2",
+ relatedFileInfo.templateInfo.download_limit,
+ "templateInfo download_limit should be correct"
+ );
+ browser.test.assertEq(
+ "Superior Mochitest Service",
+ relatedFileInfo.templateInfo.service_name,
+ "templateInfo service_name should be correct"
+ );
+ return { url: "https://cloud.provider.net/3" };
+ }
+ browser.cloudFile.onFileUpload.addListener(cloudFileUploadListener);
+
+ let changed5 = await browser.compose.updateAttachment(
+ composeTab.id,
+ attachment2.id,
+ { file: file2 }
+ );
+
+ browser.test.assertEq("cloud file2 with a new name.txt", changed5.name);
+ browser.test.assertEq(41, changed5.size);
+ await checkData(changed5, file2.size);
+
+ // Remove the second/cloud attachment.
+
+ await browser.compose.removeAttachment(composeTab.id, attachment2.id);
+
+ await listener.checkEvent(
+ "onAttachmentRemoved",
+ { id: composeTab.id },
+ attachment2.id
+ );
+
+ await checkUI(composeTab);
+
+ await browser.tabs.remove(composeTab.id);
+ browser.test.assertEq(0, listener.events.length);
+
+ await removeCloudfileAccount(createdAccount.id);
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+
+ let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
+ Ci.nsIMessenger
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ cloud_file: {
+ name: "mochitest",
+ management_url: "/content/management.html",
+ },
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ applications: { gecko: { id: "compose.attachments@mochi.test" } },
+ },
+ });
+
+ extension.onMessage("checkUI", (details, expected) => {
+ let composeWindow = findWindow(details.subject);
+ let composeDocument = composeWindow.document;
+
+ let bucket = composeDocument.getElementById("attachmentBucket");
+ Assert.equal(bucket.itemCount, expected.length);
+
+ let totalSize = 0;
+ for (let i = 0; i < expected.length; i++) {
+ let item = bucket.itemChildren[i];
+ let { name, size, htmlSize, contentType } = expected[i];
+ totalSize += htmlSize ? htmlSize : size;
+
+ let displaySize = messenger.formatFileSize(size);
+ if (htmlSize) {
+ displaySize = `${messenger.formatFileSize(htmlSize)} (${displaySize})`;
+ Assert.equal(
+ item.cloudHtmlFileSize,
+ htmlSize,
+ "htmlSize should be correct."
+ );
+ }
+
+ if (contentType) {
+ Assert.equal(
+ item.attachment.contentType,
+ contentType,
+ "contentType should be correct."
+ );
+ }
+
+ Assert.equal(
+ item.querySelector(".attachmentcell-name").textContent +
+ item.querySelector(".attachmentcell-extension").textContent,
+ name,
+ "Displayed name should be correct."
+ );
+ Assert.equal(
+ item.querySelector(".attachmentcell-size").textContent,
+ displaySize,
+ "Displayed size should be correct."
+ );
+ }
+
+ let bucketTotal = composeDocument.getElementById("attachmentBucketSize");
+ if (totalSize == 0) {
+ Assert.equal(bucketTotal.textContent, "");
+ } else {
+ Assert.equal(
+ bucketTotal.textContent,
+ messenger.formatFileSize(totalSize),
+ "Total size should match."
+ );
+ }
+
+ extension.sendMessage();
+ });
+
+ extension.onMessage("createAccount", () => {
+ cloudFileAccounts.createAccount("ext-compose.attachments@mochi.test");
+ });
+
+ extension.onMessage("removeAccount", id => {
+ cloudFileAccounts.removeAccount(id);
+ });
+
+ extension.onMessage("convertFile", (cloudFileAccountId, attachmentName) => {
+ let composeWindow = Services.wm.getMostRecentWindow("msgcompose");
+ let composeDocument = composeWindow.document;
+ let bucket = composeDocument.getElementById("attachmentBucket");
+ let account = cloudFileAccounts.getAccount(cloudFileAccountId);
+
+ let attachmentItem = bucket.itemChildren.find(
+ item => item.attachment && item.attachment.name == attachmentName
+ );
+
+ composeWindow.convertListItemsToCloudAttachment([attachmentItem], account);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_compose_attachments() {
+ let files = {
+ "background.js": async () => {
+ let listener = {
+ events: [],
+ currentPromise: null,
+
+ pushEvent(...args) {
+ browser.test.log(JSON.stringify(args));
+ this.events.push(args);
+ if (this.currentPromise) {
+ let p = this.currentPromise;
+ this.currentPromise = null;
+ p.resolve();
+ }
+ },
+ async checkEvent(expectedEvent, ...expectedArgs) {
+ if (this.events.length == 0) {
+ await new Promise(resolve => (this.currentPromise = { resolve }));
+ }
+ let [actualEvent, ...actualArgs] = this.events.shift();
+ browser.test.assertEq(expectedEvent, actualEvent);
+ browser.test.assertEq(expectedArgs.length, actualArgs.length);
+
+ for (let i = 0; i < expectedArgs.length; i++) {
+ browser.test.assertEq(typeof expectedArgs[i], typeof actualArgs[i]);
+ if (typeof expectedArgs[i] == "object") {
+ for (let key of Object.keys(expectedArgs[i])) {
+ browser.test.assertEq(expectedArgs[i][key], actualArgs[i][key]);
+ }
+ } else {
+ browser.test.assertEq(expectedArgs[i], actualArgs[i]);
+ }
+ }
+
+ return actualArgs;
+ },
+ };
+ browser.compose.onAttachmentAdded.addListener((...args) =>
+ listener.pushEvent("onAttachmentAdded", ...args)
+ );
+ browser.compose.onAttachmentRemoved.addListener((...args) =>
+ listener.pushEvent("onAttachmentRemoved", ...args)
+ );
+
+ let checkData = async (attachment, size) => {
+ let data = await browser.compose.getAttachmentFile(attachment.id);
+ browser.test.assertTrue(
+ // eslint-disable-next-line mozilla/use-isInstance
+ data instanceof File,
+ "Returned file obj should be a File instance."
+ );
+ browser.test.assertEq(
+ size,
+ data.size,
+ "Reported size should be correct."
+ );
+ browser.test.assertEq(
+ attachment.name,
+ data.name,
+ "Name of the File object should match the name of the attachment."
+ );
+ };
+
+ let checkUI = async (composeTab, ...expected) => {
+ let attachments = await browser.compose.listAttachments(composeTab.id);
+ browser.test.assertEq(
+ expected.length,
+ attachments.length,
+ "Number of found attachments should be correct."
+ );
+ for (let i = 0; i < expected.length; i++) {
+ browser.test.assertEq(expected[i].id, attachments[i].id);
+ browser.test.assertEq(expected[i].size, attachments[i].size);
+ }
+ let details = await browser.compose.getComposeDetails(composeTab.id);
+ return window.sendMessage("checkUI", details, expected);
+ };
+
+ let createCloudfileAccount = () => {
+ let addListener = window.waitForEvent("cloudFile.onAccountAdded");
+ browser.test.sendMessage("createAccount");
+ return addListener;
+ };
+
+ let removeCloudfileAccount = id => {
+ let deleteListener = window.waitForEvent("cloudFile.onAccountDeleted");
+ browser.test.sendMessage("removeAccount", id);
+ return deleteListener;
+ };
+
+ async function cloneAttachment(
+ attachment,
+ composeTab,
+ name = attachment.name
+ ) {
+ let clone;
+
+ // If the name is not changed, try to pass in the full object.
+ if (name == attachment.name) {
+ clone = await browser.compose.addAttachment(
+ composeTab.id,
+ attachment
+ );
+ } else {
+ clone = await browser.compose.addAttachment(composeTab.id, {
+ id: attachment.id,
+ name,
+ });
+ }
+
+ browser.test.assertEq(name, clone.name);
+ browser.test.assertEq(attachment.size, clone.size);
+ await checkData(clone, attachment.size);
+
+ let [, added] = await listener.checkEvent(
+ "onAttachmentAdded",
+ { id: composeTab.id },
+ { id: clone.id, name }
+ );
+ await checkData(added, attachment.size);
+ return clone;
+ }
+
+ let [createdAccount] = await createCloudfileAccount();
+
+ let file1 = new File(["File number one!"], "file1.txt");
+ let file2 = new File(
+ ["File number two? Yes, this is number two."],
+ "file2.txt"
+ );
+
+ // -----------------------------------------------------------------------
+
+ let composeTab1 = await browser.compose.beginNew({
+ subject: "Message #2",
+ });
+ await checkUI(composeTab1);
+
+ // Add an attachment to composeTab1.
+
+ let tab1_attachment1 = await browser.compose.addAttachment(
+ composeTab1.id,
+ {
+ file: file1,
+ }
+ );
+ browser.test.assertEq("file1.txt", tab1_attachment1.name);
+ browser.test.assertEq(16, tab1_attachment1.size);
+ await checkData(tab1_attachment1, file1.size);
+
+ let [, tab1_added1] = await listener.checkEvent(
+ "onAttachmentAdded",
+ { id: composeTab1.id },
+ { id: tab1_attachment1.id, name: "file1.txt" }
+ );
+ await checkData(tab1_added1, file1.size);
+
+ await checkUI(composeTab1, {
+ id: tab1_attachment1.id,
+ name: "file1.txt",
+ size: file1.size,
+ });
+
+ // Add another attachment to composeTab1.
+
+ let tab1_attachment2 = await browser.compose.addAttachment(
+ composeTab1.id,
+ {
+ file: file2,
+ name: "this is file2.txt",
+ }
+ );
+ browser.test.assertEq("this is file2.txt", tab1_attachment2.name);
+ browser.test.assertEq(41, tab1_attachment2.size);
+ await checkData(tab1_attachment2, file2.size);
+
+ let [, tab1_added2] = await listener.checkEvent(
+ "onAttachmentAdded",
+ { id: composeTab1.id },
+ { id: tab1_attachment2.id, name: "this is file2.txt" }
+ );
+ await checkData(tab1_added2, file2.size);
+
+ await checkUI(
+ composeTab1,
+ { id: tab1_attachment1.id, name: "file1.txt", size: file1.size },
+ { id: tab1_attachment2.id, name: "this is file2.txt", size: file2.size }
+ );
+
+ // Convert the second attachment to a cloudFile attachment.
+
+ await new Promise(resolve => {
+ function fileListener(account, fileInfo, tab, relatedFileInfo) {
+ browser.cloudFile.onFileUpload.removeListener(fileListener);
+ browser.test.assertEq(1, fileInfo.id);
+ browser.test.assertEq(undefined, relatedFileInfo);
+ setTimeout(() => resolve());
+ return { url: "https://cloud.provider.net/1" };
+ }
+
+ browser.cloudFile.onFileUpload.addListener(fileListener);
+ // Conversion/upload is not yet supported via WebExt API.
+ browser.test.sendMessage(
+ "convertFile",
+ createdAccount.id,
+ "this is file2.txt"
+ );
+ });
+
+ await checkUI(
+ composeTab1,
+ {
+ id: tab1_attachment1.id,
+ name: "file1.txt",
+ size: file1.size,
+ },
+ {
+ id: tab1_attachment2.id,
+ name: "this is file2.txt",
+ size: 41,
+ htmlSize: 4300,
+ contentLocation: "https://cloud.provider.net/1",
+ }
+ );
+
+ // -----------------------------------------------------------------------
+
+ // Create a second compose window and clone both attachments from tab1. The
+ // second one should be cloned as a cloud attachment, having no size and the
+ // correct contentLocation. Both attachments will be renamed while cloning.
+
+ // The cloud file rename should be handled as a new file upload, because
+ // the same url is used in tab1. The original attachment should be passed
+ // as relatedFileInfo.
+ let tab2_uploadPromise = new Promise(resolve => {
+ function fileListener(account, fileInfo, tab, relatedFileInfo) {
+ browser.cloudFile.onFileUpload.removeListener(fileListener);
+ browser.test.assertEq(2, fileInfo.id);
+ browser.test.assertEq("this is renamed file2.txt", fileInfo.name);
+ browser.test.assertEq(1, relatedFileInfo.id);
+ browser.test.assertEq("this is file2.txt", relatedFileInfo.name);
+ browser.test.assertFalse(
+ relatedFileInfo.dataChanged,
+ `data should not have changed`
+ );
+ setTimeout(() => resolve());
+ return { url: "https://cloud.provider.net/2" };
+ }
+
+ browser.cloudFile.onFileUpload.addListener(fileListener);
+ });
+
+ let composeTab2 = await browser.compose.beginNew({
+ subject: "Message #3",
+ });
+ let tab2_attachment1 = await cloneAttachment(
+ tab1_attachment1,
+ composeTab2,
+ "I want to be called file3.txt"
+ );
+ await checkUI(composeTab2, {
+ id: tab2_attachment1.id,
+ name: "I want to be called file3.txt",
+ size: file1.size,
+ });
+
+ let tab2_attachment2 = await cloneAttachment(
+ tab1_attachment2,
+ composeTab2,
+ "this is renamed file2.txt"
+ );
+
+ await checkUI(
+ composeTab2,
+ {
+ id: tab2_attachment1.id,
+ name: "I want to be called file3.txt",
+ size: file1.size,
+ },
+ {
+ id: tab2_attachment2.id,
+ name: "this is renamed file2.txt",
+ size: 41,
+ htmlSize: 4324,
+ contentLocation: "https://cloud.provider.net/2",
+ }
+ );
+
+ await tab2_uploadPromise;
+
+ // -----------------------------------------------------------------------
+
+ // Create a 3rd compose window and clone both attachments from tab1. The
+ // second one should be cloned as cloud attachment, having no size and the
+ // correct contentLocation. Files are not renamed this time, so there should
+ // not be an upload request (which would fail without upload listener), as
+ // we simply re-attach the cloudFileUpload data.
+
+ let composeTab3 = await browser.compose.beginNew({
+ subject: "Message #4",
+ });
+ let tab3_attachment1 = await cloneAttachment(
+ tab1_attachment1,
+ composeTab3
+ );
+ await checkUI(composeTab3, {
+ id: tab3_attachment1.id,
+ name: "file1.txt",
+ size: file1.size,
+ });
+
+ let tab3_attachment2 = await cloneAttachment(
+ tab1_attachment2,
+ composeTab3
+ );
+
+ await checkUI(
+ composeTab3,
+ {
+ id: tab3_attachment1.id,
+ name: "file1.txt",
+ size: file1.size,
+ },
+ {
+ id: tab3_attachment2.id,
+ name: "this is file2.txt",
+ size: 41,
+ htmlSize: 4300,
+ contentLocation: "https://cloud.provider.net/1",
+ }
+ );
+
+ // Rename the cloned cloud attachments of tab3. It should trigger a new
+ // upload, to not invalidate the original url still used in tab1.
+
+ let tab3_uploadPromise = new Promise(resolve => {
+ function fileListener(account, fileInfo, tab, relatedFileInfo) {
+ browser.cloudFile.onFileUpload.removeListener(fileListener);
+ browser.test.assertEq(3, fileInfo.id);
+ browser.test.assertEq(
+ "That is going to be interesting.txt",
+ fileInfo.name
+ );
+ browser.test.assertEq(1, relatedFileInfo.id);
+ browser.test.assertEq("this is file2.txt", relatedFileInfo.name);
+ browser.test.assertFalse(
+ relatedFileInfo.dataChanged,
+ `data should not have changed`
+ );
+ setTimeout(() => resolve());
+ return { url: "https://cloud.provider.net/3" };
+ }
+
+ browser.cloudFile.onFileUpload.addListener(fileListener);
+ });
+
+ let tab3_changed2 = await browser.compose.updateAttachment(
+ composeTab3.id,
+ tab3_attachment2.id,
+ {
+ name: "That is going to be interesting.txt",
+ }
+ );
+ browser.test.assertEq(
+ "That is going to be interesting.txt",
+ tab3_changed2.name
+ );
+ browser.test.assertEq(41, tab3_changed2.size);
+ await checkData(tab3_changed2, file2.size);
+
+ await checkUI(
+ composeTab3,
+ {
+ id: tab3_attachment1.id,
+ name: "file1.txt",
+ size: file1.size,
+ },
+ {
+ id: tab3_attachment2.id,
+ name: "That is going to be interesting.txt",
+ size: 41,
+ htmlSize: 4354,
+ contentLocation: "https://cloud.provider.net/3",
+ }
+ );
+
+ await tab3_uploadPromise;
+
+ // -----------------------------------------------------------------------
+
+ // Open a 4th compose window and directly clone attachment1 and attachment2,
+ // renaming both. This should trigger a new file upload.
+
+ let tab4_uploadPromise = new Promise(resolve => {
+ function fileListener(account, fileInfo, tab, relatedFileInfo) {
+ browser.cloudFile.onFileUpload.removeListener(fileListener);
+ browser.test.assertEq(4, fileInfo.id);
+ browser.test.assertEq(
+ "I got renamed too, how crazy is that!.txt",
+ fileInfo.name
+ );
+ browser.test.assertEq(1, relatedFileInfo.id);
+ browser.test.assertEq("this is file2.txt", relatedFileInfo.name);
+ browser.test.assertFalse(
+ relatedFileInfo.dataChanged,
+ `data should not have changed`
+ );
+ setTimeout(() => resolve());
+ return { url: "https://cloud.provider.net/4" };
+ }
+
+ browser.cloudFile.onFileUpload.addListener(fileListener);
+ });
+
+ let tab4_details = { subject: "Message #5" };
+ tab4_details.attachments = [
+ Object.assign({}, tab1_attachment1),
+ Object.assign({}, tab1_attachment2),
+ ];
+ tab4_details.attachments[0].name = "I got renamed.txt";
+ tab4_details.attachments[1].name =
+ "I got renamed too, how crazy is that!.txt";
+ let composeTab4 = await browser.compose.beginNew(tab4_details);
+
+ // In this test we need to manually request the id of the added attachments.
+ let [tab4_attachment1, tab4_attachment2] =
+ await browser.compose.listAttachments(composeTab4.id);
+
+ let [, addedReClone1] = await listener.checkEvent(
+ "onAttachmentAdded",
+ { id: composeTab4.id },
+ { id: tab4_attachment1.id, name: "I got renamed.txt" }
+ );
+ await checkData(addedReClone1, file1.size);
+ let [, addedReClone2] = await listener.checkEvent(
+ "onAttachmentAdded",
+ { id: composeTab4.id },
+ {
+ id: tab4_attachment2.id,
+ name: "I got renamed too, how crazy is that!.txt",
+ }
+ );
+ await checkData(addedReClone2, file2.size);
+
+ await checkUI(
+ composeTab4,
+ {
+ id: tab4_attachment1.id,
+ name: "I got renamed.txt",
+ size: file1.size,
+ },
+ {
+ id: tab4_attachment2.id,
+ name: "I got renamed too, how crazy is that!.txt",
+ size: 41,
+ htmlSize: 4372,
+ contentLocation: "https://cloud.provider.net/4",
+ }
+ );
+
+ await tab4_uploadPromise;
+
+ // -----------------------------------------------------------------------
+
+ // Open a 5th compose window and directly clone attachment1 and attachment2
+ // from tab1.
+
+ let tab5_details = { subject: "Message #6" };
+ tab5_details.attachments = [tab1_attachment1, tab1_attachment2];
+ let composeTab5 = await browser.compose.beginNew(tab5_details);
+
+ // In this test we need to manually request the id of the added attachments.
+ let [tab5_attachment1, tab5_attachment2] =
+ await browser.compose.listAttachments(composeTab5.id);
+
+ await listener.checkEvent(
+ "onAttachmentAdded",
+ { id: composeTab5.id },
+ { id: tab5_attachment1.id, name: "file1.txt" }
+ );
+ await listener.checkEvent(
+ "onAttachmentAdded",
+ { id: composeTab5.id },
+ { id: tab5_attachment2.id, name: "this is file2.txt" }
+ );
+
+ // Delete the cloud attachment2 in tab1, which should not trigger a cloud
+ // delete, as the url is still used in tab5.
+
+ function fileListener(account, id, tab) {
+ browser.test.fail(
+ `The onFileDeleted listener should not fire for deleting a cloud file which is still used in another tab.`
+ );
+ }
+ browser.cloudFile.onFileDeleted.addListener(fileListener);
+
+ await browser.compose.removeAttachment(
+ composeTab1.id,
+ tab1_attachment2.id
+ );
+ await listener.checkEvent(
+ "onAttachmentRemoved",
+ { id: composeTab1.id },
+ tab1_attachment2.id
+ );
+ browser.cloudFile.onFileDeleted.removeListener(fileListener);
+
+ // Renaming cloud attachment2 in tab5 should now be a simple rename, as the
+ // url is not used anywhere anymore.
+
+ let tab5_renamePromise = new Promise(resolve => {
+ function fileListener() {
+ browser.cloudFile.onFileRename.removeListener(fileListener);
+ setTimeout(() => resolve());
+ }
+ browser.cloudFile.onFileRename.addListener(fileListener);
+ });
+
+ await browser.compose.updateAttachment(
+ composeTab5.id,
+ tab5_attachment2.id,
+ {
+ name: "I am the only one left.txt",
+ }
+ );
+ await tab5_renamePromise;
+
+ // Delete the cloud attachment2 in tab5, which now should trigger a cloud
+ // delete.
+
+ let tab5_deletePromise = new Promise(resolve => {
+ function fileListener(account, id, tab) {
+ browser.cloudFile.onFileDeleted.removeListener(fileListener);
+ setTimeout(() => resolve(id));
+ }
+ browser.cloudFile.onFileDeleted.addListener(fileListener);
+ });
+
+ await browser.compose.removeAttachment(
+ composeTab5.id,
+ tab5_attachment2.id
+ );
+ await listener.checkEvent(
+ "onAttachmentRemoved",
+ { id: composeTab5.id },
+ tab5_attachment2.id
+ );
+ await tab5_deletePromise;
+
+ // Clean up
+
+ await browser.tabs.remove(composeTab5.id);
+ await browser.tabs.remove(composeTab4.id);
+ await browser.tabs.remove(composeTab3.id);
+ await browser.tabs.remove(composeTab2.id);
+ await browser.tabs.remove(composeTab1.id);
+ browser.test.assertEq(0, listener.events.length);
+
+ await removeCloudfileAccount(createdAccount.id);
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+
+ let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
+ Ci.nsIMessenger
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ cloud_file: {
+ name: "mochitest",
+ management_url: "/content/management.html",
+ },
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ applications: { gecko: { id: "compose.attachments@mochi.test" } },
+ },
+ });
+
+ extension.onMessage("checkUI", (details, expected) => {
+ let composeWindow = findWindow(details.subject);
+ let composeDocument = composeWindow.document;
+
+ let bucket = composeDocument.getElementById("attachmentBucket");
+ Assert.equal(bucket.itemCount, expected.length);
+
+ let totalSize = 0;
+ for (let i = 0; i < expected.length; i++) {
+ let item = bucket.itemChildren[i];
+ let { name, size, htmlSize, contentLocation } = expected[i];
+ totalSize += htmlSize ? htmlSize : size;
+
+ let displaySize = messenger.formatFileSize(size);
+ if (htmlSize) {
+ displaySize = `${messenger.formatFileSize(htmlSize)} (${displaySize})`;
+ Assert.equal(
+ item.cloudHtmlFileSize,
+ htmlSize,
+ "htmlSize should be correct."
+ );
+ }
+
+ // size and name are checked against the displayed values, contentLocation
+ // is checked against the associated attachment.
+
+ if (contentLocation) {
+ Assert.equal(
+ item.attachment.contentLocation,
+ contentLocation,
+ "contentLocation for cloud files should be correct."
+ );
+ Assert.equal(
+ item.attachment.sendViaCloud,
+ true,
+ "sendViaCloud for cloud files should be correct."
+ );
+ } else {
+ Assert.equal(
+ item.attachment.contentLocation,
+ "",
+ "contentLocation for cloud files should be correct."
+ );
+ Assert.equal(
+ item.attachment.sendViaCloud,
+ false,
+ "sendViaCloud for cloud files should be correct."
+ );
+ }
+
+ Assert.equal(
+ item.querySelector(".attachmentcell-name").textContent +
+ item.querySelector(".attachmentcell-extension").textContent,
+ name,
+ "Name should be correct."
+ );
+ Assert.equal(
+ item.querySelector(".attachmentcell-size").textContent,
+ displaySize,
+ "Displayed size should be correct."
+ );
+ }
+
+ let bucketTotal = composeDocument.getElementById("attachmentBucketSize");
+ if (totalSize == 0) {
+ Assert.equal(bucketTotal.textContent, "");
+ } else {
+ Assert.equal(
+ bucketTotal.textContent,
+ messenger.formatFileSize(totalSize)
+ );
+ }
+
+ extension.sendMessage();
+ });
+
+ extension.onMessage("createAccount", () => {
+ cloudFileAccounts.createAccount("ext-compose.attachments@mochi.test");
+ });
+
+ extension.onMessage("removeAccount", id => {
+ cloudFileAccounts.removeAccount(id);
+ });
+
+ extension.onMessage("convertFile", (cloudFileAccountId, attachmentName) => {
+ let composeWindow = Services.wm.getMostRecentWindow("msgcompose");
+ let composeDocument = composeWindow.document;
+ let bucket = composeDocument.getElementById("attachmentBucket");
+ let account = cloudFileAccounts.getAccount(cloudFileAccountId);
+
+ let attachmentItem = bucket.itemChildren.find(
+ item => item.attachment && item.attachment.name == attachmentName
+ );
+
+ composeWindow.convertListItemsToCloudAttachment([attachmentItem], account);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_compose_attachments_immutable() {
+ MockCompleteGenericSendMessage.register();
+
+ let files = {
+ "background.js": async () => {
+ let listener = {
+ events: [],
+ currentPromise: null,
+
+ pushEvent(...args) {
+ browser.test.log(JSON.stringify(args));
+ this.events.push(args);
+ if (this.currentPromise) {
+ let p = this.currentPromise;
+ this.currentPromise = null;
+ p.resolve();
+ }
+ },
+ async checkEvent(expectedEvent, ...expectedArgs) {
+ if (this.events.length == 0) {
+ await new Promise(resolve => (this.currentPromise = { resolve }));
+ }
+ let [actualEvent, ...actualArgs] = this.events.shift();
+ browser.test.assertEq(expectedEvent, actualEvent);
+ browser.test.assertEq(expectedArgs.length, actualArgs.length);
+
+ for (let i = 0; i < expectedArgs.length; i++) {
+ browser.test.assertEq(typeof expectedArgs[i], typeof actualArgs[i]);
+ if (typeof expectedArgs[i] == "object") {
+ for (let key of Object.keys(expectedArgs[i])) {
+ browser.test.assertEq(expectedArgs[i][key], actualArgs[i][key]);
+ }
+ } else {
+ browser.test.assertEq(expectedArgs[i], actualArgs[i]);
+ }
+ }
+
+ return actualArgs;
+ },
+ };
+ browser.compose.onAttachmentAdded.addListener((...args) =>
+ listener.pushEvent("onAttachmentAdded", ...args)
+ );
+ browser.compose.onAttachmentRemoved.addListener((...args) =>
+ listener.pushEvent("onAttachmentRemoved", ...args)
+ );
+
+ let checkData = async (attachment, size) => {
+ let data = await browser.compose.getAttachmentFile(attachment.id);
+ browser.test.assertTrue(
+ // eslint-disable-next-line mozilla/use-isInstance
+ data instanceof File,
+ "Returned file obj should be a File instance."
+ );
+ browser.test.assertEq(
+ size,
+ data.size,
+ "Reported size should be correct."
+ );
+ browser.test.assertEq(
+ attachment.name,
+ data.name,
+ "Name of the File object should match the name of the attachment."
+ );
+ };
+
+ let checkUI = async (composeTab, ...expected) => {
+ let attachments = await browser.compose.listAttachments(composeTab.id);
+ browser.test.assertEq(
+ expected.length,
+ attachments.length,
+ "Number of found attachments should be correct."
+ );
+ for (let i = 0; i < expected.length; i++) {
+ browser.test.assertEq(expected[i].id, attachments[i].id);
+ browser.test.assertEq(expected[i].size, attachments[i].size);
+ }
+ let details = await browser.compose.getComposeDetails(composeTab.id);
+ return window.sendMessage("checkUI", details, expected);
+ };
+
+ let createCloudfileAccount = () => {
+ let addListener = window.waitForEvent("cloudFile.onAccountAdded");
+ browser.test.sendMessage("createAccount");
+ return addListener;
+ };
+
+ let removeCloudfileAccount = id => {
+ let deleteListener = window.waitForEvent("cloudFile.onAccountDeleted");
+ browser.test.sendMessage("removeAccount", id);
+ return deleteListener;
+ };
+
+ async function cloneAttachment(
+ attachment,
+ composeTab,
+ name = attachment.name
+ ) {
+ let clone;
+
+ // If the name is not changed, try to pass in the full object.
+ if (name == attachment.name) {
+ clone = await browser.compose.addAttachment(
+ composeTab.id,
+ attachment
+ );
+ } else {
+ clone = await browser.compose.addAttachment(composeTab.id, {
+ id: attachment.id,
+ name,
+ });
+ }
+
+ browser.test.assertEq(name, clone.name);
+ browser.test.assertEq(attachment.size, clone.size);
+ await checkData(clone, attachment.size);
+
+ let [, added] = await listener.checkEvent(
+ "onAttachmentAdded",
+ { id: composeTab.id },
+ { id: clone.id, name }
+ );
+ await checkData(added, attachment.size);
+ return clone;
+ }
+
+ let [createdAccount] = await createCloudfileAccount();
+
+ let file1 = new File(["File number one!"], "file1.txt");
+ let file2 = new File(
+ ["File number two? Yes, this is number two."],
+ "file2.txt"
+ );
+
+ // -----------------------------------------------------------------------
+
+ let composeTab1 = await browser.compose.beginNew({
+ to: "user@inter.net",
+ subject: "Test",
+ });
+ await checkUI(composeTab1);
+
+ // Add an attachment to composeTab1.
+
+ let tab1_attachment1 = await browser.compose.addAttachment(
+ composeTab1.id,
+ {
+ file: file1,
+ }
+ );
+ browser.test.assertEq("file1.txt", tab1_attachment1.name);
+ browser.test.assertEq(16, tab1_attachment1.size);
+ await checkData(tab1_attachment1, file1.size);
+
+ let [, tab1_added1] = await listener.checkEvent(
+ "onAttachmentAdded",
+ { id: composeTab1.id },
+ { id: tab1_attachment1.id, name: "file1.txt" }
+ );
+ await checkData(tab1_added1, file1.size);
+
+ await checkUI(composeTab1, {
+ id: tab1_attachment1.id,
+ name: "file1.txt",
+ size: file1.size,
+ });
+
+ // Add another attachment to composeTab1.
+
+ let tab1_attachment2 = await browser.compose.addAttachment(
+ composeTab1.id,
+ {
+ file: file2,
+ name: "this is file2.txt",
+ }
+ );
+ browser.test.assertEq("this is file2.txt", tab1_attachment2.name);
+ browser.test.assertEq(41, tab1_attachment2.size);
+ await checkData(tab1_attachment2, file2.size);
+
+ let [, tab1_added2] = await listener.checkEvent(
+ "onAttachmentAdded",
+ { id: composeTab1.id },
+ { id: tab1_attachment2.id, name: "this is file2.txt" }
+ );
+ await checkData(tab1_added2, file2.size);
+
+ await checkUI(
+ composeTab1,
+ { id: tab1_attachment1.id, name: "file1.txt", size: file1.size },
+ { id: tab1_attachment2.id, name: "this is file2.txt", size: file2.size }
+ );
+
+ // Convert the second attachment to a cloudFile attachment.
+
+ await new Promise(resolve => {
+ function fileListener(account, fileInfo, tab, relatedFileInfo) {
+ browser.cloudFile.onFileUpload.removeListener(fileListener);
+ browser.test.assertEq(1, fileInfo.id);
+ browser.test.assertEq(undefined, relatedFileInfo);
+ setTimeout(() => resolve());
+ return { url: "https://cloud.provider.net/1" };
+ }
+
+ browser.cloudFile.onFileUpload.addListener(fileListener);
+ // Conversion/upload is not yet supported via WebExt API.
+ browser.test.sendMessage(
+ "convertFile",
+ createdAccount.id,
+ "this is file2.txt"
+ );
+ });
+
+ await checkUI(
+ composeTab1,
+ {
+ id: tab1_attachment1.id,
+ name: "file1.txt",
+ size: file1.size,
+ },
+ {
+ id: tab1_attachment2.id,
+ name: "this is file2.txt",
+ size: 41,
+ htmlSize: 4300,
+ contentLocation: "https://cloud.provider.net/1",
+ }
+ );
+
+ // -----------------------------------------------------------------------
+
+ // Create a second compose window and clone both attachments from tab1. The
+ // second one should be cloned as a cloud attachment, having no size and the
+ // correct contentLocation.
+
+ let composeTab2 = await browser.compose.beginNew({
+ subject: "Message #7",
+ });
+ let tab2_attachment1 = await cloneAttachment(
+ tab1_attachment1,
+ composeTab2
+ );
+ await checkUI(composeTab2, {
+ id: tab2_attachment1.id,
+ name: "file1.txt",
+ size: file1.size,
+ });
+
+ let tab2_attachment2 = await cloneAttachment(
+ tab1_attachment2,
+ composeTab2,
+ "this is file2.txt"
+ );
+
+ await checkUI(
+ composeTab2,
+ {
+ id: tab2_attachment1.id,
+ name: "file1.txt",
+ size: file1.size,
+ },
+ {
+ id: tab2_attachment2.id,
+ name: "this is file2.txt",
+ size: 41,
+ htmlSize: 4300,
+ contentLocation: "https://cloud.provider.net/1",
+ }
+ );
+
+ // Send the message and have its attachment marked as immutable.
+ await browser.compose.sendMessage(composeTab1.id, { mode: "sendNow" });
+ await browser.tabs.remove(composeTab1.id);
+
+ // Delete the cloud attachment2 in tab2, which should not trigger a cloud
+ // delete, as the url has been marked as immutable by sending the message
+ // in tab1.
+
+ function fileListener(account, id, tab) {
+ browser.test.fail(
+ `The onFileDeleted listener should not fire for deleting a cloud file marked as immutable.`
+ );
+ }
+ browser.cloudFile.onFileDeleted.addListener(fileListener);
+
+ await browser.compose.removeAttachment(
+ composeTab2.id,
+ tab2_attachment2.id
+ );
+ await listener.checkEvent(
+ "onAttachmentRemoved",
+ { id: composeTab2.id },
+ tab2_attachment2.id
+ );
+
+ // Clean up
+
+ await browser.tabs.remove(composeTab2.id);
+ browser.test.assertEq(0, listener.events.length);
+
+ await removeCloudfileAccount(createdAccount.id);
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+
+ let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
+ Ci.nsIMessenger
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ cloud_file: {
+ name: "mochitest",
+ management_url: "/content/management.html",
+ },
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose", "compose.send"],
+ applications: { gecko: { id: "compose.attachments@mochi.test" } },
+ },
+ });
+
+ extension.onMessage("checkUI", (details, expected) => {
+ let composeWindow = findWindow(details.subject);
+ let composeDocument = composeWindow.document;
+
+ let bucket = composeDocument.getElementById("attachmentBucket");
+ Assert.equal(bucket.itemCount, expected.length);
+
+ let totalSize = 0;
+ for (let i = 0; i < expected.length; i++) {
+ let item = bucket.itemChildren[i];
+ let { name, size, htmlSize, contentLocation } = expected[i];
+ totalSize += htmlSize ? htmlSize : size;
+
+ let displaySize = messenger.formatFileSize(size);
+ if (htmlSize) {
+ displaySize = `${messenger.formatFileSize(htmlSize)} (${displaySize})`;
+ Assert.equal(
+ item.cloudHtmlFileSize,
+ htmlSize,
+ "htmlSize should be correct."
+ );
+ }
+
+ // size and name are checked against the displayed values, contentLocation
+ // is checked against the associated attachment.
+
+ if (contentLocation) {
+ Assert.equal(
+ item.attachment.contentLocation,
+ contentLocation,
+ "contentLocation for cloud files should be correct."
+ );
+ Assert.equal(
+ item.attachment.sendViaCloud,
+ true,
+ "sendViaCloud for cloud files should be correct."
+ );
+ } else {
+ Assert.equal(
+ item.attachment.contentLocation,
+ "",
+ "contentLocation for cloud files should be correct."
+ );
+ Assert.equal(
+ item.attachment.sendViaCloud,
+ false,
+ "sendViaCloud for cloud files should be correct."
+ );
+ }
+
+ Assert.equal(
+ item.querySelector(".attachmentcell-name").textContent +
+ item.querySelector(".attachmentcell-extension").textContent,
+ name,
+ "Name should be correct."
+ );
+ Assert.equal(
+ item.querySelector(".attachmentcell-size").textContent,
+ displaySize,
+ "Displayed size should be correct."
+ );
+ }
+
+ let bucketTotal = composeDocument.getElementById("attachmentBucketSize");
+ if (totalSize == 0) {
+ Assert.equal(bucketTotal.textContent, "");
+ } else {
+ Assert.equal(
+ bucketTotal.textContent,
+ messenger.formatFileSize(totalSize)
+ );
+ }
+
+ extension.sendMessage();
+ });
+
+ extension.onMessage("createAccount", () => {
+ cloudFileAccounts.createAccount("ext-compose.attachments@mochi.test");
+ });
+
+ extension.onMessage("removeAccount", id => {
+ cloudFileAccounts.removeAccount(id);
+ });
+
+ extension.onMessage("convertFile", (cloudFileAccountId, attachmentName) => {
+ let composeWindow = Services.wm.getMostRecentWindow("msgcompose");
+ let composeDocument = composeWindow.document;
+ let bucket = composeDocument.getElementById("attachmentBucket");
+ let account = cloudFileAccounts.getAccount(cloudFileAccountId);
+
+ let attachmentItem = bucket.itemChildren.find(
+ item => item.attachment && item.attachment.name == attachmentName
+ );
+
+ composeWindow.convertListItemsToCloudAttachment([attachmentItem], account);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ MockCompleteGenericSendMessage.unregister();
+});
+
+add_task(async function test_compose_attachments_no_reuse() {
+ let files = {
+ "background.js": async () => {
+ let listener = {
+ events: [],
+ currentPromise: null,
+
+ pushEvent(...args) {
+ browser.test.log(JSON.stringify(args));
+ this.events.push(args);
+ if (this.currentPromise) {
+ let p = this.currentPromise;
+ this.currentPromise = null;
+ p.resolve();
+ }
+ },
+ async checkEvent(expectedEvent, ...expectedArgs) {
+ if (this.events.length == 0) {
+ await new Promise(resolve => (this.currentPromise = { resolve }));
+ }
+ let [actualEvent, ...actualArgs] = this.events.shift();
+ browser.test.assertEq(expectedEvent, actualEvent);
+ browser.test.assertEq(expectedArgs.length, actualArgs.length);
+
+ for (let i = 0; i < expectedArgs.length; i++) {
+ browser.test.assertEq(typeof expectedArgs[i], typeof actualArgs[i]);
+ if (typeof expectedArgs[i] == "object") {
+ for (let key of Object.keys(expectedArgs[i])) {
+ browser.test.assertEq(expectedArgs[i][key], actualArgs[i][key]);
+ }
+ } else {
+ browser.test.assertEq(expectedArgs[i], actualArgs[i]);
+ }
+ }
+
+ return actualArgs;
+ },
+ };
+ browser.compose.onAttachmentAdded.addListener((...args) =>
+ listener.pushEvent("onAttachmentAdded", ...args)
+ );
+ browser.compose.onAttachmentRemoved.addListener((...args) =>
+ listener.pushEvent("onAttachmentRemoved", ...args)
+ );
+
+ let checkData = async (attachment, size) => {
+ let data = await browser.compose.getAttachmentFile(attachment.id);
+ browser.test.assertTrue(
+ // eslint-disable-next-line mozilla/use-isInstance
+ data instanceof File,
+ "Returned file obj should be a File instance."
+ );
+ browser.test.assertEq(
+ size,
+ data.size,
+ "Reported size should be correct."
+ );
+ browser.test.assertEq(
+ attachment.name,
+ data.name,
+ "Name of the File object should match the name of the attachment."
+ );
+ };
+
+ let checkUI = async (composeTab, ...expected) => {
+ let attachments = await browser.compose.listAttachments(composeTab.id);
+ browser.test.assertEq(
+ expected.length,
+ attachments.length,
+ "Number of found attachments should be correct."
+ );
+ for (let i = 0; i < expected.length; i++) {
+ browser.test.assertEq(expected[i].id, attachments[i].id);
+ browser.test.assertEq(expected[i].size, attachments[i].size);
+ }
+ let details = await browser.compose.getComposeDetails(composeTab.id);
+ return window.sendMessage("checkUI", details, expected);
+ };
+
+ let createCloudfileAccount = () => {
+ let addListener = window.waitForEvent("cloudFile.onAccountAdded");
+ browser.test.sendMessage("createAccount");
+ return addListener;
+ };
+
+ let removeCloudfileAccount = id => {
+ let deleteListener = window.waitForEvent("cloudFile.onAccountDeleted");
+ browser.test.sendMessage("removeAccount", id);
+ return deleteListener;
+ };
+
+ async function cloneAttachment(
+ attachment,
+ composeTab,
+ name = attachment.name
+ ) {
+ let clone;
+
+ // If the name is not changed, try to pass in the full object.
+ if (name == attachment.name) {
+ clone = await browser.compose.addAttachment(
+ composeTab.id,
+ attachment
+ );
+ } else {
+ clone = await browser.compose.addAttachment(composeTab.id, {
+ id: attachment.id,
+ name,
+ });
+ }
+
+ browser.test.assertEq(name, clone.name);
+ browser.test.assertEq(attachment.size, clone.size);
+ await checkData(clone, attachment.size);
+
+ let [, added] = await listener.checkEvent(
+ "onAttachmentAdded",
+ { id: composeTab.id },
+ { id: clone.id, name }
+ );
+ await checkData(added, attachment.size);
+ return clone;
+ }
+
+ let [createdAccount] = await createCloudfileAccount();
+
+ let file1 = new File(["File number one!"], "file1.txt");
+ let file2 = new File(
+ ["File number two? Yes, this is number two."],
+ "file2.txt"
+ );
+
+ // -----------------------------------------------------------------------
+
+ let composeTab1 = await browser.compose.beginNew({
+ subject: "Message #8",
+ });
+ await checkUI(composeTab1);
+
+ // Add an attachment to composeTab1.
+
+ let tab1_attachment1 = await browser.compose.addAttachment(
+ composeTab1.id,
+ {
+ file: file1,
+ }
+ );
+ browser.test.assertEq("file1.txt", tab1_attachment1.name);
+ browser.test.assertEq(16, tab1_attachment1.size);
+ await checkData(tab1_attachment1, file1.size);
+
+ let [, tab1_added1] = await listener.checkEvent(
+ "onAttachmentAdded",
+ { id: composeTab1.id },
+ { id: tab1_attachment1.id, name: "file1.txt" }
+ );
+ await checkData(tab1_added1, file1.size);
+
+ await checkUI(composeTab1, {
+ id: tab1_attachment1.id,
+ name: "file1.txt",
+ size: file1.size,
+ });
+
+ // Add another attachment to composeTab1.
+
+ let tab1_attachment2 = await browser.compose.addAttachment(
+ composeTab1.id,
+ {
+ file: file2,
+ name: "this is file2.txt",
+ }
+ );
+ browser.test.assertEq("this is file2.txt", tab1_attachment2.name);
+ browser.test.assertEq(41, tab1_attachment2.size);
+ await checkData(tab1_attachment2, file2.size);
+
+ let [, tab1_added2] = await listener.checkEvent(
+ "onAttachmentAdded",
+ { id: composeTab1.id },
+ { id: tab1_attachment2.id, name: "this is file2.txt" }
+ );
+ await checkData(tab1_added2, file2.size);
+
+ await checkUI(
+ composeTab1,
+ { id: tab1_attachment1.id, name: "file1.txt", size: file1.size },
+ { id: tab1_attachment2.id, name: "this is file2.txt", size: file2.size }
+ );
+
+ // Convert the second attachment to a cloudFile attachment.
+
+ await new Promise(resolve => {
+ function fileListener(account, fileInfo, tab, relatedFileInfo) {
+ browser.cloudFile.onFileUpload.removeListener(fileListener);
+ browser.test.assertEq(1, fileInfo.id);
+ browser.test.assertEq(undefined, relatedFileInfo);
+ setTimeout(() => resolve());
+ return { url: "https://cloud.provider.net/1" };
+ }
+
+ browser.cloudFile.onFileUpload.addListener(fileListener);
+ // Conversion/upload is not yet supported via WebExt API.
+ browser.test.sendMessage(
+ "convertFile",
+ createdAccount.id,
+ "this is file2.txt"
+ );
+ });
+
+ await checkUI(
+ composeTab1,
+ {
+ id: tab1_attachment1.id,
+ name: "file1.txt",
+ size: file1.size,
+ },
+ {
+ id: tab1_attachment2.id,
+ name: "this is file2.txt",
+ size: 41,
+ htmlSize: 4300,
+ contentLocation: "https://cloud.provider.net/1",
+ }
+ );
+
+ // -----------------------------------------------------------------------
+
+ // Create a second compose window and clone both attachments from tab1. The
+ // second one should be cloned as a cloud attachment, having no size and the
+ // correct contentLocation.
+ // Attachments are not renamed, but since reuse_uploads is disabled, a new
+ // upload request must be issued. The original attachment should be passed
+ // as relatedFileInfo.
+ let tab2_uploadPromise = new Promise(resolve => {
+ function fileListener(account, fileInfo, tab, relatedFileInfo) {
+ browser.cloudFile.onFileUpload.removeListener(fileListener);
+ browser.test.assertEq(2, fileInfo.id);
+ browser.test.assertEq("this is file2.txt", fileInfo.name);
+ browser.test.assertEq(1, relatedFileInfo.id);
+ browser.test.assertEq("this is file2.txt", relatedFileInfo.name);
+ browser.test.assertFalse(
+ relatedFileInfo.dataChanged,
+ `data should not have changed`
+ );
+ setTimeout(() => resolve());
+ return { url: "https://cloud.provider.net/2" };
+ }
+
+ browser.cloudFile.onFileUpload.addListener(fileListener);
+ });
+
+ let composeTab2 = await browser.compose.beginNew({
+ subject: "Message #9",
+ });
+ let tab2_attachment1 = await cloneAttachment(
+ tab1_attachment1,
+ composeTab2
+ );
+ await checkUI(composeTab2, {
+ id: tab2_attachment1.id,
+ name: "file1.txt",
+ size: file1.size,
+ });
+
+ let tab2_attachment2 = await cloneAttachment(
+ tab1_attachment2,
+ composeTab2,
+ "this is file2.txt"
+ );
+
+ await checkUI(
+ composeTab2,
+ {
+ id: tab2_attachment1.id,
+ name: "file1.txt",
+ size: file1.size,
+ },
+ {
+ id: tab2_attachment2.id,
+ name: "this is file2.txt",
+ size: 41,
+ htmlSize: 4300,
+ contentLocation: "https://cloud.provider.net/2",
+ }
+ );
+
+ await tab2_uploadPromise;
+
+ await browser.tabs.remove(composeTab2.id);
+ await browser.tabs.remove(composeTab1.id);
+ browser.test.assertEq(0, listener.events.length);
+
+ await removeCloudfileAccount(createdAccount.id);
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+
+ let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
+ Ci.nsIMessenger
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ cloud_file: {
+ name: "mochitest",
+ management_url: "/content/management.html",
+ reuse_uploads: false,
+ },
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ applications: { gecko: { id: "compose.attachments@mochi.test" } },
+ },
+ });
+
+ extension.onMessage("checkUI", (details, expected) => {
+ let composeWindow = findWindow(details.subject);
+ let composeDocument = composeWindow.document;
+
+ let bucket = composeDocument.getElementById("attachmentBucket");
+ Assert.equal(bucket.itemCount, expected.length);
+
+ let totalSize = 0;
+ for (let i = 0; i < expected.length; i++) {
+ let item = bucket.itemChildren[i];
+ let { name, size, htmlSize, contentLocation } = expected[i];
+ totalSize += htmlSize ? htmlSize : size;
+
+ let displaySize = messenger.formatFileSize(size);
+ if (htmlSize) {
+ displaySize = `${messenger.formatFileSize(htmlSize)} (${displaySize})`;
+ Assert.equal(
+ item.cloudHtmlFileSize,
+ htmlSize,
+ "htmlSize should be correct."
+ );
+ }
+
+ // size and name are checked against the displayed values, contentLocation
+ // is checked against the associated attachment.
+
+ if (contentLocation) {
+ Assert.equal(
+ item.attachment.contentLocation,
+ contentLocation,
+ "contentLocation for cloud files should be correct."
+ );
+ Assert.equal(
+ item.attachment.sendViaCloud,
+ true,
+ "sendViaCloud for cloud files should be correct."
+ );
+ } else {
+ Assert.equal(
+ item.attachment.contentLocation,
+ "",
+ "contentLocation for cloud files should be correct."
+ );
+ Assert.equal(
+ item.attachment.sendViaCloud,
+ false,
+ "sendViaCloud for cloud files should be correct."
+ );
+ }
+
+ Assert.equal(
+ item.querySelector(".attachmentcell-name").textContent +
+ item.querySelector(".attachmentcell-extension").textContent,
+ name,
+ "Name should be correct."
+ );
+ Assert.equal(
+ item.querySelector(".attachmentcell-size").textContent,
+ displaySize,
+ "Displayed size should be correct."
+ );
+ }
+
+ let bucketTotal = composeDocument.getElementById("attachmentBucketSize");
+ if (totalSize == 0) {
+ Assert.equal(bucketTotal.textContent, "");
+ } else {
+ Assert.equal(
+ bucketTotal.textContent,
+ messenger.formatFileSize(totalSize)
+ );
+ }
+
+ extension.sendMessage();
+ });
+
+ extension.onMessage("createAccount", () => {
+ cloudFileAccounts.createAccount("ext-compose.attachments@mochi.test");
+ });
+
+ extension.onMessage("removeAccount", id => {
+ cloudFileAccounts.removeAccount(id);
+ });
+
+ extension.onMessage("convertFile", (cloudFileAccountId, attachmentName) => {
+ let composeWindow = Services.wm.getMostRecentWindow("msgcompose");
+ let composeDocument = composeWindow.document;
+ let bucket = composeDocument.getElementById("attachmentBucket");
+ let account = cloudFileAccounts.getAccount(cloudFileAccountId);
+
+ let attachmentItem = bucket.itemChildren.find(
+ item => item.attachment && item.attachment.name == attachmentName
+ );
+
+ composeWindow.convertListItemsToCloudAttachment([attachmentItem], account);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_without_permission() {
+ let files = {
+ "background.js": async () => {
+ // Try to use onAttachmentAdded.
+ await browser.test.assertThrows(
+ () => browser.compose.onAttachmentAdded.addListener(),
+ /browser\.compose\.onAttachmentAdded is undefined/,
+ "Should reject listener without proper permission"
+ );
+
+ // Try to use onAttachmentRemoved.
+ await browser.test.assertThrows(
+ () => browser.compose.onAttachmentRemoved.addListener(),
+ /browser\.compose\.onAttachmentRemoved is undefined/,
+ "Should reject listener without proper permission"
+ );
+
+ // Try to use listAttachments.
+ await browser.test.assertThrows(
+ () => browser.compose.listAttachments(),
+ `browser.compose.listAttachments is not a function`,
+ "Should reject function without proper permission"
+ );
+
+ // Try to use addAttachment.
+ await browser.test.assertThrows(
+ () => browser.compose.addAttachment(),
+ `browser.compose.addAttachment is not a function`,
+ "Should reject function without proper permission"
+ );
+
+ // Try to use updateAttachment.
+ await browser.test.assertThrows(
+ () => browser.compose.updateAttachment(),
+ `browser.compose.updateAttachment is not a function`,
+ "Should reject function without proper permission"
+ );
+
+ // Try to use removeAttachment.
+ await browser.test.assertThrows(
+ () => browser.compose.removeAttachment(),
+ `browser.compose.removeAttachment is not a function`,
+ "Should reject function without proper permission"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: [],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_attachment_MV3_event_pages() {
+ let files = {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, the eventCounter is reset and
+ // allows to observe the order of events fired. In case of a wake-up, the
+ // first observed event is the one that woke up the background.
+ let eventCounter = 0;
+
+ browser.compose.onAttachmentAdded.addListener(async (tab, attachment) => {
+ browser.test.sendMessage("attachment added", {
+ eventCount: ++eventCounter,
+ attachment,
+ });
+ });
+
+ browser.compose.onAttachmentRemoved.addListener(
+ async (tab, attachmentId) => {
+ browser.test.sendMessage("attachment removed", {
+ eventCount: ++eventCounter,
+ attachmentId,
+ });
+ }
+ );
+
+ browser.test.sendMessage("background started");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ manifest_version: 3,
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "compose", "messagesRead"],
+ browser_specific_settings: {
+ gecko: { id: "compose.attachment@mochi.test" },
+ },
+ },
+ });
+
+ async function addAttachment(ordinal) {
+ let attachment = Cc[
+ "@mozilla.org/messengercompose/attachment;1"
+ ].createInstance(Ci.nsIMsgAttachment);
+ attachment.name = `${ordinal}.txt`;
+ attachment.url = `data:text/plain,I'm the ${ordinal} attachment!`;
+ attachment.size = attachment.url.length - 16;
+
+ await composeWindow.AddAttachments([attachment]);
+ return attachment;
+ }
+
+ async function removeAttachment(attachment) {
+ let item =
+ composeWindow.gAttachmentBucket.findItemForAttachment(attachment);
+ await composeWindow.RemoveAttachments([item]);
+ }
+
+ function checkPersistentListeners({ primed }) {
+ // A persistent event is referenced by its moduleName as defined in
+ // ext-mails.json, not by its actual namespace.
+ const persistent_events = [
+ "compose.onAttachmentAdded",
+ "compose.onAttachmentRemoved",
+ ];
+
+ for (let event of persistent_events) {
+ let [moduleName, eventName] = event.split(".");
+ assertPersistentListeners(extension, moduleName, eventName, {
+ primed,
+ });
+ }
+ }
+
+ let composeWindow = await openComposeWindow(account);
+ await focusWindow(composeWindow);
+
+ await extension.startup();
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+
+ // Trigger events without terminating the background first.
+
+ let rawFirstAttachment = await addAttachment("first");
+ let addedFirst = await extension.awaitMessage("attachment added");
+ Assert.equal(
+ "first.txt",
+ rawFirstAttachment.name,
+ "Created attachment should be correct"
+ );
+ Assert.equal(
+ "first.txt",
+ addedFirst.attachment.name,
+ "Attachment returned by onAttachmentAdded should be correct"
+ );
+ Assert.equal(1, addedFirst.eventCount, "Event counter should be correct");
+
+ await removeAttachment(rawFirstAttachment);
+
+ let removedFirst = await extension.awaitMessage("attachment removed");
+ Assert.equal(
+ addedFirst.attachment.id,
+ removedFirst.attachmentId,
+ "Attachment id returned by onAttachmentRemoved should be correct"
+ );
+ Assert.equal(2, removedFirst.eventCount, "Event counter should be correct");
+
+ // Terminate background and re-trigger onAttachmentAdded event.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // The listeners should be primed.
+ checkPersistentListeners({ primed: true });
+
+ let rawSecondAttachment = await addAttachment("second");
+ let addedSecond = await extension.awaitMessage("attachment added");
+ Assert.equal(
+ "second.txt",
+ rawSecondAttachment.name,
+ "Created attachment should be correct"
+ );
+ Assert.equal(
+ "second.txt",
+ addedSecond.attachment.name,
+ "Attachment returned by onAttachmentAdded should be correct"
+ );
+ Assert.equal(1, addedSecond.eventCount, "Event counter should be correct");
+
+ // The background should have been restarted.
+ await extension.awaitMessage("background started");
+ // The listeners should no longer be primed.
+ checkPersistentListeners({ primed: false });
+
+ // Terminate background and re-trigger onAttachmentRemoved event.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // The listeners should be primed.
+ checkPersistentListeners({ primed: true });
+
+ await removeAttachment(rawSecondAttachment);
+ let removedSecond = await extension.awaitMessage("attachment removed");
+ Assert.equal(
+ addedSecond.attachment.id,
+ removedSecond.attachmentId,
+ "Attachment id returned by onAttachmentRemoved should be correct"
+ );
+ Assert.equal(1, removedSecond.eventCount, "Event counter should be correct");
+
+ // The background should have been restarted.
+ await extension.awaitMessage("background started");
+ // The listeners should no longer be primed.
+ checkPersistentListeners({ primed: false });
+
+ await extension.unload();
+ composeWindow.close();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_attachments.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_attachments.js
new file mode 100644
index 0000000000..26f4d0ab5e
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_attachments.js
@@ -0,0 +1,116 @@
+/* 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"
+);
+
+let account = createAccount("pop3");
+createAccount("local");
+MailServices.accounts.defaultAccount = account;
+
+addIdentity(account);
+
+let rootFolder = account.incomingServer.rootFolder;
+rootFolder.createSubfolder("test", null);
+let folder = rootFolder.getChildNamed("test");
+createMessages(folder, 4);
+
+add_task(async function testAttachments() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(2, accounts.length, "number of accounts");
+ let popAccount = accounts.find(a => a.type == "pop3");
+ let folder = popAccount.folders.find(f => f.name == "test");
+ let { messages } = await browser.messages.list(folder);
+
+ let newTab = await browser.compose.beginNew({
+ attachments: [
+ { file: new File(["one"], "attachment1.txt") },
+ { file: new File(["two"], "attachment-två.txt") },
+ ],
+ });
+
+ let attachments = await browser.compose.listAttachments(newTab.id);
+ browser.test.assertEq(2, attachments.length);
+ browser.test.assertEq("attachment1.txt", attachments[0].name);
+ browser.test.assertEq("attachment-två.txt", attachments[1].name);
+
+ let replyTab = await browser.compose.beginReply(messages[0].id, {
+ attachments: [
+ { file: new File(["three"], "attachment3.txt") },
+ { file: new File(["four"], "attachment4.txt") },
+ ],
+ });
+
+ attachments = await browser.compose.listAttachments(replyTab.id);
+ browser.test.assertEq(2, attachments.length);
+ browser.test.assertEq("attachment3.txt", attachments[0].name);
+ browser.test.assertEq("attachment4.txt", attachments[1].name);
+
+ let forwardTab = await browser.compose.beginForward(
+ messages[1].id,
+ "forwardAsAttachment",
+ {
+ attachments: [
+ { file: new File(["five"], "attachment5.txt") },
+ { file: new File(["six"], "attachment6.txt") },
+ ],
+ }
+ );
+
+ attachments = await browser.compose.listAttachments(forwardTab.id);
+ browser.test.assertEq(3, attachments.length);
+ browser.test.assertEq(`${messages[1].subject}.eml`, attachments[0].name);
+ browser.test.assertEq("attachment5.txt", attachments[1].name);
+ browser.test.assertEq("attachment6.txt", attachments[2].name);
+
+ // Forward inline adds attachments differently, so check it works too.
+
+ let forwardTab2 = await browser.compose.beginForward(
+ messages[2].id,
+ "forwardInline",
+ {
+ attachments: [
+ { file: new File(["seven"], "attachment7.txt") },
+ { file: new File(["eight"], "attachment-Ã¥tta.txt") },
+ ],
+ }
+ );
+
+ attachments = await browser.compose.listAttachments(forwardTab2.id);
+ browser.test.assertEq(2, attachments.length);
+ browser.test.assertEq("attachment7.txt", attachments[0].name);
+ browser.test.assertEq("attachment-Ã¥tta.txt", attachments[1].name);
+
+ let newTab2 = await browser.compose.beginNew(messages[3].id, {
+ attachments: [
+ { file: new File(["nine"], "attachment9.txt") },
+ { file: new File(["ten"], "attachment10.txt") },
+ ],
+ });
+
+ attachments = await browser.compose.listAttachments(newTab2.id);
+ browser.test.assertEq(2, attachments.length);
+ browser.test.assertEq("attachment9.txt", attachments[0].name);
+ browser.test.assertEq("attachment10.txt", attachments[1].name);
+
+ await browser.tabs.remove(newTab.id);
+ await browser.tabs.remove(replyTab.id);
+ await browser.tabs.remove(forwardTab.id);
+ await browser.tabs.remove(forwardTab2.id);
+ await browser.tabs.remove(newTab2.id);
+
+ browser.test.notifyPass();
+ },
+ manifest: {
+ permissions: ["compose", "accountsRead", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_body.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_body.js
new file mode 100644
index 0000000000..3b454a9c8b
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_body.js
@@ -0,0 +1,397 @@
+/* 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/. */
+
+requestLongerTimeout(2);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+let account = createAccount("pop3");
+createAccount("local");
+MailServices.accounts.defaultAccount = account;
+
+let defaultIdentity = addIdentity(account);
+defaultIdentity.composeHtml = true;
+let nonDefaultIdentity = addIdentity(account);
+nonDefaultIdentity.composeHtml = false;
+
+let rootFolder = account.incomingServer.rootFolder;
+rootFolder.createSubfolder("test", null);
+let folder = rootFolder.getChildNamed("test");
+createMessages(folder, 4);
+
+add_task(async function testBody() {
+ let files = {
+ "background.js": async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(2, accounts.length, "number of accounts");
+ let popAccount = accounts.find(a => a.type == "pop3");
+ browser.test.assertEq(
+ 2,
+ popAccount.identities.length,
+ "number of identities"
+ );
+ let [htmlIdentity, plainTextIdentity] = popAccount.identities;
+ let folder = popAccount.folders.find(f => f.name == "test");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(4, messages.length, "number of messages");
+
+ let message0 = await browser.messages.getFull(messages[0].id);
+ let message0body = message0.parts[0].body;
+
+ // Editor content of a newly opened composeWindow without setting a body.
+ let defaultHTML = "<body><p><br></p></body>";
+ // Editor content after composeWindow.SetComposeDetails() has been used
+ // to clear the body.
+ let setEmptyHTML = "<body><br></body>";
+ let plainTextBodyTag =
+ '<body style="font-family: -moz-fixed; white-space: pre-wrap; width: 72ch;">';
+ let tests = [
+ {
+ // No arguments.
+ funcName: "beginNew",
+ arguments: [],
+ expected: {
+ isHTML: true,
+ htmlIncludes: defaultHTML,
+ plainTextIs: "\n",
+ },
+ },
+ {
+ // Empty arguments.
+ funcName: "beginNew",
+ arguments: [{}],
+ expected: {
+ isHTML: true,
+ htmlIncludes: defaultHTML,
+ plainTextIs: "\n",
+ },
+ },
+ {
+ // Empty HTML.
+ funcName: "beginNew",
+ arguments: [{ body: "" }],
+ expected: {
+ isHTML: true,
+ htmlIncludes: setEmptyHTML,
+ plainTextIs: "",
+ },
+ },
+ {
+ // Empty plain text.
+ funcName: "beginNew",
+ arguments: [{ plainTextBody: "" }],
+ expected: {
+ isHTML: false,
+ plainTextIs: "",
+ },
+ },
+ {
+ // Empty enforced plain text with default identity.
+ funcName: "beginNew",
+ arguments: [{ plainTextBody: "", isPlainText: true }],
+ expected: {
+ isHTML: false,
+ plainTextIs: "",
+ },
+ },
+ {
+ // Empty HTML for plaintext identity.
+ funcName: "beginNew",
+ arguments: [{ body: "", identityId: plainTextIdentity.id }],
+ expected: {
+ isHTML: true,
+ htmlIncludes: setEmptyHTML,
+ plainTextIs: "",
+ },
+ },
+ {
+ // Empty plain text for plaintext identity.
+ funcName: "beginNew",
+ arguments: [{ plainTextBody: "", identityId: plainTextIdentity.id }],
+ expected: {
+ isHTML: false,
+ plainTextIs: "",
+ },
+ },
+ {
+ // Empty HTML for plaintext identity enforcing HTML.
+ funcName: "beginNew",
+ arguments: [
+ { body: "", identityId: plainTextIdentity.id, isPlainText: false },
+ ],
+ expected: {
+ isHTML: true,
+ htmlIncludes: setEmptyHTML,
+ plainTextIs: "",
+ },
+ },
+ {
+ // Empty plain text and isPlainText.
+ funcName: "beginNew",
+ arguments: [{ plainTextBody: "", isPlainText: true }],
+ expected: { isHTML: false, plainTextIs: "" },
+ },
+ {
+ // Non-empty HTML.
+ funcName: "beginNew",
+ arguments: [{ body: "<p>I'm an HTML message!</p>" }],
+ expected: {
+ isHTML: true,
+ htmlIncludes: "<body><p>I'm an HTML message!</p></body>",
+ plainTextIs: "I'm an HTML message!",
+ },
+ },
+ {
+ // Non-empty plain text.
+ funcName: "beginNew",
+ arguments: [{ plainTextBody: "I'm a plain text message!" }],
+ expected: {
+ isHTML: false,
+ htmlIncludes: plainTextBodyTag + "I'm a plain text message!</body>",
+ plainTextIs: "I'm a plain text message!",
+ },
+ },
+ {
+ // Non-empty plain text and isPlainText.
+ funcName: "beginNew",
+ arguments: [
+ {
+ plainTextBody: "I'm a plain text message!",
+ isPlainText: true,
+ },
+ ],
+ expected: {
+ isHTML: false,
+ htmlIncludes: plainTextBodyTag + "I'm a plain text message!</body>",
+ plainTextIs: "I'm a plain text message!",
+ },
+ },
+ {
+ // HTML body and plain text body without isPlainText. Use default format.
+ funcName: "beginNew",
+ arguments: [{ body: "I am HTML", plainTextBody: "I am TEXT" }],
+ expected: {
+ isHTML: true,
+ htmlIncludes: "I am HTML",
+ plainTextIs: "I am HTML",
+ },
+ },
+ {
+ // HTML body and plain text body with isPlainText. Use the specified
+ // format.
+ funcName: "beginNew",
+ arguments: [
+ {
+ body: "I am HTML",
+ plainTextBody: "I am TEXT",
+ isPlainText: true,
+ },
+ ],
+ expected: {
+ isHTML: false,
+ plainTextIs: "I am TEXT",
+ },
+ },
+ {
+ // Providing an HTML body only and isPlainText = true. Conflicting and
+ // thus invalid.
+ funcName: "beginNew",
+ arguments: [{ body: "I am HTML", isPlainText: true }],
+ throws: true,
+ },
+ {
+ // Providing a plain text body only and isPlainText = false. Conflicting
+ // and thus invalid.
+ funcName: "beginNew",
+ arguments: [{ plainTextBody: "I am TEXT", isPlainText: false }],
+ throws: true,
+ },
+ {
+ // HTML body only and isPlainText false.
+ funcName: "beginNew",
+ arguments: [{ body: "I am HTML", isPlainText: false }],
+ expected: {
+ isHTML: true,
+ htmlIncludes: "I am HTML",
+ plainTextIs: "I am HTML",
+ },
+ },
+ {
+ // Edit as new.
+ funcName: "beginNew",
+ arguments: [messages[0].id],
+ expected: {
+ isHTML: true,
+ htmlIncludes: message0body.trim(),
+ },
+ },
+ {
+ // Edit as new with plaintext identity
+ funcName: "beginNew",
+ arguments: [messages[0].id, { identityId: plainTextIdentity.id }],
+ expected: {
+ isHTML: false,
+ plainTextIs: message0body,
+ },
+ },
+ {
+ // Edit as new with default identity enforcing HTML
+ funcName: "beginNew",
+ arguments: [messages[0].id, { isPlainText: false }],
+ expected: {
+ isHTML: true,
+ htmlIncludes: message0body.trim(),
+ },
+ },
+ {
+ // Edit as new with plaintext identity enforcing HTML by setting a body.
+ funcName: "beginNew",
+ arguments: [
+ messages[0].id,
+ {
+ body: "<p>This is some HTML text</p>",
+ identityId: plainTextIdentity.id,
+ },
+ ],
+ expected: {
+ isHTML: true,
+ htmlIncludes: "<p>This is some HTML text</p>",
+ },
+ },
+ {
+ // Edit as new with html identity enforcing plain text by setting a plainTextBody.
+ funcName: "beginNew",
+ arguments: [
+ messages[0].id,
+ {
+ plainTextBody: "This is some plain text",
+ identityId: htmlIdentity.id,
+ },
+ ],
+ expected: {
+ isHTML: false,
+ plainText: "This is some plain text",
+ },
+ },
+ {
+ // ForwardInline with plaintext identity enforcing HTML
+ funcName: "beginForward",
+ arguments: [
+ messages[0].id,
+ { identityId: plainTextIdentity.id, isPlainText: false },
+ ],
+ expected: {
+ isHTML: true,
+ htmlIncludes: message0body.trim(),
+ },
+ },
+ {
+ // Reply.
+ funcName: "beginReply",
+ arguments: [messages[0].id],
+ expected: {
+ isHTML: true,
+ htmlIncludes: message0body.trim(),
+ },
+ },
+ {
+ // Forward inline.
+ funcName: "beginForward",
+ arguments: [messages[0].id],
+ expected: {
+ isHTML: true,
+ htmlIncludes: message0body.trim(),
+ },
+ },
+ {
+ // Forward as attachment.
+ funcName: "beginForward",
+ arguments: [messages[0].id, "forwardAsAttachment"],
+ expected: {
+ isHTML: true,
+ htmlIncludes: defaultHTML,
+ plainText: "",
+ },
+ },
+ ];
+
+ for (let test of tests) {
+ browser.test.log(JSON.stringify(test));
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ try {
+ await browser.compose[test.funcName](...test.arguments);
+ if (test.throws) {
+ browser.test.fail(
+ "calling beginNew with these arguments should throw"
+ );
+ }
+ } catch (ex) {
+ if (test.throws) {
+ browser.test.succeed("expected exception thrown");
+ } else {
+ browser.test.fail(`unexpected exception thrown: ${ex.message}`);
+ }
+ }
+
+ let [createdWindow] = await createdWindowPromise;
+ browser.test.assertEq("messageCompose", createdWindow.type);
+ if (test.expected) {
+ browser.test.sendMessage("checkBody", test.expected);
+ await window.waitForMessage();
+ }
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+ extension.onMessage("checkBody", async expected => {
+ let composeWindows = [...Services.wm.getEnumerator("msgcompose")];
+ is(composeWindows.length, 1);
+ await new Promise(resolve => composeWindows[0].setTimeout(resolve));
+
+ is(composeWindows[0].IsHTMLEditor(), expected.isHTML, "composition mode");
+
+ let editor = composeWindows[0].GetCurrentEditor();
+ // Get the actual message body. Fold Windows line-endings \r\n to \n.
+ let actualHTML = editor
+ .outputToString("text/html", Ci.nsIDocumentEncoder.OutputRaw)
+ .replace(/\r/g, "");
+ let actualPlainText = editor
+ .outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw)
+ .replace(/\r/g, "");
+ if ("htmlIncludes" in expected) {
+ info(actualHTML);
+ ok(
+ actualHTML.includes(expected.htmlIncludes.replace(/\r/g, "")),
+ `HTML content is correct (${actualHTML} vs ${expected.htmlIncludes})`
+ );
+ }
+ if ("plainTextIs" in expected) {
+ is(
+ actualPlainText,
+ expected.plainTextIs.replace(/\r/g, ""),
+ "plainText content is correct"
+ );
+ }
+
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_bug1691254.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_bug1691254.js
new file mode 100644
index 0000000000..6a763d5c43
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_bug1691254.js
@@ -0,0 +1,141 @@
+/* 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/. */
+
+requestLongerTimeout(2);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+let account = createAccount("pop3");
+createAccount("local");
+MailServices.accounts.defaultAccount = account;
+
+let defaultIdentity = addIdentity(account);
+defaultIdentity.composeHtml = true;
+let nonDefaultIdentity = addIdentity(account);
+nonDefaultIdentity.composeHtml = false;
+
+let rootFolder = account.incomingServer.rootFolder;
+rootFolder.createSubfolder("test", null);
+let folder = rootFolder.getChildNamed("test");
+createMessages(folder, 4);
+
+/* Test if line breaks in HTML are ignored (see bug 1691254). */
+add_task(async function testBR() {
+ let files = {
+ "background.js": async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(2, accounts.length, "number of accounts");
+ let popAccount = accounts.find(a => a.type == "pop3");
+ let folder = popAccount.folders.find(f => f.name == "test");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(4, messages.length, "number of messages");
+
+ let body = `<html><head>\r\n\r\n \r\n<meta http-equiv="content-type" content="text/html; charset=UTF-8">\r\n\r\n </head><body>\r\n \r\n<p><font face="monospace">This is some <br> HTML text</font><br>\r\n </p>\r\n\r\n \r\n\r\n\r\n</body></html>\r\n\r\n\r\n`;
+ let tests = [
+ {
+ description: "Begin new.",
+ funcName: "beginNew",
+ arguments: [{ body }],
+ },
+ {
+ description: "Edit as new.",
+ funcName: "beginNew",
+ arguments: [messages[0].id, { body }],
+ },
+ {
+ description: "Reply default.",
+ funcName: "beginReply",
+ arguments: [messages[0].id, { body }],
+ },
+ {
+ description: "Reply as replyToSender.",
+ funcName: "beginReply",
+ arguments: [messages[0].id, "replyToSender", { body }],
+ },
+ {
+ description: "Reply as replyToList.",
+ funcName: "beginReply",
+ arguments: [messages[0].id, "replyToList", { body }],
+ },
+ {
+ description: "Reply as replyToAll.",
+ funcName: "beginReply",
+ arguments: [messages[0].id, "replyToAll", { body }],
+ },
+ {
+ description: "Forward default.",
+ funcName: "beginForward",
+ arguments: [messages[0].id, { body }],
+ },
+ {
+ description: "Forward inline.",
+ funcName: "beginForward",
+ arguments: [messages[0].id, "forwardInline", { body }],
+ },
+ {
+ description: "Forward as attachment.",
+ funcName: "beginForward",
+ arguments: [messages[0].id, "forwardAsAttachment", { body }],
+ },
+ ];
+
+ for (let test of tests) {
+ browser.test.log(JSON.stringify(test));
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose[test.funcName](...test.arguments);
+
+ let [createdWindow] = await createdWindowPromise;
+ browser.test.assertEq("messageCompose", createdWindow.type);
+ browser.test.sendMessage("checkBody", test);
+ await window.waitForMessage();
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+ extension.onMessage("checkBody", async test => {
+ let composeWindows = [...Services.wm.getEnumerator("msgcompose")];
+ is(composeWindows.length, 1);
+ await new Promise(resolve => composeWindows[0].setTimeout(resolve));
+
+ is(composeWindows[0].IsHTMLEditor(), true, "composition mode");
+
+ let editor = composeWindows[0].GetCurrentEditor();
+ let actualHTML = editor.outputToString(
+ "text/html",
+ Ci.nsIDocumentEncoder.OutputRaw
+ );
+ let brCounts = (actualHTML.match(/<br>/g) || []).length;
+ is(
+ brCounts,
+ 2,
+ `[${test.description}] Number of br tags in html is correct (${actualHTML}).`
+ );
+
+ let eqivCounts = (actualHTML.match(/http-equiv/g) || []).length;
+ is(
+ eqivCounts,
+ 1,
+ `[${test.description}] Number of http-equiv meta tags in html is correct (${actualHTML}).`
+ );
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_forward.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_forward.js
new file mode 100644
index 0000000000..621947609d
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_forward.js
@@ -0,0 +1,339 @@
+/* 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"
+);
+
+add_setup(() => {
+ let account = createAccount("pop3");
+ createAccount("local");
+ MailServices.accounts.defaultAccount = account;
+
+ addIdentity(account);
+
+ let rootFolder = account.incomingServer.rootFolder;
+ rootFolder.createSubfolder("test", null);
+ let folder = rootFolder.getChildNamed("test");
+ createMessages(folder, 4);
+});
+
+/* Test if getComposeDetails() is waiting until the entire init procedure of
+ * the composeWindow has finished, before returning values. */
+add_task(async function testComposerIsReady() {
+ let files = {
+ "background.js": async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(2, accounts.length, "number of accounts");
+ let popAccount = accounts.find(a => a.type == "pop3");
+ let folder = popAccount.folders.find(f => f.name == "test");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(4, messages.length, "number of messages");
+
+ let details = {
+ plainTextBody: "This is Text",
+ to: ["Mr. Holmes <holmes@bakerstreet.invalid>"],
+ subject: "Test Email",
+ };
+
+ let tests = [
+ {
+ description: "Forward default.",
+ funcName: "beginForward",
+ arguments: [messages[0].id, details],
+ },
+ {
+ description: "Forward inline.",
+ funcName: "beginForward",
+ arguments: [messages[0].id, "forwardInline", details],
+ },
+ {
+ description: "Forward as attachment.",
+ funcName: "beginForward",
+ arguments: [messages[0].id, "forwardAsAttachment", details],
+ },
+ ];
+
+ for (let test of tests) {
+ browser.test.log(JSON.stringify(test));
+ let expectedDetails = test.arguments[test.arguments.length - 1];
+
+ // Test with windows.onCreated
+ {
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ // Explicitly do not await this call.
+ browser.compose[test.funcName](...test.arguments);
+ let [createdWindow] = await createdWindowPromise;
+ let [tab] = await browser.tabs.query({ windowId: createdWindow.id });
+
+ let actualDetails = await browser.compose.getComposeDetails(tab.id);
+ for (let detail of Object.keys(expectedDetails)) {
+ browser.test.assertEq(
+ expectedDetails[detail].toString(),
+ actualDetails[detail].toString(),
+ `After windows.OnCreated: Detail ${detail} is correct for ${test.description}`
+ );
+ }
+
+ // Test the windows API being able to return the messageCompose window as
+ // the current one.
+ await window.waitForCondition(async () => {
+ let win = await browser.windows.get(createdWindow.id);
+ return win.focused;
+ }, `Window should have received focus.`);
+
+ let composeWindow = await browser.windows.get(tab.windowId);
+ browser.test.assertEq(composeWindow.type, "messageCompose");
+ let curWindow = await browser.windows.getCurrent();
+ browser.test.assertEq(tab.windowId, curWindow.id);
+ // Test the tabs API being able to return the correct current tab.
+ let [currentTab] = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+ browser.test.assertEq(tab.id, currentTab.id);
+
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+ }
+
+ // Test with tabs.onCreated
+ {
+ let createdTabPromise = window.waitForEvent("tabs.onCreated");
+ // Explicitly do not await this call.
+ browser.compose[test.funcName](...test.arguments);
+ let [createdTab] = await createdTabPromise;
+ let actualDetails = await browser.compose.getComposeDetails(
+ createdTab.id
+ );
+
+ for (let detail of Object.keys(expectedDetails)) {
+ browser.test.assertEq(
+ expectedDetails[detail].toString(),
+ actualDetails[detail].toString(),
+ `After tabs.OnCreated: Detail ${detail} is correct for ${test.description}`
+ );
+ }
+
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ let createdWindow = await browser.windows.get(createdTab.windowId);
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+ }
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose", "accountsRead", "messagesRead"],
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+/* Test the compose API accessing the forwarded message added by beginForward. */
+add_task(async function testBeginForward() {
+ let files = {
+ "background.js": async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(2, accounts.length, "number of accounts");
+ let popAccount = accounts.find(a => a.type == "pop3");
+ let folder = popAccount.folders.find(f => f.name == "test");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(4, messages.length, "number of messages");
+
+ let details = {
+ plainTextBody: "This is Text",
+ to: ["Mr. Holmes <holmes@bakerstreet.invalid>"],
+ subject: "Test Email",
+ };
+
+ let tests = [
+ {
+ description: "Forward as attachment.",
+ funcName: "beginForward",
+ arguments: [messages[0].id, "forwardAsAttachment", details],
+ expectedAttachments: [
+ {
+ name: "Big Meeting Today.eml",
+ type: "message/rfc822",
+ size: 281,
+ content: "Hello Bob Bell!",
+ },
+ ],
+ },
+ ];
+
+ for (let test of tests) {
+ browser.test.log(JSON.stringify(test));
+
+ let tab = await browser.compose[test.funcName](...test.arguments);
+ let attachments = await browser.compose.listAttachments(tab.id);
+ browser.test.assertEq(
+ test.expectedAttachments.length,
+ attachments.length,
+ `Should have the expected number of attachments`
+ );
+ for (let i = 0; i < attachments.length; i++) {
+ let file = await browser.compose.getAttachmentFile(attachments[i].id);
+ for (let [property, value] of Object.entries(
+ test.expectedAttachments[i]
+ )) {
+ if (property == "content") {
+ let content = await file.text();
+ browser.test.assertTrue(
+ content.includes(value),
+ `Attachment body should include ${value}`
+ );
+ } else {
+ browser.test.assertEq(
+ value,
+ file[property],
+ `Attachment should have the correct value for ${property}`
+ );
+ }
+ }
+ }
+
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(tab.windowId);
+ await removedWindowPromise;
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose", "accountsRead", "messagesRead"],
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+/* The forward inline code path uses a hacky way to identify the correct window
+ * after it has been opened via MailServices.compose.OpenComposeWindow. Test it.*/
+add_task(async function testBeginForwardInlineMixUp() {
+ let files = {
+ "background.js": async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(2, accounts.length, "number of accounts");
+ let popAccount = accounts.find(a => a.type == "pop3");
+ let folder = popAccount.folders.find(f => f.name == "test");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(4, messages.length, "number of messages");
+
+ // Test opening different messages.
+ {
+ let promisedTabs = [];
+ promisedTabs.push(
+ browser.compose.beginForward(messages[0].id, "forwardInline")
+ );
+ promisedTabs.push(
+ browser.compose.beginForward(messages[1].id, "forwardInline")
+ );
+ promisedTabs.push(
+ browser.compose.beginForward(messages[2].id, "forwardInline")
+ );
+ promisedTabs.push(
+ browser.compose.beginForward(messages[3].id, "forwardInline")
+ );
+
+ let foundIds = new Set();
+ let openedTabs = await Promise.allSettled(promisedTabs);
+ for (let i = 0; i < 4; i++) {
+ browser.test.assertEq(
+ "fulfilled",
+ openedTabs[i].status,
+ `Promise for the opened compose window should have been fulfilled for message ${i}`
+ );
+
+ browser.test.assertTrue(
+ !foundIds.has(openedTabs[i].value.id),
+ `Tab ${i} should have a unique id ${openedTabs[i].value.id}`
+ );
+ foundIds.add(openedTabs[i].value.id);
+
+ let details = await browser.compose.getComposeDetails(
+ openedTabs[i].value.id
+ );
+ browser.test.assertEq(
+ messages[i].id,
+ details.relatedMessageId,
+ `Should see the correct message in compose window ${i}`
+ );
+ await browser.tabs.remove(openedTabs[i].value.id);
+ }
+ }
+
+ // Test opening identical messages.
+ {
+ let promisedTabs = [];
+ promisedTabs.push(
+ browser.compose.beginForward(messages[0].id, "forwardInline")
+ );
+ promisedTabs.push(
+ browser.compose.beginForward(messages[0].id, "forwardInline")
+ );
+ promisedTabs.push(
+ browser.compose.beginForward(messages[0].id, "forwardInline")
+ );
+ promisedTabs.push(
+ browser.compose.beginForward(messages[0].id, "forwardInline")
+ );
+
+ let foundIds = new Set();
+ let openedTabs = await Promise.allSettled(promisedTabs);
+ for (let i = 0; i < 4; i++) {
+ browser.test.assertEq(
+ "fulfilled",
+ openedTabs[i].status,
+ `Promise for the opened compose window should have been fulfilled for message ${i}`
+ );
+
+ browser.test.assertTrue(
+ !foundIds.has(openedTabs[i].value.id),
+ `Tab ${i} should have a unique id ${openedTabs[i].value.id}`
+ );
+ foundIds.add(openedTabs[i].value.id);
+
+ let details = await browser.compose.getComposeDetails(
+ openedTabs[i].value.id
+ );
+ browser.test.assertEq(
+ messages[0].id,
+ details.relatedMessageId,
+ `Should see the correct message in compose window ${i}`
+ );
+ await browser.tabs.remove(openedTabs[i].value.id);
+ }
+ }
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose", "accountsRead", "messagesRead"],
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_headers.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_headers.js
new file mode 100644
index 0000000000..7d360b7920
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_headers.js
@@ -0,0 +1,178 @@
+/* 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"
+);
+
+let account = createAccount("pop3");
+createAccount("local");
+MailServices.accounts.defaultAccount = account;
+
+addIdentity(account);
+
+let rootFolder = account.incomingServer.rootFolder;
+rootFolder.createSubfolder("test", null);
+let folder = rootFolder.getChildNamed("test");
+createMessages(folder, 4);
+
+add_task(async function testHeaders() {
+ let files = {
+ "background.js": async () => {
+ async function checkHeaders(expected) {
+ let [createdWindow] = await createdWindowPromise;
+ browser.test.assertEq("messageCompose", createdWindow.type);
+ browser.test.sendMessage("checkHeaders", expected);
+ await window.waitForMessage();
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+ }
+
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(2, accounts.length, "number of accounts");
+ let popAccount = accounts.find(a => a.type == "pop3");
+ let folder = popAccount.folders.find(f => f.name == "test");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(4, messages.length, "number of messages");
+
+ let addressBook = await browser.addressBooks.create({
+ name: "Baker Street",
+ });
+ let contacts = {
+ sherlock: await browser.contacts.create(addressBook, {
+ DisplayName: "Sherlock Holmes",
+ PrimaryEmail: "sherlock@bakerstreet.invalid",
+ }),
+ john: await browser.contacts.create(addressBook, {
+ DisplayName: "John Watson",
+ PrimaryEmail: "john@bakerstreet.invalid",
+ }),
+ };
+ let list = await browser.mailingLists.create(addressBook, {
+ name: "Holmes and Watson",
+ description: "Tenants221B",
+ });
+ await browser.mailingLists.addMember(list, contacts.sherlock);
+ await browser.mailingLists.addMember(list, contacts.john);
+
+ let createdWindowPromise;
+
+ // Start a new message.
+
+ createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew();
+ await checkHeaders({});
+
+ // Start a new message, with a subject and recipients as strings.
+
+ createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew({
+ to: "Sherlock Holmes <sherlock@bakerstreet.invalid>",
+ cc: "John Watson <john@bakerstreet.invalid>",
+ subject: "Did you miss me?",
+ });
+ await checkHeaders({
+ to: ["Sherlock Holmes <sherlock@bakerstreet.invalid>"],
+ cc: ["John Watson <john@bakerstreet.invalid>"],
+ subject: "Did you miss me?",
+ });
+
+ // Start a new message, with a subject and recipients as string arrays.
+
+ createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew({
+ to: ["Sherlock Holmes <sherlock@bakerstreet.invalid>"],
+ cc: ["John Watson <john@bakerstreet.invalid>"],
+ subject: "Did you miss me?",
+ });
+ await checkHeaders({
+ to: ["Sherlock Holmes <sherlock@bakerstreet.invalid>"],
+ cc: ["John Watson <john@bakerstreet.invalid>"],
+ subject: "Did you miss me?",
+ });
+
+ // Start a new message, with a subject and recipients as contacts.
+
+ createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew({
+ to: [{ id: contacts.sherlock, type: "contact" }],
+ cc: [{ id: contacts.john, type: "contact" }],
+ subject: "Did you miss me?",
+ });
+ await checkHeaders({
+ to: ["Sherlock Holmes <sherlock@bakerstreet.invalid>"],
+ cc: ["John Watson <john@bakerstreet.invalid>"],
+ subject: "Did you miss me?",
+ });
+
+ // Start a new message, with a subject and recipients as a mailing list.
+
+ createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew({
+ to: [{ id: list, type: "mailingList" }],
+ subject: "Did you miss me?",
+ });
+ await checkHeaders({
+ to: ["Holmes and Watson <Tenants221B>"],
+ subject: "Did you miss me?",
+ });
+
+ // Reply to a message.
+
+ createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginReply(messages[0].id);
+ await checkHeaders({
+ to: [messages[0].author.replace(/"/g, "")],
+ subject: `Re: ${messages[0].subject}`,
+ });
+
+ // Forward a message.
+
+ createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginForward(
+ messages[1].id,
+ "forwardAsAttachment",
+ {
+ to: ["Mycroft Holmes <mycroft@bakerstreet.invalid>"],
+ }
+ );
+ await checkHeaders({
+ to: ["Mycroft Holmes <mycroft@bakerstreet.invalid>"],
+ subject: `Fwd: ${messages[1].subject}`,
+ });
+
+ // Forward a message inline. This uses a different code path.
+
+ createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginForward(messages[2].id, "forwardInline", {
+ to: ["Mycroft Holmes <mycroft@bakerstreet.invalid>"],
+ });
+ await checkHeaders({
+ to: ["Mycroft Holmes <mycroft@bakerstreet.invalid>"],
+ subject: `Fwd: ${messages[2].subject}`,
+ });
+
+ await browser.addressBooks.delete(addressBook);
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "addressBooks", "messagesRead"],
+ },
+ });
+
+ extension.onMessage("checkHeaders", async expected => {
+ await checkComposeHeaders(expected);
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_identity.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_identity.js
new file mode 100644
index 0000000000..34ee180582
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_identity.js
@@ -0,0 +1,102 @@
+/* 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"
+);
+
+let account = createAccount("pop3");
+createAccount("local");
+MailServices.accounts.defaultAccount = account;
+
+let defaultIdentity = addIdentity(account);
+defaultIdentity.composeHtml = true;
+let nonDefaultIdentity = addIdentity(account);
+nonDefaultIdentity.composeHtml = false;
+
+let rootFolder = account.incomingServer.rootFolder;
+rootFolder.createSubfolder("test", null);
+let folder = rootFolder.getChildNamed("test");
+createMessages(folder, 4);
+
+add_task(async function testIdentity() {
+ let files = {
+ "background.js": async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(2, accounts.length, "number of accounts");
+ let popAccount = accounts.find(a => a.type == "pop3");
+ browser.test.assertEq(
+ 2,
+ popAccount.identities.length,
+ "number of identities"
+ );
+ let [defaultIdentity, nonDefaultIdentity] = popAccount.identities;
+ let folder = popAccount.folders.find(f => f.name == "test");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(4, messages.length, "number of messages");
+
+ browser.test.log(defaultIdentity.id);
+ browser.test.log(nonDefaultIdentity.id);
+
+ let funcs = [
+ { name: "beginNew", args: [] },
+ { name: "beginReply", args: [messages[0].id] },
+ { name: "beginForward", args: [messages[1].id, "forwardAsAttachment"] },
+ // Uses a different code path.
+ { name: "beginForward", args: [messages[2].id, "forwardInline"] },
+ { name: "beginNew", args: [messages[3].id] },
+ ];
+ let tests = [
+ { args: [], isDefault: true },
+ {
+ args: [{ identityId: defaultIdentity.id }],
+ isDefault: true,
+ },
+ {
+ args: [{ identityId: nonDefaultIdentity.id }],
+ isDefault: false,
+ },
+ ];
+ for (let func of funcs) {
+ browser.test.log(func.name);
+ for (let test of tests) {
+ browser.test.log(JSON.stringify(test.args));
+ let tab = await browser.compose[func.name](
+ ...func.args.concat(test.args)
+ );
+ browser.test.assertEq("object", typeof tab);
+ browser.test.assertEq("number", typeof tab.id);
+ await window.sendMessage("checkIdentity", test.isDefault);
+ }
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ extension.onMessage("checkIdentity", async isDefault => {
+ let composeWindows = [...Services.wm.getEnumerator("msgcompose")];
+ is(composeWindows.length, 1);
+ await new Promise(resolve => composeWindows[0].setTimeout(resolve));
+
+ is(
+ composeWindows[0].getCurrentIdentityKey(),
+ isDefault ? defaultIdentity.key : nonDefaultIdentity.key
+ );
+ composeWindows[0].close();
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_new.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_new.js
new file mode 100644
index 0000000000..298da47578
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_new.js
@@ -0,0 +1,136 @@
+/* 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"
+);
+
+add_setup(() => {
+ let account = createAccount("pop3");
+ createAccount("local");
+ MailServices.accounts.defaultAccount = account;
+
+ addIdentity(account);
+
+ let rootFolder = account.incomingServer.rootFolder;
+ rootFolder.createSubfolder("test", null);
+ let folder = rootFolder.getChildNamed("test");
+ createMessages(folder, 4);
+});
+
+/* Test if getComposeDetails() is waiting until the entire init procedure of
+ * the composeWindow has finished, before returning values. */
+add_task(async function testComposerIsReady() {
+ let files = {
+ "background.js": async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(2, accounts.length, "number of accounts");
+ let popAccount = accounts.find(a => a.type == "pop3");
+ let folder = popAccount.folders.find(f => f.name == "test");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(4, messages.length, "number of messages");
+
+ let details = {
+ plainTextBody: "This is Text",
+ to: ["Mr. Holmes <holmes@bakerstreet.invalid>"],
+ subject: "Test Email",
+ };
+
+ let tests = [
+ {
+ description: "Begin new.",
+ funcName: "beginNew",
+ arguments: [details],
+ },
+ {
+ description: "Edit as new.",
+ funcName: "beginNew",
+ arguments: [messages[0].id, details],
+ },
+ ];
+
+ for (let test of tests) {
+ browser.test.log(JSON.stringify(test));
+ let expectedDetails = test.arguments[test.arguments.length - 1];
+
+ // Test with windows.onCreated
+ {
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ // Explicitly do not await this call.
+ browser.compose[test.funcName](...test.arguments);
+ let [createdWindow] = await createdWindowPromise;
+ let [tab] = await browser.tabs.query({ windowId: createdWindow.id });
+
+ let actualDetails = await browser.compose.getComposeDetails(tab.id);
+ for (let detail of Object.keys(expectedDetails)) {
+ browser.test.assertEq(
+ expectedDetails[detail].toString(),
+ actualDetails[detail].toString(),
+ `After windows.OnCreated: Detail ${detail} is correct for ${test.description}`
+ );
+ }
+
+ // Test the windows API being able to return the messageCompose window as
+ // the current one.
+ await window.waitForCondition(async () => {
+ let win = await browser.windows.get(createdWindow.id);
+ return win.focused;
+ }, `Window should have received focus.`);
+
+ let composeWindow = await browser.windows.get(tab.windowId);
+ browser.test.assertEq(composeWindow.type, "messageCompose");
+ let curWindow = await browser.windows.getCurrent();
+ browser.test.assertEq(tab.windowId, curWindow.id);
+ // Test the tabs API being able to return the correct current tab.
+ let [currentTab] = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+ browser.test.assertEq(tab.id, currentTab.id);
+
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+ }
+
+ // Test with tabs.onCreated
+ {
+ let createdTabPromise = window.waitForEvent("tabs.onCreated");
+ // Explicitly do not await this call.
+ browser.compose[test.funcName](...test.arguments);
+ let [createdTab] = await createdTabPromise;
+ let actualDetails = await browser.compose.getComposeDetails(
+ createdTab.id
+ );
+
+ for (let detail of Object.keys(expectedDetails)) {
+ browser.test.assertEq(
+ expectedDetails[detail].toString(),
+ actualDetails[detail].toString(),
+ `After tabs.OnCreated: Detail ${detail} is correct for ${test.description}`
+ );
+ }
+
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ let createdWindow = await browser.windows.get(createdTab.windowId);
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+ }
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose", "accountsRead", "messagesRead"],
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_reply.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_reply.js
new file mode 100644
index 0000000000..979901d2a5
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_begin_reply.js
@@ -0,0 +1,146 @@
+/* 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"
+);
+
+add_setup(() => {
+ let account = createAccount("pop3");
+ createAccount("local");
+ MailServices.accounts.defaultAccount = account;
+
+ addIdentity(account);
+
+ let rootFolder = account.incomingServer.rootFolder;
+ rootFolder.createSubfolder("test", null);
+ let folder = rootFolder.getChildNamed("test");
+ createMessages(folder, 4);
+});
+
+/* Test if getComposeDetails() is waiting until the entire init procedure of
+ * the composeWindow has finished, before returning values. */
+add_task(async function testComposerIsReady() {
+ let files = {
+ "background.js": async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(2, accounts.length, "number of accounts");
+ let popAccount = accounts.find(a => a.type == "pop3");
+ let folder = popAccount.folders.find(f => f.name == "test");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(4, messages.length, "number of messages");
+
+ let details = {
+ plainTextBody: "This is Text",
+ to: ["Mr. Holmes <holmes@bakerstreet.invalid>"],
+ subject: "Test Email",
+ };
+
+ let tests = [
+ {
+ description: "Reply default.",
+ funcName: "beginReply",
+ arguments: [messages[0].id, details],
+ },
+ {
+ description: "Reply as replyToSender.",
+ funcName: "beginReply",
+ arguments: [messages[0].id, "replyToSender", details],
+ },
+ {
+ description: "Reply as replyToList.",
+ funcName: "beginReply",
+ arguments: [messages[0].id, "replyToList", details],
+ },
+ {
+ description: "Reply as replyToAll.",
+ funcName: "beginReply",
+ arguments: [messages[0].id, "replyToAll", details],
+ },
+ ];
+
+ for (let test of tests) {
+ browser.test.log(JSON.stringify(test));
+ let expectedDetails = test.arguments[test.arguments.length - 1];
+
+ // Test with windows.onCreated
+ {
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ // Explicitly do not await this call.
+ browser.compose[test.funcName](...test.arguments);
+ let [createdWindow] = await createdWindowPromise;
+ let [tab] = await browser.tabs.query({ windowId: createdWindow.id });
+
+ let actualDetails = await browser.compose.getComposeDetails(tab.id);
+ for (let detail of Object.keys(expectedDetails)) {
+ browser.test.assertEq(
+ expectedDetails[detail].toString(),
+ actualDetails[detail].toString(),
+ `After windows.OnCreated: Detail ${detail} is correct for ${test.description}`
+ );
+ }
+
+ // Test the windows API being able to return the messageCompose window as
+ // the current one.
+ await window.waitForCondition(async () => {
+ let win = await browser.windows.get(createdWindow.id);
+ return win.focused;
+ }, `Window should have received focus.`);
+
+ let composeWindow = await browser.windows.get(tab.windowId);
+ browser.test.assertEq(composeWindow.type, "messageCompose");
+ let curWindow = await browser.windows.getCurrent();
+ browser.test.assertEq(tab.windowId, curWindow.id);
+ // Test the tabs API being able to return the correct current tab.
+ let [currentTab] = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+ browser.test.assertEq(tab.id, currentTab.id);
+
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+ }
+
+ // Test with tabs.onCreated
+ {
+ let createdTabPromise = window.waitForEvent("tabs.onCreated");
+ // Explicitly do not await this call.
+ browser.compose[test.funcName](...test.arguments);
+ let [createdTab] = await createdTabPromise;
+ let actualDetails = await browser.compose.getComposeDetails(
+ createdTab.id
+ );
+
+ for (let detail of Object.keys(expectedDetails)) {
+ browser.test.assertEq(
+ expectedDetails[detail].toString(),
+ actualDetails[detail].toString(),
+ `After tabs.OnCreated: Detail ${detail} is correct for ${test.description}`
+ );
+ }
+
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ let createdWindow = await browser.windows.get(createdTab.windowId);
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+ }
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose", "accountsRead", "messagesRead"],
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_bug1692439.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_bug1692439.js
new file mode 100644
index 0000000000..17d6a968ed
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_bug1692439.js
@@ -0,0 +1,160 @@
+/* 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 { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+// Import the smtp server scripts
+var {
+ nsMailServer,
+ gThreadManager,
+ fsDebugNone,
+ fsDebugAll,
+ fsDebugRecv,
+ fsDebugRecvSend,
+} = ChromeUtils.import("resource://testing-common/mailnews/Maild.jsm");
+var { SmtpDaemon, SMTP_RFC2821_handler } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Smtpd.jsm"
+);
+var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Auth.jsm"
+);
+
+// Setup the daemon and server
+function setupServerDaemon(handler) {
+ if (!handler) {
+ handler = function (d) {
+ return new SMTP_RFC2821_handler(d);
+ };
+ }
+ var server = new nsMailServer(handler, new SmtpDaemon());
+ return server;
+}
+
+function getBasicSmtpServer(port = 1, hostname = "localhost") {
+ let server = localAccountUtils.create_outgoing_server(
+ port,
+ "user",
+ "password",
+ hostname
+ );
+
+ // Override the default greeting so we get something predictable
+ // in the ELHO message
+ Services.prefs.setCharPref("mail.smtpserver.default.hello_argument", "test");
+
+ return server;
+}
+
+function getSmtpIdentity(senderName, smtpServer) {
+ // Set up the identity.
+ let identity = MailServices.accounts.createIdentity();
+ identity.email = senderName;
+ identity.smtpServerKey = smtpServer.key;
+
+ return identity;
+}
+
+var gServer;
+var gOutbox;
+
+add_setup(() => {
+ gServer = setupServerDaemon();
+ gServer.start();
+
+ // Test needs a non-local default account to be able to send messages.
+ let popAccount = createAccount("pop3");
+ let localAccount = createAccount("local");
+ MailServices.accounts.defaultAccount = popAccount;
+
+ let identity = getSmtpIdentity(
+ "identity@foo.invalid",
+ getBasicSmtpServer(gServer.port)
+ );
+ popAccount.addIdentity(identity);
+ popAccount.defaultIdentity = identity;
+
+ // Test is using the Sent folder and Outbox folder of the local account.
+ let rootFolder = localAccount.incomingServer.rootFolder;
+ rootFolder.createSubfolder("Sent", null);
+ MailServices.accounts.setSpecialFolders();
+ gOutbox = rootFolder.getChildNamed("Outbox");
+
+ registerCleanupFunction(() => {
+ gServer.stop();
+ });
+});
+
+add_task(async function testIsReflexive() {
+ let files = {
+ "background.js": async () => {
+ function trimContent(content) {
+ let data = content.replaceAll("\r\n", "\n").split("\n");
+ while (data[data.length - 1] == "") {
+ data.pop();
+ }
+ return data.join("\n");
+ }
+
+ // Create a plain text message.
+ let createdTextWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew({
+ plainTextBody: "This is some PLAIN text.",
+ isPlainText: true,
+ to: "rcpt@invalid.foo",
+ subject: "Test message",
+ });
+ let [createdTextWindow] = await createdTextWindowPromise;
+ let [createdTextTab] = await browser.tabs.query({
+ windowId: createdTextWindow.id,
+ });
+
+ // Call getComposeDetails() to trigger the actual bug.
+ let details = await browser.compose.getComposeDetails(createdTextTab.id);
+ browser.test.assertEq("This is some PLAIN text.", details.plainTextBody);
+
+ // Send the message.
+ let removedTextWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.compose.sendMessage(createdTextTab.id);
+ await removedTextWindowPromise;
+
+ // Find the message in the send folder.
+ let accounts = await browser.accounts.list();
+ let account = accounts.find(a => a.folders.find(f => f.type == "sent"));
+ let { messages } = await browser.messages.list(
+ account.folders.find(f => f.type == "sent")
+ );
+
+ // Read the message.
+ browser.test.assertEq(
+ "Test message",
+ messages[0].subject,
+ "Should find the sent message"
+ );
+ let message = await browser.messages.getFull(messages[0].id);
+ let content = trimContent(message.parts[0].body);
+
+ // Test that the first line is not an empty line.
+ browser.test.assertEq(
+ "This is some PLAIN text.",
+ content,
+ "The content should not start with an empty line"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "compose", "compose.send", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_bug1804796.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_bug1804796.js
new file mode 100644
index 0000000000..394a7906c4
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_bug1804796.js
@@ -0,0 +1,80 @@
+/* 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"
+);
+
+let account = createAccount("pop3");
+createAccount("local");
+MailServices.accounts.defaultAccount = account;
+
+addIdentity(account);
+
+let rootFolder = account.incomingServer.rootFolder;
+rootFolder.createSubfolder("test", null);
+let folder = rootFolder.getChildNamed("test");
+createMessages(folder, 4);
+
+add_task(async function test_update_plaintext_before_send() {
+ let files = {
+ "background.js": async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(2, accounts.length, "number of accounts");
+ let popAccount = accounts.find(a => a.type == "pop3");
+ let folder = popAccount.folders.find(f => f.name == "test");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(4, messages.length, "number of messages");
+
+ // Setup onBeforeSend listener.
+
+ let listener = async tab => {
+ let details1 = await browser.compose.getComposeDetails(tab.id);
+ details1.plainTextBody =
+ "Pre Text\n\n" + details1.plainTextBody + "\n\nPost Text";
+ await browser.compose.setComposeDetails(tab.id, details1);
+ await new Promise(resolve => window.setTimeout(resolve));
+
+ let details2 = await browser.compose.getComposeDetails(tab.id);
+ browser.test.assertEq(
+ details1.plainTextBody,
+ details2.plainTextBody,
+ "PlainTextBody should be correct after updated in onBeforeSend"
+ );
+
+ return {};
+ };
+ browser.compose.onBeforeSend.addListener(listener);
+
+ // Reply to a message.
+
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ let tab = await browser.compose.beginReply(messages[0].id, {
+ isPlainText: true,
+ });
+ await createdWindowPromise;
+
+ // Send message and trigger onBeforeSend event.
+
+ await new Promise(resolve => window.setTimeout(resolve));
+ let closedWindowPromise = window.waitForEvent("windows.onRemoved");
+ await browser.compose.sendMessage(tab.id, { mode: "sendLater" });
+ await closedWindowPromise;
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose", "accountsRead", "messagesRead", "compose.send"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_details.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_details.js
new file mode 100644
index 0000000000..3eb16102c5
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_details.js
@@ -0,0 +1,725 @@
+/* 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 account = createAccount();
+let defaultIdentity = addIdentity(account);
+let nonDefaultIdentity = addIdentity(account);
+defaultIdentity.attachVCard = false;
+nonDefaultIdentity.attachVCard = true;
+
+let gRootFolder = account.incomingServer.rootFolder;
+
+gRootFolder.createSubfolder("test", null);
+let gTestFolder = gRootFolder.getChildNamed("test");
+createMessages(gTestFolder, 4);
+
+// TODO: Figure out why naming this folder drafts is problematic.
+gRootFolder.createSubfolder("something", null);
+let gDraftsFolder = gRootFolder.getChildNamed("something");
+gDraftsFolder.flags = Ci.nsMsgFolderFlags.Drafts;
+createMessages(gDraftsFolder, 2);
+let gDrafts = [...gDraftsFolder.messages];
+
+// Verifies ComposeDetails of a given composer can be applied to a different
+// composer, even if they have different compose formats. The composer should pick
+// the matching body/plaintextBody value, if both are specified. The value for
+// isPlainText is ignored by setComposeDetails.
+add_task(async function testIsReflexive() {
+ let files = {
+ "background.js": async () => {
+ // Start a new TEXT message.
+ let createdTextWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew({
+ plainTextBody: "This is some PLAIN text.",
+ isPlainText: true,
+ });
+ let [createdTextWindow] = await createdTextWindowPromise;
+ let [createdTextTab] = await browser.tabs.query({
+ windowId: createdTextWindow.id,
+ });
+
+ // Get details, TEXT message.
+ let textDetails = await browser.compose.getComposeDetails(
+ createdTextTab.id
+ );
+ browser.test.assertTrue(textDetails.isPlainText);
+ browser.test.assertTrue(
+ textDetails.body.includes("This is some PLAIN text")
+ );
+ browser.test.assertEq(
+ "This is some PLAIN text.",
+ textDetails.plainTextBody
+ );
+
+ // Start a new HTML message.
+ let createdHtmlWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew({
+ body: "<p>This is some <i>HTML</i> text.</p>",
+ isPlainText: false,
+ });
+ let [createdHtmlWindow] = await createdHtmlWindowPromise;
+ let [createdHtmlTab] = await browser.tabs.query({
+ windowId: createdHtmlWindow.id,
+ });
+
+ // Get details, HTML message.
+ let htmlDetails = await browser.compose.getComposeDetails(
+ createdHtmlTab.id
+ );
+ browser.test.assertFalse(htmlDetails.isPlainText);
+ browser.test.assertTrue(
+ htmlDetails.body.includes("<p>This is some <i>HTML</i> text.</p>")
+ );
+ browser.test.assertEq(
+ "This is some /HTML/ text.",
+ htmlDetails.plainTextBody
+ );
+
+ // Set HTML details on HTML composer. It should not throw.
+ await browser.compose.setComposeDetails(createdHtmlTab.id, htmlDetails);
+
+ // Set TEXT details on TEXT composer. It should not throw.
+ await browser.compose.setComposeDetails(createdTextTab.id, textDetails);
+
+ // Set TEXT details on HTML composer and verify the changed content.
+ await browser.compose.setComposeDetails(createdHtmlTab.id, textDetails);
+ let htmlDetails2 = await browser.compose.getComposeDetails(
+ createdHtmlTab.id
+ );
+ browser.test.assertFalse(htmlDetails2.isPlainText);
+ browser.test.assertTrue(
+ htmlDetails2.body.includes("This is some PLAIN text")
+ );
+ browser.test.assertEq(
+ "This is some PLAIN text.",
+ htmlDetails2.plainTextBody
+ );
+
+ // Set HTML details on TEXT composer and verify the changed content.
+ await browser.compose.setComposeDetails(createdTextTab.id, htmlDetails);
+ let textDetails2 = await browser.compose.getComposeDetails(
+ createdTextTab.id
+ );
+ browser.test.assertTrue(textDetails2.isPlainText);
+ browser.test.assertTrue(
+ textDetails2.body.includes("This is some /HTML/ text.")
+ );
+ browser.test.assertEq(
+ "This is some /HTML/ text.",
+ textDetails2.plainTextBody
+ );
+
+ // Clean up.
+
+ let removedHtmlWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(createdHtmlWindow.id);
+ await removedHtmlWindowPromise;
+
+ let removedTextWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(createdTextWindow.id);
+ await removedTextWindowPromise;
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "compose"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function testType() {
+ let files = {
+ "background.js": async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(1, accounts.length, "number of accounts");
+
+ let testFolder = accounts[0].folders.find(f => f.name == "test");
+ let messages = (await browser.messages.list(testFolder)).messages;
+ browser.test.assertEq(4, messages.length, "number of messages");
+
+ let draftFolder = accounts[0].folders.find(f => f.name == "something");
+ let drafts = (await browser.messages.list(draftFolder)).messages;
+ browser.test.assertEq(2, drafts.length, "number of drafts");
+
+ async function checkComposer(tab, expected) {
+ browser.test.assertEq("object", typeof tab, "type of tab");
+ browser.test.assertEq("number", typeof tab.id, "type of tab ID");
+ browser.test.assertEq(
+ "number",
+ typeof tab.windowId,
+ "type of window ID"
+ );
+
+ let details = await browser.compose.getComposeDetails(tab.id);
+ browser.test.assertEq(expected.type, details.type, "type of composer");
+ browser.test.assertEq(
+ expected.relatedMessageId,
+ details.relatedMessageId,
+ `related message id (${details.type})`
+ );
+ await browser.windows.remove(tab.windowId);
+ }
+
+ let tests = [
+ {
+ funcName: "beginNew",
+ args: [],
+ expected: { type: "new", relatedMessageId: null },
+ },
+ {
+ funcName: "beginReply",
+ args: [messages[0].id],
+ expected: { type: "reply", relatedMessageId: messages[0].id },
+ },
+ {
+ funcName: "beginReply",
+ args: [messages[1].id, "replyToAll"],
+ expected: { type: "reply", relatedMessageId: messages[1].id },
+ },
+ {
+ funcName: "beginReply",
+ args: [messages[2].id, "replyToList"],
+ expected: { type: "reply", relatedMessageId: messages[2].id },
+ },
+ {
+ funcName: "beginReply",
+ args: [messages[3].id, "replyToSender"],
+ expected: { type: "reply", relatedMessageId: messages[3].id },
+ },
+ {
+ funcName: "beginForward",
+ args: [messages[0].id],
+ expected: { type: "forward", relatedMessageId: messages[0].id },
+ },
+ {
+ funcName: "beginForward",
+ args: [messages[1].id, "forwardAsAttachment"],
+ expected: { type: "forward", relatedMessageId: messages[1].id },
+ },
+ // Uses a different code path.
+ {
+ funcName: "beginForward",
+ args: [messages[2].id, "forwardInline"],
+ expected: { type: "forward", relatedMessageId: messages[2].id },
+ },
+ {
+ funcName: "beginNew",
+ args: [messages[3].id],
+ expected: { type: "new", relatedMessageId: messages[3].id },
+ },
+ ];
+ for (let test of tests) {
+ browser.test.log(test.funcName);
+ let tab = await browser.compose[test.funcName](...test.args);
+ await checkComposer(tab, test.expected);
+ }
+
+ browser.tabs.onCreated.addListener(async tab => {
+ // Bug 1702957, if composeWindow.GetComposeDetails() is not delayed
+ // until the compose window is ready, it will overwrite the compose
+ // fields.
+ let details = await browser.compose.getComposeDetails(tab.id);
+ browser.test.assertEq(
+ "Johnny Jones <johnny@jones.invalid>",
+ details.to.pop(),
+ "Check Recipients in draft after calling getComposeDetails()"
+ );
+
+ let window = await browser.windows.get(tab.windowId);
+ if (window.type == "messageCompose") {
+ await checkComposer(tab, {
+ type: "draft",
+ relatedMessageId: drafts[0].id,
+ });
+ browser.test.notifyPass("Finish");
+ }
+ });
+ browser.test.sendMessage("openDrafts");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose", "accountsRead", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+
+ // The first part of the test is done in the background script using the
+ // compose API to open compose windows. For the second part we need to open
+ // a draft, which is not possible with the compose API.
+ await extension.awaitMessage("openDrafts");
+ window.ComposeMessage(
+ Ci.nsIMsgCompType.Draft,
+ Ci.nsIMsgCompFormat.Default,
+ gDraftsFolder,
+ [gDraftsFolder.generateMessageURI(gDrafts[0].messageKey)]
+ );
+
+ await extension.awaitFinish("Finish");
+ await extension.unload();
+});
+
+add_task(async function testFcc() {
+ let files = {
+ "background.js": async () => {
+ async function checkWindow(createdTab, expected) {
+ let state = await browser.compose.getComposeDetails(createdTab.id);
+
+ browser.test.assertEq(
+ expected.overrideDefaultFcc,
+ state.overrideDefaultFcc,
+ "overrideDefaultFcc should be correct"
+ );
+
+ if (expected.overrideDefaultFccFolder) {
+ window.assertDeepEqual(
+ state.overrideDefaultFccFolder,
+ expected.overrideDefaultFccFolder,
+ "overrideDefaultFccFolder should be correct"
+ );
+ } else {
+ browser.test.assertEq(
+ expected.overrideDefaultFccFolder,
+ state.overrideDefaultFccFolder,
+ "overrideDefaultFccFolder should be correct"
+ );
+ }
+
+ if (expected.additionalFccFolder) {
+ window.assertDeepEqual(
+ state.additionalFccFolder,
+ expected.additionalFccFolder,
+ "additionalFccFolder should be correct"
+ );
+ } else {
+ browser.test.assertEq(
+ expected.additionalFccFolder,
+ state.additionalFccFolder,
+ "additionalFccFolder should be correct"
+ );
+ }
+
+ await window.sendMessage("checkWindow", expected);
+ }
+
+ let [account] = await browser.accounts.list();
+ let folder1 = account.folders.find(f => f.name == "Trash");
+ let folder2 = account.folders.find(f => f.name == "something");
+
+ // Start a new message.
+
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew();
+ let [createdWindow] = await createdWindowPromise;
+ let [createdTab] = await browser.tabs.query({
+ windowId: createdWindow.id,
+ });
+
+ await checkWindow(createdTab, {
+ overrideDefaultFcc: false,
+ overrideDefaultFccFolder: null,
+ additionalFccFolder: "",
+ });
+
+ await browser.test.assertRejects(
+ browser.compose.setComposeDetails(createdTab.id, {
+ overrideDefaultFcc: true,
+ }),
+ "Setting overrideDefaultFcc to true requires setting overrideDefaultFccFolder as well",
+ "browser.compose.setComposeDetails() should reject setting overrideDefaultFcc to true."
+ );
+
+ // Set folders.
+ await browser.compose.setComposeDetails(createdTab.id, {
+ overrideDefaultFccFolder: folder1,
+ additionalFccFolder: folder2,
+ });
+ await checkWindow(createdTab, {
+ overrideDefaultFcc: true,
+ overrideDefaultFccFolder: folder1,
+ additionalFccFolder: folder2,
+ });
+
+ // Setting overrideDefaultFcc true while it is already true should not change any values.
+ await browser.compose.setComposeDetails(createdTab.id, {
+ overrideDefaultFcc: true,
+ });
+ await checkWindow(createdTab, {
+ overrideDefaultFcc: true,
+ overrideDefaultFccFolder: folder1,
+ additionalFccFolder: folder2,
+ });
+
+ // A no-op should not change any values.
+ await browser.compose.setComposeDetails(createdTab.id, {});
+ await checkWindow(createdTab, {
+ overrideDefaultFcc: true,
+ overrideDefaultFccFolder: folder1,
+ additionalFccFolder: folder2,
+ });
+
+ // Disable fcc.
+ await browser.compose.setComposeDetails(createdTab.id, {
+ overrideDefaultFccFolder: "",
+ });
+ await checkWindow(createdTab, {
+ overrideDefaultFcc: true,
+ overrideDefaultFccFolder: "",
+ additionalFccFolder: folder2,
+ });
+
+ // Disable additional fcc.
+ await browser.compose.setComposeDetails(createdTab.id, {
+ additionalFccFolder: "",
+ });
+ await checkWindow(createdTab, {
+ overrideDefaultFcc: true,
+ overrideDefaultFccFolder: "",
+ additionalFccFolder: "",
+ });
+
+ // Clear override.
+ await browser.compose.setComposeDetails(createdTab.id, {
+ overrideDefaultFcc: false,
+ });
+ await checkWindow(createdTab, {
+ overrideDefaultFcc: false,
+ overrideDefaultFccFolder: null,
+ additionalFccFolder: "",
+ });
+
+ await browser.test.assertRejects(
+ browser.compose.setComposeDetails(createdTab.id, {
+ overrideDefaultFccFolder: {
+ path: "/bad",
+ accountId: folder1.accountId,
+ },
+ }),
+ `Invalid MailFolder: {accountId:${folder1.accountId}, path:/bad}`,
+ "browser.compose.setComposeDetails() should reject, if an invalid folder is set as overrideDefaultFccFolder."
+ );
+
+ await browser.test.assertRejects(
+ browser.compose.setComposeDetails(createdTab.id, {
+ additionalFccFolder: { path: "/bad", accountId: folder1.accountId },
+ }),
+ `Invalid MailFolder: {accountId:${folder1.accountId}, path:/bad}`,
+ "browser.compose.setComposeDetails() should reject, if an invalid folder is set as additionalFccFolder."
+ );
+
+ // Clean up.
+
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "compose", "messagesRead"],
+ },
+ });
+
+ extension.onMessage("checkWindow", async expected => {
+ await checkComposeHeaders(expected);
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function testSimpleDetails() {
+ let files = {
+ "background.js": async () => {
+ async function checkWindow(createdTab, expected) {
+ let state = await browser.compose.getComposeDetails(createdTab.id);
+
+ if (expected.priority) {
+ browser.test.assertEq(
+ expected.priority,
+ state.priority,
+ "priority should be correct"
+ );
+ }
+
+ if (expected.hasOwnProperty("returnReceipt")) {
+ browser.test.assertEq(
+ expected.returnReceipt,
+ state.returnReceipt,
+ "returnReceipt should be correct"
+ );
+ }
+
+ if (expected.hasOwnProperty("deliveryStatusNotification")) {
+ browser.test.assertEq(
+ expected.deliveryStatusNotification,
+ state.deliveryStatusNotification,
+ "deliveryStatusNotification should be correct"
+ );
+ }
+
+ if (expected.hasOwnProperty("attachVCard")) {
+ browser.test.assertEq(
+ expected.attachVCard,
+ state.attachVCard,
+ "attachVCard should be correct"
+ );
+ }
+
+ if (expected.deliveryFormat) {
+ browser.test.assertEq(
+ expected.deliveryFormat,
+ state.deliveryFormat,
+ "deliveryFormat should be correct"
+ );
+ }
+
+ await window.sendMessage("checkWindow", expected);
+ }
+
+ // Start a new message.
+
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew();
+ let [createdWindow] = await createdWindowPromise;
+ let [createdTab] = await browser.tabs.query({
+ windowId: createdWindow.id,
+ });
+
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(1, accounts.length, "number of accounts");
+ let localAccount = accounts.find(a => a.type == "none");
+ browser.test.assertEq(
+ 2,
+ localAccount.identities.length,
+ "number of identities"
+ );
+ let [defaultIdentity, nonDefaultIdentity] = localAccount.identities;
+
+ let expected = {
+ priority: "normal",
+ returnReceipt: false,
+ deliveryStatusNotification: false,
+ deliveryFormat: "auto",
+ attachVCard: false,
+ identityId: defaultIdentity.id,
+ };
+
+ async function changeDetail(key, value, _expected = {}) {
+ await browser.compose.setComposeDetails(createdTab.id, {
+ [key]: value,
+ });
+ expected[key] = value;
+ for (let [k, v] of Object.entries(_expected)) {
+ expected[k] = v;
+ }
+ await checkWindow(createdTab, expected);
+ }
+
+ // Confirm initial condition.
+ await checkWindow(createdTab, expected);
+
+ // Changing the identity without having made any changes, should load the
+ // defaults of the second identity.
+ await changeDetail("identityId", nonDefaultIdentity.id, {
+ attachVCard: true,
+ });
+
+ // Switching back should restore the defaults of the first identity.
+ await changeDetail("identityId", defaultIdentity.id, {
+ attachVCard: false,
+ });
+
+ await changeDetail("priority", "highest");
+ await changeDetail("deliveryFormat", "html");
+ await changeDetail("returnReceipt", true);
+ await changeDetail("deliveryFormat", "plaintext");
+ await changeDetail("priority", "lowest");
+ await changeDetail("attachVCard", true);
+ await changeDetail("priority", "high");
+ await changeDetail("deliveryFormat", "both");
+ await changeDetail("deliveryStatusNotification", true);
+ await changeDetail("priority", "low");
+
+ await changeDetail("priority", "normal");
+ await changeDetail("deliveryFormat", "auto");
+ await changeDetail("attachVCard", false);
+ await changeDetail("returnReceipt", false);
+ await changeDetail("deliveryStatusNotification", false);
+
+ // Changing the identity should not load the defaults of the second identity,
+ // after the values had been changed.
+ await changeDetail("identityId", nonDefaultIdentity.id);
+
+ // Clean up.
+
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "compose", "messagesRead"],
+ },
+ });
+
+ extension.onMessage("checkWindow", async expected => {
+ await checkComposeHeaders(expected);
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function testAutoComplete() {
+ let files = {
+ "background.js": async () => {
+ async function checkWindow(createdTab, expected) {
+ let state = await browser.compose.getComposeDetails(createdTab.id);
+
+ for (let [id, value] of Object.entries(expected.pills)) {
+ browser.test.assertEq(
+ value,
+ state[id].length ? state[id][0] : "",
+ `value for ${id} should be correct`
+ );
+ }
+
+ await window.sendMessage("checkWindow", expected);
+ }
+
+ // Start a new message.
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew();
+ let [createdWindow] = await createdWindowPromise;
+ let [createdTab] = await browser.tabs.query({
+ windowId: createdWindow.id,
+ });
+
+ // Create a test contact.
+ let [addressBook] = await browser.addressBooks.list(true);
+ let contactId = await browser.contacts.create(addressBook.id, {
+ PrimaryEmail: "autocomplete@invalid",
+ DisplayName: "Autocomplete Test",
+ });
+
+ // Confirm the addrTo field has focus and addrTo and replyTo fields are empty.
+ await checkWindow(createdTab, {
+ activeElement: "toAddrInput",
+ pills: { to: "", replyTo: "" },
+ values: { toAddrInput: "", replyAddrInput: "" },
+ });
+
+ // Set the replyTo field, which should not break autocomplete for the currently active addrTo
+ // field.
+ await browser.compose.setComposeDetails(createdTab.id, {
+ replyTo: "test@user.net",
+ });
+
+ // Confirm the addrTo field has focus and replyTo field is set.
+ await checkWindow(createdTab, {
+ activeElement: "toAddrInput",
+ pills: { to: "", replyTo: "test@user.net" },
+ values: { toAddrInput: "", replyAddrInput: "" },
+ });
+
+ // Manually type "Autocomplete" into the active field, which should be the toAddr field and it
+ // should autocomplete.
+ await window.sendMessage("typeIntoActiveAddrField", "Autocomplete");
+
+ // Confirm the addrTo field has focus and replyTo field is set and the addrTo field has been
+ // autocompleted.
+ await checkWindow(createdTab, {
+ activeElement: "toAddrInput",
+ pills: { to: "", replyTo: "test@user.net" },
+ values: {
+ toAddrInput: "Autocomplete Test <autocomplete@invalid>",
+ replyAddrInput: "",
+ },
+ });
+
+ // Clean up.
+ await browser.contacts.delete(contactId);
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "compose", "addressBooks"],
+ },
+ });
+
+ extension.onMessage("typeIntoActiveAddrField", async value => {
+ let composeWindows = [...Services.wm.getEnumerator("msgcompose")];
+ is(composeWindows.length, 1);
+
+ for (const s of value) {
+ EventUtils.synthesizeKey(s, {}, composeWindows[0]);
+ await new Promise(r => composeWindows[0].setTimeout(r));
+ }
+
+ extension.sendMessage();
+ });
+
+ extension.onMessage("checkWindow", async expected => {
+ let composeWindows = [...Services.wm.getEnumerator("msgcompose")];
+ is(composeWindows.length, 1);
+ let composeDocument = composeWindows[0].document;
+ await new Promise(resolve => composeWindows[0].setTimeout(resolve));
+
+ Assert.equal(
+ composeDocument.activeElement.id,
+ expected.activeElement,
+ `Active element should be correct`
+ );
+
+ for (let [id, value] of Object.entries(expected.values)) {
+ await TestUtils.waitForCondition(
+ () => composeDocument.getElementById(id).value == value,
+ `Value of field ${id} should be correct`
+ );
+ }
+
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_details_body.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_details_body.js
new file mode 100644
index 0000000000..84b4b22019
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_details_body.js
@@ -0,0 +1,469 @@
+/* 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 account = createAccount();
+let defaultIdentity = addIdentity(account);
+let nonDefaultIdentity = addIdentity(account);
+let gRootFolder = account.incomingServer.rootFolder;
+
+gRootFolder.createSubfolder("test", null);
+let gTestFolder = gRootFolder.getChildNamed("test");
+createMessages(gTestFolder, 4);
+
+add_task(async function testPlainTextBody() {
+ let files = {
+ "background.js": async () => {
+ async function checkWindow(expected) {
+ let state = await browser.compose.getComposeDetails(createdTab.id);
+ for (let field of ["isPlainText"]) {
+ if (field in expected) {
+ browser.test.assertEq(
+ expected[field],
+ state[field],
+ `Check value for ${field}`
+ );
+ }
+ }
+ for (let field of ["plainTextBody"]) {
+ if (field in expected) {
+ browser.test.assertEq(
+ JSON.stringify(expected[field]),
+ JSON.stringify(state[field]),
+ `Check value for ${field}`
+ );
+ }
+ }
+ }
+
+ // Start a new message.
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew({ isPlainText: true });
+ let [createdWindow] = await createdWindowPromise;
+ let [createdTab] = await browser.tabs.query({
+ windowId: createdWindow.id,
+ });
+
+ await checkWindow({ isPlainText: true });
+
+ let tests = [
+ {
+ // Set plaintextBody with Windows style newlines. The return value of
+ // the API is independent of the used OS and only returns LF endings.
+ input: { isPlainText: true, plainTextBody: "123\r\n456\r\n789" },
+ expected: { isPlainText: true, plainTextBody: "123\n456\n789" },
+ },
+ {
+ // Set plaintextBody with Linux style newlines. The return value of
+ // the API is independent of the used OS and only returns LF endings.
+ input: { isPlainText: true, plainTextBody: "ABC\nDEF\nGHI" },
+ expected: { isPlainText: true, plainTextBody: "ABC\nDEF\nGHI" },
+ },
+ {
+ // Bug 1792551 without newline at the end.
+ input: { isPlainText: true, plainTextBody: "123456 \n Hello " },
+ expected: { isPlainText: true, plainTextBody: "123456 \n Hello " },
+ },
+ {
+ // Bug 1792551 without newline at the end.
+ input: { isPlainText: true, plainTextBody: "123456 &nbsp; \n " },
+ expected: { isPlainText: true, plainTextBody: "123456 &nbsp; \n " },
+ },
+ {
+ // Bug 1792551 with a newline at the end.
+ input: { isPlainText: true, plainTextBody: "123456 \n Hello \n" },
+ expected: { isPlainText: true, plainTextBody: "123456 \n Hello \n" },
+ },
+ ];
+ for (let test of tests) {
+ browser.test.log(`Checking input: ${JSON.stringify(test.input)}`);
+ await browser.compose.setComposeDetails(createdTab.id, test.input);
+ await checkWindow(test.expected);
+ }
+
+ browser.test.log("Replace plainTextBody with empty string");
+ await browser.compose.setComposeDetails(createdTab.id, {
+ isPlainText: true,
+ plainTextBody: "Lorem ipsum",
+ });
+ await checkWindow({ isPlainText: true, plainTextBody: "Lorem ipsum" });
+ await browser.compose.setComposeDetails(createdTab.id, {
+ isPlainText: true,
+ plainTextBody: "",
+ });
+ await checkWindow({ isPlainText: true, plainTextBody: "" });
+
+ // Clean up.
+
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "compose"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function testBody() {
+ // Open an compose window with HTML body.
+
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ params.composeFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ params.composeFields.body = "<p>This is some <i>HTML</i> text.</p>";
+
+ let htmlWindowPromise = BrowserTestUtils.domWindowOpened();
+ MailServices.compose.OpenComposeWindowWithParams(null, params);
+ let htmlWindow = await htmlWindowPromise;
+ await BrowserTestUtils.waitForEvent(htmlWindow, "load");
+
+ // Open another compose window with plain text body.
+
+ params = Cc["@mozilla.org/messengercompose/composeparams;1"].createInstance(
+ Ci.nsIMsgComposeParams
+ );
+ params.composeFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ params.format = Ci.nsIMsgCompFormat.PlainText;
+ params.composeFields.body = "This is some plain text.";
+
+ let plainTextComposeWindowPromise = BrowserTestUtils.domWindowOpened();
+ MailServices.compose.OpenComposeWindowWithParams(null, params);
+ let plainTextWindow = await plainTextComposeWindowPromise;
+ await BrowserTestUtils.waitForEvent(plainTextWindow, "load");
+
+ // Run the extension.
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async () => {
+ let windows = await browser.windows.getAll({
+ populate: true,
+ windowTypes: ["messageCompose"],
+ });
+ let [htmlTabId, plainTextTabId] = windows.map(w => w.tabs[0].id);
+
+ let plainTextBodyTag =
+ '<body style="font-family: -moz-fixed; white-space: pre-wrap; width: 72ch;">';
+
+ // Get details, HTML message.
+
+ let htmlDetails = await browser.compose.getComposeDetails(htmlTabId);
+ browser.test.log(JSON.stringify(htmlDetails));
+ browser.test.assertTrue(!htmlDetails.isPlainText);
+ browser.test.assertTrue(
+ htmlDetails.body.includes("<p>This is some <i>HTML</i> text.</p>")
+ );
+ browser.test.assertEq(
+ "This is some /HTML/ text.",
+ htmlDetails.plainTextBody
+ );
+
+ // Set details, HTML message.
+
+ await browser.compose.setComposeDetails(htmlTabId, {
+ body: htmlDetails.body.replace("<i>HTML</i>", "<code>HTML</code>"),
+ });
+ htmlDetails = await browser.compose.getComposeDetails(htmlTabId);
+ browser.test.log(JSON.stringify(htmlDetails));
+ browser.test.assertTrue(!htmlDetails.isPlainText);
+ browser.test.assertTrue(
+ htmlDetails.body.includes("<p>This is some <code>HTML</code> text.</p>")
+ );
+ browser.test.assertTrue(
+ "This is some HTML text.",
+ htmlDetails.plainTextBody
+ );
+
+ // Get details, plain text message.
+
+ let plainTextDetails = await browser.compose.getComposeDetails(
+ plainTextTabId
+ );
+ browser.test.log(JSON.stringify(plainTextDetails));
+ browser.test.assertTrue(plainTextDetails.isPlainText);
+ browser.test.assertTrue(
+ plainTextDetails.body.includes(
+ plainTextBodyTag + "This is some plain text.</body>"
+ )
+ );
+ browser.test.assertEq(
+ "This is some plain text.",
+ plainTextDetails.plainTextBody
+ );
+
+ // Set details, plain text message.
+
+ await browser.compose.setComposeDetails(plainTextTabId, {
+ plainTextBody:
+ plainTextDetails.plainTextBody + "\nIndeed, it is plain.",
+ });
+ plainTextDetails = await browser.compose.getComposeDetails(
+ plainTextTabId
+ );
+ browser.test.log(JSON.stringify(plainTextDetails));
+ browser.test.assertTrue(plainTextDetails.isPlainText);
+ browser.test.assertTrue(
+ plainTextDetails.body.includes(
+ plainTextBodyTag +
+ "This is some plain text.<br>Indeed, it is plain.</body>"
+ )
+ );
+ browser.test.assertEq(
+ "This is some plain text.\nIndeed, it is plain.",
+ // Fold Windows line-endings \r\n to \n.
+ plainTextDetails.plainTextBody.replace(/\r/g, "")
+ );
+
+ // Some things that should fail.
+
+ try {
+ await browser.compose.setComposeDetails(plainTextTabId, {
+ body: "Providing conflicting format settings.",
+ isPlainText: true,
+ });
+ browser.test.fail(
+ "calling setComposeDetails with these arguments should throw"
+ );
+ } catch (ex) {
+ browser.test.succeed(`expected exception thrown: ${ex.message}`);
+ }
+ try {
+ await browser.compose.setComposeDetails(htmlTabId, {
+ plainTextBody: "Providing conflicting format settings.",
+ isPlainText: false,
+ });
+ browser.test.fail(
+ "calling setComposeDetails with these arguments should throw"
+ );
+ } catch (ex) {
+ browser.test.succeed(`expected exception thrown: ${ex.message}`);
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ manifest: {
+ permissions: ["compose"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ // Check the HTML message was edited.
+
+ ok(htmlWindow.gMsgCompose.composeHTML);
+ let htmlDocument = htmlWindow.GetCurrentEditor().document;
+ info(htmlDocument.body.innerHTML);
+ is(htmlDocument.querySelectorAll("i").length, 0, "<i> was removed");
+ is(htmlDocument.querySelectorAll("code").length, 1, "<code> was added");
+
+ // Close the HTML message.
+
+ let closePromises = [
+ // If the window is not marked as dirty, this Promise will never resolve.
+ BrowserTestUtils.promiseAlertDialog("extra1"),
+ BrowserTestUtils.domWindowClosed(htmlWindow),
+ ];
+ Assert.ok(
+ htmlWindow.ComposeCanClose(),
+ "compose window should be allowed to close"
+ );
+ htmlWindow.close();
+ await Promise.all(closePromises);
+
+ // Check the plain text message was edited.
+
+ ok(!plainTextWindow.gMsgCompose.composeHTML);
+ let plainTextDocument = plainTextWindow.GetCurrentEditor().document;
+ info(plainTextDocument.body.innerHTML);
+ ok(/Indeed, it is plain\./.test(plainTextDocument.body.innerHTML));
+
+ // Close the plain text message.
+
+ closePromises = [
+ // If the window is not marked as dirty, this Promise will never resolve.
+ BrowserTestUtils.promiseAlertDialog("extra1"),
+ BrowserTestUtils.domWindowClosed(plainTextWindow),
+ ];
+ Assert.ok(
+ plainTextWindow.ComposeCanClose(),
+ "compose window should be allowed to close"
+ );
+ plainTextWindow.close();
+ await Promise.all(closePromises);
+});
+
+add_task(async function testCJK() {
+ let longCJKString = "안".repeat(400);
+
+ // Open an compose window with HTML body.
+
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ params.composeFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ params.composeFields.body = longCJKString;
+
+ let htmlWindowPromise = BrowserTestUtils.domWindowOpened();
+ MailServices.compose.OpenComposeWindowWithParams(null, params);
+ let htmlWindow = await htmlWindowPromise;
+ await BrowserTestUtils.waitForEvent(htmlWindow, "load");
+
+ // Open another compose window with plain text body.
+
+ params = Cc["@mozilla.org/messengercompose/composeparams;1"].createInstance(
+ Ci.nsIMsgComposeParams
+ );
+ params.composeFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ params.format = Ci.nsIMsgCompFormat.PlainText;
+ params.composeFields.body = longCJKString;
+
+ let plainTextComposeWindowPromise = BrowserTestUtils.domWindowOpened();
+ MailServices.compose.OpenComposeWindowWithParams(null, params);
+ let plainTextWindow = await plainTextComposeWindowPromise;
+ await BrowserTestUtils.waitForEvent(plainTextWindow, "load");
+
+ // Run the extension.
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async () => {
+ let longCJKString = "안".repeat(400);
+ let windows = await browser.windows.getAll({
+ populate: true,
+ windowTypes: ["messageCompose"],
+ });
+ let [htmlTabId, plainTextTabId] = windows.map(w => w.tabs[0].id);
+
+ let plainTextBodyTag =
+ '<body style="font-family: -moz-fixed; white-space: pre-wrap; width: 72ch;">';
+
+ // Get details, HTML message.
+
+ let htmlDetails = await browser.compose.getComposeDetails(htmlTabId);
+ browser.test.log(JSON.stringify(htmlDetails));
+ browser.test.assertTrue(!htmlDetails.isPlainText);
+ browser.test.assertTrue(
+ htmlDetails.body.includes(longCJKString),
+ "getComposeDetails.body from html composer returned CJK correctly"
+ );
+ browser.test.assertEq(
+ longCJKString,
+ htmlDetails.plainTextBody,
+ "getComposeDetails.plainTextBody from html composer returned CJK correctly"
+ );
+
+ // Set details, HTML message.
+
+ await browser.compose.setComposeDetails(htmlTabId, {
+ body: longCJKString,
+ });
+ htmlDetails = await browser.compose.getComposeDetails(htmlTabId);
+ browser.test.log(JSON.stringify(htmlDetails));
+ browser.test.assertTrue(!htmlDetails.isPlainText);
+ browser.test.assertTrue(
+ htmlDetails.body.includes(longCJKString),
+ "getComposeDetails.body from html composer returned CJK correctly as set by setComposeDetails"
+ );
+ browser.test.assertTrue(
+ longCJKString,
+ htmlDetails.plainTextBody,
+ "getComposeDetails.plainTextBody from html composer returned CJK correctly as set by setComposeDetails"
+ );
+
+ // Get details, plain text message.
+
+ let plainTextDetails = await browser.compose.getComposeDetails(
+ plainTextTabId
+ );
+ browser.test.log(JSON.stringify(plainTextDetails));
+ browser.test.assertTrue(plainTextDetails.isPlainText);
+ browser.test.assertTrue(
+ plainTextDetails.body.includes(plainTextBodyTag + longCJKString),
+ "getComposeDetails.body from text composer returned CJK correctly"
+ );
+ browser.test.assertEq(
+ longCJKString,
+ plainTextDetails.plainTextBody,
+ "getComposeDetails.plainTextBody from text composer returned CJK correctly"
+ );
+
+ // Set details, plain text message.
+
+ await browser.compose.setComposeDetails(plainTextTabId, {
+ plainTextBody: longCJKString,
+ });
+ plainTextDetails = await browser.compose.getComposeDetails(
+ plainTextTabId
+ );
+ browser.test.log(JSON.stringify(plainTextDetails));
+ browser.test.assertTrue(plainTextDetails.isPlainText);
+ browser.test.assertTrue(
+ plainTextDetails.body.includes(plainTextBodyTag + longCJKString),
+ "getComposeDetails.body from text composer returned CJK correctly as set by setComposeDetails"
+ );
+ browser.test.assertEq(
+ longCJKString,
+ // Fold Windows line-endings \r\n to \n.
+ plainTextDetails.plainTextBody.replace(/\r/g, ""),
+ "getComposeDetails.plainTextBody from text composer returned CJK correctly as set by setComposeDetails"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ manifest: {
+ permissions: ["compose"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ // Close the HTML message.
+
+ let closePromises = [
+ // If the window is not marked as dirty, this Promise will never resolve.
+ BrowserTestUtils.promiseAlertDialog("extra1"),
+ BrowserTestUtils.domWindowClosed(htmlWindow),
+ ];
+ Assert.ok(
+ htmlWindow.ComposeCanClose(),
+ "compose window should be allowed to close"
+ );
+ htmlWindow.close();
+ await Promise.all(closePromises);
+
+ // Close the plain text message.
+
+ closePromises = [
+ // If the window is not marked as dirty, this Promise will never resolve.
+ BrowserTestUtils.promiseAlertDialog("extra1"),
+ BrowserTestUtils.domWindowClosed(plainTextWindow),
+ ];
+ Assert.ok(
+ plainTextWindow.ComposeCanClose(),
+ "compose window should be allowed to close"
+ );
+ plainTextWindow.close();
+ await Promise.all(closePromises);
+}).__skipMe = AppConstants.platform == "linux" && AppConstants.DEBUG; // Permanent failure on CI, bug 1766758.
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_details_headers.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_details_headers.js
new file mode 100644
index 0000000000..c5a60f307a
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_details_headers.js
@@ -0,0 +1,727 @@
+/* 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 account = createAccount();
+let defaultIdentity = addIdentity(account);
+let nonDefaultIdentity = addIdentity(account);
+let gRootFolder = account.incomingServer.rootFolder;
+
+gRootFolder.createSubfolder("test", null);
+let gTestFolder = gRootFolder.getChildNamed("test");
+createMessages(gTestFolder, 4);
+
+add_task(async function testHeaders() {
+ let files = {
+ "background.js": async () => {
+ async function checkWindow(expected) {
+ let state = await browser.compose.getComposeDetails(createdTab.id);
+ for (let field of [
+ "to",
+ "cc",
+ "bcc",
+ "replyTo",
+ "followupTo",
+ "newsgroups",
+ ]) {
+ if (field in expected) {
+ browser.test.assertEq(
+ expected[field].length,
+ state[field].length,
+ `${field} has the right number of values`
+ );
+ for (let i = 0; i < expected[field].length; i++) {
+ browser.test.assertEq(expected[field][i], state[field][i]);
+ }
+ } else {
+ browser.test.assertEq(0, state[field].length, `${field} is empty`);
+ }
+ }
+
+ if (expected.from) {
+ // From will always return a value, only check if explicitly requested.
+ browser.test.assertEq(expected.from, state.from, "from is correct");
+ }
+
+ if (expected.subject) {
+ browser.test.assertEq(
+ expected.subject,
+ state.subject,
+ "subject is correct"
+ );
+ } else {
+ browser.test.assertTrue(!state.subject, "subject is empty");
+ }
+
+ await window.sendMessage("checkWindow", expected);
+ }
+
+ let [account] = await browser.accounts.list();
+ let [defaultIdentity, nonDefaultIdentity] = account.identities;
+
+ let addressBook = await browser.addressBooks.create({
+ name: "Baker Street",
+ });
+ let contacts = {
+ sherlock: await browser.contacts.create(addressBook, {
+ DisplayName: "Sherlock Holmes",
+ PrimaryEmail: "sherlock@bakerstreet.invalid",
+ }),
+ john: await browser.contacts.create(addressBook, {
+ DisplayName: "John Watson",
+ PrimaryEmail: "john@bakerstreet.invalid",
+ }),
+ empty: await browser.contacts.create(addressBook, {
+ DisplayName: "Jim Moriarty",
+ PrimaryEmail: "",
+ }),
+ };
+ let list = await browser.mailingLists.create(addressBook, {
+ name: "Holmes and Watson",
+ description: "Tenants221B",
+ });
+ await browser.mailingLists.addMember(list, contacts.sherlock);
+ await browser.mailingLists.addMember(list, contacts.john);
+
+ let identityChanged = null;
+ browser.compose.onIdentityChanged.addListener((tab, identityId) => {
+ identityChanged = identityId;
+ });
+
+ // Start a new message.
+
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew();
+ let [createdWindow] = await createdWindowPromise;
+ let [createdTab] = await browser.tabs.query({
+ windowId: createdWindow.id,
+ });
+
+ await checkWindow({ identityId: defaultIdentity.id });
+
+ let tests = [
+ {
+ // Change the identity and check default from.
+ input: { identityId: nonDefaultIdentity.id },
+ expected: {
+ identityId: nonDefaultIdentity.id,
+ from: "mochitest@localhost",
+ },
+ expectIdentityChanged: nonDefaultIdentity.id,
+ },
+ {
+ // Don't change the identity.
+ input: {},
+ expected: {
+ identityId: nonDefaultIdentity.id,
+ from: "mochitest@localhost",
+ },
+ },
+ {
+ // Change the identity back again.
+ input: { identityId: defaultIdentity.id },
+ expected: {
+ identityId: defaultIdentity.id,
+ from: "mochitest@localhost",
+ },
+ expectIdentityChanged: defaultIdentity.id,
+ },
+ {
+ // Single input, string.
+ input: { to: "Greg Lestrade <greg@bakerstreet.invalid>" },
+ expected: { to: ["Greg Lestrade <greg@bakerstreet.invalid>"] },
+ },
+ {
+ // Empty string. Done here so we have something to clear.
+ input: { to: "" },
+ expected: {},
+ },
+ {
+ // Single input, array with string.
+ input: { to: ["John Watson <john@bakerstreet.invalid>"] },
+ expected: { to: ["John Watson <john@bakerstreet.invalid>"] },
+ },
+ {
+ // Name with a comma, not quoted per RFC 822. This is how
+ // getComposeDetails returns names with a comma.
+ input: { to: ["Holmes, Mycroft <mycroft@bakerstreet.invalid>"] },
+ expected: { to: ["Holmes, Mycroft <mycroft@bakerstreet.invalid>"] },
+ },
+ {
+ // Name with a comma, quoted per RFC 822. This should work too.
+ input: { to: [`"Holmes, Mycroft" <mycroft@bakerstreet.invalid>`] },
+ expected: { to: ["Holmes, Mycroft <mycroft@bakerstreet.invalid>"] },
+ },
+ {
+ // Name and address with non-ASCII characters.
+ input: { to: ["Jïm Morïarty <morïarty@bakerstreet.invalid>"] },
+ expected: { to: ["Jïm Morïarty <morïarty@bakerstreet.invalid>"] },
+ },
+ {
+ // Empty array. Done here so we have something to clear.
+ input: { to: [] },
+ expected: {},
+ },
+ {
+ // Single input, array with contact.
+ input: { to: [{ id: contacts.sherlock, type: "contact" }] },
+ expected: { to: ["Sherlock Holmes <sherlock@bakerstreet.invalid>"] },
+ },
+ {
+ // Null input. This should not clear the field.
+ input: { to: null },
+ expected: { to: ["Sherlock Holmes <sherlock@bakerstreet.invalid>"] },
+ },
+ {
+ // Single input, array with mailing list.
+ input: { to: [{ id: list, type: "mailingList" }] },
+ expected: { to: ["Holmes and Watson <Tenants221B>"] },
+ },
+ {
+ // Multiple inputs, string.
+ input: {
+ to: "Molly Hooper <molly@bakerstreet.invalid>, Mrs Hudson <mrs_hudson@bakerstreet.invalid>",
+ },
+ expected: {
+ to: [
+ "Molly Hooper <molly@bakerstreet.invalid>",
+ "Mrs Hudson <mrs_hudson@bakerstreet.invalid>",
+ ],
+ },
+ },
+ {
+ // Multiple inputs, array with strings.
+ input: {
+ to: [
+ "Irene Adler <irene@bakerstreet.invalid>",
+ "Mary Watson <mary@bakerstreet.invalid>",
+ ],
+ },
+ expected: {
+ to: [
+ "Irene Adler <irene@bakerstreet.invalid>",
+ "Mary Watson <mary@bakerstreet.invalid>",
+ ],
+ },
+ },
+ {
+ // Multiple inputs, mixed.
+ input: {
+ to: [
+ { id: contacts.sherlock, type: "contact" },
+ "Mycroft Holmes <mycroft@bakerstreet.invalid>",
+ ],
+ },
+ expected: {
+ to: [
+ "Sherlock Holmes <sherlock@bakerstreet.invalid>",
+ "Mycroft Holmes <mycroft@bakerstreet.invalid>",
+ ],
+ },
+ },
+ {
+ // A newsgroup, string.
+ input: {
+ to: "",
+ newsgroups: "invalid.fake.newsgroup",
+ },
+ expected: {
+ newsgroups: ["invalid.fake.newsgroup"],
+ },
+ },
+ {
+ // Multiple newsgroups, string.
+ input: {
+ newsgroups: "invalid.fake.newsgroup, invalid.real.newsgroup",
+ },
+ expected: {
+ newsgroups: ["invalid.fake.newsgroup", "invalid.real.newsgroup"],
+ },
+ },
+ {
+ // A newsgroup, array with string.
+ input: {
+ newsgroups: ["invalid.real.newsgroup"],
+ },
+ expected: {
+ newsgroups: ["invalid.real.newsgroup"],
+ },
+ },
+ {
+ // Multiple newsgroup, array with string.
+ input: {
+ newsgroups: ["invalid.fake.newsgroup", "invalid.real.newsgroup"],
+ },
+ expected: {
+ newsgroups: ["invalid.fake.newsgroup", "invalid.real.newsgroup"],
+ },
+ },
+ {
+ // Change the subject.
+ input: {
+ newsgroups: "",
+ subject: "This is a test",
+ },
+ expected: {
+ subject: "This is a test",
+ },
+ },
+ {
+ // Clear the subject.
+ input: {
+ subject: "",
+ },
+ expected: {},
+ },
+ {
+ // Override from with string address
+ input: { from: "Mycroft Holmes <mycroft@bakerstreet.invalid>" },
+ expected: { from: "Mycroft Holmes <mycroft@bakerstreet.invalid>" },
+ },
+ {
+ // Override from with contact id
+ input: { from: { id: contacts.sherlock, type: "contact" } },
+ expected: { from: "Sherlock Holmes <sherlock@bakerstreet.invalid>" },
+ },
+ {
+ // Override from with multiple string address
+ input: {
+ from: "Mycroft Holmes <mycroft@bakerstreet.invalid>, Mary Watson <mary@bakerstreet.invalid>",
+ },
+ expected: {
+ errorDescription:
+ "Setting from to multiple addresses should throw.",
+ errorRejected:
+ "ComposeDetails.from: Exactly one address instead of 2 is required.",
+ },
+ },
+ {
+ // Override from with empty string address 1
+ input: { from: "Mycroft Holmes <>" },
+ expected: {
+ errorDescription:
+ "Setting from to a display name without address should throw (#1).",
+ errorRejected: "ComposeDetails.from: Invalid address: ",
+ },
+ },
+ {
+ // Override from with empty string address 2
+ input: { from: "Mycroft Holmes" },
+ expected: {
+ errorDescription:
+ "Setting from to a display name without address should throw (#2).",
+ errorRejected:
+ "ComposeDetails.from: Invalid address: Mycroft Holmes",
+ },
+ },
+ {
+ // Override from with contact id with empty address
+ input: { from: { id: contacts.empty, type: "contact" } },
+ expected: {
+ errorDescription:
+ "Setting from to a contact with an empty PrimaryEmail should throw.",
+ errorRejected: `ComposeDetails.from: Contact does not have a valid email address: ${contacts.empty}`,
+ },
+ },
+ {
+ // Override from with invalid contact id
+ input: { from: { id: "1234", type: "contact" } },
+ expected: {
+ errorDescription:
+ "Setting from to a contact with an invalid contact id should throw.",
+ errorRejected:
+ "ComposeDetails.from: contact with id=1234 could not be found.",
+ },
+ },
+ {
+ // Override from with mailinglist id
+ input: { from: { id: list, type: "mailingList" } },
+ expected: {
+ errorDescription: "Setting from to a mailing list should throw.",
+ errorRejected: "ComposeDetails.from: Mailing list not allowed.",
+ },
+ },
+ {
+ // From may not be cleared.
+ input: { from: "" },
+ expected: {
+ errorDescription: "Setting from to an empty string should throw.",
+ errorRejected:
+ "ComposeDetails.from: Address must not be set to an empty string.",
+ },
+ },
+ ];
+ for (let test of tests) {
+ browser.test.log(`Checking input: ${JSON.stringify(test.input)}`);
+
+ if (test.expected.errorRejected) {
+ await browser.test.assertRejects(
+ browser.compose.setComposeDetails(createdTab.id, test.input),
+ test.expected.errorRejected,
+ test.expected.errorDescription
+ );
+ continue;
+ }
+
+ await browser.compose.setComposeDetails(createdTab.id, test.input);
+ await checkWindow(test.expected);
+
+ if (test.expectIdentityChanged) {
+ browser.test.assertEq(
+ test.expectIdentityChanged,
+ identityChanged,
+ "onIdentityChanged fired"
+ );
+ } else {
+ browser.test.assertEq(
+ null,
+ identityChanged,
+ "onIdentityChanged not fired"
+ );
+ }
+ identityChanged = null;
+ }
+
+ // Change the identity through the UI to check onIdentityChanged works.
+
+ browser.test.log("Checking external identity change");
+ await window.sendMessage("changeIdentity", nonDefaultIdentity.id);
+ browser.test.assertEq(
+ nonDefaultIdentity.id,
+ identityChanged,
+ "onIdentityChanged fired"
+ );
+
+ // Clean up.
+
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+
+ await browser.addressBooks.delete(addressBook);
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "addressBooks", "compose", "messagesRead"],
+ },
+ });
+
+ extension.onMessage("checkWindow", async expected => {
+ await checkComposeHeaders(expected);
+ extension.sendMessage();
+ });
+
+ extension.onMessage("changeIdentity", newIdentity => {
+ let composeWindows = [...Services.wm.getEnumerator("msgcompose")];
+ is(composeWindows.length, 1);
+ let composeDocument = composeWindows[0].document;
+
+ let identityList = composeDocument.getElementById("msgIdentity");
+ let identityItem = identityList.querySelector(
+ `[identitykey="${newIdentity}"]`
+ );
+ ok(identityItem);
+ identityList.selectedItem = identityItem;
+ composeWindows[0].LoadIdentity(false);
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_onIdentityChanged_MV3_event_pages() {
+ let files = {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, the eventCounter is reset and
+ // allows to observe the order of events fired. In case of a wake-up, the
+ // first observed event is the one that woke up the background.
+ let eventCounter = 0;
+
+ browser.compose.onIdentityChanged.addListener(async (tab, identityId) => {
+ browser.test.sendMessage("identity changed", {
+ eventCount: ++eventCounter,
+ identityId,
+ });
+ });
+
+ browser.compose.onComposeStateChanged.addListener(async (tab, state) => {
+ browser.test.sendMessage("compose state changed", {
+ eventCount: ++eventCounter,
+ state,
+ });
+ });
+
+ browser.test.sendMessage("background started");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ manifest_version: 3,
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "addressBooks", "compose", "messagesRead"],
+ browser_specific_settings: { gecko: { id: "compose@mochi.test" } },
+ },
+ });
+
+ function changeIdentity(newIdentity) {
+ let composeDocument = composeWindow.document;
+
+ let identityList = composeDocument.getElementById("msgIdentity");
+ let identityItem = identityList.querySelector(
+ `[identitykey="${newIdentity}"]`
+ );
+ ok(identityItem);
+ identityList.selectedItem = identityItem;
+ composeWindow.LoadIdentity(false);
+ }
+
+ function setToAddr(to) {
+ composeWindow.SetComposeDetails({ to });
+ }
+
+ function checkPersistentListeners({ primed }) {
+ // A persistent event is referenced by its moduleName as defined in
+ // ext-mails.json, not by its actual namespace.
+ const persistent_events = [
+ "compose.onIdentityChanged",
+ "compose.onComposeStateChanged",
+ ];
+
+ for (let event of persistent_events) {
+ let [moduleName, eventName] = event.split(".");
+ assertPersistentListeners(extension, moduleName, eventName, {
+ primed,
+ });
+ }
+ }
+
+ let composeWindow = await openComposeWindow(account);
+ await focusWindow(composeWindow);
+
+ await extension.startup();
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+
+ // Trigger events without terminating the background first.
+
+ changeIdentity(nonDefaultIdentity.key);
+ {
+ let rv = await extension.awaitMessage("identity changed");
+ Assert.deepEqual(
+ {
+ eventCount: 1,
+ identityId: nonDefaultIdentity.key,
+ },
+ rv,
+ "The non-primed onIdentityChanged event should return the correct values"
+ );
+ }
+
+ setToAddr("user@invalid.net");
+ {
+ let rv = await extension.awaitMessage("compose state changed");
+ Assert.deepEqual(
+ {
+ eventCount: 2,
+ state: {
+ canSendNow: true,
+ canSendLater: true,
+ },
+ },
+ rv,
+ "The non-primed onComposeStateChanged should return the correct values"
+ );
+ }
+
+ // Terminate background and re-trigger onIdentityChanged event.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // The listeners should be primed.
+ checkPersistentListeners({ primed: true });
+
+ changeIdentity(defaultIdentity.key);
+ {
+ let rv = await extension.awaitMessage("identity changed");
+ Assert.deepEqual(
+ {
+ eventCount: 1,
+ identityId: defaultIdentity.key,
+ },
+ rv,
+ "The primed onIdentityChanged event should return the correct values"
+ );
+ }
+
+ // The background should have been restarted.
+ await extension.awaitMessage("background started");
+ // The listeners should no longer be primed.
+ checkPersistentListeners({ primed: false });
+
+ // Terminate background and re-trigger onComposeStateChanged event.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // The listeners should be primed.
+ checkPersistentListeners({ primed: true });
+
+ setToAddr("invalid");
+ {
+ let rv = await extension.awaitMessage("compose state changed");
+ Assert.deepEqual(
+ {
+ eventCount: 1,
+ state: {
+ canSendNow: false,
+ canSendLater: false,
+ },
+ },
+ rv,
+ "The primed onComposeStateChanged should return the correct values"
+ );
+ }
+
+ // The background should have been restarted.
+ await extension.awaitMessage("background started");
+ // The listeners should no longer be primed.
+ checkPersistentListeners({ primed: false });
+
+ await extension.unload();
+ composeWindow.close();
+});
+
+add_task(async function testCustomHeaders() {
+ let files = {
+ "background.js": async () => {
+ async function checkCustomHeaders(tab, expectedCustomHeaders) {
+ let [testHeader] = await window.sendMessage("getTestHeader");
+ browser.test.assertEq(
+ "CannotTouchThis",
+ testHeader,
+ "Should include the test header."
+ );
+
+ let details = await browser.compose.getComposeDetails(tab.id);
+
+ browser.test.assertEq(
+ expectedCustomHeaders.length,
+ details.customHeaders.length,
+ "Should have the correct number of custom headers"
+ );
+ for (let i = 0; i < expectedCustomHeaders.length; i++) {
+ browser.test.assertEq(
+ expectedCustomHeaders[i].name,
+ details.customHeaders[i].name,
+ "Should have the correct header name"
+ );
+ browser.test.assertEq(
+ expectedCustomHeaders[i].value,
+ details.customHeaders[i].value,
+ "Should have the correct header value"
+ );
+ }
+ }
+
+ // Start a new message with custom headers.
+ let customHeaders = [{ name: "X-TEST1", value: "some header" }];
+ let tab = await browser.compose.beginNew(null, { customHeaders });
+
+ // Add a header which does not start with X- and should not be touched by
+ // the API.
+ await window.sendMessage("addTestHeader");
+
+ let expectedHeaders = [{ name: "X-Test1", value: "some header" }];
+ await checkCustomHeaders(tab, expectedHeaders);
+
+ // Update details without changing headers.
+ await browser.compose.setComposeDetails(tab.id, {});
+ await checkCustomHeaders(tab, expectedHeaders);
+
+ // Update existing header and add a new one.
+ customHeaders = [
+ { name: "X-TEST1", value: "this is header #1" },
+ { name: "X-TEST2", value: "this is header #2" },
+ { name: "X-TEST3", value: "this is header #3" },
+ { name: "X-TEST4", value: "this is header #4" },
+ ];
+ await browser.compose.setComposeDetails(tab.id, { customHeaders });
+ expectedHeaders = [
+ { name: "X-Test1", value: "this is header #1" },
+ { name: "X-Test2", value: "this is header #2" },
+ { name: "X-Test3", value: "this is header #3" },
+ { name: "X-Test4", value: "this is header #4" },
+ ];
+ await checkCustomHeaders(tab, expectedHeaders);
+
+ // Update existing header and remove some of the others. Test support for
+ // empty headers.
+ customHeaders = [
+ { name: "X-TEST2", value: "this is a header" },
+ { name: "X-TEST3", value: "" },
+ ];
+ await browser.compose.setComposeDetails(tab.id, { customHeaders });
+ expectedHeaders = [
+ { name: "X-Test2", value: "this is a header" },
+ { name: "X-Test3", value: "" },
+ ];
+ await checkCustomHeaders(tab, expectedHeaders);
+
+ // Clear headers.
+ customHeaders = [];
+ await browser.compose.setComposeDetails(tab.id, { customHeaders });
+ await checkCustomHeaders(tab, []);
+
+ // Should throw for invalid custom headers.
+ customHeaders = [
+ { name: "TEST2", value: "this is an invalid custom header" },
+ ];
+ await browser.test.assertThrows(
+ () => browser.compose.setComposeDetails(tab.id, { customHeaders }),
+ 'Type error for parameter details (Error processing customHeaders.0.name: String "TEST2" must match /^X-.*$/) for compose.setComposeDetails.',
+ "Should throw for invalid custom headers"
+ );
+
+ // Clean up.
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(tab.windowId);
+ await removedWindowPromise;
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "addressBooks", "compose", "messagesRead"],
+ },
+ });
+
+ extension.onMessage("addTestHeader", () => {
+ let composeWindow = Services.wm.getMostRecentWindow("msgcompose");
+ composeWindow.gMsgCompose.compFields.setHeader(
+ "ATestHeader",
+ "CannotTouchThis"
+ );
+ extension.sendMessage();
+ });
+
+ extension.onMessage("getTestHeader", () => {
+ let composeWindow = Services.wm.getMostRecentWindow("msgcompose");
+ let value = composeWindow.gMsgCompose.compFields.getHeader("ATestHeader");
+ extension.sendMessage(value);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_dictionaries.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_dictionaries.js
new file mode 100644
index 0000000000..e77e5f47bf
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_dictionaries.js
@@ -0,0 +1,214 @@
+/* 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 account = createAccount();
+let defaultIdentity = addIdentity(account);
+
+add_task(async function test_dictionaries() {
+ let files = {
+ "background.js": async () => {
+ function verifyDictionaries(dictionaries, expected) {
+ browser.test.assertEq(
+ Object.values(expected).length,
+ Object.values(dictionaries).length,
+ "Should find the correct number of installed dictionaries"
+ );
+ browser.test.assertEq(
+ Object.values(expected).filter(active => active).length,
+ Object.values(dictionaries).filter(active => active).length,
+ "Should find the correct number of active dictionaries"
+ );
+ for (let i = 0; i < expected.length; i++) {
+ browser.test.assertEq(
+ Object.keys(expected)[i],
+ Object.keys(dictionaries)[i],
+ "Should find the correct dictionary"
+ );
+ }
+ }
+ async function setDictionaries(newActiveDictionaries, expected) {
+ let changes = new Promise(resolve => {
+ let listener = (tab, dictionaries) => {
+ browser.compose.onActiveDictionariesChanged.removeListener(
+ listener
+ );
+ resolve({ tab, dictionaries });
+ };
+ browser.compose.onActiveDictionariesChanged.addListener(listener);
+ });
+
+ await browser.compose.setActiveDictionaries(
+ createdTab.id,
+ newActiveDictionaries
+ );
+ let eventData = await changes;
+ verifyDictionaries(expected.dictionaries, eventData.dictionaries);
+
+ browser.test.assertEq(
+ expected.tab.id,
+ eventData.tab.id,
+ "Should find the correct tab"
+ );
+
+ let dictionaries = await browser.compose.getActiveDictionaries(
+ createdTab.id
+ );
+ verifyDictionaries(expected.dictionaries, dictionaries);
+ }
+
+ // Start a new message.
+
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew();
+ let [createdWindow] = await createdWindowPromise;
+ let [createdTab] = await browser.tabs.query({
+ windowId: createdWindow.id,
+ });
+
+ await browser.test.assertRejects(
+ browser.compose.setActiveDictionaries(createdTab.id, ["invalid"]),
+ `Dictionary not found: invalid`,
+ "should reject for invalid dictionaries"
+ );
+
+ await setDictionaries([], {
+ dictionaries: { "en-US": false },
+ tab: createdTab,
+ });
+ await setDictionaries(["en-US"], {
+ dictionaries: { "en-US": true },
+ tab: createdTab,
+ });
+
+ // Clean up.
+
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_onActiveDictionariesChanged_MV3_event_pages() {
+ let files = {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, hasFired is set to false. In
+ // case of a wake-up, the first fired event is the one that woke up the background.
+ let hasFired = false;
+
+ browser.compose.onActiveDictionariesChanged.addListener(
+ async (tab, dictionaries) => {
+ // Only send the first event after background wake-up, this should be
+ // the only one expected.
+ if (!hasFired) {
+ hasFired = true;
+ browser.test.sendMessage(
+ "onActiveDictionariesChanged received",
+ dictionaries
+ );
+ }
+ }
+ );
+
+ browser.test.sendMessage("background started");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ manifest_version: 3,
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ browser_specific_settings: {
+ gecko: { id: "compose.dictionary@xpcshell.test" },
+ },
+ },
+ });
+
+ function checkPersistentListeners({ primed }) {
+ // A persistent event is referenced by its moduleName as defined in
+ // ext-mails.json, not by its actual namespace.
+ const persistent_events = ["compose.onActiveDictionariesChanged"];
+
+ for (let event of persistent_events) {
+ let [moduleName, eventName] = event.split(".");
+ assertPersistentListeners(extension, moduleName, eventName, {
+ primed,
+ });
+ }
+ }
+
+ async function setActiveDictionaries(activeDictionaries) {
+ let installedDictionaries = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine)
+ .getDictionaryList();
+
+ for (let dict of activeDictionaries) {
+ if (!installedDictionaries.includes(dict)) {
+ throw new Error(`Dictionary not found: ${dict}`);
+ }
+ }
+
+ await composeWindow.ComposeChangeLanguage(activeDictionaries);
+ }
+
+ let composeWindow = await openComposeWindow(account);
+ await focusWindow(composeWindow);
+
+ await extension.startup();
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+
+ // Trigger onActiveDictionariesChanged without terminating the background first.
+
+ setActiveDictionaries(["en-US"]);
+ let newActiveDictionary1 = await extension.awaitMessage(
+ "onActiveDictionariesChanged received"
+ );
+ Assert.equal(
+ newActiveDictionary1["en-US"],
+ true,
+ "Returned active dictionary should be correct"
+ );
+
+ // Terminate background and re-trigger onActiveDictionariesChanged.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // The listeners should be primed.
+ checkPersistentListeners({ primed: true });
+
+ setActiveDictionaries([]);
+ let newActiveDictionary2 = await extension.awaitMessage(
+ "onActiveDictionariesChanged received"
+ );
+ Assert.equal(
+ newActiveDictionary2["en-US"],
+ false,
+ "Returned active dictionary should be correct"
+ );
+
+ // The background should have been restarted.
+ await extension.awaitMessage("background started");
+ // The listener should no longer be primed.
+ checkPersistentListeners({ primed: false });
+
+ await extension.unload();
+ composeWindow.close();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_onBeforeSend.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_onBeforeSend.js
new file mode 100644
index 0000000000..285c4df33f
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_onBeforeSend.js
@@ -0,0 +1,1010 @@
+/* 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 { ExtensionSupport } = ChromeUtils.import(
+ "resource:///modules/ExtensionSupport.jsm"
+);
+
+let account = createAccount();
+let defaultIdentity = addIdentity(account);
+let nonDefaultIdentity = addIdentity(account, "nondefault@invalid");
+
+// A local outbox is needed so we can use "send later".
+let localAccount = createAccount("local");
+let outbox = localAccount.incomingServer.rootFolder.getChildNamed("outbox");
+
+function messagesInOutbox(count) {
+ info(`Checking for ${count} messages in outbox`);
+
+ count -= [...outbox.messages].length;
+ if (count <= 0) {
+ return Promise.resolve();
+ }
+
+ info(`Waiting for ${count} messages in outbox`);
+ return new Promise(resolve => {
+ MailServices.mfn.addListener(
+ {
+ msgAdded(msgHdr) {
+ if (--count == 0) {
+ MailServices.mfn.removeListener(this);
+ resolve();
+ }
+ },
+ },
+ MailServices.mfn.msgAdded
+ );
+ });
+}
+
+add_task(async function testCancel() {
+ let files = {
+ "background.js": async () => {
+ async function beginSend(sendExpected, lockExpected) {
+ await window.sendMessage("beginSend");
+ return checkIfSent(sendExpected, lockExpected);
+ }
+
+ function checkIfSent(sendExpected, lockExpected = null) {
+ return window.sendMessage("checkIfSent", sendExpected, lockExpected);
+ }
+
+ function checkWindow(expected) {
+ return window.sendMessage("checkWindow", expected);
+ }
+
+ // Open a compose window with a message. The message will never send
+ // because we removed the sending function, so we can attempt to send
+ // it over and over.
+
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew({
+ to: ["test@test.invalid"],
+ subject: "Test",
+ });
+ let [createdWindow] = await createdWindowPromise;
+ browser.test.assertEq("messageCompose", createdWindow.type);
+
+ await checkWindow({ to: ["test@test.invalid"], subject: "Test" });
+
+ let [tab] = await browser.tabs.query({ windowId: createdWindow.id });
+
+ // Send the message. No listeners exist, so sending should continue.
+
+ await beginSend(true);
+
+ // Add a non-cancelling listener. Sending should continue.
+
+ let listener1 = tab => {
+ listener1.tab = tab;
+ return {};
+ };
+ browser.compose.onBeforeSend.addListener(listener1);
+ await beginSend(true);
+ browser.test.assertEq(tab.id, listener1.tab.id, "listener1 was fired");
+ browser.compose.onBeforeSend.removeListener(listener1);
+ delete listener1.tab;
+
+ // Add a cancelling listener. Sending should not continue.
+
+ let listener2 = tab => {
+ listener2.tab = tab;
+ return { cancel: true };
+ };
+ browser.compose.onBeforeSend.addListener(listener2);
+ await beginSend(false, false);
+ browser.test.assertEq(tab.id, listener2.tab.id, "listener2 was fired");
+ browser.compose.onBeforeSend.removeListener(listener2);
+ delete listener2.tab;
+ await beginSend(true); // Removing the listener worked.
+
+ // Add a listener returning a Promise. Resolve the Promise to unblock.
+ // Sending should continue.
+
+ let listener3 = tab => {
+ listener3.tab = tab;
+ return new Promise(resolve => {
+ listener3.resolve = resolve;
+ });
+ };
+ browser.compose.onBeforeSend.addListener(listener3);
+ await beginSend(false, true);
+ browser.test.assertEq(tab.id, listener3.tab.id, "listener3 was fired");
+ listener3.resolve({ cancel: false });
+ await checkIfSent(true);
+ browser.compose.onBeforeSend.removeListener(listener3);
+ delete listener3.tab;
+
+ // Add a listener returning a Promise. Resolve the Promise to cancel.
+ // Sending should not continue.
+
+ let listener4 = tab => {
+ listener4.tab = tab;
+ return new Promise(resolve => {
+ listener4.resolve = resolve;
+ });
+ };
+ browser.compose.onBeforeSend.addListener(listener4);
+ await beginSend(false, true);
+ browser.test.assertEq(tab.id, listener4.tab.id, "listener4 was fired");
+ listener4.resolve({ cancel: true });
+ await checkIfSent(false, false);
+ browser.compose.onBeforeSend.removeListener(listener4);
+ delete listener4.tab;
+ await beginSend(true); // Removing the listener worked.
+
+ // Clean up.
+
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+
+ browser.test.assertTrue(
+ !listener1.tab,
+ "listener1 was not fired after removal"
+ );
+ browser.test.assertTrue(
+ !listener2.tab,
+ "listener2 was not fired after removal"
+ );
+ browser.test.assertTrue(
+ !listener3.tab,
+ "listener3 was not fired after removal"
+ );
+ browser.test.assertTrue(
+ !listener4.tab,
+ "listener4 was not fired after removal"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ },
+ });
+
+ // We can't allow sending to actually happen, this is a test. For every
+ // compose window that opens, replace the function which does the actual
+ // sending with one that only records when it has been called.
+ let didTryToSendMessage = false;
+ let windowListenerRemoved = false;
+ ExtensionSupport.registerWindowListener("mochitest", {
+ chromeURLs: [
+ "chrome://messenger/content/messengercompose/messengercompose.xhtml",
+ ],
+ onLoadWindow(window) {
+ window.CompleteGenericSendMessage = function (msgType) {
+ didTryToSendMessage = true;
+ Services.obs.notifyObservers(
+ {
+ composeWindow: window,
+ },
+ "mail:composeSendProgressStop"
+ );
+ };
+ },
+ });
+ registerCleanupFunction(() => {
+ if (!windowListenerRemoved) {
+ ExtensionSupport.unregisterWindowListener("mochitest");
+ }
+ });
+
+ extension.onMessage("beginSend", async () => {
+ let composeWindows = [...Services.wm.getEnumerator("msgcompose")];
+ is(composeWindows.length, 1);
+
+ composeWindows[0]
+ .GenericSendMessage(Ci.nsIMsgCompDeliverMode.Now)
+ .catch(() => {
+ // This test is ignoring errors thrown by GenericSendMessage, but looks
+ // at didTryToSendMessage of the mocked CompleteGenericSendMessage to
+ // check if onBeforeSend aborted the send process.
+ });
+ extension.sendMessage();
+ });
+
+ extension.onMessage("checkIfSent", async (sendExpected, lockExpected) => {
+ // Wait a moment to see if send happens asynchronously.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 500));
+ is(didTryToSendMessage, sendExpected, "did try to send a message");
+
+ if (lockExpected !== null) {
+ let composeWindows = [...Services.wm.getEnumerator("msgcompose")];
+ is(composeWindows.length, 1);
+ is(composeWindows[0].gWindowLocked, lockExpected, "window is locked");
+ }
+
+ didTryToSendMessage = false;
+ extension.sendMessage();
+ });
+
+ extension.onMessage("checkWindow", async expected => {
+ await checkComposeHeaders(expected);
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ ExtensionSupport.unregisterWindowListener("mochitest");
+ windowListenerRemoved = true;
+});
+
+add_task(async function testChangeDetails() {
+ let files = {
+ "background.js": async () => {
+ function beginSend() {
+ return window.sendMessage("beginSend");
+ }
+
+ function checkWindow(expected) {
+ return window.sendMessage("checkWindow", expected);
+ }
+
+ let accounts = await browser.accounts.list();
+ // If this test is run alone, the order of accounts is different compared
+ // to running all tests. We need the account with the 2 added identities.
+ let account = accounts.find(a => a.identities.length == 2);
+ let [defaultIdentity, nonDefaultIdentity] = account.identities;
+
+ // Add a listener that changes the headers and body. Sending should
+ // continue and the headers should change. This is largely the same code
+ // as tested in browser_ext_compose_details.js, so just test that the
+ // changes happen.
+
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew({
+ to: ["test@test.invalid"],
+ subject: "Test",
+ body: "Original body.",
+ });
+ let [createdWindow] = await createdWindowPromise;
+ browser.test.assertEq("messageCompose", createdWindow.type);
+
+ await checkWindow({
+ to: ["test@test.invalid"],
+ subject: "Test",
+ body: "Original body.",
+ });
+
+ let [tab] = await browser.tabs.query({ windowId: createdWindow.id });
+
+ let listener5 = (tab, details) => {
+ listener5.tab = tab;
+ listener5.details = details;
+ return {
+ details: {
+ identityId: nonDefaultIdentity.id,
+ to: ["to@test5.invalid"],
+ cc: ["cc@test5.invalid"],
+ subject: "Changed by listener5",
+ body: "New body from listener5.",
+ },
+ };
+ };
+ browser.compose.onBeforeSend.addListener(listener5);
+ await beginSend();
+ browser.test.assertEq(tab.id, listener5.tab.id, "listener5 was fired");
+ browser.test.assertEq(defaultIdentity.id, listener5.details.identityId);
+ browser.test.assertEq(1, listener5.details.to.length);
+ browser.test.assertEq(
+ "test@test.invalid",
+ listener5.details.to[0],
+ "listener5 recipient correct"
+ );
+ browser.test.assertEq(
+ "Test",
+ listener5.details.subject,
+ "listener5 subject correct"
+ );
+ browser.compose.onBeforeSend.removeListener(listener5);
+ delete listener5.tab;
+
+ // Do the same thing, but this time with a Promise.
+
+ createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew({
+ to: ["test@test.invalid"],
+ subject: "Test",
+ body: "Original body.",
+ });
+ [createdWindow] = await createdWindowPromise;
+ browser.test.assertEq("messageCompose", createdWindow.type);
+
+ await checkWindow({
+ to: ["test@test.invalid"],
+ subject: "Test",
+ body: "Original body.",
+ });
+
+ [tab] = await browser.tabs.query({ windowId: createdWindow.id });
+
+ let listener6 = (tab, details) => {
+ listener6.tab = tab;
+ listener6.details = details;
+ return new Promise(resolve => {
+ listener6.resolve = resolve;
+ });
+ };
+ browser.compose.onBeforeSend.addListener(listener6);
+ await beginSend();
+ browser.test.assertEq(tab.id, listener6.tab.id, "listener6 was fired");
+ browser.test.assertEq(defaultIdentity.id, listener6.details.identityId);
+ browser.test.assertEq(1, listener6.details.to.length);
+ browser.test.assertEq(
+ "test@test.invalid",
+ listener6.details.to[0],
+ "listener6 recipient correct"
+ );
+ browser.test.assertEq(
+ "Test",
+ listener6.details.subject,
+ "listener6 subject correct"
+ );
+ listener6.resolve({
+ details: {
+ identityId: nonDefaultIdentity.id,
+ to: ["to@test6.invalid"],
+ cc: ["cc@test6.invalid"],
+ subject: "Changed by listener6",
+ body: "New body from listener6.",
+ },
+ });
+ browser.compose.onBeforeSend.removeListener(listener6);
+ delete listener6.tab;
+
+ browser.test.assertTrue(
+ !listener5.tab,
+ "listener5 was not fired after removal"
+ );
+ browser.test.assertTrue(
+ !listener6.tab,
+ "listener6 was not fired after removal"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "compose"],
+ },
+ });
+
+ extension.onMessage("beginSend", async () => {
+ let composeWindows = [...Services.wm.getEnumerator("msgcompose")];
+ is(composeWindows.length, 1);
+
+ composeWindows[0]
+ .GenericSendMessage(Ci.nsIMsgCompDeliverMode.Later)
+ .catch(() => {
+ // This test is ignoring errors thrown by GenericSendMessage, but looks
+ // at didTryToSendMessage of the mocked CompleteGenericSendMessage to
+ // check if onBeforeSend aborted the send process.
+ });
+ extension.sendMessage();
+ });
+
+ extension.onMessage("checkWindow", async expected => {
+ await checkComposeHeaders(expected);
+
+ let composeWindow = Services.wm.getMostRecentWindow("msgcompose");
+ let body = composeWindow
+ .GetCurrentEditor()
+ .outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw);
+ is(body, expected.body);
+
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ await messagesInOutbox(2);
+
+ let outboxMessages = [...outbox.messages];
+ ok(outboxMessages.length > 0);
+ let sentMessage5 = outboxMessages.shift();
+ is(sentMessage5.author, "nondefault@invalid", "author was changed");
+ is(sentMessage5.subject, "Changed by listener5", "subject was changed");
+ is(sentMessage5.recipients, "to@test5.invalid", "to was changed");
+ is(sentMessage5.ccList, "cc@test5.invalid", "cc was changed");
+
+ await new Promise(resolve => {
+ window.MsgHdrToMimeMessage(sentMessage5, null, (msgHdr, mimeMessage) => {
+ is(
+ // Fold Windows line-endings \r\n to \n.
+ mimeMessage.parts[0].body.replace(/\r/g, ""),
+ "New body from listener5.\n"
+ );
+ resolve();
+ });
+ });
+
+ ok(outboxMessages.length > 0);
+ let sentMessage6 = outboxMessages.shift();
+ is(sentMessage6.author, "nondefault@invalid", "author was changed");
+ is(sentMessage6.subject, "Changed by listener6", "subject was changed");
+ is(sentMessage6.recipients, "to@test6.invalid", "to was changed");
+ is(sentMessage6.ccList, "cc@test6.invalid", "cc was changed");
+
+ await new Promise(resolve => {
+ window.MsgHdrToMimeMessage(sentMessage6, null, (msgHdr, mimeMessage) => {
+ is(
+ // Fold Windows line-endings \r\n to \n.
+ mimeMessage.parts[0].body.replace(/\r/g, ""),
+ "New body from listener6.\n"
+ );
+ resolve();
+ });
+ });
+
+ ok(outboxMessages.length == 0);
+
+ await new Promise(resolve => {
+ outbox.deleteMessages(
+ [sentMessage5, sentMessage6],
+ null,
+ true,
+ false,
+ { OnStopCopy: resolve },
+ false
+ );
+ });
+});
+
+add_task(async function testChangeAttachments() {
+ let files = {
+ "background.js": async () => {
+ // Add a listener that changes attachments. Sending should continue and
+ // the attachments should change.
+
+ let tab = await browser.compose.beginNew({
+ to: ["test@test.invalid"],
+ subject: "Test",
+ body: "Original body.",
+ attachments: [
+ { file: new File(["remove"], "remove.txt") },
+ { file: new File(["change"], "change.txt") },
+ ],
+ });
+
+ let listener12 = async (tab, details) => {
+ let attachments = await browser.compose.listAttachments(tab.id);
+ browser.test.assertEq("remove.txt", attachments[0].name);
+ browser.test.assertEq("change.txt", attachments[1].name);
+
+ await browser.compose.removeAttachment(tab.id, attachments[0].id);
+ await browser.compose.updateAttachment(tab.id, attachments[1].id, {
+ name: "changed.txt",
+ });
+ await browser.compose.addAttachment(tab.id, {
+ file: new File(["added"], "added.txt"),
+ });
+
+ attachments = await browser.compose.listAttachments(tab.id);
+ browser.test.assertEq("changed.txt", attachments[0].name);
+ browser.test.assertEq("added.txt", attachments[1].name);
+
+ listener12.tab = tab;
+ };
+ browser.compose.onBeforeSend.addListener(listener12);
+
+ await window.sendMessage("beginSend");
+ browser.test.assertEq(tab.id, listener12.tab.id, "listener12 completed");
+ browser.compose.onBeforeSend.removeListener(listener12);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "compose"],
+ },
+ });
+
+ extension.onMessage("beginSend", async () => {
+ let composeWindows = [...Services.wm.getEnumerator("msgcompose")];
+ is(composeWindows.length, 1);
+
+ let sendPromise = BrowserTestUtils.waitForEvent(
+ composeWindows[0],
+ "aftersend"
+ );
+ composeWindows[0]
+ .GenericSendMessage(Ci.nsIMsgCompDeliverMode.Later)
+ .catch(() => {
+ // This test is ignoring errors thrown by GenericSendMessage, but looks
+ // at didTryToSendMessage of the mocked CompleteGenericSendMessage to
+ // check if onBeforeSend aborted the send process.
+ });
+ await sendPromise;
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ await messagesInOutbox(1);
+
+ let outboxMessages = [...outbox.messages];
+ ok(outboxMessages.length > 0);
+ let sentMessage12 = outboxMessages.shift();
+
+ await new Promise(resolve => {
+ window.MsgHdrToMimeMessage(sentMessage12, null, (msgHdr, mimeMessage) => {
+ Assert.equal(mimeMessage.parts.length, 1);
+ Assert.equal(mimeMessage.parts[0].parts.length, 3);
+ Assert.equal(mimeMessage.parts[0].parts[1].name, "changed.txt");
+ Assert.equal(mimeMessage.parts[0].parts[2].name, "added.txt");
+ resolve();
+ });
+ });
+
+ ok(outboxMessages.length == 0);
+
+ await new Promise(resolve => {
+ outbox.deleteMessages(
+ [sentMessage12],
+ null,
+ true,
+ false,
+ { OnStopCopy: resolve },
+ false
+ );
+ });
+});
+
+add_task(async function testListExpansion() {
+ let files = {
+ "background.js": async () => {
+ function beginSend() {
+ return window.sendMessage("beginSend");
+ }
+
+ function checkWindow(expected) {
+ return window.sendMessage("checkWindow", expected);
+ }
+
+ let addressBook = await browser.addressBooks.create({
+ name: "Baker Street",
+ });
+ let contacts = {
+ sherlock: await browser.contacts.create(addressBook, {
+ DisplayName: "Sherlock Holmes",
+ PrimaryEmail: "sherlock@bakerstreet.invalid",
+ }),
+ john: await browser.contacts.create(addressBook, {
+ DisplayName: "John Watson",
+ PrimaryEmail: "john@bakerstreet.invalid",
+ }),
+ };
+ let list = await browser.mailingLists.create(addressBook, {
+ name: "Holmes and Watson",
+ description: "Tenants221B",
+ });
+ await browser.mailingLists.addMember(list, contacts.sherlock);
+ await browser.mailingLists.addMember(list, contacts.john);
+
+ // Add a listener that changes the headers. Sending should continue and
+ // the headers should change. The mailing list should be expanded in both
+ // the To: and Bcc: headers.
+
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew({
+ to: [{ id: list, type: "mailingList" }],
+ subject: "Test",
+ });
+ let [createdWindow] = await createdWindowPromise;
+ browser.test.assertEq("messageCompose", createdWindow.type);
+
+ await checkWindow({
+ to: ["Holmes and Watson <Tenants221B>"],
+ subject: "Test",
+ });
+
+ let [tab] = await browser.tabs.query({ windowId: createdWindow.id });
+
+ let listener7 = (tab, details) => {
+ listener7.tab = tab;
+ listener7.details = details;
+ return {
+ details: {
+ bcc: details.to,
+ subject: "Changed by listener7",
+ },
+ };
+ };
+ browser.compose.onBeforeSend.addListener(listener7);
+ await beginSend();
+ browser.test.assertEq(tab.id, listener7.tab.id, "listener7 was fired");
+ browser.test.assertEq(1, listener7.details.to.length);
+ browser.test.assertEq(
+ "Holmes and Watson <Tenants221B>",
+ listener7.details.to[0],
+ "listener7 recipient correct"
+ );
+ browser.test.assertEq(
+ "Test",
+ listener7.details.subject,
+ "listener7 subject correct"
+ );
+ browser.compose.onBeforeSend.removeListener(listener7);
+
+ // Return nothing from the listener. The mailing list should be expanded.
+
+ createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew({
+ to: [{ id: list, type: "mailingList" }],
+ subject: "Test",
+ });
+ [createdWindow] = await createdWindowPromise;
+ browser.test.assertEq("messageCompose", createdWindow.type);
+
+ await checkWindow({
+ to: ["Holmes and Watson <Tenants221B>"],
+ subject: "Test",
+ });
+
+ [tab] = await browser.tabs.query({ windowId: createdWindow.id });
+
+ let listener8 = (tab, details) => {
+ listener8.tab = tab;
+ listener8.details = details;
+ };
+ browser.compose.onBeforeSend.addListener(listener8);
+ await beginSend();
+ browser.test.assertEq(tab.id, listener8.tab.id, "listener8 was fired");
+ browser.test.assertEq(1, listener8.details.to.length);
+ browser.test.assertEq(
+ "Holmes and Watson <Tenants221B>",
+ listener8.details.to[0],
+ "listener8 recipient correct"
+ );
+ browser.test.assertEq(
+ "Test",
+ listener8.details.subject,
+ "listener8 subject correct"
+ );
+ browser.compose.onBeforeSend.removeListener(listener8);
+
+ await browser.addressBooks.delete(addressBook);
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["addressBooks", "compose"],
+ },
+ });
+
+ extension.onMessage("beginSend", async () => {
+ let composeWindows = [...Services.wm.getEnumerator("msgcompose")];
+ is(composeWindows.length, 1);
+
+ composeWindows[0]
+ .GenericSendMessage(Ci.nsIMsgCompDeliverMode.Later)
+ .catch(() => {
+ // This test is ignoring errors thrown by GenericSendMessage, but looks
+ // at didTryToSendMessage of the mocked CompleteGenericSendMessage to
+ // check if onBeforeSend aborted the send process.
+ });
+ extension.sendMessage();
+ });
+
+ extension.onMessage("checkWindow", async expected => {
+ await checkComposeHeaders(expected);
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ await messagesInOutbox(2);
+
+ let outboxMessages = [...outbox.messages];
+ ok(outboxMessages.length > 0);
+ let sentMessage7 = outboxMessages.shift();
+ is(sentMessage7.subject, "Changed by listener7", "subject was changed");
+ is(
+ sentMessage7.recipients,
+ "Sherlock Holmes <sherlock@bakerstreet.invalid>, John Watson <john@bakerstreet.invalid>",
+ "list in unchanged field was expanded"
+ );
+ is(
+ sentMessage7.bccList,
+ "Sherlock Holmes <sherlock@bakerstreet.invalid>, John Watson <john@bakerstreet.invalid>",
+ "list in changed field was expanded"
+ );
+
+ ok(outboxMessages.length > 0);
+ let sentMessage8 = outboxMessages.shift();
+ is(sentMessage8.subject, "Test", "subject was not changed");
+ is(
+ sentMessage8.recipients,
+ "Sherlock Holmes <sherlock@bakerstreet.invalid>, John Watson <john@bakerstreet.invalid>",
+ "list in unchanged field was expanded"
+ );
+
+ ok(outboxMessages.length == 0);
+
+ await new Promise(resolve => {
+ outbox.deleteMessages(
+ [sentMessage7, sentMessage8],
+ null,
+ true,
+ false,
+ { OnStopCopy: resolve },
+ false
+ );
+ });
+});
+
+add_task(async function testMultipleListeners() {
+ let extensionA = ExtensionTestUtils.loadExtension({
+ background: async () => {
+ let listener9 = (tab, details) => {
+ browser.test.log("listener9 was fired");
+ browser.test.sendMessage("listener9", details);
+ browser.compose.onBeforeSend.removeListener(listener9);
+ return {
+ details: {
+ to: ["recipient2@invalid"],
+ subject: "Changed by listener9",
+ },
+ };
+ };
+ browser.compose.onBeforeSend.addListener(listener9);
+
+ await browser.compose.beginNew({
+ to: "recipient1@invalid",
+ subject: "Initial subject",
+ });
+ browser.test.sendMessage("ready");
+ },
+ manifest: { permissions: ["compose"] },
+ });
+
+ let extensionB = ExtensionTestUtils.loadExtension({
+ background: async () => {
+ let listener10 = (tab, details) => {
+ browser.test.log("listener10 was fired");
+ browser.test.sendMessage("listener10", details);
+ browser.compose.onBeforeSend.removeListener(listener10);
+ return {
+ details: {
+ to: ["recipient3@invalid"],
+ subject: "Changed by listener10",
+ },
+ };
+ };
+ browser.compose.onBeforeSend.addListener(listener10);
+
+ let listener11 = (tab, details) => {
+ browser.test.log("listener11 was fired");
+ browser.test.sendMessage("listener11", details);
+ browser.compose.onBeforeSend.removeListener(listener11);
+ return {
+ details: {
+ to: ["recipient4@invalid"],
+ subject: "Changed by listener11",
+ },
+ };
+ };
+ browser.compose.onBeforeSend.addListener(listener11);
+ browser.test.sendMessage("ready");
+ },
+ manifest: { permissions: ["compose"] },
+ });
+
+ await extensionA.startup();
+ await extensionB.startup();
+
+ await extensionA.awaitMessage("ready");
+ await extensionB.awaitMessage("ready");
+
+ let composeWindows = [...Services.wm.getEnumerator("msgcompose")];
+ Assert.equal(composeWindows.length, 1);
+ Assert.equal(composeWindows[0].document.readyState, "complete");
+ composeWindows[0]
+ .GenericSendMessage(Ci.nsIMsgCompDeliverMode.Later)
+ .catch(() => {
+ // This test is ignoring errors thrown by GenericSendMessage, but looks
+ // at didTryToSendMessage of the mocked CompleteGenericSendMessage to
+ // check if onBeforeSend aborted the send process.
+ });
+
+ let listener9Details = await extensionA.awaitMessage("listener9");
+ Assert.equal(listener9Details.to.length, 1);
+ Assert.equal(
+ listener9Details.to[0],
+ "recipient1@invalid",
+ "listener9 recipient correct"
+ );
+ Assert.equal(
+ listener9Details.subject,
+ "Initial subject",
+ "listener9 subject correct"
+ );
+
+ let listener10Details = await extensionB.awaitMessage("listener10");
+ Assert.equal(listener10Details.to.length, 1);
+ Assert.equal(
+ listener10Details.to[0],
+ "recipient2@invalid",
+ "listener10 recipient correct"
+ );
+ Assert.equal(
+ listener10Details.subject,
+ "Changed by listener9",
+ "listener10 subject correct"
+ );
+
+ let listener11Details = await extensionB.awaitMessage("listener11");
+ Assert.equal(listener11Details.to.length, 1);
+ Assert.equal(
+ listener11Details.to[0],
+ "recipient3@invalid",
+ "listener11 recipient correct"
+ );
+ Assert.equal(
+ listener11Details.subject,
+ "Changed by listener10",
+ "listener11 subject correct"
+ );
+
+ await extensionA.unload();
+ await extensionB.unload();
+
+ await messagesInOutbox(1);
+
+ let outboxMessages = [...outbox.messages];
+ Assert.ok(outboxMessages.length > 0);
+ let sentMessage = outboxMessages.shift();
+ Assert.equal(
+ sentMessage.subject,
+ "Changed by listener11",
+ "subject was changed"
+ );
+ Assert.equal(
+ sentMessage.recipients,
+ "recipient4@invalid",
+ "recipient was changed"
+ );
+
+ Assert.ok(outboxMessages.length == 0);
+
+ await new Promise(resolve => {
+ outbox.deleteMessages(
+ [sentMessage],
+ null,
+ true,
+ false,
+ { OnStopCopy: resolve },
+ false
+ );
+ });
+});
+
+add_task(async function test_MV3_event_pages() {
+ let files = {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, hasFired is set to false. In
+ // case of a wake-up, the first fired event is the one that woke up the background.
+ let hasFired = false;
+
+ browser.compose.onBeforeSend.addListener((tab, details) => {
+ // Only send the first event after background wake-up, this should be
+ // the only one expected.
+ if (!hasFired) {
+ hasFired = true;
+ browser.test.sendMessage("onBeforeSend received", details);
+ }
+
+ // Let us abort, so we do not have to re-open the compose window for
+ // multiple tests.
+ return {
+ cancel: true,
+ };
+ });
+
+ browser.test.sendMessage("background started");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ manifest_version: 3,
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ browser_specific_settings: {
+ gecko: { id: "compose.onBeforeSend@xpcshell.test" },
+ },
+ },
+ });
+
+ function checkPersistentListeners({ primed }) {
+ // A persistent event is referenced by its moduleName as defined in
+ // ext-mails.json, not by its actual namespace.
+ const persistent_events = ["compose.onBeforeSend"];
+
+ for (let event of persistent_events) {
+ let [moduleName, eventName] = event.split(".");
+ assertPersistentListeners(extension, moduleName, eventName, {
+ primed,
+ });
+ }
+ }
+
+ function beginSend() {
+ composeWindow.GenericSendMessage(Ci.nsIMsgCompDeliverMode.Now).catch(() => {
+ // This test is ignoring errors thrown by GenericSendMessage, but looks
+ // at didTryToSendMessage of the mocked CompleteGenericSendMessage to
+ // check if onBeforeSend aborted the send process.
+ });
+ }
+
+ let composeWindow = await openComposeWindow(account);
+ await focusWindow(composeWindow);
+
+ await extension.startup();
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+
+ // Trigger onBeforeSend without terminating the background first.
+
+ composeWindow.SetComposeDetails({ to: "first@invalid.net" });
+ beginSend();
+ let firstDetails = await extension.awaitMessage("onBeforeSend received");
+ Assert.equal(
+ "first@invalid.net",
+ firstDetails.to,
+ "Returned details should be correct"
+ );
+
+ // Terminate background and re-trigger onBeforeSend.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // The listeners should be primed.
+ checkPersistentListeners({ primed: true });
+
+ composeWindow.SetComposeDetails({ to: "second@invalid.net" });
+ beginSend();
+ let secondDetails = await extension.awaitMessage("onBeforeSend received");
+ Assert.equal(
+ "second@invalid.net",
+ secondDetails.to,
+ "Returned details should be correct"
+ );
+
+ // The background should have been restarted.
+ await extension.awaitMessage("background started");
+ // The listener should no longer be primed.
+ checkPersistentListeners({ primed: false });
+
+ await extension.unload();
+ composeWindow.close();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_saveDraft.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_saveDraft.js
new file mode 100644
index 0000000000..7e779e5798
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_saveDraft.js
@@ -0,0 +1,416 @@
+/* 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 { ExtensionSupport } = ChromeUtils.import(
+ "resource:///modules/ExtensionSupport.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+// Import the smtp server scripts
+var {
+ nsMailServer,
+ gThreadManager,
+ fsDebugNone,
+ fsDebugAll,
+ fsDebugRecv,
+ fsDebugRecvSend,
+} = ChromeUtils.import("resource://testing-common/mailnews/Maild.jsm");
+var { SmtpDaemon, SMTP_RFC2821_handler } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Smtpd.jsm"
+);
+var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Auth.jsm"
+);
+
+// Setup the daemon and server
+function setupServerDaemon(handler) {
+ if (!handler) {
+ handler = function (d) {
+ return new SMTP_RFC2821_handler(d);
+ };
+ }
+ var server = new nsMailServer(handler, new SmtpDaemon());
+ return server;
+}
+
+function getBasicSmtpServer(port = 1, hostname = "localhost") {
+ let server = localAccountUtils.create_outgoing_server(
+ port,
+ "user",
+ "password",
+ hostname
+ );
+
+ // Override the default greeting so we get something predictable
+ // in the ELHO message
+ Services.prefs.setCharPref("mail.smtpserver.default.hello_argument", "test");
+
+ return server;
+}
+
+function getSmtpIdentity(senderName, smtpServer) {
+ // Set up the identity.
+ let identity = MailServices.accounts.createIdentity();
+ identity.email = senderName;
+ identity.smtpServerKey = smtpServer.key;
+
+ return identity;
+}
+
+var gServer;
+var gLocalRootFolder;
+let gPopAccount;
+let gLocalAccount;
+
+add_setup(() => {
+ gServer = setupServerDaemon();
+ gServer.start();
+
+ // Test needs a non-local default account to be able to send messages.
+ gPopAccount = createAccount("pop3");
+ gLocalAccount = createAccount("local");
+ MailServices.accounts.defaultAccount = gPopAccount;
+
+ let identity = getSmtpIdentity(
+ "identity@foo.invalid",
+ getBasicSmtpServer(gServer.port)
+ );
+ gPopAccount.addIdentity(identity);
+ gPopAccount.defaultIdentity = identity;
+
+ // Test is using the Sent folder and Outbox folder of the local account.
+ gLocalRootFolder = gLocalAccount.incomingServer.rootFolder;
+ gLocalRootFolder.createSubfolder("Sent", null);
+ gLocalRootFolder.createSubfolder("Drafts", null);
+ gLocalRootFolder.createSubfolder("Fcc", null);
+ MailServices.accounts.setSpecialFolders();
+
+ requestLongerTimeout(4);
+
+ registerCleanupFunction(() => {
+ gServer.stop();
+ });
+});
+
+// Helper function to test saving messages.
+async function runTest(config) {
+ let files = {
+ "background.js": async () => {
+ let [config] = await window.sendMessage("getConfig");
+
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(2, accounts.length, "number of accounts");
+ let localAccount = accounts.find(a => a.type == "none");
+ let fccFolder = localAccount.folders.find(f => f.name == "Fcc");
+ browser.test.assertTrue(
+ !!fccFolder,
+ "should find the additional fcc folder"
+ );
+
+ // Prepare test data.
+ let allDetails = [];
+ for (let i = 0; i < 5; i++) {
+ allDetails.push({
+ to: [`test${i}@test.invalid`],
+ subject: `Test${i} save as ${config.expected.mode}`,
+ additionalFccFolder:
+ config.expected.fcc.length > 1 ? fccFolder : null,
+ });
+ }
+
+ // Open multiple compose windows.
+ for (let details of allDetails) {
+ details.tab = await browser.compose.beginNew(details);
+ }
+
+ // Add onAfterSave listener
+ let collectedEventsMap = new Map();
+ function onAfterSaveListener(tab, info) {
+ collectedEventsMap.set(tab.id, info);
+ }
+ browser.compose.onAfterSave.addListener(onAfterSaveListener);
+
+ // Initiate saving of all compose windows at the same time.
+ let allPromises = [];
+ for (let details of allDetails) {
+ allPromises.push(
+ browser.compose.saveMessage(details.tab.id, config.mode)
+ );
+ }
+
+ // Wait until all messages have been saved.
+ let allRv = await Promise.all(allPromises);
+
+ for (let i = 0; i < allDetails.length; i++) {
+ let rv = allRv[i];
+ let details = allDetails[i];
+ // Find the message with a matching headerMessageId.
+
+ browser.test.assertEq(
+ config.expected.mode,
+ rv.mode,
+ "The mode of the last message operation should be correct."
+ );
+ browser.test.assertEq(
+ config.expected.fcc.length,
+ rv.messages.length,
+ "Should find the correct number of saved messages for this save operation."
+ );
+
+ // Check expected FCC folders.
+ for (let i = 0; i < config.expected.fcc.length; i++) {
+ // Read the actual messages in the fcc folder.
+ let savedMessages = await window.sendMessage(
+ "getMessagesInFolder",
+ `${config.expected.fcc[i]}`
+ );
+ // Find the currently processed message.
+ let savedMessage = savedMessages.find(
+ m => m.messageId == rv.messages[i].headerMessageId
+ );
+ // Compare saved message to original message.
+ browser.test.assertEq(
+ details.subject,
+ savedMessage.subject,
+ "The subject of the message in the fcc folder should be correct."
+ );
+
+ // Check returned details.
+ browser.test.assertEq(
+ details.subject,
+ rv.messages[i].subject,
+ "The subject of the saved message should be correct."
+ );
+ browser.test.assertEq(
+ details.to[0],
+ rv.messages[i].recipients[0],
+ "The recipients of the saved message should be correct."
+ );
+ browser.test.assertEq(
+ `/${config.expected.fcc[i]}`,
+ rv.messages[i].folder.path,
+ "The saved message should be in the correct folder."
+ );
+ }
+
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.tabs.remove(details.tab.id);
+ await removedWindowPromise;
+ }
+
+ // Check onAfterSave listener
+ browser.compose.onAfterSave.removeListener(onAfterSaveListener);
+ browser.test.assertEq(
+ allDetails.length,
+ collectedEventsMap.size,
+ "Should have received the correct number of onAfterSave events"
+ );
+ let collectedEvents = [...collectedEventsMap.values()];
+ for (let detail of allDetails) {
+ let msg = collectedEvents.find(
+ e => e.messages[0].subject == detail.subject
+ );
+ browser.test.assertTrue(
+ msg,
+ "Should have received an onAfterSave event for every single message"
+ );
+ }
+ browser.test.assertEq(
+ collectedEventsMap.size,
+ collectedEvents.filter(e => e.mode == config.expected.mode).length,
+ "All events should have the correct mode."
+ );
+
+ // Remove all saved messages.
+ for (let fcc of config.expected.fcc) {
+ await window.sendMessage("clearMessagesInFolder", fcc);
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose", "compose.save", "messagesRead", "accountsRead"],
+ },
+ });
+
+ extension.onMessage("getConfig", async () => {
+ extension.sendMessage(config);
+ });
+
+ extension.onMessage("getMessagesInFolder", async folderName => {
+ let folder = gLocalRootFolder.getChildNamed(folderName);
+ let messages = [...folder.messages].map(m => {
+ let { subject, messageId, recipients } = m;
+ return { subject, messageId, recipients };
+ });
+ extension.sendMessage(...messages);
+ });
+
+ extension.onMessage("clearMessagesInFolder", async folderName => {
+ let folder = gLocalRootFolder.getChildNamed(folderName);
+ let messages = [...folder.messages];
+ await new Promise(resolve => {
+ folder.deleteMessages(
+ messages,
+ null,
+ true,
+ false,
+ { OnStopCopy: resolve },
+ false
+ );
+ });
+
+ Assert.equal(0, [...folder.messages].length, "folder should be empty");
+ extension.sendMessage();
+ });
+
+ extension.onMessage("checkWindow", async expected => {
+ await checkComposeHeaders(expected);
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ gServer.resetTest();
+}
+
+// Test with default save mode.
+add_task(async function test_default() {
+ await runTest({
+ mode: null,
+ expected: {
+ mode: "draft",
+ fcc: ["Drafts"],
+ },
+ });
+});
+
+// Test with default save mode and additional fcc.
+add_task(async function test_default_with_additional_fcc() {
+ await runTest({
+ mode: null,
+ expected: {
+ mode: "draft",
+ fcc: ["Drafts", "Fcc"],
+ },
+ });
+});
+
+// Test with draft save mode.
+add_task(async function test_saveAsDraft() {
+ await runTest({
+ mode: { mode: "draft" },
+ expected: {
+ mode: "draft",
+ fcc: ["Drafts"],
+ },
+ });
+});
+
+// Test with draft save mode and additional fcc.
+add_task(async function test_saveAsDraft_with_additional_fcc() {
+ await runTest({
+ mode: { mode: "draft" },
+ expected: {
+ mode: "draft",
+ fcc: ["Drafts", "Fcc"],
+ },
+ });
+});
+
+// Test onAfterSave when saving drafts for MV3
+add_task(async function test_onAfterSave_MV3_event_pages() {
+ let files = {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, hasFired is set to false. In
+ // case of a wake-up, the first fired event is the one that woke up the background.
+ let hasFired = false;
+
+ browser.compose.onAfterSave.addListener((tab, saveInfo) => {
+ // Only send the first event after background wake-up, this should be
+ // the only one expected.
+ if (!hasFired) {
+ hasFired = true;
+ browser.test.sendMessage("onAfterSave received", saveInfo);
+ }
+ });
+
+ browser.test.sendMessage("background started");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ manifest_version: 3,
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ browser_specific_settings: {
+ gecko: { id: "compose.onAfterSave@xpcshell.test" },
+ },
+ },
+ });
+
+ function checkPersistentListeners({ primed }) {
+ // A persistent event is referenced by its moduleName as defined in
+ // ext-mails.json, not by its actual namespace.
+ const persistent_events = ["compose.onAfterSave"];
+
+ for (let event of persistent_events) {
+ let [moduleName, eventName] = event.split(".");
+ assertPersistentListeners(extension, moduleName, eventName, {
+ primed,
+ });
+ }
+ }
+
+ let composeWindow = await openComposeWindow(gPopAccount);
+ await focusWindow(composeWindow);
+
+ await extension.startup();
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+
+ // Trigger onAfterSave without terminating the background first.
+
+ composeWindow.SetComposeDetails({ to: "first@invalid.net" });
+ composeWindow.SaveAsDraft();
+ let firstSaveInfo = await extension.awaitMessage("onAfterSave received");
+ Assert.equal(
+ "draft",
+ firstSaveInfo.mode,
+ "Returned SaveInfo should be correct"
+ );
+
+ // Terminate background and re-trigger onAfterSave.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // The listeners should be primed.
+ checkPersistentListeners({ primed: true });
+
+ composeWindow.SetComposeDetails({ to: "second@invalid.net" });
+ composeWindow.SaveAsDraft();
+ let secondSaveInfo = await extension.awaitMessage("onAfterSave received");
+ Assert.equal(
+ "draft",
+ secondSaveInfo.mode,
+ "Returned SaveInfo should be correct"
+ );
+
+ // The background should have been restarted.
+ await extension.awaitMessage("background started");
+ // The listener should no longer be primed.
+ checkPersistentListeners({ primed: false });
+
+ await extension.unload();
+ composeWindow.close();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_saveTemplate.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_saveTemplate.js
new file mode 100644
index 0000000000..d9ce180011
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_saveTemplate.js
@@ -0,0 +1,432 @@
+/* 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 { ExtensionSupport } = ChromeUtils.import(
+ "resource:///modules/ExtensionSupport.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+// Import the smtp server scripts
+var {
+ nsMailServer,
+ gThreadManager,
+ fsDebugNone,
+ fsDebugAll,
+ fsDebugRecv,
+ fsDebugRecvSend,
+} = ChromeUtils.import("resource://testing-common/mailnews/Maild.jsm");
+var { SmtpDaemon, SMTP_RFC2821_handler } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Smtpd.jsm"
+);
+var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Auth.jsm"
+);
+
+// Setup the daemon and server
+function setupServerDaemon(handler) {
+ if (!handler) {
+ handler = function (d) {
+ return new SMTP_RFC2821_handler(d);
+ };
+ }
+ var server = new nsMailServer(handler, new SmtpDaemon());
+ return server;
+}
+
+function getBasicSmtpServer(port = 1, hostname = "localhost") {
+ let server = localAccountUtils.create_outgoing_server(
+ port,
+ "user",
+ "password",
+ hostname
+ );
+
+ // Override the default greeting so we get something predictable
+ // in the ELHO message
+ Services.prefs.setCharPref("mail.smtpserver.default.hello_argument", "test");
+
+ return server;
+}
+
+function getSmtpIdentity(senderName, smtpServer) {
+ // Set up the identity.
+ let identity = MailServices.accounts.createIdentity();
+ identity.email = senderName;
+ identity.smtpServerKey = smtpServer.key;
+
+ return identity;
+}
+
+var gServer;
+var gLocalRootFolder;
+let gPopAccount;
+let gLocalAccount;
+
+add_setup(() => {
+ gServer = setupServerDaemon();
+ gServer.start();
+
+ // Test needs a non-local default account to be able to send messages.
+ gPopAccount = createAccount("pop3");
+ gLocalAccount = createAccount("local");
+ MailServices.accounts.defaultAccount = gPopAccount;
+
+ let identity = getSmtpIdentity(
+ "identity@foo.invalid",
+ getBasicSmtpServer(gServer.port)
+ );
+ gPopAccount.addIdentity(identity);
+ gPopAccount.defaultIdentity = identity;
+
+ // Test is using the Sent folder and Outbox folder of the local account.
+ gLocalRootFolder = gLocalAccount.incomingServer.rootFolder;
+ gLocalRootFolder.createSubfolder("Sent", null);
+ gLocalRootFolder.createSubfolder("Templates", null);
+ gLocalRootFolder.createSubfolder("Fcc", null);
+ MailServices.accounts.setSpecialFolders();
+
+ registerCleanupFunction(() => {
+ gServer.stop();
+ });
+});
+
+add_task(async function test_no_permission() {
+ let files = {
+ "background.js": async () => {
+ let details = {
+ to: ["send@test.invalid"],
+ subject: "Test send",
+ };
+
+ // Open a compose window with a message.
+ let tab = await browser.compose.beginNew(details);
+
+ // Send now. It should fail due to the missing compose.send permission.
+ await browser.test.assertThrows(
+ () => browser.compose.saveMessage(tab.id),
+ /browser.compose.saveMessage is not a function/,
+ "browser.compose.saveMessage() should reject, if the permission compose.save is not granted."
+ );
+
+ // Clean up.
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.tabs.remove(tab.id);
+ await removedWindowPromise;
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+// Helper function to test saving messages.
+async function runTest(config) {
+ let files = {
+ "background.js": async () => {
+ let [config] = await window.sendMessage("getConfig");
+
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(2, accounts.length, "number of accounts");
+ let localAccount = accounts.find(a => a.type == "none");
+ let fccFolder = localAccount.folders.find(f => f.name == "Fcc");
+ browser.test.assertTrue(
+ !!fccFolder,
+ "should find the additional fcc folder"
+ );
+
+ // Prepare test data.
+ let allDetails = [];
+ for (let i = 0; i < 5; i++) {
+ allDetails.push({
+ to: [`test${i}@test.invalid`],
+ subject: `Test${i} save as ${config.expected.mode}`,
+ additionalFccFolder:
+ config.expected.fcc.length > 1 ? fccFolder : null,
+ });
+ }
+
+ // Open multiple compose windows.
+ for (let details of allDetails) {
+ details.tab = await browser.compose.beginNew(details);
+ }
+
+ // Add onAfterSave listener
+ let collectedEventsMap = new Map();
+ function onAfterSaveListener(tab, info) {
+ collectedEventsMap.set(tab.id, info);
+ }
+ browser.compose.onAfterSave.addListener(onAfterSaveListener);
+
+ // Initiate saving of all compose windows at the same time.
+ let allPromises = [];
+ for (let details of allDetails) {
+ allPromises.push(
+ browser.compose.saveMessage(details.tab.id, config.mode)
+ );
+ }
+
+ // Wait until all messages have been saved.
+ let allRv = await Promise.all(allPromises);
+
+ for (let i = 0; i < allDetails.length; i++) {
+ let rv = allRv[i];
+ let details = allDetails[i];
+ // Find the message with a matching headerMessageId.
+
+ browser.test.assertEq(
+ config.expected.mode,
+ rv.mode,
+ "The mode of the last message operation should be correct."
+ );
+ browser.test.assertEq(
+ config.expected.fcc.length,
+ rv.messages.length,
+ "Should find the correct number of saved messages for this save operation."
+ );
+
+ // Check expected FCC folders.
+ for (let i = 0; i < config.expected.fcc.length; i++) {
+ // Read the actual messages in the fcc folder.
+ let savedMessages = await window.sendMessage(
+ "getMessagesInFolder",
+ `${config.expected.fcc[i]}`
+ );
+ // Find the currently processed message.
+ let savedMessage = savedMessages.find(
+ m => m.messageId == rv.messages[i].headerMessageId
+ );
+ // Compare saved message to original message.
+ browser.test.assertEq(
+ details.subject,
+ savedMessage.subject,
+ "The subject of the message in the fcc folder should be correct."
+ );
+
+ // Check returned details.
+ browser.test.assertEq(
+ details.subject,
+ rv.messages[i].subject,
+ "The subject of the saved message should be correct."
+ );
+ browser.test.assertEq(
+ details.to[0],
+ rv.messages[i].recipients[0],
+ "The recipients of the saved message should be correct."
+ );
+ browser.test.assertEq(
+ `/${config.expected.fcc[i]}`,
+ rv.messages[i].folder.path,
+ "The saved message should be in the correct folder."
+ );
+ }
+
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.tabs.remove(details.tab.id);
+ await removedWindowPromise;
+ }
+
+ // Check onAfterSave listener
+ browser.compose.onAfterSave.removeListener(onAfterSaveListener);
+ browser.test.assertEq(
+ allDetails.length,
+ collectedEventsMap.size,
+ "Should have received the correct number of onAfterSave events"
+ );
+ let collectedEvents = [...collectedEventsMap.values()];
+ for (let detail of allDetails) {
+ let msg = collectedEvents.find(
+ e => e.messages[0].subject == detail.subject
+ );
+ browser.test.assertTrue(
+ msg,
+ "Should have received an onAfterSave event for every single message"
+ );
+ }
+ browser.test.assertEq(
+ collectedEventsMap.size,
+ collectedEvents.filter(e => e.mode == config.expected.mode).length,
+ "All events should have the correct mode."
+ );
+
+ // Remove all saved messages.
+ for (let fcc of config.expected.fcc) {
+ await window.sendMessage("clearMessagesInFolder", fcc);
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose", "compose.save", "messagesRead", "accountsRead"],
+ },
+ });
+
+ extension.onMessage("getConfig", async () => {
+ extension.sendMessage(config);
+ });
+
+ extension.onMessage("getMessagesInFolder", async folderName => {
+ let folder = gLocalRootFolder.getChildNamed(folderName);
+ let messages = [...folder.messages].map(m => {
+ let { subject, messageId, recipients } = m;
+ return { subject, messageId, recipients };
+ });
+ extension.sendMessage(...messages);
+ });
+
+ extension.onMessage("clearMessagesInFolder", async folderName => {
+ let folder = gLocalRootFolder.getChildNamed(folderName);
+ let messages = [...folder.messages];
+ await new Promise(resolve => {
+ folder.deleteMessages(
+ messages,
+ null,
+ true,
+ false,
+ { OnStopCopy: resolve },
+ false
+ );
+ });
+
+ Assert.equal(0, [...folder.messages].length, "folder should be empty");
+ extension.sendMessage();
+ });
+
+ extension.onMessage("checkWindow", async expected => {
+ await checkComposeHeaders(expected);
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ gServer.resetTest();
+}
+
+// Test with template save mode.
+add_task(async function test_saveAsTemplate() {
+ await runTest({
+ mode: { mode: "template" },
+ expected: {
+ mode: "template",
+ fcc: ["Templates"],
+ },
+ });
+});
+
+// Test with template save mode and additional fcc
+add_task(async function test_saveAsTemplate_with_additional_fcc() {
+ await runTest({
+ mode: { mode: "template" },
+ expected: {
+ mode: "template",
+ fcc: ["Templates", "Fcc"],
+ },
+ });
+});
+
+// Test onAfterSave when saving templates for MV3
+add_task(async function test_onAfterSave_MV3_event_pages() {
+ let files = {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, hasFired is set to false. In
+ // case of a wake-up, the first fired event is the one that woke up the background.
+ let hasFired = false;
+
+ browser.compose.onAfterSave.addListener((tab, saveInfo) => {
+ // Only send the first event after background wake-up, this should be
+ // the only one expected.
+ if (!hasFired) {
+ hasFired = true;
+ browser.test.sendMessage("onAfterSave received", saveInfo);
+ }
+ });
+
+ browser.test.sendMessage("background started");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ manifest_version: 3,
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ browser_specific_settings: {
+ gecko: { id: "compose.onAfterSave@xpcshell.test" },
+ },
+ },
+ });
+
+ function checkPersistentListeners({ primed }) {
+ // A persistent event is referenced by its moduleName as defined in
+ // ext-mails.json, not by its actual namespace.
+ const persistent_events = ["compose.onAfterSave"];
+
+ for (let event of persistent_events) {
+ let [moduleName, eventName] = event.split(".");
+ assertPersistentListeners(extension, moduleName, eventName, {
+ primed,
+ });
+ }
+ }
+
+ let composeWindow = await openComposeWindow(gPopAccount);
+ await focusWindow(composeWindow);
+
+ await extension.startup();
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+
+ // Trigger onAfterSave without terminating the background first.
+
+ composeWindow.SetComposeDetails({ to: "first@invalid.net" });
+ composeWindow.SaveAsTemplate();
+ let firstSaveInfo = await extension.awaitMessage("onAfterSave received");
+ Assert.equal(
+ "template",
+ firstSaveInfo.mode,
+ "Returned SaveInfo should be correct"
+ );
+
+ // Terminate background and re-trigger onAfterSave.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // The listeners should be primed.
+ checkPersistentListeners({ primed: true });
+
+ composeWindow.SetComposeDetails({ to: "second@invalid.net" });
+ composeWindow.SaveAsTemplate();
+ let secondSaveInfo = await extension.awaitMessage("onAfterSave received");
+ Assert.equal(
+ "template",
+ secondSaveInfo.mode,
+ "Returned SaveInfo should be correct"
+ );
+
+ // The background should have been restarted.
+ await extension.awaitMessage("background started");
+ // The listener should no longer be primed.
+ checkPersistentListeners({ primed: false });
+
+ await extension.unload();
+ composeWindow.close();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_sendMessage.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_sendMessage.js
new file mode 100644
index 0000000000..4fd983e8e5
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_sendMessage.js
@@ -0,0 +1,733 @@
+/* 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 { ExtensionSupport } = ChromeUtils.import(
+ "resource:///modules/ExtensionSupport.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+// Import the smtp server scripts
+var {
+ nsMailServer,
+ gThreadManager,
+ fsDebugNone,
+ fsDebugAll,
+ fsDebugRecv,
+ fsDebugRecvSend,
+} = ChromeUtils.import("resource://testing-common/mailnews/Maild.jsm");
+var { SmtpDaemon, SMTP_RFC2821_handler } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Smtpd.jsm"
+);
+var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Auth.jsm"
+);
+
+// Setup the daemon and server
+function setupServerDaemon(handler) {
+ if (!handler) {
+ handler = function (d) {
+ return new SMTP_RFC2821_handler(d);
+ };
+ }
+ var server = new nsMailServer(handler, new SmtpDaemon());
+ return server;
+}
+
+function getBasicSmtpServer(port = 1, hostname = "localhost") {
+ let server = localAccountUtils.create_outgoing_server(
+ port,
+ "user",
+ "password",
+ hostname
+ );
+
+ // Override the default greeting so we get something predictable
+ // in the ELHO message
+ Services.prefs.setCharPref("mail.smtpserver.default.hello_argument", "test");
+
+ return server;
+}
+
+function getSmtpIdentity(senderName, smtpServer) {
+ // Set up the identity.
+ let identity = MailServices.accounts.createIdentity();
+ identity.email = senderName;
+ identity.smtpServerKey = smtpServer.key;
+
+ return identity;
+}
+
+function tracksentMessages(aSubject, aTopic, aMsgID) {
+ // The aMsgID starts with < and ends with > which is not used by the API.
+ let headerMessageId = aMsgID.replace(/^<|>$/g, "");
+ gSentMessages.push(headerMessageId);
+}
+
+var gServer;
+var gOutbox;
+var gSentMessages = [];
+let gPopAccount;
+let gLocalAccount;
+
+add_setup(() => {
+ gServer = setupServerDaemon();
+ gServer.start();
+
+ // Test needs a non-local default account to be able to send messages.
+ gPopAccount = createAccount("pop3");
+ gLocalAccount = createAccount("local");
+ MailServices.accounts.defaultAccount = gPopAccount;
+
+ let identity = getSmtpIdentity(
+ "identity@foo.invalid",
+ getBasicSmtpServer(gServer.port)
+ );
+ gPopAccount.addIdentity(identity);
+ gPopAccount.defaultIdentity = identity;
+
+ // Test is using the Sent folder and Outbox folder of the local account.
+ let rootFolder = gLocalAccount.incomingServer.rootFolder;
+ rootFolder.createSubfolder("Sent", null);
+ MailServices.accounts.setSpecialFolders();
+ gOutbox = rootFolder.getChildNamed("Outbox");
+
+ Services.obs.addObserver(tracksentMessages, "mail:composeSendSucceeded");
+
+ registerCleanupFunction(() => {
+ gServer.stop();
+ Services.obs.removeObserver(tracksentMessages, "mail:composeSendSucceeded");
+ });
+});
+
+add_task(async function test_no_permission() {
+ let files = {
+ "background.js": async () => {
+ let details = {
+ to: ["send@test.invalid"],
+ subject: "Test send",
+ };
+
+ // Open a compose window with a message.
+ let tab = await browser.compose.beginNew(details);
+
+ // Send now. It should fail due to the missing compose.send permission.
+ await browser.test.assertThrows(
+ () => browser.compose.sendMessage(tab.id),
+ /browser.compose.sendMessage is not a function/,
+ "browser.compose.sendMessage() should reject, if the permission compose.send is not granted."
+ );
+
+ // Clean up.
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.tabs.remove(tab.id);
+ await removedWindowPromise;
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_fail() {
+ let files = {
+ "background.js": async () => {
+ let details = {
+ to: ["send@test.invalid"],
+ subject: "Test send",
+ };
+
+ // Open a compose window with a message.
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew(details);
+ let [createdWindow] = await createdWindowPromise;
+ browser.test.assertEq("messageCompose", createdWindow.type);
+
+ await window.sendMessage("checkWindow", details);
+
+ let [tab] = await browser.tabs.query({ windowId: createdWindow.id });
+
+ browser.compose.onBeforeSend.addListener(() => {
+ return { cancel: true };
+ });
+
+ // Add onAfterSend listener
+ let collectedEventsMap = new Map();
+ function onAfterSendListener(tab, info) {
+ collectedEventsMap.set(tab.id, info);
+ }
+ browser.compose.onAfterSend.addListener(onAfterSendListener);
+
+ // Send now. It should fail due to the aborting onBeforeSend listener.
+ await browser.test.assertRejects(
+ browser.compose.sendMessage(tab.id),
+ /Send aborted by an onBeforeSend event/,
+ "browser.compose.sendMessage() should reject, if the message could not be send."
+ );
+
+ // Check onAfterSend listener
+ browser.compose.onAfterSend.removeListener(onAfterSendListener);
+ browser.test.assertEq(
+ 1,
+ collectedEventsMap.size,
+ "Should have received the correct number of onAfterSend events"
+ );
+ browser.test.assertEq(
+ "Send aborted by an onBeforeSend event",
+ collectedEventsMap.get(tab.id).error,
+ "Should have received the correct error"
+ );
+
+ // Clean up.
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose", "compose.send"],
+ },
+ });
+
+ extension.onMessage("checkWindow", async expected => {
+ await checkComposeHeaders(expected);
+ extension.sendMessage();
+ });
+
+ extension.onMessage("getSentMessages", async () => {
+ extension.sendMessage(gSentMessages);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_send() {
+ let files = {
+ "background.js": async () => {
+ let details = {
+ to: ["send@test.invalid"],
+ subject: "Test send",
+ };
+
+ // Open a compose window with a message.
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew(details);
+ let [createdWindow] = await createdWindowPromise;
+ browser.test.assertEq("messageCompose", createdWindow.type);
+
+ await window.sendMessage("checkWindow", details);
+
+ let [tab] = await browser.tabs.query({ windowId: createdWindow.id });
+
+ // Add onAfterSend listener
+ let collectedEventsMap = new Map();
+ function onAfterSendListener(tab, info) {
+ collectedEventsMap.set(tab.id, info);
+ }
+ browser.compose.onAfterSend.addListener(onAfterSendListener);
+
+ // Send now.
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ let rv = await browser.compose.sendMessage(tab.id);
+ let [sentMessages] = await window.sendMessage("getSentMessages");
+
+ browser.test.assertEq(
+ 1,
+ sentMessages.length,
+ "Number of total messages sent should be correct."
+ );
+ browser.test.assertEq(
+ "sendNow",
+ rv.mode,
+ "The mode of the last message operation should be correct."
+ );
+ browser.test.assertEq(
+ sentMessages[0],
+ rv.headerMessageId,
+ "The headerMessageId of last message sent should be correct."
+ );
+ browser.test.assertEq(
+ sentMessages[0],
+ rv.messages[0].headerMessageId,
+ "The headerMessageId in the copy of last message sent should be correct."
+ );
+
+ // Window should have closed after send.
+ await removedWindowPromise;
+
+ // Check onAfterSend listener
+ browser.compose.onAfterSend.removeListener(onAfterSendListener);
+ browser.test.assertEq(
+ 1,
+ collectedEventsMap.size,
+ "Should have received the correct number of onAfterSend events"
+ );
+ browser.test.assertTrue(
+ collectedEventsMap.has(tab.id),
+ "The received event should belong to the correct tab."
+ );
+ browser.test.assertEq(
+ "sendNow",
+ collectedEventsMap.get(tab.id).mode,
+ "The received event should have the correct mode."
+ );
+ browser.test.assertEq(
+ rv.headerMessageId,
+ collectedEventsMap.get(tab.id).headerMessageId,
+ "The received event should have the correct headerMessageId."
+ );
+ browser.test.assertEq(
+ rv.headerMessageId,
+ collectedEventsMap.get(tab.id).messages[0].headerMessageId,
+ "The message in the received event should have the correct headerMessageId."
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose", "compose.send"],
+ },
+ });
+
+ extension.onMessage("checkWindow", async expected => {
+ await checkComposeHeaders(expected);
+ extension.sendMessage();
+ });
+
+ extension.onMessage("getSentMessages", async () => {
+ extension.sendMessage(gSentMessages);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_sendDefault() {
+ let files = {
+ "background.js": async () => {
+ let details = {
+ to: ["sendDefault@test.invalid"],
+ subject: "Test sendDefault",
+ };
+
+ // Open a compose window with a message.
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew(details);
+ let [createdWindow] = await createdWindowPromise;
+ browser.test.assertEq("messageCompose", createdWindow.type);
+
+ await window.sendMessage("checkWindow", details);
+
+ let [tab] = await browser.tabs.query({ windowId: createdWindow.id });
+
+ // Send via default mode, which should be sendNow.
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ let rv = await browser.compose.sendMessage(tab.id, { mode: "default" });
+ let [sentMessages] = await window.sendMessage("getSentMessages");
+
+ browser.test.assertEq(
+ 2,
+ sentMessages.length,
+ "Number of total messages sent should be correct."
+ );
+ browser.test.assertEq(
+ "sendNow",
+ rv.mode,
+ "The mode of the last message operation should be correct."
+ );
+ browser.test.assertEq(
+ sentMessages[1],
+ rv.headerMessageId,
+ "The headerMessageId of last message sent should be correct."
+ );
+ browser.test.assertEq(
+ sentMessages[1],
+ rv.messages[0].headerMessageId,
+ "The headerMessageId in the copy of last message sent should be correct."
+ );
+
+ // Window should have closed after send.
+ await removedWindowPromise;
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose", "compose.send"],
+ },
+ });
+
+ extension.onMessage("checkWindow", async expected => {
+ await checkComposeHeaders(expected);
+ extension.sendMessage();
+ });
+
+ extension.onMessage("getSentMessages", async () => {
+ extension.sendMessage(gSentMessages);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ gServer.resetTest();
+});
+
+add_task(async function test_sendNow() {
+ let files = {
+ "background.js": async () => {
+ let details = {
+ to: ["sendNow@test.invalid"],
+ subject: "Test sendNow",
+ };
+
+ // Open a compose window with a message.
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew(details);
+ let [createdWindow] = await createdWindowPromise;
+ browser.test.assertEq("messageCompose", createdWindow.type);
+
+ await window.sendMessage("checkWindow", details);
+
+ let [tab] = await browser.tabs.query({ windowId: createdWindow.id });
+
+ // Send via sendNow mode.
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ let rv = await browser.compose.sendMessage(tab.id, { mode: "sendNow" });
+ let [sentMessages] = await window.sendMessage("getSentMessages");
+
+ browser.test.assertEq(
+ 3,
+ sentMessages.length,
+ "Number of total messages sent should be correct."
+ );
+ browser.test.assertEq(
+ "sendNow",
+ rv.mode,
+ "The mode of the last message operation should be correct."
+ );
+ browser.test.assertEq(
+ sentMessages[2],
+ rv.headerMessageId,
+ "The headerMessageId of last message sent should be correct."
+ );
+ browser.test.assertEq(
+ sentMessages[2],
+ rv.messages[0].headerMessageId,
+ "The headerMessageId in the copy of last message sent should be correct."
+ );
+
+ // Window should have closed after send.
+ await removedWindowPromise;
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose", "compose.send"],
+ },
+ });
+
+ extension.onMessage("checkWindow", async expected => {
+ await checkComposeHeaders(expected);
+ extension.sendMessage();
+ });
+
+ extension.onMessage("getSentMessages", async () => {
+ extension.sendMessage(gSentMessages);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_sendLater() {
+ let files = {
+ "background.js": async () => {
+ let details = {
+ to: ["sendLater@test.invalid"],
+ subject: "Test sendLater",
+ };
+
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew(details);
+ let [createdWindow] = await createdWindowPromise;
+ browser.test.assertEq("messageCompose", createdWindow.type);
+
+ await window.sendMessage("checkWindow", details);
+
+ let [tab] = await browser.tabs.query({ windowId: createdWindow.id });
+
+ // Send Later.
+
+ let rv = await browser.compose.sendMessage(tab.id, { mode: "sendLater" });
+ let [outboxMessage] = await window.sendMessage(
+ "checkMessagesInOutbox",
+ details
+ );
+
+ browser.test.assertEq(
+ "sendLater",
+ rv.mode,
+ "The mode of the last message operation should be correct."
+ );
+ browser.test.assertEq(
+ outboxMessage,
+ rv.messages[0].headerMessageId,
+ "The headerMessageId in the copy of last message sent should be correct."
+ );
+
+ await window.sendMessage("clearMessagesInOutbox");
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose", "compose.send", "messagesRead", "accountsRead"],
+ },
+ });
+
+ extension.onMessage("checkMessagesInOutbox", async expected => {
+ // Check if the sendLater request did put the message in the outbox.
+ let outboxMessages = [...gOutbox.messages];
+ Assert.ok(outboxMessages.length == 1);
+ let sentMessage = outboxMessages.shift();
+ Assert.equal(sentMessage.subject, expected.subject, "subject is correct");
+ Assert.equal(sentMessage.recipients, expected.to, "recipient is correct");
+ extension.sendMessage(sentMessage.messageId);
+ });
+
+ extension.onMessage("clearMessagesInOutbox", async () => {
+ let outboxMessages = [...gOutbox.messages];
+ await new Promise(resolve => {
+ gOutbox.deleteMessages(
+ outboxMessages,
+ null,
+ true,
+ false,
+ { OnStopCopy: resolve },
+ false
+ );
+ });
+
+ Assert.equal(0, [...gOutbox.messages].length, "outbox should be empty");
+ extension.sendMessage();
+ });
+
+ extension.onMessage("checkWindow", async expected => {
+ await checkComposeHeaders(expected);
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_onComposeStateChanged() {
+ let files = {
+ "background.js": async () => {
+ let numberOfEvents = 0;
+ browser.compose.onComposeStateChanged.addListener(async (tab, state) => {
+ numberOfEvents++;
+ browser.test.log(`State #${numberOfEvents}: ${JSON.stringify(state)}`);
+ switch (numberOfEvents) {
+ case 1:
+ // The fresh created composer has no recipient, send is disabled.
+ browser.test.assertEq(false, state.canSendNow);
+ browser.test.assertEq(false, state.canSendLater);
+ break;
+
+ case 2:
+ // The composer updated its initial details data, send is enabled.
+ browser.test.assertEq(true, state.canSendNow);
+ browser.test.assertEq(true, state.canSendLater);
+ break;
+
+ case 3:
+ // The recipient has been invalidated, send is disabled.
+ browser.test.assertEq(false, state.canSendNow);
+ browser.test.assertEq(false, state.canSendLater);
+ break;
+
+ case 4:
+ // The recipient has been reverted, send is enabled.
+ browser.test.assertEq(true, state.canSendNow);
+ browser.test.assertEq(true, state.canSendLater);
+
+ // Clean up.
+
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.windows.remove(createdWindow.id);
+ await removedWindowPromise;
+
+ browser.test.notifyPass("finished");
+ break;
+ }
+ });
+
+ // The call to beginNew should create two onComposeStateChanged events,
+ // one after the empty window has been created and one after the initial
+ // details have been set.
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ let createdTab = await browser.compose.beginNew({
+ to: ["test@test.invalid"],
+ subject: "Test part 1",
+ body: "Original body.",
+ });
+ let [createdWindow] = await createdWindowPromise;
+ browser.test.assertEq("messageCompose", createdWindow.type);
+
+ // Trigger an onComposeStateChanged event by invalidating the recipient.
+ await browser.compose.setComposeDetails(createdTab.id, {
+ to: ["test"],
+ subject: "Test part 2",
+ });
+
+ // Trigger an onComposeStateChanged event by reverting the recipient.
+ await browser.compose.setComposeDetails(createdTab.id, {
+ to: ["test@test.invalid"],
+ subject: "Test part 3",
+ });
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+// Test onAfterSend for MV3
+add_task(async function test_onAfterSend_MV3_event_pages() {
+ let files = {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, hasFired is set to false. In
+ // case of a wake-up, the first fired event is the one that woke up the background.
+ let hasFired = false;
+
+ browser.compose.onAfterSend.addListener(async (tab, sendInfo) => {
+ // Only send the first event after background wake-up, this should be
+ // the only one expected.
+ if (!hasFired) {
+ hasFired = true;
+ browser.test.sendMessage("onAfterSend received", sendInfo);
+ }
+ });
+
+ browser.test.sendMessage("background started");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ manifest_version: 3,
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ browser_specific_settings: {
+ gecko: { id: "compose.onAfterSend@xpcshell.test" },
+ },
+ },
+ });
+
+ function checkPersistentListeners({ primed }) {
+ // A persistent event is referenced by its moduleName as defined in
+ // ext-mails.json, not by its actual namespace.
+ const persistent_events = ["compose.onAfterSend"];
+
+ for (let event of persistent_events) {
+ let [moduleName, eventName] = event.split(".");
+ assertPersistentListeners(extension, moduleName, eventName, {
+ primed,
+ });
+ }
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+
+ // Trigger onAfterSend without terminating the background first.
+
+ let firstComposeWindow = await openComposeWindow(gPopAccount);
+ await focusWindow(firstComposeWindow);
+ firstComposeWindow.SetComposeDetails({ to: "first@invalid.net" });
+ firstComposeWindow.SetComposeDetails({ subject: "First message" });
+ firstComposeWindow.SendMessage();
+ let firstSaveInfo = await extension.awaitMessage("onAfterSend received");
+ Assert.equal(
+ "sendNow",
+ firstSaveInfo.mode,
+ "Returned SaveInfo should be correct"
+ );
+
+ // Terminate background and re-trigger onAfterSend.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // The listeners should be primed.
+ checkPersistentListeners({ primed: true });
+ let secondComposeWindow = await openComposeWindow(gPopAccount);
+ await focusWindow(secondComposeWindow);
+ secondComposeWindow.SetComposeDetails({ to: "second@invalid.net" });
+ secondComposeWindow.SetComposeDetails({ subject: "Second message" });
+ secondComposeWindow.SendMessage();
+ let secondSaveInfo = await extension.awaitMessage("onAfterSend received");
+ Assert.equal(
+ "sendNow",
+ secondSaveInfo.mode,
+ "Returned SaveInfo should be correct"
+ );
+
+ // The background should have been restarted.
+ await extension.awaitMessage("background started");
+ // The listener should no longer be primed.
+ checkPersistentListeners({ primed: false });
+
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_contentScripts.js b/comm/mail/components/extensions/test/browser/browser_ext_contentScripts.js
new file mode 100644
index 0000000000..50ae11ffab
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_contentScripts.js
@@ -0,0 +1,438 @@
+/* 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 CONTENT_PAGE =
+ "http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data/content.html";
+const UNCHANGED_VALUES = {
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ color: "rgb(0, 0, 0)",
+ foo: null,
+ textContent: "\n This is text.\n This is a link with text.\n \n\n\n",
+};
+
+/** Tests browser.tabs.insertCSS and browser.tabs.removeCSS. */
+add_task(async function testInsertRemoveCSS() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let [tab] = await browser.tabs.query({ active: true });
+
+ await browser.tabs.insertCSS(tab.id, {
+ code: "body { background-color: lime; }",
+ });
+ await window.sendMessage();
+
+ await browser.tabs.removeCSS(tab.id, {
+ code: "body { background-color: lime; }",
+ });
+ await window.sendMessage();
+
+ await browser.tabs.insertCSS(tab.id, { file: "test.css" });
+ await window.sendMessage();
+
+ await browser.tabs.removeCSS(tab.id, { file: "test.css" });
+ browser.test.notifyPass("finished");
+ },
+ "test.css": "body { background-color: green; }",
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["*://mochi.test/*"],
+ },
+ });
+
+ let tab = window.openContentTab(CONTENT_PAGE);
+ await awaitBrowserLoaded(tab.browser, CONTENT_PAGE);
+
+ await extension.startup();
+
+ await extension.awaitMessage(); // insertCSS with code
+ await checkContent(tab.browser, { backgroundColor: "rgb(0, 255, 0)" });
+ extension.sendMessage();
+
+ await extension.awaitMessage(); // removeCSS with code
+ await checkContent(tab.browser, UNCHANGED_VALUES);
+ extension.sendMessage();
+
+ await extension.awaitMessage(); // insertCSS with file
+ await checkContent(tab.browser, { backgroundColor: "rgb(0, 128, 0)" });
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished"); // removeCSS with file
+ await checkContent(tab.browser, UNCHANGED_VALUES);
+
+ await extension.unload();
+
+ document.getElementById("tabmail").closeTab(tab);
+});
+
+/** Tests browser.tabs.insertCSS fails without the host permission. */
+add_task(async function testInsertRemoveCSSNoPermissions() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let [tab] = await browser.tabs.query({ active: true });
+
+ await browser.test.assertRejects(
+ browser.tabs.insertCSS(tab.id, {
+ code: "body { background-color: darkred; }",
+ }),
+ /Missing host permission for the tab/,
+ "insertCSS without permission should throw"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.insertCSS(tab.id, { file: "test.css" }),
+ /Missing host permission for the tab/,
+ "insertCSS without permission should throw"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.insertCSS(tab.id, {
+ file: "test.css",
+ matchAboutBlank: true,
+ }),
+ /Missing host permission for the tab/,
+ "insertCSS without permission should throw"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "test.css": "body { background-color: red; }",
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: [],
+ },
+ });
+
+ let tab = window.openContentTab(CONTENT_PAGE);
+ await awaitBrowserLoaded(tab.browser, CONTENT_PAGE);
+
+ await extension.startup();
+
+ await extension.awaitFinish("finished");
+ await checkContent(tab.browser, UNCHANGED_VALUES);
+
+ await extension.unload();
+
+ document.getElementById("tabmail").closeTab(tab);
+});
+
+/** Tests browser.tabs.executeScript. */
+add_task(async function testExecuteScript() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let tab = await browser.tabs.query({ active: true });
+
+ await browser.tabs.executeScript(tab.id, {
+ code: `document.body.setAttribute("foo", "bar");`,
+ });
+ await window.sendMessage();
+
+ await browser.tabs.executeScript(tab.id, { file: "test.js" });
+ browser.test.notifyPass("finished");
+ },
+ "test.js": () => {
+ document.body.textContent = "Hey look, the script ran!";
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["*://mochi.test/*"],
+ },
+ });
+
+ let tab = window.openContentTab(CONTENT_PAGE);
+ await awaitBrowserLoaded(tab.browser, CONTENT_PAGE);
+
+ await extension.startup();
+
+ await extension.awaitMessage(); // executeScript with code
+ await checkContent(tab.browser, { foo: "bar" });
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished"); // executeScript with file
+ await checkContent(tab.browser, {
+ foo: "bar",
+ textContent: "Hey look, the script ran!",
+ });
+
+ await extension.unload();
+
+ document.getElementById("tabmail").closeTab(tab);
+});
+
+/** Tests browser.tabs.executeScript fails without the host permission. */
+add_task(async function testExecuteScriptNoPermissions() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let tab = await browser.tabs.query({ active: true });
+
+ await browser.test.assertRejects(
+ browser.tabs.executeScript(tab.id, {
+ code: `document.body.setAttribute("foo", "bar");`,
+ }),
+ /Missing host permission for the tab/,
+ "executeScript without permission should throw"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.executeScript(tab.id, { file: "test.js" }),
+ /Missing host permission for the tab/,
+ "executeScript without permission should throw"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.executeScript(tab.id, {
+ file: "test.js",
+ matchAboutBlank: true,
+ }),
+ /Missing host permission for the tab/,
+ "executeScript without permission should throw"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "test.js": () => {
+ document.body.textContent = "Hey look, the script ran!";
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: [],
+ },
+ });
+
+ let tab = window.openContentTab(CONTENT_PAGE);
+ await awaitBrowserLoaded(tab.browser, CONTENT_PAGE);
+
+ await extension.startup();
+
+ await extension.awaitFinish("finished");
+ await checkContent(tab.browser, UNCHANGED_VALUES);
+
+ await extension.unload();
+
+ document.getElementById("tabmail").closeTab(tab);
+});
+
+/** Tests the messenger alias is available. */
+add_task(async function testExecuteScriptAlias() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let tab = await browser.tabs.query({ active: true });
+
+ await browser.tabs.executeScript(tab.id, {
+ code: `document.body.textContent = messenger.runtime.getManifest().applications.gecko.id;`,
+ });
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ applications: { gecko: { id: "content_scripts@mochitest" } },
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["*://mochi.test/*"],
+ },
+ });
+
+ let tab = window.openContentTab(CONTENT_PAGE);
+ await awaitBrowserLoaded(tab.browser, CONTENT_PAGE);
+
+ await extension.startup();
+
+ await extension.awaitFinish("finished");
+ await checkContent(tab.browser, { textContent: "content_scripts@mochitest" });
+
+ await extension.unload();
+
+ document.getElementById("tabmail").closeTab(tab);
+});
+
+/**
+ * Tests browser.contentScripts.register correctly adds CSS and JavaScript to
+ * message composition windows opened after it was called. Also tests calling
+ * `unregister` on the returned object.
+ */
+add_task(async function testRegister() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let registeredScript = await browser.contentScripts.register({
+ css: [{ code: "body { color: white }" }, { file: "test.css" }],
+ js: [
+ { code: `document.body.setAttribute("foo", "bar");` },
+ { file: "test.js" },
+ ],
+ matches: ["*://mochi.test/*"],
+ });
+ await window.sendMessage();
+
+ await registeredScript.unregister();
+ await window.sendMessage();
+
+ browser.test.notifyPass("finished");
+ },
+ "test.css": "body { background-color: green; }",
+ "test.js": () => {
+ document.body.textContent = "Hey look, the script ran!";
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["*://mochi.test/*"],
+ },
+ });
+
+ // Tab 1: loads before the script is registered.
+ let tab1 = window.openContentTab(CONTENT_PAGE + "?tab1");
+ await awaitBrowserLoaded(tab1.browser, CONTENT_PAGE + "?tab1");
+
+ await extension.startup();
+
+ await extension.awaitMessage(); // register
+ await checkContent(tab1.browser, UNCHANGED_VALUES);
+
+ // Tab 2: loads after the script is registered.
+ let tab2 = window.openContentTab(CONTENT_PAGE + "?tab2");
+ await awaitBrowserLoaded(tab2.browser, CONTENT_PAGE + "?tab2");
+ // Despite the fact we've just waited for the page to load, sometimes the
+ // content script mechanism gets triggered late. Wait a moment.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 1000));
+ await checkContent(tab2.browser, {
+ backgroundColor: "rgb(0, 128, 0)",
+ color: "rgb(255, 255, 255)",
+ foo: "bar",
+ textContent: "Hey look, the script ran!",
+ });
+
+ extension.sendMessage();
+ await extension.awaitMessage(); // unregister
+
+ await checkContent(tab2.browser, {
+ backgroundColor: "rgb(0, 128, 0)",
+ color: "rgb(255, 255, 255)",
+ foo: "bar",
+ textContent: "Hey look, the script ran!",
+ });
+
+ // Tab 3: loads after the script is unregistered.
+ let tab3 = window.openContentTab(CONTENT_PAGE + "?tab3");
+ await awaitBrowserLoaded(tab3.browser, CONTENT_PAGE + "?tab3");
+ await checkContent(tab3.browser, UNCHANGED_VALUES);
+
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ // Tab 2 should have the CSS removed.
+ await checkContent(tab2.browser, {
+ backgroundColor: UNCHANGED_VALUES.backgroundColor,
+ color: UNCHANGED_VALUES.color,
+ foo: "bar",
+ textContent: "Hey look, the script ran!",
+ });
+
+ let tabmail = document.getElementById("tabmail");
+ tabmail.closeOtherTabs(tabmail.tabInfo[0]);
+});
+
+/** Tests content_scripts in the manifest with permission work. */
+add_task(async function testManifest() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "test.css": "body { background-color: lime; }",
+ "test.js": () => {
+ document.body.textContent = "Hey look, the script ran!";
+ },
+ },
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["<all_urls>"],
+ css: ["test.css"],
+ js: ["test.js"],
+ },
+ ],
+ },
+ });
+
+ // Tab 1: loads before the script is registered.
+ let tab1 = window.openContentTab(CONTENT_PAGE + "?tab1");
+ await awaitBrowserLoaded(tab1.browser, CONTENT_PAGE + "?tab1");
+ // Despite the fact we've just waited for the page to load, sometimes the
+ // content script mechanism gets triggered late. Wait a moment.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 1000));
+
+ await extension.startup();
+
+ await checkContent(tab1.browser, UNCHANGED_VALUES);
+
+ // Tab 2: loads after the script is registered.
+ let tab2 = window.openContentTab(CONTENT_PAGE + "?tab2");
+ await awaitBrowserLoaded(tab2.browser, CONTENT_PAGE + "?tab2");
+ // Despite the fact we've just waited for the page to load, sometimes the
+ // content script mechanism gets triggered late. Wait a moment.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 1000));
+
+ await checkContent(tab2.browser, {
+ backgroundColor: "rgb(0, 255, 0)",
+ textContent: "Hey look, the script ran!",
+ });
+
+ await extension.unload();
+
+ // Tab 2 should have the CSS removed.
+ await checkContent(tab2.browser, {
+ backgroundColor: UNCHANGED_VALUES.backgroundColor,
+ textContent: "Hey look, the script ran!",
+ });
+
+ let tabmail = document.getElementById("tabmail");
+ tabmail.closeOtherTabs(tabmail.tabInfo[0]);
+});
+
+/** Tests content_scripts match patterns in the manifest. */
+add_task(async function testManifestNoPermissions() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "test.css": "body { background-color: red; }",
+ "test.js": () => {
+ document.body.textContent = "Hey look, the script ran!";
+ },
+ },
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["*://example.org/*"],
+ css: ["test.css"],
+ js: ["test.js"],
+ },
+ ],
+ },
+ });
+
+ await extension.startup();
+
+ let tab = window.openContentTab(CONTENT_PAGE);
+ await awaitBrowserLoaded(tab.browser, CONTENT_PAGE);
+ await checkContent(tab.browser, UNCHANGED_VALUES);
+
+ await extension.unload();
+
+ document.getElementById("tabmail").closeTab(tab);
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_content_handler.js b/comm/mail/components/extensions/test/browser/browser_ext_content_handler.js
new file mode 100644
index 0000000000..bfd4b1e787
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_content_handler.js
@@ -0,0 +1,334 @@
+/* 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 getCommonFiles = async () => {
+ return {
+ "utils.js": await getUtilsJS(),
+ "common.js": () => {
+ window.CreateTabPromise = class {
+ constructor() {
+ this.promise = new Promise(resolve => {
+ let createListener = tab => {
+ browser.tabs.onCreated.removeListener(createListener);
+ resolve(tab);
+ };
+ browser.tabs.onCreated.addListener(createListener);
+ });
+ }
+ async done() {
+ return this.promise;
+ }
+ };
+
+ window.UpdateTabPromise = class {
+ constructor(options) {
+ this.logWindowId = options?.logWindowId;
+ this.promise = new Promise(resolve => {
+ let updateLog = new Map();
+ let updateListener = (tabId, changes, tab) => {
+ let id = this.logWindowId ? tab.windowId : tabId;
+
+ if (changes?.url != "about:blank") {
+ let log = updateLog.get(id) || {};
+
+ if (changes.url) {
+ log.url = changes.url;
+ }
+ // The complete is only valid, if we have seen a url (which was
+ // not "about:blank")
+ if (log.url && changes?.status == "complete") {
+ log.complete = true;
+ }
+
+ updateLog.set(id, log);
+ if (log.url && log.complete) {
+ browser.tabs.onUpdated.removeListener(updateListener);
+ resolve(updateLog);
+ }
+ }
+ };
+ browser.tabs.onUpdated.addListener(updateListener);
+ });
+ }
+ async verify(id, url) {
+ // The updatePromise resolves after we have seen the "complete" state
+ // and a url.
+ let updateLog = await this.promise;
+ browser.test.assertEq(
+ 1,
+ updateLog.size,
+ `Should have seen exactly one tab being updated - ${JSON.stringify(
+ Array.from(updateLog)
+ )}`
+ );
+ browser.test.assertTrue(
+ updateLog.has(id),
+ `Updates must belong to the current tab ${id}`
+ );
+ browser.test.assertEq(
+ url,
+ updateLog.get(id).url,
+ "Should have seen the correct url loaded."
+ );
+ }
+ };
+ },
+ "background.js": async () => {
+ // Open a local extension page and click a handler link. They are all
+ // expected to open in a new tab.
+ let testSelectors = ["#link1", "#link2", "#link3", "#link4"];
+ for (let linkSelector of testSelectors) {
+ await window.expectLinkOpenInNewTab(
+ browser.runtime.getURL("test.html"),
+ linkSelector,
+ browser.runtime.getURL("handler.html#ext%2Btest%3Apayload")
+ );
+ }
+ browser.test.notifyPass();
+ },
+ "handler.html": `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <title>EXAMPLE</title>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ </head>
+ <body>
+ <p>This is an example page</p>
+ </body>
+ </html>`,
+ "test.html": `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <title>TEST</title>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ </head>
+ <body>
+ <ul>
+ <li><a id="link1" href="ext+test:payload">extension handler without target</a>
+ <li><a id="link2" href="ext+test:payload" target = "_self">extension handler with _self target</a>
+ <li><a id="link3" href="ext+test:payload" target = "_blank">extension handler with _blank target</a>
+ <li><a id="link4" href="ext+test:payload" target = "_other">extension handler with _other target</a>
+ </ul>
+ </body>
+ </html>`,
+ };
+};
+
+const subtest_clickInBrowser = async (extension, getBrowser) => {
+ async function clickLink(linkSelector, browser) {
+ await awaitBrowserLoaded(browser, url => url != "about:blank");
+ await synthesizeMouseAtCenterAndRetry(linkSelector, {}, browser);
+ }
+
+ await extension.startup();
+
+ let testSelectors = ["#link1", "#link2", "#link3", "#link4"];
+
+ for (let expectedSelector of testSelectors) {
+ // Wait for click on link (new tab)
+ let { linkSelector } = await extension.awaitMessage("click");
+ Assert.equal(
+ expectedSelector,
+ linkSelector,
+ `Test should click on the correct link.`
+ );
+ await clickLink(linkSelector, getBrowser());
+ await extension.sendMessage();
+ }
+
+ await extension.awaitFinish();
+ await extension.unload();
+};
+
+add_setup(async () => {
+ let account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ rootFolder.createSubfolder("test0", null);
+
+ let subFolders = {};
+ for (let folder of rootFolder.subFolders) {
+ subFolders[folder.name] = folder;
+ }
+ createMessages(subFolders.test0, 5);
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(subFolders.test0.URI);
+});
+
+add_task(async function test_tabs() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "tabFunctions.js": async () => {
+ let openTestTab = async url => {
+ let createdTestTab = new window.CreateTabPromise();
+ let updatedTestTab = new window.UpdateTabPromise();
+ let testTab = await browser.tabs.create({ url });
+ await createdTestTab.done();
+ await updatedTestTab.verify(testTab.id, url);
+ return testTab;
+ };
+
+ window.expectLinkOpenInNewTab = async (
+ testUrl,
+ linkSelector,
+ expectedUrl
+ ) => {
+ let testTab = await openTestTab(testUrl);
+
+ // Click a link in testTab to open a new tab.
+ let createdNewTab = new window.CreateTabPromise();
+ let updatedNewTab = new window.UpdateTabPromise();
+ await window.sendMessage("click", { linkSelector });
+ let createdTab = await createdNewTab.done();
+ await updatedNewTab.verify(createdTab.id, expectedUrl);
+
+ await browser.tabs.remove(createdTab.id);
+ await browser.tabs.remove(testTab.id);
+ };
+ },
+ ...(await getCommonFiles()),
+ },
+ manifest: {
+ background: {
+ scripts: ["utils.js", "common.js", "tabFunctions.js", "background.js"],
+ },
+ permissions: ["tabs"],
+ protocol_handlers: [
+ {
+ protocol: "ext+test",
+ name: "Protocol Handler Example",
+ uriTemplate: "/handler.html#%s",
+ },
+ ],
+ },
+ });
+
+ await subtest_clickInBrowser(
+ extension,
+ () => document.getElementById("tabmail").currentTabInfo.browser
+ );
+});
+
+add_task(async function test_windows() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "windowFunctions.js": async () => {
+ let openTestWin = async url => {
+ let createdTestTab = new window.CreateTabPromise();
+ let updatedTestTab = new window.UpdateTabPromise({
+ logWindowId: true,
+ });
+ let testWindow = await browser.windows.create({ type: "popup", url });
+ await createdTestTab.done();
+ await updatedTestTab.verify(testWindow.id, url);
+ return testWindow;
+ };
+
+ window.expectLinkOpenInNewTab = async (
+ testUrl,
+ linkSelector,
+ expectedUrl
+ ) => {
+ let testWindow = await openTestWin(testUrl);
+
+ // Click a link in testWindow to open a new tab.
+ let createdNewTab = new window.CreateTabPromise();
+ let updatedNewTab = new window.UpdateTabPromise();
+ await window.sendMessage("click", { linkSelector });
+ let createdTab = await createdNewTab.done();
+ await updatedNewTab.verify(createdTab.id, expectedUrl);
+
+ await browser.tabs.remove(createdTab.id);
+ await browser.windows.remove(testWindow.id);
+ };
+ },
+ ...(await getCommonFiles()),
+ },
+ manifest: {
+ background: {
+ scripts: [
+ "utils.js",
+ "common.js",
+ "windowFunctions.js",
+ "background.js",
+ ],
+ },
+ permissions: ["tabs"],
+ protocol_handlers: [
+ {
+ protocol: "ext+test",
+ name: "Protocol Handler Example",
+ uriTemplate: "/handler.html#%s",
+ },
+ ],
+ },
+ });
+
+ await subtest_clickInBrowser(
+ extension,
+ () => Services.wm.getMostRecentWindow("mail:extensionPopup").browser
+ );
+});
+
+add_task(async function test_mail3pane() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "mail3paneFunctions.js": async () => {
+ let updateTestTab = async url => {
+ let updatedTestTab = new window.UpdateTabPromise();
+ let mailTabs = await browser.tabs.query({ type: "mail" });
+ browser.test.assertEq(
+ 1,
+ mailTabs.length,
+ "Should find a single mailTab"
+ );
+ await browser.tabs.update(mailTabs[0].id, { url });
+ await updatedTestTab.verify(mailTabs[0].id, url);
+ return mailTabs[0];
+ };
+
+ window.expectLinkOpenInNewTab = async (
+ testUrl,
+ linkSelector,
+ expectedUrl
+ ) => {
+ await updateTestTab(testUrl);
+
+ // Click a link in testTab to open a new tab.
+ let createdNewTab = new window.CreateTabPromise();
+ let updatedNewTab = new window.UpdateTabPromise();
+ await window.sendMessage("click", { linkSelector });
+ let createdTab = await createdNewTab.done();
+ await updatedNewTab.verify(createdTab.id, expectedUrl);
+
+ await browser.tabs.remove(createdTab.id);
+ };
+ },
+ ...(await getCommonFiles()),
+ },
+ manifest: {
+ background: {
+ scripts: [
+ "utils.js",
+ "common.js",
+ "mail3paneFunctions.js",
+ "background.js",
+ ],
+ },
+ permissions: ["tabs"],
+ protocol_handlers: [
+ {
+ protocol: "ext+test",
+ name: "Protocol Handler Example",
+ uriTemplate: "/handler.html#%s",
+ },
+ ],
+ },
+ });
+
+ await subtest_clickInBrowser(
+ extension,
+ () => document.getElementById("tabmail").currentTabInfo.browser
+ );
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_content_tabs_navigation_menu.js b/comm/mail/components/extensions/test/browser/browser_ext_content_tabs_navigation_menu.js
new file mode 100644
index 0000000000..49d9340b3b
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_content_tabs_navigation_menu.js
@@ -0,0 +1,250 @@
+/* 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/. *
+ */
+
+// Load subscript shared with all menu tests.
+Services.scriptloader.loadSubScript(
+ new URL("head_menus.js", gTestPath).href,
+ this
+);
+
+const getCommonFiles = async () => {
+ return {
+ "utils.js": await getUtilsJS(),
+ "example.html": `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <title>EXAMPLE</title>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ </head>
+ <body>
+ <p id="description">This is text.</p>
+ </body>
+ </html>`,
+ "test.html": `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <title>TEST</title>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ </head>
+ <body>
+ <p id="description">This is text.</p>
+ <ul>
+ <li><a id="link" href="example.html">link to example page</a>
+ </ul>
+ </body>
+ </html>`,
+ };
+};
+
+const subtest_clickOpenInBrowserContextMenu = async (extension, getBrowser) => {
+ function waitForLoad(browser, expectedUrl) {
+ return awaitBrowserLoaded(browser, url => url.endsWith(expectedUrl));
+ }
+
+ async function testMenuNavItems(description, browser, expected) {
+ let menuId = browser.getAttribute("context");
+ let menu = browser.ownerGlobal.top.document.getElementById(menuId);
+ await rightClickOnContent(menu, "#description", browser);
+ for (let [key, value] of Object.entries(expected)) {
+ Assert.ok(
+ menu.querySelector(key),
+ `[${description}] ${key} menu item should exist`
+ );
+ switch (value) {
+ case "disabled":
+ case "enabled":
+ Assert.ok(
+ menu.querySelector(key).hasAttribute("disabled") ==
+ (value == "disabled"),
+ `[${description}] ${key} menu item should have the correct disabled state`
+ );
+ break;
+ case "hidden":
+ case "shown":
+ Assert.ok(
+ menu.querySelector(key).hidden == (value == "hidden"),
+ `[${description}] ${key} menu item should have the correct hidden state`
+ );
+ break;
+ }
+ }
+ // Wait a moment to make the test not fail.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => window.setTimeout(r, 125));
+ menu.hidePopup();
+ }
+
+ async function clickLink(browser) {
+ await synthesizeMouseAtCenterAndRetry("#link", {}, browser);
+ }
+
+ await extension.startup();
+
+ await extension.awaitMessage("contextClick");
+ let browser = getBrowser();
+
+ // Wait till test.html is fully loaded and check the state of the nav items.
+ await waitForLoad(browser, "test.html");
+ await testMenuNavItems("after initial load", browser, {
+ "#browserContext-back": browser.webNavigation.canGoBack
+ ? "enabled"
+ : "disabled",
+ "#browserContext-forward": "disabled",
+ "#browserContext-reload": "shown",
+ "#browserContext-stop": "hidden",
+ });
+
+ // Click on a link to load example.html and wait till page load has started.
+ // The navigation items should have the stop item shown.
+ let startLoadPromise = BrowserTestUtils.browserStarted(browser);
+ await clickLink(browser);
+ await startLoadPromise;
+ await testMenuNavItems("before link load", browser, {
+ "#browserContext-back": browser.webNavigation.canGoBack
+ ? "enabled"
+ : "disabled",
+ "#browserContext-forward": "disabled",
+ "#browserContext-reload": "hidden",
+ "#browserContext-stop": "shown",
+ });
+
+ // Wait till example.html is fully loaded and check the state of the nav
+ // items.
+ await waitForLoad(browser, "example.html");
+ await testMenuNavItems("after link load", browser, {
+ "#browserContext-back": "enabled",
+ "#browserContext-forward": "disabled",
+ "#browserContext-reload": "shown",
+ "#browserContext-stop": "hidden",
+ });
+
+ // Navigate back and wait till the load of test.html has started. The
+ // navigation items should have the stop item shown.
+ startLoadPromise = BrowserTestUtils.browserStarted(browser);
+ browser.webNavigation.goBack();
+ await startLoadPromise;
+ await testMenuNavItems("before navigate back load", browser, {
+ "#browserContext-back": "enabled",
+ "#browserContext-forward": "disabled",
+ "#browserContext-reload": "hidden",
+ "#browserContext-stop": "shown",
+ });
+
+ // Wait till test.html is fully loaded and check the state of the nav items.
+ await waitForLoad(browser, "test.html");
+ await testMenuNavItems("after navigate back load", browser, {
+ "#browserContext-back": browser.webNavigation.canGoBack
+ ? "enabled"
+ : "disabled",
+ "#browserContext-forward": "enabled",
+ "#browserContext-reload": "shown",
+ "#browserContext-stop": "hidden",
+ });
+
+ await extension.sendMessage();
+ await extension.awaitFinish();
+ await extension.unload();
+};
+
+add_setup(() => {
+ let account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ rootFolder.createSubfolder("test0", null);
+
+ let subFolders = {};
+ for (let folder of rootFolder.subFolders) {
+ subFolders[folder.name] = folder;
+ }
+ createMessages(subFolders.test0, 5);
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(subFolders.test0.URI);
+});
+
+add_task(async function test_tabs() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ const url = "test.html";
+ let testTab = await browser.tabs.create({ url });
+ await window.sendMessage("contextClick");
+ await browser.tabs.remove(testTab.id);
+
+ browser.test.notifyPass();
+ },
+ ...(await getCommonFiles()),
+ },
+ manifest: {
+ background: {
+ scripts: ["utils.js", "background.js"],
+ },
+ permissions: ["tabs"],
+ },
+ });
+
+ await subtest_clickOpenInBrowserContextMenu(
+ extension,
+ () => document.getElementById("tabmail").currentTabInfo.browser
+ );
+});
+
+add_task(async function test_windows() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ const url = "test.html";
+ let testWindow = await browser.windows.create({ type: "popup", url });
+ await window.sendMessage("contextClick");
+ await browser.windows.remove(testWindow.id);
+
+ browser.test.notifyPass();
+ },
+ ...(await getCommonFiles()),
+ },
+ manifest: {
+ background: {
+ scripts: ["utils.js", "background.js"],
+ },
+ permissions: ["tabs"],
+ },
+ });
+
+ await subtest_clickOpenInBrowserContextMenu(
+ extension,
+ () => Services.wm.getMostRecentWindow("mail:extensionPopup").browser
+ );
+});
+
+add_task(async function test_mail3pane() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ const url = "test.html";
+ let mailTabs = await browser.tabs.query({ type: "mail" });
+ browser.test.assertEq(
+ 1,
+ mailTabs.length,
+ "Should find a single mailTab"
+ );
+ await browser.tabs.update(mailTabs[0].id, { url });
+ await window.sendMessage("contextClick");
+
+ browser.test.notifyPass();
+ },
+ ...(await getCommonFiles()),
+ },
+ manifest: {
+ background: {
+ scripts: ["utils.js", "background.js"],
+ },
+ permissions: ["tabs"],
+ },
+ });
+
+ await subtest_clickOpenInBrowserContextMenu(
+ extension,
+ () => document.getElementById("tabmail").currentAbout3Pane.webBrowser
+ );
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_mailTabs.js b/comm/mail/components/extensions/test/browser/browser_ext_mailTabs.js
new file mode 100644
index 0000000000..7d0cf00e12
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_mailTabs.js
@@ -0,0 +1,898 @@
+/* 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 account, rootFolder, subFolders;
+let tabmail = document.getElementById("tabmail");
+
+add_setup(async () => {
+ account = createAccount();
+ rootFolder = account.incomingServer.rootFolder;
+ rootFolder.createSubfolder("test1", null);
+ rootFolder.createSubfolder("test2", null);
+ subFolders = {};
+ for (let folder of rootFolder.subFolders) {
+ subFolders[folder.name] = folder;
+ }
+ createMessages(subFolders.test1, 10);
+ createMessages(subFolders.test2, 50);
+
+ tabmail.currentTabInfo.folder = rootFolder;
+ tabmail.currentAbout3Pane.displayFolder(subFolders.test1.URI);
+ await ensure_table_view();
+
+ Services.prefs.setIntPref("extensions.webextensions.messagesPerPage", 10);
+ registerCleanupFunction(async () => {
+ await ensure_cards_view();
+ Services.prefs.clearUserPref("extensions.webextensions.messagesPerPage");
+ });
+ await new Promise(resolve => executeSoon(resolve));
+});
+
+add_task(async function test_update() {
+ async function background() {
+ async function checkCurrent(expected) {
+ let [current] = await browser.mailTabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ window.assertDeepEqual(expected, current);
+
+ // Check if getCurrent() returns the same.
+ let current2 = await browser.mailTabs.getCurrent();
+ window.assertDeepEqual(expected, current2);
+ }
+
+ let [accountId] = await window.waitForMessage();
+ let { folders } = await browser.accounts.get(accountId);
+
+ await browser.mailTabs.update({ displayedFolder: folders[0] });
+ let expected = {
+ sortType: "date",
+ sortOrder: "ascending",
+ viewType: "groupedByThread",
+ layout: "standard",
+ folderPaneVisible: true,
+ messagePaneVisible: true,
+ displayedFolder: folders[0],
+ };
+ delete expected.displayedFolder.subFolders;
+
+ await checkCurrent(expected);
+ await window.sendMessage("checkRealLayout", expected);
+ await window.sendMessage("checkRealSort", expected);
+ await window.sendMessage("checkRealView", expected);
+
+ expected.sortOrder = "descending";
+ for (let value of ["date", "subject", "author"]) {
+ await browser.mailTabs.update({
+ sortType: value,
+ sortOrder: "descending",
+ });
+ expected.sortType = value;
+ await window.sendMessage("checkRealSort", expected);
+ await window.sendMessage("checkRealView", expected);
+ }
+ expected.sortOrder = "ascending";
+ for (let value of ["author", "subject", "date"]) {
+ await browser.mailTabs.update({
+ sortType: value,
+ sortOrder: "ascending",
+ });
+ expected.sortType = value;
+ await window.sendMessage("checkRealSort", expected);
+ await window.sendMessage("checkRealView", expected);
+ }
+
+ for (let key of ["folderPaneVisible", "messagePaneVisible"]) {
+ for (let value of [false, true]) {
+ await browser.mailTabs.update({ [key]: value });
+ expected[key] = value;
+ await checkCurrent(expected);
+ await window.sendMessage("checkRealLayout", expected);
+ await window.sendMessage("checkRealView", expected);
+ }
+ }
+ for (let value of ["wide", "vertical", "standard"]) {
+ await browser.mailTabs.update({ layout: value });
+ expected.layout = value;
+ await checkCurrent(expected);
+ await window.sendMessage("checkRealLayout", expected);
+ await window.sendMessage("checkRealView", expected);
+ }
+
+ // Test all possible switch combination.
+ for (let viewType of [
+ "ungrouped",
+ "groupedByThread",
+ "ungrouped",
+ "groupedBySortType",
+ "groupedByThread",
+ "groupedBySortType",
+ "ungrouped",
+ ]) {
+ await browser.mailTabs.update({ viewType });
+ expected.viewType = viewType;
+ await checkCurrent(expected);
+ await window.sendMessage("checkRealLayout", expected);
+ await window.sendMessage("checkRealSort", expected);
+ await window.sendMessage("checkRealView", expected);
+ }
+
+ let selectedMessages = await browser.mailTabs.getSelectedMessages();
+ browser.test.assertEq(null, selectedMessages.id);
+ browser.test.assertEq(0, selectedMessages.messages.length);
+
+ browser.test.notifyPass("mailTabs");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ extension.onMessage("checkRealLayout", async expected => {
+ let intValue = ["standard", "wide", "vertical"].indexOf(expected.layout);
+ is(Services.prefs.getIntPref("mail.pane_config.dynamic"), intValue);
+ await check3PaneState(
+ expected.folderPaneVisible,
+ expected.messagePaneVisible
+ );
+ Assert.equal(
+ "/" + (tabmail.currentTabInfo.folder.URI || "").split("/").pop(),
+ expected.displayedFolder.path,
+ "Should display the correct folder"
+ );
+ extension.sendMessage();
+ });
+
+ extension.onMessage("checkRealSort", expected => {
+ const sortTypes = {
+ date: Ci.nsMsgViewSortType.byDate,
+ subject: Ci.nsMsgViewSortType.bySubject,
+ author: Ci.nsMsgViewSortType.byAuthor,
+ };
+
+ let { primarySortType, primarySortOrder } =
+ tabmail.currentAbout3Pane.gViewWrapper;
+
+ Assert.equal(
+ primarySortOrder,
+ Ci.nsMsgViewSortOrder[expected.sortOrder],
+ `sort order should be ${expected.sortOrder}`
+ );
+ Assert.equal(
+ primarySortType,
+ sortTypes[expected.sortType],
+ `sort type should be ${expected.sortType}`
+ );
+
+ extension.sendMessage();
+ });
+
+ extension.onMessage("checkRealView", expected => {
+ const viewTypes = {
+ groupedBySortType: {
+ showGroupedBySort: true,
+ showThreaded: false,
+ showUnthreaded: false,
+ },
+ groupedByThread: {
+ showGroupedBySort: false,
+ showThreaded: true,
+ showUnthreaded: false,
+ },
+ ungrouped: {
+ showGroupedBySort: false,
+ showThreaded: false,
+ showUnthreaded: true,
+ },
+ };
+
+ let { showThreaded, showUnthreaded, showGroupedBySort } =
+ tabmail.currentAbout3Pane.gViewWrapper;
+
+ Assert.equal(
+ showThreaded,
+ viewTypes[expected.viewType].showThreaded,
+ `Correct value for showThreaded for viewType <${expected.viewType}>`
+ );
+ Assert.equal(
+ showUnthreaded,
+ viewTypes[expected.viewType].showUnthreaded,
+ `Correct value for showUnthreaded for viewType <${expected.viewType}>`
+ );
+ Assert.equal(
+ showGroupedBySort,
+ viewTypes[expected.viewType].showGroupedBySort,
+ `Correct value for showGroupedBySort for viewType <${expected.viewType}>`
+ );
+ extension.sendMessage();
+ });
+
+ await check3PaneState(true, true);
+
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("mailTabs");
+ await extension.unload();
+
+ tabmail.currentTabInfo.folder = rootFolder;
+});
+
+add_task(async function test_displayedFolderChanged() {
+ async function background() {
+ let [accountId] = await window.waitForMessage();
+
+ let [current] = await browser.mailTabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ browser.test.assertEq(accountId, current.displayedFolder.accountId);
+ browser.test.assertEq("/", current.displayedFolder.path);
+
+ async function selectFolder(newFolderPath) {
+ let changeListener = window.waitForEvent(
+ "mailTabs.onDisplayedFolderChanged"
+ );
+ browser.test.sendMessage("selectFolder", newFolderPath);
+ let [tab, folder] = await changeListener;
+ browser.test.assertEq(current.id, tab.id);
+ browser.test.assertEq(accountId, folder.accountId);
+ browser.test.assertEq(newFolderPath, folder.path);
+ }
+ await selectFolder("/test1");
+ await selectFolder("/test2");
+ await selectFolder("/");
+
+ async function selectFolderByUpdate(newFolderPath) {
+ let changeListener = window.waitForEvent(
+ "mailTabs.onDisplayedFolderChanged"
+ );
+ browser.mailTabs.update({
+ displayedFolder: { accountId, path: newFolderPath },
+ });
+ let [tab, folder] = await changeListener;
+ browser.test.assertEq(current.id, tab.id);
+ browser.test.assertEq(accountId, folder.accountId);
+ browser.test.assertEq(newFolderPath, folder.path);
+ }
+ await selectFolderByUpdate("/test1");
+ await selectFolderByUpdate("/test2");
+ await selectFolderByUpdate("/");
+ await selectFolderByUpdate("/test1");
+
+ await new Promise(resolve => setTimeout(resolve));
+ browser.test.notifyPass("mailTabs");
+ }
+
+ let folderMap = new Map([
+ ["/", rootFolder],
+ ["/test1", subFolders.test1],
+ ["/test2", subFolders.test2],
+ ]);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ extension.onMessage("selectFolder", async newFolderPath => {
+ tabmail.currentTabInfo.folder = folderMap.get(newFolderPath);
+ await new Promise(resolve => executeSoon(resolve));
+ });
+
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("mailTabs");
+ await extension.unload();
+
+ tabmail.currentTabInfo.folder = rootFolder;
+});
+
+add_task(async function test_selectedMessagesChanged() {
+ async function background() {
+ function checkMessageList(expectedId, expectedCount, actual) {
+ if (expectedId) {
+ browser.test.assertEq(36, actual.id.length);
+ } else {
+ browser.test.assertEq(null, actual.id);
+ }
+ browser.test.assertEq(expectedCount, actual.messages.length);
+ }
+
+ // Because of bad design, we must wait for the WebExtensions mechanism to load ext-mailTabs.js,
+ // or when we call addListener below, it won't happen before the event is fired.
+ // This only applies if none of the earlier tests are run, but I'm saving you from wasting
+ // time figuring out what's going on like I did.
+ await browser.mailTabs.query({});
+
+ async function selectMessages(...newMessages) {
+ let selectPromise = window.waitForEvent(
+ "mailTabs.onSelectedMessagesChanged"
+ );
+ browser.test.sendMessage("selectMessage", newMessages);
+ let [, messageList] = await selectPromise;
+ return messageList;
+ }
+
+ let messageList;
+ messageList = await selectMessages(3);
+ checkMessageList(false, 1, messageList);
+ messageList = await selectMessages(7);
+ checkMessageList(false, 1, messageList);
+ messageList = await selectMessages(4, 6);
+ checkMessageList(false, 2, messageList);
+ messageList = await selectMessages();
+ checkMessageList(false, 0, messageList);
+ messageList = await selectMessages(
+ 2,
+ 3,
+ 5,
+ 7,
+ 11,
+ 13,
+ 17,
+ 19,
+ 23,
+ 29,
+ 31,
+ 37
+ );
+ checkMessageList(true, 10, messageList);
+ messageList = await browser.messages.continueList(messageList.id);
+ checkMessageList(false, 2, messageList);
+ messageList = await browser.mailTabs.getSelectedMessages();
+ checkMessageList(true, 10, messageList);
+ messageList = await browser.messages.continueList(messageList.id);
+ checkMessageList(false, 2, messageList);
+
+ await new Promise(resolve => setTimeout(resolve));
+ browser.test.notifyPass("mailTabs");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ tabmail.currentTabInfo.folder = subFolders.test2;
+ tabmail.currentTabInfo.messagePaneVisible = true;
+
+ extension.onMessage("selectMessage", newMessages => {
+ tabmail.currentAbout3Pane.threadTree.selectedIndices = newMessages;
+ });
+
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("mailTabs");
+ await extension.unload();
+
+ tabmail.currentTabInfo.folder = rootFolder;
+});
+
+add_task(async function test_background_tab() {
+ async function background() {
+ let [accountId] = await window.waitForMessage();
+ let { folders } = await browser.accounts.get(accountId);
+ let allTabs = await browser.tabs.query({});
+ let queryTabs = await browser.tabs.query({ mailTab: true });
+ let allMailTabs = await browser.mailTabs.query({});
+
+ browser.test.assertEq(4, allTabs.length);
+ browser.test.assertEq(2, queryTabs.length);
+ browser.test.assertEq(2, allMailTabs.length);
+
+ browser.test.assertEq(accountId, allMailTabs[0].displayedFolder.accountId);
+ browser.test.assertEq("/", allMailTabs[0].displayedFolder.path);
+
+ browser.test.assertEq(accountId, allMailTabs[1].displayedFolder.accountId);
+ browser.test.assertEq("/test1", allMailTabs[1].displayedFolder.path);
+ browser.test.assertTrue(allMailTabs[1].active);
+
+ // Check the initial state.
+ await window.sendMessage("checkRealLayout", {
+ messagePaneVisible: true,
+ folderPaneVisible: true,
+ displayedFolder: "/test1",
+ });
+
+ await browser.mailTabs.update(allMailTabs[0].id, {
+ folderPaneVisible: false,
+ messagePaneVisible: false,
+ displayedFolder: folders.find(f => f.name == "test2"),
+ });
+
+ // Should be in the same state, since we're updating a background tab.
+ await window.sendMessage("checkRealLayout", {
+ messagePaneVisible: true,
+ folderPaneVisible: true,
+ displayedFolder: "/test1",
+ });
+
+ allMailTabs = await browser.mailTabs.query({});
+ browser.test.assertEq(2, allMailTabs.length);
+
+ browser.test.assertEq(accountId, allMailTabs[0].displayedFolder.accountId);
+ browser.test.assertEq("/test2", allMailTabs[0].displayedFolder.path);
+
+ browser.test.assertEq(accountId, allMailTabs[1].displayedFolder.accountId);
+ browser.test.assertEq("/test1", allMailTabs[1].displayedFolder.path);
+ browser.test.assertTrue(allMailTabs[1].active);
+
+ // Switch to the other mail tab.
+ await browser.tabs.update(allMailTabs[0].id, { active: true });
+
+ // Should have changed to the updated state.
+ await window.sendMessage("checkRealLayout", {
+ messagePaneVisible: false,
+ folderPaneVisible: false,
+ displayedFolder: "/test2",
+ });
+
+ await browser.mailTabs.update(allMailTabs[0].id, {
+ folderPaneVisible: true,
+ messagePaneVisible: true,
+ });
+ await window.sendMessage("checkRealLayout", {
+ messagePaneVisible: true,
+ folderPaneVisible: true,
+ displayedFolder: "/test2",
+ });
+
+ // Switch back to the first mail tab.
+ await browser.tabs.update(allMailTabs[1].id, { active: true });
+
+ // Should be in the same state it was in.
+ await window.sendMessage("checkRealLayout", {
+ messagePaneVisible: true,
+ folderPaneVisible: true,
+ displayedFolder: "/test1",
+ });
+
+ browser.test.notifyPass("mailTabs");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead"],
+ },
+ });
+
+ extension.onMessage("checkRealLayout", async expected => {
+ await check3PaneState(
+ expected.folderPaneVisible,
+ expected.messagePaneVisible
+ );
+ Assert.equal(
+ "/" + (tabmail.currentTabInfo.folder.URI || "").split("/").pop(),
+ expected.displayedFolder,
+ "Should display the correct folder"
+ );
+ extension.sendMessage();
+ });
+
+ window.openContentTab("about:buildconfig");
+ window.openContentTab("about:mozilla");
+ tabmail.openTab("mail3PaneTab", { folderURI: subFolders.test1.URI });
+ await BrowserTestUtils.waitForEvent(
+ tabmail.currentTabInfo.chromeBrowser,
+ "folderURIChanged",
+ false,
+ event => event.detail == subFolders.test1.URI
+ );
+
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("mailTabs");
+ await extension.unload();
+
+ tabmail.closeOtherTabs(0);
+ tabmail.currentTabInfo.folder = rootFolder;
+});
+
+add_task(async function test_get_and_query() {
+ async function background() {
+ async function checkTab(expected) {
+ // Check mailTabs.get().
+ let mailTab = await browser.mailTabs.get(expected.tab.id);
+ browser.test.assertEq(expected.tab.id, mailTab.id);
+
+ // Check if a query for all tabs in the same window included the expected tab.
+ let mailTabs = await browser.mailTabs.query({
+ windowId: expected.tab.windowId,
+ });
+ let filteredMailTabs = mailTabs.filter(e => e.id == expected.tab.id);
+ browser.test.assertEq(1, filteredMailTabs.length);
+
+ // Check if a query for the current tab in the given window returns the current tab.
+ if (expected.isCurrentTab) {
+ let currentTabs = await browser.mailTabs.query({
+ active: true,
+ windowId: expected.tab.windowId,
+ });
+ browser.test.assertEq(1, currentTabs.length);
+ browser.test.assertEq(expected.tab.id, currentTabs[0].id);
+ }
+
+ // Check if a query for all tabs in the currentWindow includes the expected tab.
+ if (expected.isCurrentWindow) {
+ let mailTabsCurrentWindow = await browser.mailTabs.query({
+ currentWindow: true,
+ });
+ let filteredMailTabsCurrentWindow = mailTabsCurrentWindow.filter(
+ e => e.id == expected.tab.id
+ );
+ browser.test.assertEq(1, filteredMailTabsCurrentWindow.length);
+ }
+
+ // Check mailTabs.getCurrent() and mailTabs.query({ active: true, currentWindow: true })
+ if (expected.isCurrentTab && expected.isCurrentWindow) {
+ let currentTab = await browser.mailTabs.getCurrent();
+ browser.test.assertEq(expected.tab.id, currentTab.id);
+
+ let currentTabs = await browser.mailTabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ browser.test.assertEq(1, currentTabs.length);
+ browser.test.assertEq(expected.tab.id, currentTabs[0].id);
+ }
+ }
+
+ let [accountId] = await window.waitForMessage();
+ let allTabs = await browser.tabs.query({});
+ let queryMailTabs = await browser.tabs.query({ mailTab: true });
+ let allMailTabs = await browser.mailTabs.query({});
+
+ browser.test.assertEq(8, allTabs.length);
+ browser.test.assertEq(6, queryMailTabs.length);
+ browser.test.assertEq(6, allMailTabs.length);
+
+ // Each window has an active tab.
+ browser.test.assertTrue(allMailTabs[2].active);
+ browser.test.assertTrue(allMailTabs[5].active);
+
+ // Check tabs of window #1.
+ browser.test.assertEq(accountId, allMailTabs[0].displayedFolder.accountId);
+ browser.test.assertEq("/", allMailTabs[0].displayedFolder.path);
+ browser.test.assertEq(accountId, allMailTabs[1].displayedFolder.accountId);
+ browser.test.assertEq("/test1", allMailTabs[1].displayedFolder.path);
+ browser.test.assertEq(accountId, allMailTabs[2].displayedFolder.accountId);
+ browser.test.assertEq("/test2", allMailTabs[2].displayedFolder.path);
+ // Check tabs of window #2 (active).
+ browser.test.assertEq(accountId, allMailTabs[3].displayedFolder.accountId);
+ browser.test.assertEq("/", allMailTabs[3].displayedFolder.path);
+ browser.test.assertEq(accountId, allMailTabs[4].displayedFolder.accountId);
+ browser.test.assertEq("/test1", allMailTabs[4].displayedFolder.path);
+ browser.test.assertEq(accountId, allMailTabs[5].displayedFolder.accountId);
+ browser.test.assertEq("/test2", allMailTabs[5].displayedFolder.path);
+
+ for (let mailTab of allMailTabs) {
+ await checkTab({
+ tab: mailTab,
+ isCurrentTab: [allMailTabs[2].id, allMailTabs[5].id].includes(
+ mailTab.id
+ ),
+ isCurrentWindow: mailTab.windowId == allMailTabs[5].windowId,
+ });
+ }
+
+ // get(id) should throw if id does not belong to a mail tab.
+ for (let tab of [allTabs[1], allTabs[5]]) {
+ await browser.test.assertRejects(
+ browser.mailTabs.get(tab.id),
+ `Invalid mail tab ID: ${tab.id}`,
+ "It rejects for invalid mail tab ID."
+ );
+ }
+
+ // Switch to the second mail tab in both windows.
+ for (let tab of [allMailTabs[1], allMailTabs[4]]) {
+ await browser.tabs.update(tab.id, { active: true });
+ // Check if the new active tab is returned.
+ await checkTab({
+ tab,
+ isCurrentTab: true,
+ isCurrentWindow: tab.id == allMailTabs[5].id,
+ });
+ }
+
+ // Switch active window to a non-mailtab, getCurrent() and a query for active tab should not return anything.
+ await browser.tabs.update(allTabs[5].id, { active: true });
+ let activeMailTab = await browser.mailTabs.getCurrent();
+ browser.test.assertEq(undefined, activeMailTab);
+ let activeMailTabs = await browser.mailTabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ browser.test.assertEq(0, activeMailTabs.length);
+
+ // A query over all windows should still return the active tab from the inactive window.
+ activeMailTabs = await browser.mailTabs.query({
+ active: true,
+ });
+ browser.test.assertEq(1, activeMailTabs.length);
+ browser.test.assertEq(allMailTabs[1].id, activeMailTabs[0].id);
+
+ browser.test.notifyPass("mailTabs");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead"],
+ },
+ });
+
+ let window2 = await openNewMailWindow();
+ for (let win of [window, window2]) {
+ let winTabmail = win.document.getElementById("tabmail");
+ winTabmail.currentTabInfo.folder = rootFolder;
+ win.openContentTab("about:mozilla");
+ winTabmail.openTab("mail3PaneTab", { folderURI: subFolders.test1.URI });
+ await BrowserTestUtils.waitForEvent(
+ winTabmail.currentTabInfo.chromeBrowser,
+ "folderURIChanged",
+ false,
+ event => event.detail == subFolders.test1.URI
+ );
+ winTabmail.openTab("mail3PaneTab", { folderURI: subFolders.test2.URI });
+ await BrowserTestUtils.waitForEvent(
+ winTabmail.currentTabInfo.chromeBrowser,
+ "folderURIChanged",
+ false,
+ event => event.detail == subFolders.test2.URI
+ );
+ }
+
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("mailTabs");
+ await extension.unload();
+
+ await BrowserTestUtils.closeWindow(window2);
+
+ tabmail.closeOtherTabs(0);
+ tabmail.currentTabInfo.folder = rootFolder;
+});
+
+add_task(async function test_setSelectedMessages() {
+ async function background() {
+ let [accountId] = await window.waitForMessage();
+ let { folders } = await browser.accounts.get(accountId);
+ let allTabs = await browser.tabs.query({});
+ let queryTabs = await browser.tabs.query({ mailTab: true });
+ let allMailTabs = await browser.mailTabs.query({});
+
+ let { messages: messages1 } = await browser.messages.list(
+ folders.find(f => f.path == "/test1")
+ );
+ browser.test.assertTrue(
+ messages1.length > 7,
+ "There should be more than 7 messages in /test1"
+ );
+
+ let { messages: messages2 } = await browser.messages.list(
+ folders.find(f => f.path == "/test2")
+ );
+ browser.test.assertTrue(
+ messages2.length > 4,
+ "There should be more than 4 messages in /test2"
+ );
+
+ browser.test.assertEq(3, allMailTabs.length);
+ browser.test.assertEq(5, allTabs.length);
+ browser.test.assertEq(3, queryTabs.length);
+
+ let foregroundTab = allMailTabs[1].id;
+ browser.test.assertEq(accountId, allMailTabs[1].displayedFolder.accountId);
+ browser.test.assertEq("/test1", allMailTabs[1].displayedFolder.path);
+ browser.test.assertTrue(allMailTabs[1].active);
+
+ let backgroundTab = allMailTabs[2].id;
+ browser.test.assertEq(accountId, allMailTabs[2].displayedFolder.accountId);
+ browser.test.assertEq("/", allMailTabs[2].displayedFolder.path);
+
+ // Check the initial real state.
+ await window.sendMessage("checkRealLayout", {
+ messagePaneVisible: true,
+ folderPaneVisible: true,
+ displayedFolder: "/test1",
+ });
+
+ // Change the selection in the foreground tab.
+ await browser.mailTabs.setSelectedMessages(foregroundTab, [
+ messages1[6].id,
+ messages1[7].id,
+ ]);
+ // Check the current real state.
+ await window.sendMessage("checkRealLayout", {
+ messagePaneVisible: true,
+ folderPaneVisible: true,
+ displayedFolder: "/test1",
+ });
+ // Check API return value of the foreground tab.
+ let { messages: readMessagesA } =
+ await browser.mailTabs.getSelectedMessages(foregroundTab);
+ window.assertDeepEqual(
+ [messages1[6].id, messages1[7].id],
+ readMessagesA.map(m => m.id)
+ );
+
+ // Change the selection in the background tab.
+ await browser.mailTabs.setSelectedMessages(backgroundTab, [
+ messages2[0].id,
+ messages2[3].id,
+ ]);
+ // Real state should be the same, since we're updating a background tab.
+ await window.sendMessage("checkRealLayout", {
+ messagePaneVisible: true,
+ folderPaneVisible: true,
+ displayedFolder: "/test1",
+ });
+ // Check unchanged API return value of the foreground tab.
+ let { messages: readMessagesB } =
+ await browser.mailTabs.getSelectedMessages(foregroundTab);
+ window.assertDeepEqual(
+ [messages1[6].id, messages1[7].id],
+ readMessagesB.map(m => m.id)
+ );
+ // Check API return value of the inactive background tab.
+ let { messages: readMessagesC } =
+ await browser.mailTabs.getSelectedMessages(backgroundTab);
+ window.assertDeepEqual(
+ [messages2[0].id, messages2[3].id],
+ readMessagesC.map(m => m.id)
+ );
+ // Switch to the background tab.
+ await browser.tabs.update(backgroundTab, { active: true });
+ // Check API return value of the background tab (now active).
+ let { messages: readMessagesD } =
+ await browser.mailTabs.getSelectedMessages(backgroundTab);
+ window.assertDeepEqual(
+ [messages2[0].id, messages2[3].id],
+ readMessagesD.map(m => m.id)
+ );
+ // Check real state, should now match the active background tab.
+ await window.sendMessage("checkRealLayout", {
+ messagePaneVisible: true,
+ folderPaneVisible: true,
+ displayedFolder: "/test2",
+ });
+ // Check unchanged API return value of the foreground tab (now inactive).
+ let { messages: readMessagesE } =
+ await browser.mailTabs.getSelectedMessages(foregroundTab);
+ window.assertDeepEqual(
+ [messages1[6].id, messages1[7].id],
+ readMessagesE.map(m => m.id)
+ );
+ // Switch back to the foreground tab.
+ await browser.tabs.update(foregroundTab, { active: true });
+
+ // Change the selection in the foreground tab.
+ await browser.mailTabs.setSelectedMessages(foregroundTab, [
+ messages2[2].id,
+ messages2[4].id,
+ ]);
+ // Check API return value of the foreground tab.
+ let { messages: readMessagesF } =
+ await browser.mailTabs.getSelectedMessages(foregroundTab);
+ window.assertDeepEqual(
+ [messages2[2].id, messages2[4].id],
+ readMessagesF.map(m => m.id)
+ );
+ // Check real state.
+ await window.sendMessage("checkRealLayout", {
+ messagePaneVisible: true,
+ folderPaneVisible: true,
+ displayedFolder: "/test2",
+ });
+ // Check API return value of the inactive background tab.
+ let { messages: readMessagesG } =
+ await browser.mailTabs.getSelectedMessages(backgroundTab);
+ window.assertDeepEqual(
+ [messages2[0].id, messages2[3].id],
+ readMessagesG.map(m => m.id)
+ );
+
+ // Clear selection in background tab.
+ await browser.mailTabs.setSelectedMessages(backgroundTab, []);
+ // Check API return value of the inactive background tab.
+ let { messages: readMessagesH } =
+ await browser.mailTabs.getSelectedMessages(backgroundTab);
+ browser.test.assertEq(0, readMessagesH.length);
+
+ // Clear selection in foreground tab.
+ await browser.mailTabs.setSelectedMessages(foregroundTab, []);
+ // Check API return value of the foreground tab.
+ let { messages: readMessagesI } =
+ await browser.mailTabs.getSelectedMessages(foregroundTab);
+ browser.test.assertEq(0, readMessagesI.length);
+
+ // Should throw if messages belong to different folders.
+ await browser.test.assertRejects(
+ browser.mailTabs.setSelectedMessages(foregroundTab, [
+ messages2[2].id,
+ messages1[4].id,
+ ]),
+ `Message ${messages2[2].id} and message ${messages1[4].id} are not in the same folder, cannot select them both.`,
+ "browser.mailTabs.setSelectedMessages() should reject, if the requested message do not belong to the same folder."
+ );
+
+ browser.test.notifyPass("mailTabs");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ extension.onMessage("checkRealLayout", async expected => {
+ await check3PaneState(
+ expected.folderPaneVisible,
+ expected.messagePaneVisible
+ );
+ Assert.equal(
+ "/" + (tabmail.currentTabInfo.folder.URI || "").split("/").pop(),
+ expected.displayedFolder,
+ "Should display the correct folder"
+ );
+ extension.sendMessage();
+ });
+
+ window.openContentTab("about:buildconfig");
+ window.openContentTab("about:mozilla");
+ tabmail.openTab("mail3PaneTab", { folderURI: subFolders.test1.URI });
+ tabmail.openTab("mail3PaneTab", {
+ folderURI: rootFolder.URI,
+ background: true,
+ });
+ await BrowserTestUtils.waitForEvent(
+ tabmail.currentTabInfo.chromeBrowser,
+ "folderURIChanged",
+ false,
+ event => event.detail == subFolders.test1.URI
+ );
+
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("mailTabs");
+ await extension.unload();
+
+ tabmail.closeOtherTabs(0);
+ tabmail.currentTabInfo.folder = rootFolder;
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_mailTabs_mv3.js b/comm/mail/components/extensions/test/browser/browser_ext_mailTabs_mv3.js
new file mode 100644
index 0000000000..5cacd6e771
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_mailTabs_mv3.js
@@ -0,0 +1,162 @@
+/* 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 account, rootFolder, subFolders;
+let tabmail = document.getElementById("tabmail");
+
+add_setup(async () => {
+ account = createAccount();
+ rootFolder = account.incomingServer.rootFolder;
+ rootFolder.createSubfolder("test1", null);
+ rootFolder.createSubfolder("test2", null);
+ subFolders = {};
+ for (let folder of rootFolder.subFolders) {
+ subFolders[folder.name] = folder;
+ }
+ createMessages(subFolders.test1, 10);
+ createMessages(subFolders.test2, 50);
+
+ tabmail.currentTabInfo.folder = rootFolder;
+
+ Services.prefs.setIntPref("extensions.webextensions.messagesPerPage", 10);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("extensions.webextensions.messagesPerPage");
+ });
+ await new Promise(resolve => executeSoon(resolve));
+});
+
+add_task(async function test_MV3_event_pages() {
+ let files = {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, hasFired is set to false. In
+ // case of a wake-up, the first fired event is the one that woke up the background.
+ let hasFired = false;
+
+ for (let eventName of [
+ "onDisplayedFolderChanged",
+ "onSelectedMessagesChanged",
+ ]) {
+ browser.mailTabs[eventName].addListener((...args) => {
+ // Only send the first event after background wake-up, this should be
+ // the only one expected.
+ if (!hasFired) {
+ hasFired = true;
+ browser.test.sendMessage(`${eventName} received`, args);
+ }
+ });
+ }
+
+ browser.test.sendMessage("background started");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ manifest_version: 3,
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ browser_specific_settings: {
+ gecko: { id: "mailtabs@mochi.test" },
+ },
+ },
+ });
+
+ function checkPersistentListeners({ primed }) {
+ // A persistent event is referenced by its moduleName as defined in
+ // ext-mails.json, not by its actual namespace.
+ const persistent_events = [
+ "mailTabs.onDisplayedFolderChanged",
+ "mailTabs.onSelectedMessagesChanged",
+ ];
+
+ for (let event of persistent_events) {
+ let [moduleName, eventName] = event.split(".");
+ assertPersistentListeners(extension, moduleName, eventName, {
+ primed,
+ });
+ }
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ // Select a folder.
+
+ {
+ tabmail.currentTabInfo.folder = subFolders.test1;
+ let displayInfo = await extension.awaitMessage(
+ "onDisplayedFolderChanged received"
+ );
+ Assert.deepEqual(
+ [
+ {
+ active: true,
+ type: "mail",
+ },
+ { name: "test1", path: "/test1" },
+ ],
+ [
+ {
+ active: displayInfo[0].active,
+ type: displayInfo[0].type,
+ },
+ { name: displayInfo[1].name, path: displayInfo[1].path },
+ ],
+ "The primed onDisplayedFolderChanged event should return the correct values"
+ );
+
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+ }
+
+ // Select multiple messages.
+
+ {
+ let messages = [...subFolders.test1.messages].slice(0, 5);
+ tabmail.currentAbout3Pane.threadTree.selectedIndices = messages.map(m =>
+ tabmail.currentAbout3Pane.gDBView.findIndexOfMsgHdr(m, false)
+ );
+ let displayInfo = await extension.awaitMessage(
+ "onSelectedMessagesChanged received"
+ );
+ Assert.deepEqual(
+ [
+ "Big Meeting Today",
+ "Small Party Tomorrow",
+ "Huge Shindig Yesterday",
+ "Tiny Wedding In a Fortnight",
+ "Red Document Needs Attention",
+ ],
+ displayInfo[1].messages.map(e => e.subject),
+ "The primed onSelectedMessagesChanged event should return the correct values"
+ );
+ Assert.deepEqual(
+ {
+ active: true,
+ type: "mail",
+ },
+ {
+ active: displayInfo[0].active,
+ type: displayInfo[0].type,
+ },
+ "The primed onSelectedMessagesChanged event should return the correct values"
+ );
+
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+ }
+
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_menus_context_action.js b/comm/mail/components/extensions/test/browser/browser_ext_menus_context_action.js
new file mode 100644
index 0000000000..03e255e5eb
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_menus_context_action.js
@@ -0,0 +1,424 @@
+/* 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/. */
+
+// Load subscript shared with all menu tests.
+Services.scriptloader.loadSubScript(
+ new URL("head_menus.js", gTestPath).href,
+ this
+);
+
+let gAccount, gFolders, gMessage;
+add_setup(async () => {
+ await Services.search.init();
+
+ gAccount = createAccount();
+ addIdentity(gAccount);
+ gFolders = gAccount.incomingServer.rootFolder.subFolders;
+ createMessages(gFolders[0], {
+ count: 1,
+ body: {
+ contentType: "text/html",
+ body: await fetch(`${URL_BASE}/content.html`).then(r => r.text()),
+ },
+ });
+ gMessage = [...gFolders[0].messages][0];
+
+ document.getElementById("tabmail").currentAbout3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: gAccount.incomingServer.rootFolder.URI,
+ });
+
+ await enforceState({
+ mail: ["write-message", "spacer", "search-bar", "spacer"],
+ });
+ registerCleanupFunction(async () => {
+ await enforceState({});
+ });
+});
+
+async function subtest_action_menu(
+ testWindow,
+ target,
+ expectedInfo,
+ expectedTab,
+ manifest
+) {
+ function checkVisibility(menu, visible) {
+ let removeExtension = menu.querySelector(
+ ".customize-context-removeExtension"
+ );
+ let manageExtension = menu.querySelector(
+ ".customize-context-manageExtension"
+ );
+
+ info(`Check visibility: ${visible}`);
+ is(!removeExtension.hidden, visible, "Remove Extension should be visible");
+ is(!manageExtension.hidden, visible, "Manage Extension should be visible");
+ }
+
+ async function testContextMenuRemoveExtension(extension, menu, element) {
+ let name = "Generated extension";
+ let brand = Services.strings
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandShorterName");
+
+ info(
+ `Choosing 'Remove Extension' in ${menu.id} should show confirm dialog.`
+ );
+ await rightClick(menu, element);
+ await extension.awaitMessage("onShown");
+ let removeExtension = menu.querySelector(
+ ".customize-context-removeExtension"
+ );
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ let promptPromise = BrowserTestUtils.promiseAlertDialog(
+ undefined,
+ undefined,
+ {
+ async callback(promptWindow) {
+ await TestUtils.waitForCondition(
+ () => Services.focus.activeWindow == promptWindow,
+ "waiting for prompt to become active"
+ );
+
+ let promptDocument = promptWindow.document;
+ // Check if the correct add-on is being removed.
+ is(promptDocument.title, `Remove ${name}?`);
+ if (
+ !Services.prefs.getBoolPref("prompts.windowPromptSubDialog", false)
+ ) {
+ is(
+ promptDocument.getElementById("infoBody").textContent,
+ `Remove ${name} as well as its configuration and data from ${brand}?`
+ );
+ }
+ let acceptButton = promptDocument
+ .querySelector("dialog")
+ .getButton("accept");
+ is(acceptButton.label, "Remove");
+ EventUtils.synthesizeMouseAtCenter(acceptButton, {}, promptWindow);
+ },
+ }
+ );
+ menu.activateItem(removeExtension);
+ await hiddenPromise;
+ await promptPromise;
+ }
+
+ async function testContextMenuManageExtension(extension, menu, element) {
+ let id = "menus@mochi.test";
+ let tabmail = window.document.getElementById("tabmail");
+
+ info(
+ `Choosing 'Manage Extension' in ${menu.id} should load the management page.`
+ );
+ await rightClick(menu, element);
+ await extension.awaitMessage("onShown");
+ let manageExtension = menu.querySelector(
+ ".customize-context-manageExtension"
+ );
+ let addonManagerPromise = contentTabOpenPromise(tabmail, "about:addons");
+ menu.activateItem(manageExtension);
+ let managerTab = await addonManagerPromise;
+
+ // Check the UI to make sure that the correct view is loaded.
+ let managerWindow = managerTab.linkedBrowser.contentWindow;
+ is(
+ managerWindow.gViewController.currentViewId,
+ `addons://detail/${encodeURIComponent(id)}`,
+ "Expected extension details view in about:addons"
+ );
+ // In HTML about:addons, the default view does not show the inline
+ // options browser, so we should not receive an "options-loaded" event.
+ // (if we do, the test will fail due to the unexpected message).
+
+ is(managerTab.linkedBrowser.currentURI.spec, "about:addons");
+ tabmail.closeTab(managerTab);
+ }
+
+ let extension = await getMenuExtension(manifest);
+
+ await extension.startup();
+ await extension.awaitMessage("menus-created");
+
+ let element = testWindow.document.querySelector(target.elementSelector);
+ let menu = testWindow.document.getElementById(target.menuId);
+
+ await rightClick(menu, element);
+ await checkVisibility(menu, true);
+ await checkShownEvent(
+ extension,
+ { menuIds: [target.context], contexts: [target.context, "all"] },
+ expectedTab
+ );
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ let clickedPromise = checkClickedEvent(extension, expectedInfo, expectedTab);
+ menu.activateItem(
+ menu.querySelector(`#menus_mochi_test-menuitem-_${target.context}`)
+ );
+ await clickedPromise;
+ await hiddenPromise;
+
+ // Test the non actionButton element for visibility of the management menu entries.
+ if (target.nonActionButtonSelector) {
+ let nonActionButtonElement = testWindow.document.querySelector(
+ target.nonActionButtonSelector
+ );
+ await rightClick(menu, nonActionButtonElement);
+ await checkVisibility(menu, false);
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.hidePopup();
+ await hiddenPromise;
+ }
+
+ await testContextMenuManageExtension(extension, menu, element);
+ await testContextMenuRemoveExtension(extension, menu, element);
+ await extension.unload();
+}
+add_task(async function test_browser_action_menu_mv2() {
+ await subtest_action_menu(
+ window,
+ {
+ menuId: "unifiedToolbarMenu",
+ elementSelector: `.unified-toolbar [extension="menus@mochi.test"]`,
+ context: "browser_action",
+ nonActionButtonSelector: `.unified-toolbar .write-message button`,
+ },
+ {
+ menuItemId: "browser_action",
+ },
+ { active: true, index: 0, mailTab: true },
+ {
+ manifest_version: 2,
+ browser_action: {
+ default_title: "This is a test",
+ },
+ }
+ );
+});
+add_task(async function test_message_display_action_menu_pane_mv2() {
+ let tab = await openMessageInTab(gMessage);
+ // No check for menu entries in nonActionButtonElements as the header-toolbar
+ // does not have a context menu associated.
+ await subtest_action_menu(
+ tab.chromeBrowser.contentWindow,
+ {
+ menuId: "header-toolbar-context-menu",
+ elementSelector: "#menus_mochi_test-messageDisplayAction-toolbarbutton",
+ context: "message_display_action",
+ },
+ {
+ pageUrl: /^mailbox\:/,
+ menuItemId: "message_display_action",
+ },
+ { active: true, index: 1, mailTab: false },
+ {
+ manifest_version: 2,
+ message_display_action: {
+ default_title: "This is a test",
+ },
+ }
+ );
+ window.document.getElementById("tabmail").closeTab(tab);
+});
+add_task(async function test_message_display_action_menu_window_mv2() {
+ let testWindow = await openMessageInWindow(gMessage);
+ await focusWindow(testWindow);
+ // No check for menu entries in nonActionButtonElements as the header-toolbar
+ // does not have a context menu associated.
+ await subtest_action_menu(
+ testWindow.messageBrowser.contentWindow,
+ {
+ menuId: "header-toolbar-context-menu",
+ elementSelector: "#menus_mochi_test-messageDisplayAction-toolbarbutton",
+ context: "message_display_action",
+ },
+ {
+ pageUrl: /^mailbox\:/,
+ menuItemId: "message_display_action",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 2,
+ message_display_action: {
+ default_title: "This is a test",
+ },
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+});
+add_task(async function test_compose_action_menu_mv2() {
+ let testWindow = await openComposeWindow(gAccount);
+ await focusWindow(testWindow);
+ await subtest_action_menu(
+ testWindow,
+ {
+ menuId: "toolbar-context-menu",
+ elementSelector: "#menus_mochi_test-composeAction-toolbarbutton",
+ context: "compose_action",
+ nonActionButtonSelector: "#button-attach",
+ },
+ {
+ pageUrl: "about:blank?compose",
+ menuItemId: "compose_action",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 2,
+ compose_action: {
+ default_title: "This is a test",
+ },
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+});
+add_task(async function test_compose_action_menu_formattoolbar_mv2() {
+ let testWindow = await openComposeWindow(gAccount);
+ await focusWindow(testWindow);
+ await subtest_action_menu(
+ testWindow,
+ {
+ menuId: "format-toolbar-context-menu",
+ elementSelector: "#menus_mochi_test-composeAction-toolbarbutton",
+ context: "compose_action",
+ },
+ {
+ pageUrl: "about:blank?compose",
+ menuItemId: "compose_action",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 2,
+ compose_action: {
+ default_title: "This is a test",
+ default_area: "formattoolbar",
+ },
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+});
+
+add_task(async function test_browser_action_menu_mv3() {
+ await subtest_action_menu(
+ window,
+ {
+ menuId: "unifiedToolbarMenu",
+ elementSelector: `.unified-toolbar [extension="menus@mochi.test"]`,
+ context: "action",
+ nonActionButtonSelector: `.unified-toolbar .write-message button`,
+ },
+ {
+ menuItemId: "action",
+ },
+ { active: true, index: 0, mailTab: true },
+ {
+ manifest_version: 3,
+ action: {
+ default_title: "This is a test",
+ },
+ }
+ );
+});
+add_task(async function test_message_display_action_menu_pane_mv3() {
+ let tab = await openMessageInTab(gMessage);
+ // No check for menu entries in nonActionButtonElements as the header-toolbar
+ // does not have a context menu associated.
+ await subtest_action_menu(
+ tab.chromeBrowser.contentWindow,
+ {
+ menuId: "header-toolbar-context-menu",
+ elementSelector: "#menus_mochi_test-messageDisplayAction-toolbarbutton",
+ context: "message_display_action",
+ },
+ {
+ pageUrl: /^mailbox\:/,
+ menuItemId: "message_display_action",
+ },
+ { active: true, index: 1, mailTab: false },
+ {
+ manifest_version: 3,
+ message_display_action: {
+ default_title: "This is a test",
+ },
+ }
+ );
+ window.document.getElementById("tabmail").closeTab(tab);
+});
+add_task(async function test_message_display_action_menu_window_mv3() {
+ let testWindow = await openMessageInWindow(gMessage);
+ await focusWindow(testWindow);
+ // No check for menu entries in nonActionButtonElements as the header-toolbar
+ // does not have a context menu associated.
+ await subtest_action_menu(
+ testWindow.messageBrowser.contentWindow,
+ {
+ menuId: "header-toolbar-context-menu",
+ elementSelector: "#menus_mochi_test-messageDisplayAction-toolbarbutton",
+ context: "message_display_action",
+ },
+ {
+ pageUrl: /^mailbox\:/,
+ menuItemId: "message_display_action",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 3,
+ message_display_action: {
+ default_title: "This is a test",
+ },
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+});
+add_task(async function test_compose_action_menu_mv3() {
+ let testWindow = await openComposeWindow(gAccount);
+ await focusWindow(testWindow);
+ await subtest_action_menu(
+ testWindow,
+ {
+ menuId: "toolbar-context-menu",
+ elementSelector: "#menus_mochi_test-composeAction-toolbarbutton",
+ context: "compose_action",
+ nonActionButtonSelector: "#button-attach",
+ },
+ {
+ pageUrl: "about:blank?compose",
+ menuItemId: "compose_action",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 3,
+ compose_action: {
+ default_title: "This is a test",
+ },
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+});
+add_task(async function test_compose_action_menu_formattoolbar_mv3() {
+ let testWindow = await openComposeWindow(gAccount);
+ await focusWindow(testWindow);
+ await subtest_action_menu(
+ testWindow,
+ {
+ menuId: "format-toolbar-context-menu",
+ elementSelector: "#menus_mochi_test-composeAction-toolbarbutton",
+ context: "compose_action",
+ },
+ {
+ pageUrl: "about:blank?compose",
+ menuItemId: "compose_action",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 3,
+ compose_action: {
+ default_title: "This is a test",
+ default_area: "formattoolbar",
+ },
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_menus_context_compose.js b/comm/mail/components/extensions/test/browser/browser_ext_menus_context_compose.js
new file mode 100644
index 0000000000..6768f5dd60
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_menus_context_compose.js
@@ -0,0 +1,179 @@
+/* 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/. */
+
+// Load subscript shared with all menu tests.
+Services.scriptloader.loadSubScript(
+ new URL("head_menus.js", gTestPath).href,
+ this
+);
+
+let gAccount, gFolders, gMessage;
+add_setup(async () => {
+ await Services.search.init();
+
+ gAccount = createAccount();
+ addIdentity(gAccount);
+ gFolders = gAccount.incomingServer.rootFolder.subFolders;
+ createMessages(gFolders[0], {
+ count: 1,
+ body: {
+ contentType: "text/html",
+ body: await fetch(`${URL_BASE}/content.html`).then(r => r.text()),
+ },
+ });
+ gMessage = [...gFolders[0].messages][0];
+
+ document.getElementById("tabmail").currentAbout3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: gAccount.incomingServer.rootFolder.URI,
+ });
+});
+
+async function subtest_compose(manifest) {
+ let extension = await getMenuExtension(manifest);
+
+ await extension.startup();
+ await extension.awaitMessage("menus-created");
+
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ params.composeFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ params.composeFields.body = await fetch(`${URL_BASE}/content_body.html`).then(
+ r => r.text()
+ );
+
+ for (let ordinal of ["first", "second", "third", "fourth"]) {
+ let attachment = Cc[
+ "@mozilla.org/messengercompose/attachment;1"
+ ].createInstance(Ci.nsIMsgAttachment);
+ attachment.name = `${ordinal}.txt`;
+ attachment.url = `data:text/plain,I'm the ${ordinal} attachment!`;
+ attachment.size = attachment.url.length - 16;
+ params.composeFields.addAttachment(attachment);
+ }
+
+ let composeWindowPromise = BrowserTestUtils.domWindowOpened();
+ MailServices.compose.OpenComposeWindowWithParams(null, params);
+ let composeWindow = await composeWindowPromise;
+ await BrowserTestUtils.waitForEvent(composeWindow, "compose-editor-ready");
+ let composeDocument = composeWindow.document;
+ await focusWindow(composeWindow);
+
+ info("Test the message being composed.");
+
+ let messagePane = composeWindow.GetCurrentEditorElement();
+
+ await subtest_compose_body(
+ extension,
+ manifest.permissions?.includes("compose"),
+ messagePane,
+ "about:blank?compose",
+ {
+ active: true,
+ index: 0,
+ mailTab: false,
+ }
+ );
+
+ const chromeElementsMap = {
+ msgSubject: "composeSubject",
+ toAddrInput: "composeTo",
+ };
+ for (let elementId of Object.keys(chromeElementsMap)) {
+ info(`Test element ${elementId}.`);
+ await subtest_element(
+ extension,
+ manifest.permissions?.includes("compose"),
+ composeWindow.document.getElementById(elementId),
+ "about:blank?compose",
+ {
+ active: true,
+ index: 0,
+ mailTab: false,
+ fieldId: chromeElementsMap[elementId],
+ }
+ );
+ }
+
+ info("Test the attachments context menu.");
+
+ composeWindow.toggleAttachmentPane("show");
+ let menu = composeDocument.getElementById("msgComposeAttachmentItemContext");
+ let attachmentBucket = composeDocument.getElementById("attachmentBucket");
+
+ EventUtils.synthesizeMouseAtCenter(
+ attachmentBucket.itemChildren[0],
+ {},
+ composeWindow
+ );
+ await rightClick(menu, attachmentBucket.itemChildren[0], composeWindow);
+ Assert.ok(
+ menu.querySelector("#menus_mochi_test-menuitem-_compose_attachments")
+ );
+ menu.hidePopup();
+
+ await checkShownEvent(
+ extension,
+ {
+ menuIds: ["compose_attachments"],
+ contexts: ["compose_attachments", "all"],
+ attachments: manifest.permissions?.includes("compose")
+ ? [{ name: "first.txt", size: 25 }]
+ : undefined,
+ },
+ { active: true, index: 0, mailTab: false }
+ );
+
+ attachmentBucket.addItemToSelection(attachmentBucket.itemChildren[3]);
+ await rightClick(menu, attachmentBucket.itemChildren[0], composeWindow);
+ Assert.ok(
+ menu.querySelector("#menus_mochi_test-menuitem-_compose_attachments")
+ );
+ menu.hidePopup();
+
+ await checkShownEvent(
+ extension,
+ {
+ menuIds: ["compose_attachments"],
+ contexts: ["compose_attachments", "all"],
+ attachments: manifest.permissions?.includes("compose")
+ ? [
+ { name: "first.txt", size: 25 },
+ { name: "fourth.txt", size: 26 },
+ ]
+ : undefined,
+ },
+ { active: true, index: 0, mailTab: false }
+ );
+
+ await extension.unload();
+
+ await BrowserTestUtils.closeWindow(composeWindow);
+}
+add_task(async function test_compose_mv2() {
+ return subtest_compose({
+ manifest_version: 2,
+ permissions: ["compose"],
+ });
+});
+add_task(async function test_compose_no_permissions_mv2() {
+ return subtest_compose({
+ manifest_version: 2,
+ });
+});
+add_task(async function test_compose_mv3() {
+ return subtest_compose({
+ manifest_version: 3,
+ permissions: ["compose"],
+ });
+});
+add_task(async function test_compose_no_permissions_mv3() {
+ return subtest_compose({
+ manifest_version: 3,
+ });
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_menus_context_content.js b/comm/mail/components/extensions/test/browser/browser_ext_menus_context_content.js
new file mode 100644
index 0000000000..27aac6d5d7
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_menus_context_content.js
@@ -0,0 +1,253 @@
+/* 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/. */
+
+// Load subscript shared with all menu tests.
+Services.scriptloader.loadSubScript(
+ new URL("head_menus.js", gTestPath).href,
+ this
+);
+
+let gAccount, gFolders, gMessage;
+add_setup(async () => {
+ await Services.search.init();
+
+ gAccount = createAccount();
+ addIdentity(gAccount);
+ gFolders = gAccount.incomingServer.rootFolder.subFolders;
+ createMessages(gFolders[0], {
+ count: 1,
+ body: {
+ contentType: "text/html",
+ body: await fetch(`${URL_BASE}/content.html`).then(r => r.text()),
+ },
+ });
+ gMessage = [...gFolders[0].messages][0];
+
+ document.getElementById("tabmail").currentAbout3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: gAccount.incomingServer.rootFolder.URI,
+ });
+ await ensure_table_view();
+});
+
+add_task(async function test_content_mv2() {
+ let tabmail = document.getElementById("tabmail");
+ let about3Pane = tabmail.currentAbout3Pane;
+ about3Pane.restoreState({
+ messagePaneVisible: true,
+ folderURI: gFolders[0].URI,
+ });
+
+ let oldPref = Services.prefs.getStringPref("mailnews.start_page.url");
+ Services.prefs.setStringPref(
+ "mailnews.start_page.url",
+ `${URL_BASE}/content.html`
+ );
+
+ let loadPromise = BrowserTestUtils.browserLoaded(about3Pane.webBrowser);
+ window.goDoCommand("cmd_goStartPage");
+ await loadPromise;
+
+ let extension = await getMenuExtension({
+ manifest_version: 2,
+ host_permissions: ["<all_urls>"],
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("menus-created");
+ await subtest_content(
+ extension,
+ true,
+ about3Pane.webBrowser,
+ `${URL_BASE}/content.html`,
+ {
+ active: true,
+ index: 0,
+ mailTab: true,
+ }
+ );
+
+ await extension.unload();
+
+ Services.prefs.setStringPref("mailnews.start_page.url", oldPref);
+});
+add_task(async function test_content_tab_mv2() {
+ let tab = window.openContentTab(`${URL_BASE}/content.html`);
+ await awaitBrowserLoaded(tab.browser);
+
+ let extension = await getMenuExtension({
+ manifest_version: 2,
+ host_permissions: ["<all_urls>"],
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("menus-created");
+
+ await subtest_content(
+ extension,
+ true,
+ tab.browser,
+ `${URL_BASE}/content.html`,
+ {
+ active: true,
+ index: 1,
+ mailTab: false,
+ }
+ );
+
+ await extension.unload();
+
+ let tabmail = document.getElementById("tabmail");
+ tabmail.closeOtherTabs(0);
+});
+add_task(async function test_content_window_mv2() {
+ let extensionWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ window.openDialog(
+ "chrome://messenger/content/extensionPopup.xhtml",
+ "_blank",
+ "width=800,height=500,resizable",
+ `${URL_BASE}/content.html`
+ );
+ let extensionWindow = await extensionWindowPromise;
+ await focusWindow(extensionWindow);
+ await awaitBrowserLoaded(
+ extensionWindow.browser,
+ url => url != "about:blank"
+ );
+
+ let extension = await getMenuExtension({
+ manifest_version: 2,
+ host_permissions: ["<all_urls>"],
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("menus-created");
+
+ await subtest_content(
+ extension,
+ true,
+ extensionWindow.browser,
+ `${URL_BASE}/content.html`,
+ {
+ active: true,
+ index: 0,
+ mailTab: false,
+ }
+ );
+
+ await extension.unload();
+
+ await BrowserTestUtils.closeWindow(extensionWindow);
+});
+add_task(async function test_content_mv3() {
+ let tabmail = document.getElementById("tabmail");
+ let about3Pane = tabmail.currentAbout3Pane;
+ about3Pane.restoreState({
+ messagePaneVisible: true,
+ folderURI: gFolders[0].URI,
+ });
+
+ let oldPref = Services.prefs.getStringPref("mailnews.start_page.url");
+ Services.prefs.setStringPref(
+ "mailnews.start_page.url",
+ `${URL_BASE}/content.html`
+ );
+
+ let loadPromise = BrowserTestUtils.browserLoaded(about3Pane.webBrowser);
+ window.goDoCommand("cmd_goStartPage");
+ await loadPromise;
+
+ let extension = await getMenuExtension({
+ manifest_version: 3,
+ host_permissions: ["<all_urls>"],
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("menus-created");
+ await subtest_content(
+ extension,
+ true,
+ about3Pane.webBrowser,
+ `${URL_BASE}/content.html`,
+ {
+ active: true,
+ index: 0,
+ mailTab: true,
+ }
+ );
+
+ await extension.unload();
+
+ Services.prefs.setStringPref("mailnews.start_page.url", oldPref);
+});
+add_task(async function test_content_tab_mv3() {
+ let tab = window.openContentTab(`${URL_BASE}/content.html`);
+ await awaitBrowserLoaded(tab.browser);
+
+ let extension = await getMenuExtension({
+ manifest_version: 3,
+ host_permissions: ["<all_urls>"],
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("menus-created");
+
+ await subtest_content(
+ extension,
+ true,
+ tab.browser,
+ `${URL_BASE}/content.html`,
+ {
+ active: true,
+ index: 1,
+ mailTab: false,
+ }
+ );
+
+ await extension.unload();
+
+ let tabmail = document.getElementById("tabmail");
+ tabmail.closeOtherTabs(0);
+});
+add_task(async function test_content_window_mv3() {
+ let extensionWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ window.openDialog(
+ "chrome://messenger/content/extensionPopup.xhtml",
+ "_blank",
+ "width=800,height=500,resizable",
+ `${URL_BASE}/content.html`
+ );
+ let extensionWindow = await extensionWindowPromise;
+ await focusWindow(extensionWindow);
+ await awaitBrowserLoaded(
+ extensionWindow.browser,
+ url => url != "about:blank"
+ );
+
+ let extension = await getMenuExtension({
+ manifest_version: 3,
+ host_permissions: ["<all_urls>"],
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("menus-created");
+
+ await subtest_content(
+ extension,
+ true,
+ extensionWindow.browser,
+ `${URL_BASE}/content.html`,
+ {
+ active: true,
+ index: 0,
+ mailTab: false,
+ }
+ );
+
+ await extension.unload();
+
+ await BrowserTestUtils.closeWindow(extensionWindow);
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_menus_context_folder_pane.js b/comm/mail/components/extensions/test/browser/browser_ext_menus_context_folder_pane.js
new file mode 100644
index 0000000000..7f55394473
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_menus_context_folder_pane.js
@@ -0,0 +1,97 @@
+/* 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/. */
+
+// Load subscript shared with all menu tests.
+Services.scriptloader.loadSubScript(
+ new URL("head_menus.js", gTestPath).href,
+ this
+);
+
+let gAccount, gFolders, gMessage;
+add_setup(async () => {
+ await Services.search.init();
+
+ gAccount = createAccount();
+ addIdentity(gAccount);
+ gFolders = gAccount.incomingServer.rootFolder.subFolders;
+ createMessages(gFolders[0], {
+ count: 1,
+ body: {
+ contentType: "text/html",
+ body: await fetch(`${URL_BASE}/content.html`).then(r => r.text()),
+ },
+ });
+ gMessage = [...gFolders[0].messages][0];
+
+ document.getElementById("tabmail").currentAbout3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: gAccount.incomingServer.rootFolder.URI,
+ });
+});
+
+async function subtest_folder_pane(manifest) {
+ let extension = await getMenuExtension(manifest);
+
+ await extension.startup();
+ await extension.awaitMessage("menus-created");
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ let folderTree = about3Pane.document.getElementById("folderTree");
+ let menu = about3Pane.document.getElementById("folderPaneContext");
+ await rightClick(menu, folderTree.rows[1].querySelector(".container"));
+ Assert.ok(menu.querySelector("#menus_mochi_test-menuitem-_folder_pane"));
+ menu.hidePopup();
+
+ await checkShownEvent(
+ extension,
+ {
+ menuIds: ["folder_pane"],
+ contexts: ["folder_pane", "all"],
+ selectedFolder: manifest?.permissions?.includes("accountsRead")
+ ? { accountId: gAccount.key, path: "/Trash" }
+ : undefined,
+ },
+ { active: true, index: 0, mailTab: true }
+ );
+
+ await rightClick(menu, folderTree.rows[0].querySelector(".container"));
+ Assert.ok(menu.querySelector("#menus_mochi_test-menuitem-_folder_pane"));
+ menu.hidePopup();
+
+ await checkShownEvent(
+ extension,
+ {
+ menuIds: ["folder_pane"],
+ contexts: ["folder_pane", "all"],
+ selectedAccount: manifest?.permissions?.includes("accountsRead")
+ ? { id: gAccount.key, type: "none" }
+ : undefined,
+ },
+ { active: true, index: 0, mailTab: true }
+ );
+
+ await extension.unload();
+}
+add_task(async function test_folder_pane_mv2() {
+ return subtest_folder_pane({
+ manifest_version: 2,
+ permissions: ["accountsRead"],
+ });
+});
+add_task(async function test_folder_pane_no_permissions_mv2() {
+ return subtest_folder_pane({
+ manifest_version: 2,
+ });
+});
+add_task(async function test_folder_pane_mv3() {
+ return subtest_folder_pane({
+ manifest_version: 3,
+ permissions: ["accountsRead"],
+ });
+});
+add_task(async function test_folder_pane_no_permissions_mv3() {
+ return subtest_folder_pane({
+ manifest_version: 3,
+ });
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_menus_context_message_panes.js b/comm/mail/components/extensions/test/browser/browser_ext_menus_context_message_panes.js
new file mode 100644
index 0000000000..fc851aa09d
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_menus_context_message_panes.js
@@ -0,0 +1,180 @@
+/* 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/. */
+
+// Load subscript shared with all menu tests.
+Services.scriptloader.loadSubScript(
+ new URL("head_menus.js", gTestPath).href,
+ this
+);
+
+let gAccount, gFolders, gMessage;
+add_setup(async () => {
+ await Services.search.init();
+
+ gAccount = createAccount();
+ addIdentity(gAccount);
+ gFolders = gAccount.incomingServer.rootFolder.subFolders;
+ createMessages(gFolders[0], {
+ count: 1,
+ body: {
+ contentType: "text/html",
+ body: await fetch(`${URL_BASE}/content.html`).then(r => r.text()),
+ },
+ });
+ gMessage = [...gFolders[0].messages][0];
+
+ document.getElementById("tabmail").currentAbout3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: gAccount.incomingServer.rootFolder.URI,
+ });
+ await ensure_table_view();
+});
+
+async function subtest_message_panes(manifest) {
+ let tabmail = document.getElementById("tabmail");
+ let about3Pane = tabmail.currentAbout3Pane;
+ about3Pane.restoreState({
+ messagePaneVisible: true,
+ folderURI: gFolders[0].URI,
+ });
+
+ let extension = await getMenuExtension(manifest);
+
+ await extension.startup();
+ await extension.awaitMessage("menus-created");
+
+ info("Test the thread pane in the 3-pane tab.");
+
+ let threadTree = about3Pane.document.getElementById("threadTree");
+ let menu = about3Pane.document.getElementById("mailContext");
+ threadTree.selectedIndex = 0;
+ await rightClick(menu, threadTree.getRowAtIndex(0));
+ Assert.ok(menu.querySelector("#menus_mochi_test-menuitem-_message_list"));
+ menu.hidePopup();
+
+ await checkShownEvent(
+ extension,
+ {
+ menuIds: ["message_list"],
+ contexts: ["message_list", "all"],
+ displayedFolder: manifest?.permissions?.includes("accountsRead")
+ ? { accountId: gAccount.key, path: "/Trash" }
+ : undefined,
+ selectedMessages: manifest?.permissions?.includes("messagesRead")
+ ? { id: null, messages: [{ subject: gMessage.subject }] }
+ : undefined,
+ },
+ { active: true, index: 0, mailTab: true }
+ );
+
+ info("Test the message pane in the 3-pane tab.");
+
+ let messagePane =
+ about3Pane.messageBrowser.contentWindow.getMessagePaneBrowser();
+
+ await subtest_content(
+ extension,
+ manifest?.permissions?.includes("messagesRead"),
+ messagePane,
+ /^mailbox\:/,
+ {
+ active: true,
+ index: 0,
+ mailTab: true,
+ }
+ );
+
+ about3Pane.threadTree.selectedIndices = [];
+ await awaitBrowserLoaded(messagePane, "about:blank");
+
+ info("Test the message pane in a tab.");
+
+ await openMessageInTab(gMessage);
+ messagePane = tabmail.currentAboutMessage.getMessagePaneBrowser();
+
+ await subtest_content(
+ extension,
+ manifest?.permissions?.includes("messagesRead"),
+ messagePane,
+ /^mailbox\:/,
+ {
+ active: true,
+ index: 1,
+ mailTab: false,
+ }
+ );
+
+ tabmail.closeOtherTabs(0);
+
+ info("Test the message pane in a separate window.");
+
+ let displayWindow = await openMessageInWindow(gMessage);
+ let displayDocument = displayWindow.document;
+ menu = displayDocument.getElementById("mailContext");
+ messagePane = displayDocument
+ .getElementById("messageBrowser")
+ .contentWindow.getMessagePaneBrowser();
+
+ await subtest_content(
+ extension,
+ manifest?.permissions?.includes("messagesRead"),
+ messagePane,
+ /^mailbox\:/,
+ {
+ active: true,
+ index: 0,
+ mailTab: false,
+ }
+ );
+
+ await extension.unload();
+
+ await BrowserTestUtils.closeWindow(displayWindow);
+}
+add_task(async function test_message_panes_mv2() {
+ return subtest_message_panes({
+ manifest_version: 2,
+ permissions: ["accountsRead", "messagesRead"],
+ });
+});
+add_task(async function test_message_panes_no_accounts_permission_mv2() {
+ return subtest_message_panes({
+ manifest_version: 2,
+ permissions: ["messagesRead"],
+ });
+});
+add_task(async function test_message_panes_no_messages_permission_mv2() {
+ return subtest_message_panes({
+ manifest_version: 2,
+ permissions: ["accountsRead"],
+ });
+});
+add_task(async function test_message_panes_no_permissions_mv2() {
+ return subtest_message_panes({
+ manifest_version: 2,
+ });
+});
+add_task(async function test_message_panes_mv3() {
+ return subtest_message_panes({
+ manifest_version: 3,
+ permissions: ["accountsRead", "messagesRead"],
+ });
+});
+add_task(async function test_message_panes_no_accounts_permission_mv3() {
+ return subtest_message_panes({
+ manifest_version: 3,
+ permissions: ["messagesRead"],
+ });
+});
+add_task(async function test_message_panes_no_messages_permission_mv3() {
+ return subtest_message_panes({
+ manifest_version: 3,
+ permissions: ["accountsRead"],
+ });
+});
+add_task(async function test_message_panes_no_permissions_mv3() {
+ return subtest_message_panes({
+ manifest_version: 3,
+ });
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_menus_context_tabs.js b/comm/mail/components/extensions/test/browser/browser_ext_menus_context_tabs.js
new file mode 100644
index 0000000000..b957548d92
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_menus_context_tabs.js
@@ -0,0 +1,77 @@
+/* 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/. */
+
+// Load subscript shared with all menu tests.
+Services.scriptloader.loadSubScript(
+ new URL("head_menus.js", gTestPath).href,
+ this
+);
+
+let gAccount, gFolders, gMessage;
+add_setup(async () => {
+ await Services.search.init();
+
+ gAccount = createAccount();
+ addIdentity(gAccount);
+ gFolders = gAccount.incomingServer.rootFolder.subFolders;
+ createMessages(gFolders[0], {
+ count: 1,
+ body: {
+ contentType: "text/html",
+ body: await fetch(`${URL_BASE}/content.html`).then(r => r.text()),
+ },
+ });
+ gMessage = [...gFolders[0].messages][0];
+
+ document.getElementById("tabmail").currentAbout3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: gAccount.incomingServer.rootFolder.URI,
+ });
+});
+
+async function subtest_tab(manifest) {
+ async function checkTabEvent(index, active, mailTab) {
+ await rightClick(menu, tabs[index]);
+ Assert.ok(menu.querySelector("#menus_mochi_test-menuitem-_tab"));
+ menu.hidePopup();
+
+ await checkShownEvent(
+ extension,
+ { menuIds: ["tab"], contexts: ["tab"] },
+ { active, index, mailTab }
+ );
+ }
+
+ let extension = await getMenuExtension(manifest);
+
+ await extension.startup();
+ await extension.awaitMessage("menus-created");
+
+ let tabmail = document.getElementById("tabmail");
+ window.openContentTab("about:config");
+ window.openContentTab("about:mozilla");
+ tabmail.openTab("mail3PaneTab", { folderURI: gFolders[0].URI });
+
+ let tabs = document.getElementById("tabmail-tabs").allTabs;
+ let menu = document.getElementById("tabContextMenu");
+
+ await checkTabEvent(0, false, true);
+ await checkTabEvent(1, false, false);
+ await checkTabEvent(2, false, false);
+ await checkTabEvent(3, true, true);
+
+ await extension.unload();
+
+ tabmail.closeOtherTabs(0);
+}
+add_task(async function test_tab_mv2() {
+ await subtest_tab({
+ manifest_version: 2,
+ });
+});
+add_task(async function test_tab_mv3() {
+ await subtest_tab({
+ manifest_version: 3,
+ });
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_menus_context_tools_main_menu.js b/comm/mail/components/extensions/test/browser/browser_ext_menus_context_tools_main_menu.js
new file mode 100644
index 0000000000..d9aed69ba3
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_menus_context_tools_main_menu.js
@@ -0,0 +1,156 @@
+/* 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/. */
+
+// Load subscript shared with all menu tests.
+Services.scriptloader.loadSubScript(
+ new URL("head_menus.js", gTestPath).href,
+ this
+);
+
+let gAccount, gFolders, gMessage;
+add_setup(async () => {
+ await Services.search.init();
+
+ gAccount = createAccount();
+ addIdentity(gAccount);
+ gFolders = gAccount.incomingServer.rootFolder.subFolders;
+ createMessages(gFolders[0], {
+ count: 1,
+ body: {
+ contentType: "text/html",
+ body: await fetch(`${URL_BASE}/content.html`).then(r => r.text()),
+ },
+ });
+ gMessage = [...gFolders[0].messages][0];
+
+ document.getElementById("tabmail").currentAbout3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: gAccount.incomingServer.rootFolder.URI,
+ });
+});
+
+async function subtest_tools_menu(
+ testWindow,
+ expectedInfo,
+ expectedTab,
+ manifest
+) {
+ let extension = await getMenuExtension(manifest);
+ await extension.startup();
+ await extension.awaitMessage("menus-created");
+
+ let element = testWindow.document.getElementById("tasksMenu");
+ let menu = testWindow.document.getElementById("taskPopup");
+ await leftClick(menu, element);
+ await checkShownEvent(
+ extension,
+ { menuIds: ["tools_menu"], contexts: ["tools_menu"] },
+ expectedTab
+ );
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ let clickedPromise = checkClickedEvent(extension, expectedInfo, expectedTab);
+ menu.activateItem(
+ menu.querySelector("#menus_mochi_test-menuitem-_tools_menu")
+ );
+ await clickedPromise;
+ await hiddenPromise;
+ await extension.unload();
+}
+add_task(async function test_tools_menu_mv2() {
+ let toolbar = window.document.getElementById("toolbar-menubar");
+ let initialState = toolbar.getAttribute("inactive");
+ toolbar.setAttribute("inactive", "false");
+
+ await subtest_tools_menu(
+ window,
+ {
+ menuItemId: "tools_menu",
+ },
+ { active: true, index: 0, mailTab: true },
+ {
+ manifest_version: 2,
+ }
+ );
+
+ toolbar.setAttribute("inactive", initialState);
+}).__skipMe = AppConstants.platform == "macosx";
+add_task(async function test_compose_tools_menu_mv2() {
+ let testWindow = await openComposeWindow(gAccount);
+ await focusWindow(testWindow);
+ await subtest_tools_menu(
+ testWindow,
+ {
+ menuItemId: "tools_menu",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 2,
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+}).__skipMe = AppConstants.platform == "macosx";
+add_task(async function test_messagewindow_tools_menu_mv2() {
+ let testWindow = await openMessageInWindow(gMessage);
+ await focusWindow(testWindow);
+ await subtest_tools_menu(
+ testWindow,
+ {
+ menuItemId: "tools_menu",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 2,
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+}).__skipMe = AppConstants.platform == "macosx";
+add_task(async function test_tools_menu_mv3() {
+ let toolbar = window.document.getElementById("toolbar-menubar");
+ let initialState = toolbar.getAttribute("inactive");
+ toolbar.setAttribute("inactive", "false");
+
+ await subtest_tools_menu(
+ window,
+ {
+ menuItemId: "tools_menu",
+ },
+ { active: true, index: 0, mailTab: true },
+ {
+ manifest_version: 3,
+ }
+ );
+
+ toolbar.setAttribute("inactive", initialState);
+}).__skipMe = AppConstants.platform == "macosx";
+add_task(async function test_compose_tools_menu_mv3() {
+ let testWindow = await openComposeWindow(gAccount);
+ await focusWindow(testWindow);
+ await subtest_tools_menu(
+ testWindow,
+ {
+ menuItemId: "tools_menu",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 3,
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+}).__skipMe = AppConstants.platform == "macosx";
+add_task(async function test_messagewindow_tools_menu_mv3() {
+ let testWindow = await openMessageInWindow(gMessage);
+ await focusWindow(testWindow);
+ await subtest_tools_menu(
+ testWindow,
+ {
+ menuItemId: "tools_menu",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 3,
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+}).__skipMe = AppConstants.platform == "macosx";
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_menus_message_one_attachment.js b/comm/mail/components/extensions/test/browser/browser_ext_menus_message_one_attachment.js
new file mode 100644
index 0000000000..7c5612ffc6
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_menus_message_one_attachment.js
@@ -0,0 +1,395 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let gAccount, gFolders, gMessage, gExpectedAttachments;
+
+const { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+
+const URL_BASE =
+ "http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data";
+
+var tabmail = document.getElementById("tabmail");
+var about3Pane = tabmail.currentAbout3Pane;
+var messagePane =
+ about3Pane.messageBrowser.contentWindow.getMessagePaneBrowser();
+
+/**
+ * Right-click on something and wait for the context menu to appear.
+ * For elements in the parent process only.
+ *
+ * @param {Element} menu - The <menu> that should appear.
+ * @param {Element} element - The element to be clicked on.
+ * @returns {Promise} A promise that resolves when the menu appears.
+ */
+function rightClick(menu, element, win) {
+ let shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(element, { type: "contextmenu" }, win);
+ return shownPromise;
+}
+
+/**
+ * Check the parameters of a browser.onShown event was fired.
+ *
+ * @see mail/components/extensions/schemas/menus.json
+ *
+ * @param extension
+ * @param {object} expectedInfo
+ * @param {Array} expectedInfo.menuIds
+ * @param {Array} expectedInfo.contexts
+ * @param {Array?} expectedInfo.attachments
+ * @param {object} expectedTab
+ * @param {boolean} expectedTab.active
+ * @param {integer} expectedTab.index
+ * @param {boolean} expectedTab.mailTab
+ */
+async function checkShownEvent(extension, expectedInfo, expectedTab) {
+ let [info, tab] = await extension.awaitMessage("onShown");
+ Assert.deepEqual(info.menuIds, expectedInfo.menuIds);
+ Assert.deepEqual(info.contexts, expectedInfo.contexts);
+
+ Assert.equal(
+ !!info.attachments,
+ !!expectedInfo.attachments,
+ "attachments in info"
+ );
+ if (expectedInfo.attachments) {
+ for (let i = 0; i < expectedInfo.attachments.length; i++) {
+ Assert.equal(info.attachments[i].name, expectedInfo.attachments[i].name);
+ Assert.equal(info.attachments[i].size, expectedInfo.attachments[i].size);
+ Assert.equal(
+ info.attachments[i].partName,
+ expectedInfo.attachments[i].partName
+ );
+ Assert.equal(
+ info.attachments[i].contentType,
+ expectedInfo.attachments[i].contentType
+ );
+ }
+ }
+
+ Assert.equal(tab.active, expectedTab.active, "tab is active");
+ Assert.equal(tab.index, expectedTab.index, "tab index");
+ Assert.equal(tab.mailTab, expectedTab.mailTab, "tab is mailTab");
+}
+
+/**
+ * Check the parameters of a browser.onClicked event was fired.
+ *
+ * @see mail/components/extensions/schemas/menus.json
+ *
+ * @param extension
+ * @param {object} expectedInfo
+ * @param {string?} expectedInfo.menuItemId
+ * @param {Array?} expectedInfo.attachments
+ * @param {object} expectedTab
+ * @param {boolean} expectedTab.active
+ * @param {integer} expectedTab.index
+ * @param {boolean} expectedTab.mailTab
+ */
+async function checkClickedEvent(extension, expectedInfo, expectedTab) {
+ let [info, tab] = await extension.awaitMessage("onClicked");
+
+ Assert.equal(
+ !!info.attachments,
+ !!expectedInfo.attachments,
+ "attachments in info"
+ );
+ if (expectedInfo.attachments) {
+ for (let i = 0; i < expectedInfo.attachments.length; i++) {
+ Assert.equal(info.attachments[i].name, expectedInfo.attachments[i].name);
+ Assert.equal(info.attachments[i].size, expectedInfo.attachments[i].size);
+ Assert.equal(
+ info.attachments[i].partName,
+ expectedInfo.attachments[i].partName
+ );
+ Assert.equal(
+ info.attachments[i].contentType,
+ expectedInfo.attachments[i].contentType
+ );
+ }
+ }
+
+ if (expectedInfo.menuItemId) {
+ Assert.equal(info.menuItemId, expectedInfo.menuItemId, "menuItemId");
+ }
+
+ Assert.equal(tab.active, expectedTab.active, "tab is active");
+ Assert.equal(tab.index, expectedTab.index, "tab index");
+ Assert.equal(tab.mailTab, expectedTab.mailTab, "tab is mailTab");
+}
+
+function getExtensionDetails(...permissions) {
+ return {
+ files: {
+ "background.js": async () => {
+ for (let context of [
+ "message_attachments",
+ "all_message_attachments",
+ ]) {
+ await new Promise(resolve => {
+ browser.menus.create(
+ {
+ id: context,
+ title: context,
+ contexts: [context],
+ },
+ resolve
+ );
+ });
+ }
+
+ browser.menus.onShown.addListener((...args) => {
+ browser.test.sendMessage("onShown", args);
+ });
+
+ browser.menus.onClicked.addListener((...args) => {
+ browser.test.sendMessage("onClicked", args);
+ });
+ browser.test.sendMessage("menus-created");
+ },
+ },
+ manifest: {
+ applications: {
+ gecko: {
+ id: "menus@mochi.test",
+ },
+ },
+ background: { scripts: ["background.js"] },
+ permissions: [...permissions, "menus"],
+ },
+ useAddonManager: "temporary",
+ };
+}
+
+add_setup(async function () {
+ await Services.search.init();
+
+ gAccount = createAccount();
+ addIdentity(gAccount);
+ gFolders = gAccount.incomingServer.rootFolder.subFolders;
+ await createMessages(gFolders[0], {
+ count: 1,
+ body: {
+ contentType: "text/html",
+ body: await fetch(`${URL_BASE}/content.html`).then(r => r.text()),
+ },
+ attachments: [
+ {
+ body: "I am an text attachment.",
+ filename: "test1.txt",
+ contentType: "text/plain",
+ },
+ ],
+ });
+ await createMessages(gFolders[0], {
+ count: 1,
+ body: {
+ contentType: "text/html",
+ body: await fetch(`${URL_BASE}/content.html`).then(r => r.text()),
+ },
+ attachments: [
+ {
+ body: "I am an text attachment.",
+ filename: "test1.txt",
+ contentType: "text/plain",
+ },
+ {
+ body: "I am another but larger attachment. ",
+ filename: "test2.txt",
+ contentType: "text/plain",
+ },
+ ],
+ });
+
+ about3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: gFolders[0].URI,
+ messagePaneVisible: true,
+ });
+
+ gExpectedAttachments = [
+ {
+ name: "test1.txt",
+ size: 24,
+ contentType: "text/plain",
+ partName: "1.2",
+ },
+ {
+ name: "test2.txt",
+ size: 36,
+ contentType: "text/plain",
+ partName: "1.3",
+ },
+ ];
+});
+
+// Test a click on an attachment item.
+async function subtest_attachmentItem(
+ extension,
+ win,
+ element,
+ expectedContext,
+ expectedAttachments
+) {
+ let menu = element.ownerGlobal.document.getElementById(
+ expectedContext == "message_attachments"
+ ? "attachmentItemContext"
+ : "attachmentListContext"
+ );
+
+ let expectedShowData = {
+ menuIds: [expectedContext],
+ contexts: [expectedContext, "all"],
+ attachments: expectedAttachments,
+ };
+ let expectedClickData = {
+ attachments: expectedAttachments,
+ };
+ let expectedTab = { active: true, index: 0, mailTab: false };
+
+ let showEventPromise = checkShownEvent(
+ extension,
+ expectedShowData,
+ expectedTab
+ );
+ await rightClick(menu, element, win);
+ let menuItem = menu.querySelector(
+ `#menus_mochi_test-menuitem-_${expectedContext}`
+ );
+ await showEventPromise;
+ Assert.ok(menuItem);
+
+ let clickEventPromise = checkClickedEvent(
+ extension,
+ expectedClickData,
+ expectedTab
+ );
+ menu.activateItem(menuItem);
+ await clickEventPromise;
+
+ // 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 => setTimeout(r, 250));
+}
+
+async function subtest_attachments(
+ extension,
+ win,
+ expectedContext,
+ expectedAttachments
+) {
+ // Test clicking on the attachmentInfo element.
+ let attachmentInfo = win.document.getElementById("attachmentInfo");
+ await subtest_attachmentItem(
+ extension,
+ win,
+ attachmentInfo,
+ expectedContext,
+ expectedAttachments
+ );
+
+ if (expectedAttachments) {
+ win.toggleAttachmentList(true);
+ let attachmentList = win.document.getElementById("attachmentList");
+ Assert.equal(
+ attachmentList.children.length,
+ expectedAttachments.length,
+ "Should see the expected number of attachments."
+ );
+
+ // Test clicking on the individual attachment elements.
+ for (let i = 0; i < attachmentList.children.length; i++) {
+ // Select the attachment.
+ attachmentList.selectItem(attachmentList.children[i]);
+
+ // Run context click check.
+ await subtest_attachmentItem(
+ extension,
+ win,
+ attachmentList.children[i],
+ "message_attachments",
+ [expectedAttachments[i]]
+ );
+ }
+ }
+}
+
+async function subtest_message_panes(
+ permissions,
+ expectedContext,
+ expectedAttachments = null
+) {
+ let extensionDetails = getExtensionDetails(...permissions);
+
+ info("Test the message pane in the 3-pane tab.");
+
+ let extension = ExtensionTestUtils.loadExtension(extensionDetails);
+ await extension.startup();
+ await extension.awaitMessage("menus-created");
+ await subtest_attachments(
+ extension,
+ tabmail.currentAboutMessage,
+ expectedContext,
+ expectedAttachments
+ );
+ await extension.unload();
+
+ info("Test the message pane in a tab.");
+
+ await openMessageInTab(gMessage);
+ extension = ExtensionTestUtils.loadExtension(extensionDetails);
+ await extension.startup();
+ await extension.awaitMessage("menus-created");
+ await subtest_attachments(
+ extension,
+ tabmail.currentAboutMessage,
+ expectedContext,
+ expectedAttachments
+ );
+ await extension.unload();
+ tabmail.closeOtherTabs(0);
+
+ info("Test the message pane in a separate window.");
+
+ let displayWindow = await openMessageInWindow(gMessage);
+ extension = ExtensionTestUtils.loadExtension(extensionDetails);
+ await extension.startup();
+ await extension.awaitMessage("menus-created");
+ await subtest_attachments(
+ extension,
+ displayWindow.messageBrowser.contentWindow,
+ expectedContext,
+ expectedAttachments
+ );
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(displayWindow);
+}
+
+// Tests using a message with one attachment.
+add_task(async function test_message_panes() {
+ gMessage = [...gFolders[0].messages][0];
+ about3Pane.threadTree.selectedIndex = 0;
+ await promiseMessageLoaded(messagePane, gMessage);
+
+ await subtest_message_panes(
+ ["accountsRead", "messagesRead"],
+ "message_attachments",
+ [gExpectedAttachments[0]]
+ );
+});
+add_task(async function test_message_panes_no_accounts_permission() {
+ return subtest_message_panes(["messagesRead"], "message_attachments", [
+ gExpectedAttachments[0],
+ ]);
+});
+add_task(async function test_message_panes_no_messages_permission() {
+ return subtest_message_panes(["accountsRead"], "message_attachments");
+});
+add_task(async function test_message_panes_no_permissions() {
+ return subtest_message_panes([], "message_attachments");
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_menus_message_two_attachments.js b/comm/mail/components/extensions/test/browser/browser_ext_menus_message_two_attachments.js
new file mode 100644
index 0000000000..7ae4d72a29
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_menus_message_two_attachments.js
@@ -0,0 +1,397 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let gAccount, gFolders, gMessage, gExpectedAttachments;
+
+const { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+
+const URL_BASE =
+ "http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data";
+
+var tabmail = document.getElementById("tabmail");
+var about3Pane = tabmail.currentAbout3Pane;
+var messagePane =
+ about3Pane.messageBrowser.contentWindow.getMessagePaneBrowser();
+
+/**
+ * Right-click on something and wait for the context menu to appear.
+ * For elements in the parent process only.
+ *
+ * @param {Element} menu - The <menu> that should appear.
+ * @param {Element} element - The element to be clicked on.
+ * @returns {Promise} A promise that resolves when the menu appears.
+ */
+function rightClick(menu, element, win) {
+ let shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(element, { type: "contextmenu" }, win);
+ return shownPromise;
+}
+
+/**
+ * Check the parameters of a browser.onShown event was fired.
+ *
+ * @see mail/components/extensions/schemas/menus.json
+ *
+ * @param extension
+ * @param {object} expectedInfo
+ * @param {Array} expectedInfo.menuIds
+ * @param {Array} expectedInfo.contexts
+ * @param {Array?} expectedInfo.attachments
+ * @param {object} expectedTab
+ * @param {boolean} expectedTab.active
+ * @param {integer} expectedTab.index
+ * @param {boolean} expectedTab.mailTab
+ */
+async function checkShownEvent(extension, expectedInfo, expectedTab) {
+ let [info, tab] = await extension.awaitMessage("onShown");
+ Assert.deepEqual(info.menuIds, expectedInfo.menuIds);
+ Assert.deepEqual(info.contexts, expectedInfo.contexts);
+
+ Assert.equal(
+ !!info.attachments,
+ !!expectedInfo.attachments,
+ "attachments in info"
+ );
+ if (expectedInfo.attachments) {
+ for (let i = 0; i < expectedInfo.attachments.length; i++) {
+ Assert.equal(info.attachments[i].name, expectedInfo.attachments[i].name);
+ Assert.equal(info.attachments[i].size, expectedInfo.attachments[i].size);
+ Assert.equal(
+ info.attachments[i].partName,
+ expectedInfo.attachments[i].partName
+ );
+ Assert.equal(
+ info.attachments[i].contentType,
+ expectedInfo.attachments[i].contentType
+ );
+ }
+ }
+
+ Assert.equal(tab.active, expectedTab.active, "tab is active");
+ Assert.equal(tab.index, expectedTab.index, "tab index");
+ Assert.equal(tab.mailTab, expectedTab.mailTab, "tab is mailTab");
+}
+
+/**
+ * Check the parameters of a browser.onClicked event was fired.
+ *
+ * @see mail/components/extensions/schemas/menus.json
+ *
+ * @param extension
+ * @param {object} expectedInfo
+ * @param {string?} expectedInfo.menuItemId
+ * @param {Array?} expectedInfo.attachments
+ * @param {object} expectedTab
+ * @param {boolean} expectedTab.active
+ * @param {integer} expectedTab.index
+ * @param {boolean} expectedTab.mailTab
+ */
+async function checkClickedEvent(extension, expectedInfo, expectedTab) {
+ let [info, tab] = await extension.awaitMessage("onClicked");
+
+ Assert.equal(
+ !!info.attachments,
+ !!expectedInfo.attachments,
+ "attachments in info"
+ );
+ if (expectedInfo.attachments) {
+ for (let i = 0; i < expectedInfo.attachments.length; i++) {
+ Assert.equal(info.attachments[i].name, expectedInfo.attachments[i].name);
+ Assert.equal(info.attachments[i].size, expectedInfo.attachments[i].size);
+ Assert.equal(
+ info.attachments[i].partName,
+ expectedInfo.attachments[i].partName
+ );
+ Assert.equal(
+ info.attachments[i].contentType,
+ expectedInfo.attachments[i].contentType
+ );
+ }
+ }
+
+ if (expectedInfo.menuItemId) {
+ Assert.equal(info.menuItemId, expectedInfo.menuItemId, "menuItemId");
+ }
+
+ Assert.equal(tab.active, expectedTab.active, "tab is active");
+ Assert.equal(tab.index, expectedTab.index, "tab index");
+ Assert.equal(tab.mailTab, expectedTab.mailTab, "tab is mailTab");
+}
+
+function getExtensionDetails(...permissions) {
+ return {
+ files: {
+ "background.js": async () => {
+ for (let context of [
+ "message_attachments",
+ "all_message_attachments",
+ ]) {
+ await new Promise(resolve => {
+ browser.menus.create(
+ {
+ id: context,
+ title: context,
+ contexts: [context],
+ },
+ resolve
+ );
+ });
+ }
+
+ browser.menus.onShown.addListener((...args) => {
+ browser.test.sendMessage("onShown", args);
+ });
+
+ browser.menus.onClicked.addListener((...args) => {
+ browser.test.sendMessage("onClicked", args);
+ });
+ browser.test.sendMessage("menus-created");
+ },
+ },
+ manifest: {
+ applications: {
+ gecko: {
+ id: "menus@mochi.test",
+ },
+ },
+ background: { scripts: ["background.js"] },
+ permissions: [...permissions, "menus"],
+ },
+ useAddonManager: "temporary",
+ };
+}
+
+add_setup(async () => {
+ await Services.search.init();
+
+ gAccount = createAccount();
+ addIdentity(gAccount);
+ gFolders = gAccount.incomingServer.rootFolder.subFolders;
+ await createMessages(gFolders[0], {
+ count: 1,
+ body: {
+ contentType: "text/html",
+ body: await fetch(`${URL_BASE}/content.html`).then(r => r.text()),
+ },
+ attachments: [
+ {
+ body: "I am an text attachment.",
+ filename: "test1.txt",
+ contentType: "text/plain",
+ },
+ ],
+ });
+ await createMessages(gFolders[0], {
+ count: 1,
+ body: {
+ contentType: "text/html",
+ body: await fetch(`${URL_BASE}/content.html`).then(r => r.text()),
+ },
+ attachments: [
+ {
+ body: "I am an text attachment.",
+ filename: "test1.txt",
+ contentType: "text/plain",
+ },
+ {
+ body: "I am another but larger attachment. ",
+ filename: "test2.txt",
+ contentType: "text/plain",
+ },
+ ],
+ });
+
+ about3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: gFolders[0].URI,
+ messagePaneVisible: true,
+ });
+
+ gExpectedAttachments = [
+ {
+ name: "test1.txt",
+ size: 24,
+ contentType: "text/plain",
+ partName: "1.2",
+ },
+ {
+ name: "test2.txt",
+ size: 36,
+ contentType: "text/plain",
+ partName: "1.3",
+ },
+ ];
+});
+
+// Test a click on an attachment item.
+async function subtest_attachmentItem(
+ extension,
+ win,
+ element,
+ expectedContext,
+ expectedAttachments
+) {
+ let menu = element.ownerGlobal.document.getElementById(
+ expectedContext == "message_attachments"
+ ? "attachmentItemContext"
+ : "attachmentListContext"
+ );
+
+ let expectedShowData = {
+ menuIds: [expectedContext],
+ contexts: [expectedContext, "all"],
+ attachments: expectedAttachments,
+ };
+ let expectedClickData = {
+ attachments: expectedAttachments,
+ };
+ let expectedTab = { active: true, index: 0, mailTab: false };
+
+ let showEventPromise = checkShownEvent(
+ extension,
+ expectedShowData,
+ expectedTab
+ );
+ await rightClick(menu, element, win);
+ let menuItem = menu.querySelector(
+ `#menus_mochi_test-menuitem-_${expectedContext}`
+ );
+ await showEventPromise;
+ Assert.ok(menuItem);
+
+ let clickEventPromise = checkClickedEvent(
+ extension,
+ expectedClickData,
+ expectedTab
+ );
+ menu.activateItem(menuItem);
+ await clickEventPromise;
+
+ // 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 => setTimeout(r, 250));
+}
+
+async function subtest_attachments(
+ extension,
+ win,
+ expectedContext,
+ expectedAttachments
+) {
+ // Test clicking on the attachmentInfo element.
+ let attachmentInfo = win.document.getElementById("attachmentInfo");
+ await subtest_attachmentItem(
+ extension,
+ win,
+ attachmentInfo,
+ expectedContext,
+ expectedAttachments
+ );
+
+ if (expectedAttachments) {
+ win.toggleAttachmentList(true);
+ let attachmentList = win.document.getElementById("attachmentList");
+ Assert.equal(
+ attachmentList.children.length,
+ expectedAttachments.length,
+ "Should see the expected number of attachments."
+ );
+
+ // Test clicking on the individual attachment elements.
+ for (let i = 0; i < attachmentList.children.length; i++) {
+ // Select the attachment.
+ attachmentList.selectItem(attachmentList.children[i]);
+
+ // Run context click check.
+ await subtest_attachmentItem(
+ extension,
+ win,
+ attachmentList.children[i],
+ "message_attachments",
+ [expectedAttachments[i]]
+ );
+ }
+ }
+}
+
+async function subtest_message_panes(
+ permissions,
+ expectedContext,
+ expectedAttachments = null
+) {
+ let extensionDetails = getExtensionDetails(...permissions);
+
+ info("Test the message pane in the 3-pane tab.");
+
+ let extension = ExtensionTestUtils.loadExtension(extensionDetails);
+ await extension.startup();
+ await extension.awaitMessage("menus-created");
+ await subtest_attachments(
+ extension,
+ tabmail.currentAboutMessage,
+ expectedContext,
+ expectedAttachments
+ );
+ await extension.unload();
+
+ info("Test the message pane in a tab.");
+
+ await openMessageInTab(gMessage);
+ extension = ExtensionTestUtils.loadExtension(extensionDetails);
+ await extension.startup();
+ await extension.awaitMessage("menus-created");
+ await subtest_attachments(
+ extension,
+ tabmail.currentAboutMessage,
+ expectedContext,
+ expectedAttachments
+ );
+ await extension.unload();
+ tabmail.closeOtherTabs(0);
+
+ info("Test the message pane in a separate window.");
+
+ let displayWindow = await openMessageInWindow(gMessage);
+ extension = ExtensionTestUtils.loadExtension(extensionDetails);
+ await extension.startup();
+ await extension.awaitMessage("menus-created");
+ await subtest_attachments(
+ extension,
+ displayWindow.messageBrowser.contentWindow,
+ expectedContext,
+ expectedAttachments
+ );
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(displayWindow);
+}
+
+// Tests using a message with two attachment.
+add_task(async function test_message_panes() {
+ gMessage = [...gFolders[0].messages][1];
+ about3Pane.threadTree.selectedIndex = 1;
+ await promiseMessageLoaded(messagePane, gMessage);
+
+ await subtest_message_panes(
+ ["accountsRead", "messagesRead"],
+ "all_message_attachments",
+ gExpectedAttachments
+ );
+});
+add_task(async function test_message_panes_no_accounts_permission() {
+ return subtest_message_panes(
+ ["messagesRead"],
+ "all_message_attachments",
+ gExpectedAttachments
+ );
+});
+add_task(async function test_message_panes_no_messages_permission() {
+ return subtest_message_panes(["accountsRead"], "all_message_attachments");
+});
+add_task(async function test_message_panes_no_permissions() {
+ return subtest_message_panes([], "all_message_attachments");
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_menus_popup_action.js b/comm/mail/components/extensions/test/browser/browser_ext_menus_popup_action.js
new file mode 100644
index 0000000000..4d0660597a
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_menus_popup_action.js
@@ -0,0 +1,405 @@
+/* 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/. */
+
+// Load subscript shared with all menu tests.
+Services.scriptloader.loadSubScript(
+ new URL("head_menus.js", gTestPath).href,
+ this
+);
+
+let gAccount, gFolders, gMessage;
+add_setup(async () => {
+ gAccount = createAccount();
+ addIdentity(gAccount);
+ gFolders = gAccount.incomingServer.rootFolder.subFolders;
+ createMessages(gFolders[0], {
+ count: 1,
+ body: {
+ contentType: "text/html",
+ body: await fetch(`${URL_BASE}/content.html`).then(r => r.text()),
+ },
+ });
+ gMessage = [...gFolders[0].messages][0];
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: gFolders[0],
+ messagePaneVisible: true,
+ });
+ about3Pane.threadTree.selectedIndex = 0;
+ await awaitBrowserLoaded(
+ about3Pane.messageBrowser.contentWindow.getMessagePaneBrowser()
+ );
+});
+
+async function subtest_action_popup_menu(
+ testWindow,
+ target,
+ expectedInfo,
+ expectedTab,
+ manifest
+) {
+ let extension = await getMenuExtension(manifest);
+
+ await extension.startup();
+ await extension.awaitMessage("menus-created");
+
+ let element = testWindow.document.querySelector(target.elementSelector);
+ let menu = element.querySelector("menupopup");
+
+ await leftClick(menu, element);
+ await checkShownEvent(
+ extension,
+ { menuIds: [target.context], contexts: [target.context, "all"] },
+ expectedTab
+ );
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ let clickedPromise = checkClickedEvent(extension, expectedInfo, expectedTab);
+ menu.activateItem(
+ menu.querySelector(`#menus_mochi_test-menuitem-_${target.context}`)
+ );
+ await clickedPromise;
+ await hiddenPromise;
+
+ await extension.unload();
+}
+
+add_task(async function test_browser_action_menu_popup_mv2() {
+ await subtest_action_popup_menu(
+ window,
+ {
+ elementSelector: `.unified-toolbar [extension="menus@mochi.test"]`,
+ context: "browser_action_menu",
+ },
+ {
+ pageUrl: /^mailbox\:/,
+ menuItemId: "browser_action_menu",
+ },
+ { active: true, index: 0, mailTab: true },
+ {
+ manifest_version: 2,
+ browser_action: {
+ default_title: "This is a test",
+ type: "menu",
+ },
+ }
+ );
+});
+add_task(async function test_browser_action_menu_popup_message_window_mv2() {
+ let testWindow = await openMessageInWindow(gMessage);
+ await focusWindow(testWindow);
+ await subtest_action_popup_menu(
+ testWindow,
+ {
+ elementSelector: "#menus_mochi_test-browserAction-toolbarbutton",
+ context: "browser_action_menu",
+ },
+ {
+ pageUrl: /^mailbox\:/,
+ menuItemId: "browser_action_menu",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 2,
+ browser_action: {
+ default_title: "This is a test",
+ default_windows: ["messageDisplay"],
+ type: "menu",
+ },
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+});
+add_task(async function test_message_display_action_menu_popup_pane_mv2() {
+ let tabmail = document.getElementById("tabmail");
+ let aboutMessage = tabmail.currentAboutMessage;
+ await SimpleTest.promiseFocus(aboutMessage);
+
+ await subtest_action_popup_menu(
+ aboutMessage,
+ {
+ elementSelector: "#menus_mochi_test-messageDisplayAction-toolbarbutton",
+ context: "message_display_action_menu",
+ },
+ {
+ pageUrl: /^mailbox\:/,
+ menuItemId: "message_display_action_menu",
+ },
+ { active: true, index: 0, mailTab: true },
+ {
+ manifest_version: 2,
+ message_display_action: {
+ default_title: "This is a test",
+ type: "menu",
+ },
+ }
+ );
+});
+add_task(async function test_message_display_action_menu_popup_tab_mv2() {
+ let tab = await openMessageInTab(gMessage);
+ await subtest_action_popup_menu(
+ tab.chromeBrowser.contentWindow,
+ {
+ elementSelector: "#menus_mochi_test-messageDisplayAction-toolbarbutton",
+ context: "message_display_action_menu",
+ },
+ {
+ pageUrl: /^mailbox\:/,
+ menuItemId: "message_display_action_menu",
+ },
+ { active: true, index: 1, mailTab: false },
+ {
+ manifest_version: 2,
+ message_display_action: {
+ default_title: "This is a test",
+ type: "menu",
+ },
+ }
+ );
+ window.document.getElementById("tabmail").closeTab(tab);
+});
+add_task(async function test_message_display_action_menu_popup_window_mv2() {
+ let testWindow = await openMessageInWindow(gMessage);
+ await focusWindow(testWindow);
+ await subtest_action_popup_menu(
+ testWindow.messageBrowser.contentWindow,
+ {
+ elementSelector: "#menus_mochi_test-messageDisplayAction-toolbarbutton",
+ context: "message_display_action_menu",
+ },
+ {
+ pageUrl: /^mailbox\:/,
+ menuItemId: "message_display_action_menu",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 2,
+ message_display_action: {
+ default_title: "This is a test",
+ type: "menu",
+ },
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+});
+add_task(async function test_compose_action_menu_popup_mv2() {
+ let testWindow = await openComposeWindow(gAccount);
+ await focusWindow(testWindow);
+ await subtest_action_popup_menu(
+ testWindow,
+ {
+ elementSelector: "#menus_mochi_test-composeAction-toolbarbutton",
+ context: "compose_action_menu",
+ },
+ {
+ pageUrl: "about:blank?compose",
+ menuItemId: "compose_action_menu",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 2,
+ compose_action: {
+ default_title: "This is a test",
+ type: "menu",
+ },
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+});
+add_task(async function test_compose_action_menu_popup_formattoolbar_mv2() {
+ let testWindow = await openComposeWindow(gAccount);
+ await focusWindow(testWindow);
+ await subtest_action_popup_menu(
+ testWindow,
+ {
+ elementSelector: "#menus_mochi_test-composeAction-toolbarbutton",
+ context: "compose_action_menu",
+ },
+ {
+ pageUrl: "about:blank?compose",
+ menuItemId: "compose_action_menu",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 2,
+ compose_action: {
+ default_title: "This is a test",
+ default_area: "formattoolbar",
+ type: "menu",
+ },
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+});
+
+add_task(async function test_browser_action_menu_popup_mv3() {
+ await subtest_action_popup_menu(
+ window,
+ {
+ elementSelector: `.unified-toolbar [extension="menus@mochi.test"]`,
+ context: "action_menu",
+ },
+ {
+ pageUrl: /^mailbox\:/,
+ menuItemId: "action_menu",
+ },
+ { active: true, index: 0, mailTab: true },
+ {
+ manifest_version: 3,
+ action: {
+ default_title: "This is a test",
+ type: "menu",
+ },
+ }
+ );
+});
+add_task(async function test_browser_action_menu_popup_message_window_mv3() {
+ let testWindow = await openMessageInWindow(gMessage);
+ await focusWindow(testWindow);
+ await subtest_action_popup_menu(
+ testWindow,
+ {
+ elementSelector: "#menus_mochi_test-browserAction-toolbarbutton",
+ context: "action_menu",
+ },
+ {
+ pageUrl: /^mailbox\:/,
+ menuItemId: "action_menu",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 3,
+ action: {
+ default_title: "This is a test",
+ default_windows: ["messageDisplay"],
+ type: "menu",
+ },
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+});
+add_task(async function test_message_display_action_menu_popup_pane_mv3() {
+ let tabmail = document.getElementById("tabmail");
+ let aboutMessage = tabmail.currentAboutMessage;
+ await SimpleTest.promiseFocus(aboutMessage);
+
+ await subtest_action_popup_menu(
+ aboutMessage,
+ {
+ elementSelector: "#menus_mochi_test-messageDisplayAction-toolbarbutton",
+ context: "message_display_action_menu",
+ },
+ {
+ pageUrl: /^mailbox\:/,
+ menuItemId: "message_display_action_menu",
+ },
+ { active: true, index: 0, mailTab: true },
+ {
+ manifest_version: 3,
+ message_display_action: {
+ default_title: "This is a test",
+ type: "menu",
+ },
+ }
+ );
+});
+add_task(async function test_message_display_action_menu_popup_tab_mv3() {
+ let tab = await openMessageInTab(gMessage);
+ await subtest_action_popup_menu(
+ tab.chromeBrowser.contentWindow,
+ {
+ elementSelector: "#menus_mochi_test-messageDisplayAction-toolbarbutton",
+ context: "message_display_action_menu",
+ },
+ {
+ pageUrl: /^mailbox\:/,
+ menuItemId: "message_display_action_menu",
+ },
+ { active: true, index: 1, mailTab: false },
+ {
+ manifest_version: 3,
+ message_display_action: {
+ default_title: "This is a test",
+ type: "menu",
+ },
+ }
+ );
+ window.document.getElementById("tabmail").closeTab(tab);
+});
+add_task(async function test_message_display_action_menu_popup_window_mv3() {
+ let testWindow = await openMessageInWindow(gMessage);
+ await focusWindow(testWindow);
+ await subtest_action_popup_menu(
+ testWindow.messageBrowser.contentWindow,
+ {
+ elementSelector: "#menus_mochi_test-messageDisplayAction-toolbarbutton",
+ context: "message_display_action_menu",
+ },
+ {
+ pageUrl: /^mailbox\:/,
+ menuItemId: "message_display_action_menu",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 3,
+ message_display_action: {
+ default_title: "This is a test",
+ type: "menu",
+ },
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+});
+add_task(async function test_compose_action_menu_popup_mv3() {
+ let testWindow = await openComposeWindow(gAccount);
+ await focusWindow(testWindow);
+ await subtest_action_popup_menu(
+ testWindow,
+ {
+ elementSelector: "#menus_mochi_test-composeAction-toolbarbutton",
+ context: "compose_action_menu",
+ nonActionButtonSelector: "#button-attach",
+ },
+ {
+ pageUrl: "about:blank?compose",
+ menuItemId: "compose_action_menu",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 3,
+ compose_action: {
+ default_title: "This is a test",
+ type: "menu",
+ },
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+});
+add_task(async function test_compose_action_menu_popup_formattoolbar_mv3() {
+ let testWindow = await openComposeWindow(gAccount);
+ await focusWindow(testWindow);
+ await subtest_action_popup_menu(
+ testWindow,
+ {
+ elementSelector: "#menus_mochi_test-composeAction-toolbarbutton",
+ context: "compose_action_menu",
+ },
+ {
+ pageUrl: "about:blank?compose",
+ menuItemId: "compose_action_menu",
+ },
+ { active: true, index: 0, mailTab: false },
+ {
+ manifest_version: 3,
+ compose_action: {
+ default_title: "This is a test",
+ default_area: "formattoolbar",
+ type: "menu",
+ },
+ }
+ );
+ await BrowserTestUtils.closeWindow(testWindow);
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_menus_replace_menu.js b/comm/mail/components/extensions/test/browser/browser_ext_menus_replace_menu.js
new file mode 100644
index 0000000000..bc773afa44
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_menus_replace_menu.js
@@ -0,0 +1,582 @@
+/* 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";
+
+function getVisibleChildrenIds(menuElem) {
+ return Array.from(menuElem.children)
+ .filter(elem => !elem.hidden)
+ .map(elem => elem.id || elem.tagName);
+}
+
+function checkIsDefaultMenuItemVisible(visibleMenuItemIds) {
+ // In this whole test file, we open a menu on a link. Assume that all
+ // default menu items are shown if one link-specific menu item is shown.
+ ok(
+ visibleMenuItemIds.includes("browserContext-copylink"),
+ `The default 'Copy Link Location' menu item should be in ${visibleMenuItemIds}.`
+ );
+}
+
+// Tests the following:
+// - Calling overrideContext({}) during oncontextmenu forces the menu to only
+// show an extension's own items.
+// - These menu items all appear in the root menu.
+// - The usual extension filtering behavior (e.g. documentUrlPatterns and
+// targetUrlPatterns) is still applied; some menu items are therefore hidden.
+// - Calling overrideContext({showDefaults:true}) causes the default menu items
+// to be shown, but only after the extension's.
+// - overrideContext expires after the menu is opened once.
+// - overrideContext can be called from shadow DOM.
+add_task(async function overrideContext_in_extension_tab() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.allow_eval_with_system_principal", true]],
+ });
+
+ function extensionTabScript() {
+ document.addEventListener(
+ "contextmenu",
+ () => {
+ browser.menus.overrideContext({});
+ browser.test.sendMessage("oncontextmenu_in_dom_part_1");
+ },
+ { once: true }
+ );
+
+ let shadowRoot = document
+ .getElementById("shadowHost")
+ .attachShadow({ mode: "open" });
+ shadowRoot.innerHTML = `<a href="http://example.com/">Link</a>`;
+ shadowRoot.firstChild.addEventListener(
+ "contextmenu",
+ () => {
+ browser.menus.overrideContext({});
+ browser.test.sendMessage("oncontextmenu_in_shadow_dom");
+ },
+ { once: true }
+ );
+
+ browser.menus.create({
+ id: "tab_1",
+ title: "tab_1",
+ documentUrlPatterns: [document.URL],
+ onclick() {
+ document.addEventListener(
+ "contextmenu",
+ () => {
+ // Verifies that last call takes precedence.
+ browser.menus.overrideContext({ showDefaults: false });
+ browser.menus.overrideContext({ showDefaults: true });
+ browser.test.sendMessage("oncontextmenu_in_dom_part_2");
+ },
+ { once: true }
+ );
+ browser.test.sendMessage("onClicked_tab_1");
+ },
+ });
+ browser.menus.create(
+ {
+ id: "tab_2",
+ title: "tab_2",
+ onclick() {
+ browser.test.sendMessage("onClicked_tab_2");
+ },
+ },
+ () => {
+ browser.test.sendMessage("menu-registered");
+ }
+ );
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus", "menus.overrideContext"],
+ },
+ files: {
+ "tab.html": `
+ <!DOCTYPE html><meta charset="utf-8">
+ <a href="http://example.com/">Link</a>
+ <div id="shadowHost"></div>
+ <script src="tab.js"></script>
+ `,
+ "tab.js": extensionTabScript,
+ },
+ background() {
+ // Expected to match and thus be visible.
+ browser.menus.create({ id: "bg_1", title: "bg_1" });
+ browser.menus.create({
+ id: "bg_2",
+ title: "bg_2",
+ targetUrlPatterns: ["*://example.com/*"],
+ });
+
+ // Expected to not match and be hidden.
+ browser.menus.create({
+ id: "bg_3",
+ title: "bg_3",
+ targetUrlPatterns: ["*://nomatch/*"],
+ });
+ browser.menus.create({
+ id: "bg_4",
+ title: "bg_4",
+ documentUrlPatterns: [document.URL],
+ });
+
+ browser.menus.onShown.addListener(info => {
+ browser.test.assertEq("tab", info.viewType, "Expected viewType");
+ browser.test.assertEq(
+ "bg_1,bg_2,tab_1,tab_2",
+ info.menuIds.join(","),
+ "Expected menu items."
+ );
+ browser.test.assertEq(
+ "all,link",
+ info.contexts.sort().join(","),
+ "Expected menu contexts"
+ );
+ browser.test.sendMessage("onShown");
+ });
+
+ browser.tabs.create({ url: "tab.html" });
+ },
+ });
+
+ let otherExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ background() {
+ browser.menus.create(
+ { id: "other_extension_item", title: "other_extension_item" },
+ () => {
+ browser.test.sendMessage("other_extension_item_created");
+ }
+ );
+ },
+ });
+ await otherExtension.startup();
+ await otherExtension.awaitMessage("other_extension_item_created");
+
+ await extension.startup();
+ await extension.awaitMessage("menu-registered");
+
+ const EXPECTED_EXTENSION_MENU_IDS = [
+ `${makeWidgetId(extension.id)}-menuitem-_bg_1`,
+ `${makeWidgetId(extension.id)}-menuitem-_bg_2`,
+ `${makeWidgetId(extension.id)}-menuitem-_tab_1`,
+ `${makeWidgetId(extension.id)}-menuitem-_tab_2`,
+ ];
+ const OTHER_EXTENSION_MENU_ID = `${makeWidgetId(
+ otherExtension.id
+ )}-menuitem-_other_extension_item`;
+
+ {
+ // Tests overrideContext({})
+ info("Expecting the menu to be replaced by overrideContext.");
+ let menu = await openContextMenu("a");
+ await extension.awaitMessage("oncontextmenu_in_dom_part_1");
+ await extension.awaitMessage("onShown");
+
+ Assert.deepEqual(
+ getVisibleChildrenIds(menu),
+ EXPECTED_EXTENSION_MENU_IDS,
+ "Expected only extension menu items"
+ );
+
+ let menuItems = menu.getElementsByAttribute("label", "tab_1");
+ await closeExtensionContextMenu(menuItems[0]);
+ await extension.awaitMessage("onClicked_tab_1");
+ }
+
+ {
+ // Tests overrideContext({showDefaults:true}))
+ info(
+ "Expecting the menu to be replaced by overrideContext, including default menu items."
+ );
+ let menu = await openContextMenu("a");
+ await extension.awaitMessage("oncontextmenu_in_dom_part_2");
+ await extension.awaitMessage("onShown");
+
+ let visibleMenuItemIds = getVisibleChildrenIds(menu);
+ Assert.deepEqual(
+ visibleMenuItemIds.slice(0, EXPECTED_EXTENSION_MENU_IDS.length),
+ EXPECTED_EXTENSION_MENU_IDS,
+ "Expected extension menu items at the start."
+ );
+
+ checkIsDefaultMenuItemVisible(visibleMenuItemIds);
+
+ is(
+ visibleMenuItemIds[visibleMenuItemIds.length - 1],
+ OTHER_EXTENSION_MENU_ID,
+ "Other extension menu item should be at the end."
+ );
+
+ let menuItems = menu.getElementsByAttribute("label", "tab_2");
+ await closeExtensionContextMenu(menuItems[0]);
+ await extension.awaitMessage("onClicked_tab_2");
+ }
+
+ {
+ // Tests that previous overrideContext call has been forgotten,
+ // so the default behavior should occur (=move items into submenu).
+ info(
+ "Expecting the default menu to be used when overrideContext is not called."
+ );
+ let menu = await openContextMenu("a");
+ await extension.awaitMessage("onShown");
+
+ checkIsDefaultMenuItemVisible(getVisibleChildrenIds(menu));
+
+ let menuItems = menu.getElementsByAttribute("ext-type", "top-level-menu");
+ is(menuItems.length, 1, "Expected top-level menu element for extension.");
+ let topLevelExtensionMenuItem = menuItems[0];
+ is(
+ topLevelExtensionMenuItem.nextSibling,
+ null,
+ "Extension menu should be the last element."
+ );
+
+ const submenu = await openSubmenu(topLevelExtensionMenuItem);
+ is(submenu, topLevelExtensionMenuItem.menupopup, "Correct submenu opened");
+
+ Assert.deepEqual(
+ getVisibleChildrenIds(submenu),
+ EXPECTED_EXTENSION_MENU_IDS,
+ "Extension menu items should be in the submenu by default."
+ );
+
+ await closeContextMenu();
+ }
+
+ {
+ info(
+ "Expecting the menu to be replaced by overrideContext from a listener inside shadow DOM."
+ );
+ // Tests that overrideContext({}) can be used from a listener inside shadow DOM.
+ let menu = await openContextMenu(
+ () => this.document.getElementById("shadowHost").shadowRoot.firstChild
+ );
+ await extension.awaitMessage("oncontextmenu_in_shadow_dom");
+ await extension.awaitMessage("onShown");
+
+ Assert.deepEqual(
+ getVisibleChildrenIds(menu),
+ EXPECTED_EXTENSION_MENU_IDS,
+ "Expected only extension menu items after overrideContext({}) in shadow DOM"
+ );
+
+ await closeContextMenu();
+ }
+
+ // Unloading the extension will automatically close the extension's tab.html
+ await extension.unload();
+ await otherExtension.unload();
+
+ let tabmail = document.getElementById("tabmail");
+ tabmail.closeTab(tabmail.currentTabInfo);
+});
+
+async function run_overrideContext_test_in_popup(testWindow, buttonSelector) {
+ function extensionPopupScript() {
+ document.addEventListener(
+ "contextmenu",
+ () => {
+ browser.menus.overrideContext({});
+ browser.test.sendMessage("oncontextmenu_in_dom_part_1");
+ },
+ { once: true }
+ );
+
+ let shadowRoot = document
+ .getElementById("shadowHost")
+ .attachShadow({ mode: "open" });
+ shadowRoot.innerHTML = `<a href="http://example.com/">Link2</a>`;
+ shadowRoot.firstChild.addEventListener(
+ "contextmenu",
+ () => {
+ browser.menus.overrideContext({});
+ browser.test.sendMessage("oncontextmenu_in_shadow_dom");
+ },
+ { once: true }
+ );
+
+ browser.menus.create({
+ id: "popup_1",
+ title: "popup_1",
+ documentUrlPatterns: [document.URL],
+ onclick() {
+ document.addEventListener(
+ "contextmenu",
+ () => {
+ // Verifies that last call takes precedence.
+ browser.menus.overrideContext({ showDefaults: false });
+ browser.menus.overrideContext({ showDefaults: true });
+ browser.test.sendMessage("oncontextmenu_in_dom_part_2");
+ },
+ { once: true }
+ );
+ browser.test.sendMessage("onClicked_popup_1");
+ },
+ });
+ browser.menus.create(
+ {
+ id: "popup_2",
+ title: "popup_2",
+ onclick() {
+ browser.test.sendMessage("onClicked_popup_2");
+ },
+ },
+ () => {
+ browser.test.sendMessage("menu-registered");
+ }
+ );
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: {
+ id: `overrideContext@mochi.test`,
+ },
+ },
+ permissions: ["menus", "menus.overrideContext"],
+ browser_action: {
+ default_popup: "popup.html",
+ default_title: "Popup",
+ },
+ compose_action: {
+ default_popup: "popup.html",
+ default_title: "Popup",
+ },
+ message_display_action: {
+ default_popup: "popup.html",
+ default_title: "Popup",
+ },
+ },
+ files: {
+ "popup.html": `
+ <!DOCTYPE html><meta charset="utf-8">
+ <a id="link1" href="http://example.com/">Link1</a>
+ <div id="shadowHost"></div>
+ <script src="popup.js"></script>
+ `,
+ "popup.js": extensionPopupScript,
+ },
+ background() {
+ // Expected to match and thus be visible.
+ browser.menus.create({
+ id: "bg_1",
+ title: "bg_1",
+ viewTypes: ["popup"],
+ });
+ // Expected to not match and be hidden.
+ browser.menus.create({
+ id: "bg_2",
+ title: "bg_2",
+ viewTypes: ["tab"],
+ });
+ browser.menus.onShown.addListener(info => {
+ browser.test.assertEq("popup", info.viewType, "Expected viewType");
+ browser.test.assertEq(
+ "bg_1,popup_1,popup_2",
+ info.menuIds.join(","),
+ "Expected menu items."
+ );
+ browser.test.sendMessage("onShown");
+ });
+
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ const EXPECTED_EXTENSION_MENU_IDS = [
+ `${makeWidgetId(extension.id)}-menuitem-_bg_1`,
+ `${makeWidgetId(extension.id)}-menuitem-_popup_1`,
+ `${makeWidgetId(extension.id)}-menuitem-_popup_2`,
+ ];
+ const button = testWindow.document.querySelector(buttonSelector);
+ Assert.ok(button, "Button created");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, testWindow);
+ await extension.awaitMessage("menu-registered");
+
+ {
+ // Tests overrideContext({})
+ info("Expecting the menu to be replaced by overrideContext.");
+
+ let menu = await openContextMenuInPopup(extension, "#link1", testWindow);
+ await extension.awaitMessage("oncontextmenu_in_dom_part_1");
+ await extension.awaitMessage("onShown");
+
+ Assert.deepEqual(
+ getVisibleChildrenIds(menu),
+ EXPECTED_EXTENSION_MENU_IDS,
+ "Expected only extension menu items"
+ );
+
+ let menuItems = menu.getElementsByAttribute("label", "popup_1");
+
+ await closeExtensionContextMenu(menuItems[0], {}, testWindow);
+ await extension.awaitMessage("onClicked_popup_1");
+ }
+
+ {
+ // Tests overrideContext({showDefaults:true}))
+ info(
+ "Expecting the menu to be replaced by overrideContext, including default menu items."
+ );
+ let menu = await openContextMenuInPopup(extension, "#link1", testWindow);
+ await extension.awaitMessage("oncontextmenu_in_dom_part_2");
+ await extension.awaitMessage("onShown");
+ let visibleMenuItemIds = getVisibleChildrenIds(menu);
+ Assert.deepEqual(
+ visibleMenuItemIds.slice(0, EXPECTED_EXTENSION_MENU_IDS.length),
+ EXPECTED_EXTENSION_MENU_IDS,
+ "Expected extension menu items at the start."
+ );
+ checkIsDefaultMenuItemVisible(visibleMenuItemIds);
+
+ let menuItems = menu.getElementsByAttribute("label", "popup_2");
+ await closeExtensionContextMenu(menuItems[0], {}, testWindow);
+ await extension.awaitMessage("onClicked_popup_2");
+ }
+
+ {
+ // Tests that previous overrideContext call has been forgotten,
+ // so the default behavior should occur (=move items into submenu).
+ info(
+ "Expecting the default menu to be used when overrideContext is not called."
+ );
+ let menu = await openContextMenuInPopup(extension, "#link1", testWindow);
+ await extension.awaitMessage("onShown");
+
+ checkIsDefaultMenuItemVisible(getVisibleChildrenIds(menu));
+
+ let menuItems = menu.getElementsByAttribute("ext-type", "top-level-menu");
+ is(menuItems.length, 1, "Expected top-level menu element for extension.");
+ let topLevelExtensionMenuItem = menuItems[0];
+ is(
+ topLevelExtensionMenuItem.nextSibling,
+ null,
+ "Extension menu should be the last element."
+ );
+
+ const submenu = await openSubmenu(topLevelExtensionMenuItem);
+ is(submenu, topLevelExtensionMenuItem.menupopup, "Correct submenu opened");
+
+ Assert.deepEqual(
+ getVisibleChildrenIds(submenu),
+ EXPECTED_EXTENSION_MENU_IDS,
+ "Extension menu items should be in the submenu by default."
+ );
+
+ await closeContextMenu(menu);
+ }
+
+ {
+ info("Testing overrideContext from a listener inside a shadow DOM.");
+ // Tests that overrideContext({}) can be used from a listener inside shadow DOM.
+ let menu = await openContextMenuInPopup(
+ extension,
+ () => this.document.getElementById("shadowHost").shadowRoot.firstChild,
+ testWindow
+ );
+ await extension.awaitMessage("oncontextmenu_in_shadow_dom");
+ await extension.awaitMessage("onShown");
+
+ Assert.deepEqual(
+ getVisibleChildrenIds(menu),
+ EXPECTED_EXTENSION_MENU_IDS,
+ "Expected only extension menu items after overrideContext({}) in shadow DOM"
+ );
+
+ await closeContextMenu(menu);
+ }
+
+ await closeBrowserAction(extension, testWindow);
+ await extension.unload();
+}
+
+add_task(async function overrideContext_in_extension_browser_action_popup() {
+ await run_overrideContext_test_in_popup(
+ window,
+ `.unified-toolbar [extension="overrideContext@mochi.test"]`
+ );
+});
+
+add_task(async function overrideContext_in_extension_compose_action_popup() {
+ let account = createAccount();
+ addIdentity(account);
+
+ let composeWindow = await openComposeWindow(account);
+ await focusWindow(composeWindow);
+ await run_overrideContext_test_in_popup(
+ composeWindow,
+ "#overridecontext_mochi_test-composeAction-toolbarbutton"
+ );
+ composeWindow.close();
+});
+
+add_task(
+ async function overrideContext_in_extension_message_display_action_popup_of_mail3pane() {
+ let account = createAccount();
+ addIdentity(account);
+ let rootFolder = account.incomingServer.rootFolder;
+ let subFolders = rootFolder.subFolders;
+ createMessages(subFolders[0], 10);
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(subFolders[0]);
+ about3Pane.threadTree.selectedIndex = 0;
+
+ await run_overrideContext_test_in_popup(
+ about3Pane.messageBrowser.contentWindow,
+ "#overridecontext_mochi_test-messageDisplayAction-toolbarbutton"
+ );
+
+ about3Pane.displayFolder(rootFolder);
+ }
+);
+
+add_task(
+ async function overrideContext_in_extension_message_display_action_popup_of_window() {
+ let account = createAccount();
+ addIdentity(account);
+ let rootFolder = account.incomingServer.rootFolder;
+ let subFolders = rootFolder.subFolders;
+ createMessages(subFolders[0], 10);
+ let messages = subFolders[0].messages;
+
+ let messageWindow = await openMessageInWindow(messages.getNext());
+ await focusWindow(messageWindow);
+ await run_overrideContext_test_in_popup(
+ messageWindow.messageBrowser.contentWindow,
+ "#overridecontext_mochi_test-messageDisplayAction-toolbarbutton"
+ );
+ messageWindow.close();
+ }
+);
+
+add_task(
+ async function overrideContext_in_extension_message_display_action_popup_of_tab() {
+ let account = createAccount();
+ addIdentity(account);
+ let rootFolder = account.incomingServer.rootFolder;
+ let subFolders = rootFolder.subFolders;
+ createMessages(subFolders[0], 10);
+ let messages = subFolders[0].messages;
+
+ await openMessageInTab(messages.getNext());
+
+ let tabmail = document.getElementById("tabmail");
+ await run_overrideContext_test_in_popup(
+ tabmail.currentAboutMessage,
+ "#overridecontext_mochi_test-messageDisplayAction-toolbarbutton"
+ );
+ tabmail.closeOtherTabs(0);
+ }
+);
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_menus_replace_menu_context.js b/comm/mail/components/extensions/test/browser/browser_ext_menus_replace_menu_context.js
new file mode 100644
index 0000000000..2a7192fe70
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_menus_replace_menu_context.js
@@ -0,0 +1,375 @@
+/* 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";
+
+function getVisibleChildrenIds(menuElem) {
+ return Array.from(menuElem.children)
+ .filter(elem => !elem.hidden)
+ .map(elem => elem.id || elem.tagName);
+}
+
+function checkIsDefaultMenuItemVisible(visibleMenuItemIds) {
+ // In this whole test file, we open a menu on a link. Assume that all
+ // default menu items are shown if one link-specific menu item is shown.
+ ok(
+ visibleMenuItemIds.includes("browserContext-copylink"),
+ `The default 'Copy Link Location' menu item should be in ${visibleMenuItemIds}.`
+ );
+}
+
+// Tests that the context of an extension menu can be changed to:
+// - tab
+add_task(async function overrideContext_with_context() {
+ // Background script of the main test extension and the auxiliary other extension.
+ function background() {
+ const HTTP_URL = "https://example.com/?SomeTab";
+ browser.test.onMessage.addListener(async (msg, tabId) => {
+ browser.test.assertEq(
+ "testTabAccess",
+ msg,
+ `Expected message in ${browser.runtime.id}`
+ );
+ let tab = await browser.tabs.get(tabId);
+ if (!tab.url) {
+ // tabs or activeTab not active.
+ browser.test.sendMessage("testTabAccessDone", "tab_no_url");
+ return;
+ }
+ try {
+ let [url] = await browser.tabs.executeScript(tabId, {
+ code: "document.URL",
+ });
+ browser.test.assertEq(
+ HTTP_URL,
+ url,
+ "Expected successful executeScript"
+ );
+ browser.test.sendMessage("testTabAccessDone", "executeScript_ok");
+ return;
+ } catch (e) {
+ browser.test.assertEq(
+ "Missing host permission for the tab",
+ e.message,
+ "Expected error message"
+ );
+ browser.test.sendMessage("testTabAccessDone", "executeScript_failed");
+ }
+ });
+ browser.menus.onShown.addListener((info, tab) => {
+ browser.test.assertEq(
+ "tab",
+ info.viewType,
+ "Expected viewType at onShown"
+ );
+ browser.test.assertEq(
+ undefined,
+ info.linkUrl,
+ "Expected linkUrl at onShown"
+ );
+ browser.test.assertEq(
+ undefined,
+ info.srckUrl,
+ "Expected srcUrl at onShown"
+ );
+ browser.test.sendMessage("onShown", {
+ menuIds: info.menuIds.sort(),
+ contexts: info.contexts,
+ bookmarkId: info.bookmarkId,
+ pageUrl: info.pageUrl,
+ frameUrl: info.frameUrl,
+ tabId: tab && tab.id,
+ });
+ });
+ browser.menus.onClicked.addListener((info, tab) => {
+ browser.test.assertEq(
+ "tab",
+ info.viewType,
+ "Expected viewType at onClicked"
+ );
+ browser.test.assertEq(
+ undefined,
+ info.linkUrl,
+ "Expected linkUrl at onClicked"
+ );
+ browser.test.assertEq(
+ undefined,
+ info.srckUrl,
+ "Expected srcUrl at onClicked"
+ );
+ browser.test.sendMessage("onClicked", {
+ menuItemId: info.menuItemId,
+ bookmarkId: info.bookmarkId,
+ pageUrl: info.pageUrl,
+ frameUrl: info.frameUrl,
+ tabId: tab && tab.id,
+ });
+ });
+
+ // Minimal properties to define menu items for a specific context.
+ browser.menus.create({
+ id: "tab_context",
+ title: "tab_context",
+ contexts: ["tab"],
+ });
+
+ // documentUrlPatterns in the tab context applies to the tab's URL.
+ browser.menus.create({
+ id: "tab_context_http",
+ title: "tab_context_http",
+ contexts: ["tab"],
+ documentUrlPatterns: [HTTP_URL],
+ });
+ browser.menus.create({
+ id: "tab_context_moz_unexpected",
+ title: "tab_context_moz",
+ contexts: ["tab"],
+ documentUrlPatterns: ["moz-extension://*/tab.html"],
+ });
+ // When viewTypes is present, the document's URL is matched instead.
+ browser.menus.create({
+ id: "tab_context_viewType_http_unexpected",
+ title: "tab_context_viewType_http",
+ contexts: ["tab"],
+ viewTypes: ["tab"],
+ documentUrlPatterns: [HTTP_URL],
+ });
+ browser.menus.create({
+ id: "tab_context_viewType_moz",
+ title: "tab_context_viewType_moz",
+ contexts: ["tab"],
+ viewTypes: ["tab"],
+ documentUrlPatterns: ["moz-extension://*/tab.html"],
+ });
+
+ browser.menus.create({ id: "link_context", title: "link_context" }, () => {
+ browser.test.sendMessage("menu_items_registered");
+ });
+
+ if (browser.runtime.id === "@menu-test-extension") {
+ browser.tabs.create({ url: "tab.html" });
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: { gecko: { id: "@menu-test-extension" } },
+ permissions: ["menus", "menus.overrideContext", "tabs"],
+ },
+ files: {
+ "tab.html": `
+ <!DOCTYPE html><meta charset="utf-8">
+ <a href="http://example.com/">Link</a>
+ <script src="tab.js"></script>
+ `,
+ "tab.js": async () => {
+ let [tab] = await browser.tabs.query({
+ url: "https://example.com/?SomeTab",
+ });
+ let testCases = [
+ {
+ context: "tab",
+ tabId: tab.id,
+ },
+ {
+ context: "tab",
+ tabId: tab.id,
+ },
+ {
+ context: "tab",
+ tabId: 123456789, // Some invalid tabId.
+ },
+ ];
+
+ // eslint-disable-next-line mozilla/balanced-listeners
+ document.addEventListener("contextmenu", () => {
+ browser.menus.overrideContext(testCases.shift());
+ browser.test.sendMessage("oncontextmenu_in_dom");
+ });
+
+ browser.test.sendMessage("setup_ready", {
+ tabId: tab.id,
+ httpUrl: tab.url,
+ extensionUrl: document.URL,
+ });
+ },
+ },
+ background,
+ });
+
+ let { browser } = window.openContentTab("https://example.com/?SomeTab");
+ await awaitBrowserLoaded(browser);
+
+ let otherExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: { gecko: { id: "@other-test-extension" } },
+ permissions: ["menus", "activeTab"],
+ },
+ background,
+ });
+ await otherExtension.startup();
+ await otherExtension.awaitMessage("menu_items_registered");
+
+ await extension.startup();
+ await extension.awaitMessage("menu_items_registered");
+
+ let { tabId, httpUrl, extensionUrl } = await extension.awaitMessage(
+ "setup_ready"
+ );
+ info(`Set up test with tabId=${tabId}.`);
+
+ {
+ // Test case 1: context=tab
+ let menu = await openContextMenu("a");
+ await extension.awaitMessage("oncontextmenu_in_dom");
+ for (let ext of [extension, otherExtension]) {
+ info(`Testing menu from ${ext.id} after changing context to tab`);
+ Assert.deepEqual(
+ await ext.awaitMessage("onShown"),
+ {
+ menuIds: [
+ "tab_context",
+ "tab_context_http",
+ "tab_context_viewType_moz",
+ ],
+ contexts: ["tab"],
+ bookmarkId: undefined,
+ pageUrl: undefined, // because extension has no host permissions.
+ frameUrl: extensionUrl,
+ tabId,
+ },
+ "Expected onShown details after changing context to tab"
+ );
+ }
+ let topLevels = menu.getElementsByAttribute("ext-type", "top-level-menu");
+ is(topLevels.length, 1, "Expected top-level menu for otherExtension");
+
+ Assert.deepEqual(
+ getVisibleChildrenIds(menu),
+ [
+ `${makeWidgetId(extension.id)}-menuitem-_tab_context`,
+ `${makeWidgetId(extension.id)}-menuitem-_tab_context_http`,
+ `${makeWidgetId(extension.id)}-menuitem-_tab_context_viewType_moz`,
+ `menuseparator`,
+ topLevels[0].id,
+ ],
+ "Expected menu items after changing context to tab"
+ );
+
+ let submenu = await openSubmenu(topLevels[0]);
+ is(submenu, topLevels[0].menupopup, "Correct submenu opened");
+
+ Assert.deepEqual(
+ getVisibleChildrenIds(submenu),
+ [
+ `${makeWidgetId(otherExtension.id)}-menuitem-_tab_context`,
+ `${makeWidgetId(otherExtension.id)}-menuitem-_tab_context_http`,
+ `${makeWidgetId(otherExtension.id)}-menuitem-_tab_context_viewType_moz`,
+ ],
+ "Expected menu items in submenu after changing context to tab"
+ );
+
+ extension.sendMessage("testTabAccess", tabId);
+ is(
+ await extension.awaitMessage("testTabAccessDone"),
+ "executeScript_failed",
+ "executeScript should fail due to the lack of permissions."
+ );
+
+ otherExtension.sendMessage("testTabAccess", tabId);
+ is(
+ await otherExtension.awaitMessage("testTabAccessDone"),
+ "tab_no_url",
+ "Other extension should not have activeTab permissions yet."
+ );
+
+ // Click on the menu item of the other extension to unlock host permissions.
+ let menuItems = menu.getElementsByAttribute("label", "tab_context");
+ is(
+ menuItems.length,
+ 2,
+ "There are two menu items with label 'tab_context'"
+ );
+ await closeExtensionContextMenu(menuItems[1]);
+
+ Assert.deepEqual(
+ await otherExtension.awaitMessage("onClicked"),
+ {
+ menuItemId: "tab_context",
+ bookmarkId: undefined,
+ pageUrl: httpUrl,
+ frameUrl: extensionUrl,
+ tabId,
+ },
+ "Expected onClicked details after changing context to tab"
+ );
+
+ extension.sendMessage("testTabAccess", tabId);
+ is(
+ await extension.awaitMessage("testTabAccessDone"),
+ "executeScript_failed",
+ "executeScript of extension that created the menu should still fail."
+ );
+
+ otherExtension.sendMessage("testTabAccess", tabId);
+ is(
+ await otherExtension.awaitMessage("testTabAccessDone"),
+ "executeScript_ok",
+ "Other extension should have activeTab permissions."
+ );
+ }
+
+ {
+ // Test case 2: context=tab, click on menu item of extension..
+ let menu = await openContextMenu("a");
+ await extension.awaitMessage("oncontextmenu_in_dom");
+
+ // The previous test has already verified the visible menu items,
+ // so we skip checking the onShown result and only test clicking.
+ await extension.awaitMessage("onShown");
+ await otherExtension.awaitMessage("onShown");
+ let menuItems = menu.getElementsByAttribute("label", "tab_context");
+ is(
+ menuItems.length,
+ 2,
+ "There are two menu items with label 'tab_context'"
+ );
+ await closeExtensionContextMenu(menuItems[0]);
+
+ Assert.deepEqual(
+ await extension.awaitMessage("onClicked"),
+ {
+ menuItemId: "tab_context",
+ bookmarkId: undefined,
+ pageUrl: httpUrl,
+ frameUrl: extensionUrl,
+ tabId,
+ },
+ "Expected onClicked details after changing context to tab"
+ );
+
+ extension.sendMessage("testTabAccess", tabId);
+ is(
+ await extension.awaitMessage("testTabAccessDone"),
+ "executeScript_failed",
+ "activeTab permission should not be available to the extension that created the menu."
+ );
+ }
+
+ {
+ // Test case 4: context=tab, invalid tabId.
+ let menu = await openContextMenu("a");
+ await extension.awaitMessage("oncontextmenu_in_dom");
+ // When an invalid tabId is used, all extension menu logic is skipped and
+ // the default menu is shown.
+ checkIsDefaultMenuItemVisible(getVisibleChildrenIds(menu));
+ await closeContextMenu(menu);
+ }
+
+ await extension.unload();
+ await otherExtension.unload();
+
+ let tabmail = document.getElementById("tabmail");
+ tabmail.closeTab(tabmail.currentTabInfo);
+ tabmail.closeTab(tabmail.currentTabInfo);
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay.js b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay.js
new file mode 100644
index 0000000000..c99ea52440
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay.js
@@ -0,0 +1,1016 @@
+/* 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 gAccount;
+var gMessages;
+var gFolder;
+
+add_setup(() => {
+ gAccount = createAccount();
+ let rootFolder = gAccount.incomingServer.rootFolder;
+ rootFolder.createSubfolder("test0", null);
+ rootFolder.createSubfolder("test1", null);
+ rootFolder.createSubfolder("test2", null);
+
+ let subFolders = {};
+ for (let folder of rootFolder.subFolders) {
+ subFolders[folder.name] = folder;
+ }
+ createMessages(subFolders.test0, 5);
+ createMessages(subFolders.test1, 5);
+ createMessages(subFolders.test2, 6);
+
+ gFolder = subFolders.test0;
+ gMessages = [...subFolders.test0.messages];
+});
+
+add_task(async function testGetDisplayedMessage() {
+ let files = {
+ "background.js": async () => {
+ let [{ id: firstTabId, displayedFolder }] = await browser.mailTabs.query({
+ active: true,
+ currentWindow: true,
+ });
+
+ let { messages } = await browser.messages.list(displayedFolder);
+
+ async function checkResults(action, expectedMessages, sameTab) {
+ let msgListener = window.waitForEvent(
+ "messageDisplay.onMessageDisplayed"
+ );
+ let msgsListener = window.waitForEvent(
+ "messageDisplay.onMessagesDisplayed"
+ );
+
+ if (typeof action == "string") {
+ await window.sendMessage(action);
+ } else {
+ action();
+ }
+
+ let tab;
+ let message;
+ if (expectedMessages.length == 1) {
+ [tab, message] = await msgListener;
+ let [msgsTab, msgs] = await msgsListener;
+ // Check listener results.
+ if (sameTab) {
+ browser.test.assertEq(firstTabId, tab.id);
+ browser.test.assertEq(firstTabId, msgsTab.id);
+ } else {
+ browser.test.assertTrue(firstTabId != tab.id);
+ browser.test.assertTrue(firstTabId != msgsTab.id);
+ }
+ browser.test.assertEq(
+ messages[expectedMessages[0]].subject,
+ message.subject
+ );
+ browser.test.assertEq(
+ messages[expectedMessages[0]].subject,
+ msgs[0].subject
+ );
+
+ // Check displayed message result.
+ message = await browser.messageDisplay.getDisplayedMessage(tab.id);
+ browser.test.assertEq(
+ messages[expectedMessages[0]].subject,
+ message.subject
+ );
+ } else {
+ // onMessageDisplayed doesn't fire for the multi-message case.
+ let msgs;
+ [tab, msgs] = await msgsListener;
+
+ for (let [i, expected] of expectedMessages.entries()) {
+ browser.test.assertEq(messages[expected].subject, msgs[i].subject);
+ }
+
+ // More than one selected, so getDisplayMessage returns null.
+ message = await browser.messageDisplay.getDisplayedMessage(tab.id);
+ browser.test.assertEq(null, message);
+ }
+
+ let displayMsgs = await browser.messageDisplay.getDisplayedMessages(
+ tab.id
+ );
+ browser.test.assertEq(expectedMessages.length, displayMsgs.length);
+ for (let [i, expected] of expectedMessages.entries()) {
+ browser.test.assertEq(
+ messages[expected].subject,
+ displayMsgs[i].subject
+ );
+ }
+ return tab;
+ }
+
+ async function testGetDisplayedMessageFunctions(tabId, expected) {
+ let messages = await browser.messageDisplay.getDisplayedMessages(tabId);
+ if (expected) {
+ browser.test.assertEq(1, messages.length);
+ browser.test.assertEq(expected.subject, messages[0].subject);
+ } else {
+ browser.test.assertEq(0, messages.length);
+ }
+
+ let message = await browser.messageDisplay.getDisplayedMessage(tabId);
+ if (expected) {
+ browser.test.assertEq(expected.subject, message.subject);
+ } else {
+ browser.test.assertEq(null, message);
+ }
+ }
+
+ // Test that selecting a different message fires the event.
+ await checkResults("show message 1", [1], true);
+
+ // ... and again, for good measure.
+ await checkResults("show message 2", [2], true);
+
+ // Test that opening a message in a new tab fires the event.
+ let tab = await checkResults("open message 0 in tab", [0], false);
+
+ // The opened tab should return message #0.
+ await testGetDisplayedMessageFunctions(tab.id, messages[0]);
+
+ // The first tab should return message #2, even if it is currently not displayed.
+ await testGetDisplayedMessageFunctions(firstTabId, messages[2]);
+
+ // Closing the tab should return us to the first tab.
+ await browser.tabs.remove(tab.id);
+
+ // Test that opening a message in a new window fires the event.
+ tab = await checkResults("open message 1 in window", [1], false);
+
+ // Test the windows API being able to return the messageDisplay window as
+ // the current one.
+ let msgWindow = await browser.windows.get(tab.windowId);
+ browser.test.assertEq(msgWindow.type, "messageDisplay");
+ let curWindow = await browser.windows.getCurrent();
+ browser.test.assertEq(tab.windowId, curWindow.id);
+ // Test the tabs API being able to return the correct current tab.
+ let [currentTab] = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+ browser.test.assertEq(tab.id, currentTab.id);
+
+ // Close the window.
+ browser.tabs.remove(tab.id);
+
+ // Test that selecting a multiple messages fires the event.
+ await checkResults("show messages 1 and 2", [1, 2], true);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(gFolder);
+ about3Pane.threadTree.selectedIndex = 0;
+
+ await extension.startup();
+
+ await extension.awaitMessage("show message 1");
+ about3Pane.threadTree.selectedIndex = 1;
+ extension.sendMessage();
+
+ await extension.awaitMessage("show message 2");
+ about3Pane.threadTree.selectedIndex = 2;
+ extension.sendMessage();
+
+ await extension.awaitMessage("open message 0 in tab");
+ await openMessageInTab(gMessages[0]);
+ extension.sendMessage();
+
+ await extension.awaitMessage("open message 1 in window");
+ await openMessageInWindow(gMessages[1]);
+ extension.sendMessage();
+
+ await extension.awaitMessage("show messages 1 and 2");
+ about3Pane.threadTree.selectedIndices = [1, 2];
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function testOpenMessagesInTabs() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ // Helper class to keep track of expected tab states and cycle though all
+ // tabs after each test to enure the returned values are as expected under
+ // different active/inactive scenarios.
+ class TabTest {
+ constructor() {
+ this.expectedTabs = new Map();
+ }
+
+ // Check the given tab to match the expected values, update the internal
+ // tracker Map, and cycle through all tabs to make sure they still match
+ // the expected values.
+ async check(description, tabId, expected) {
+ browser.test.log(`TabTest: ${description}`);
+ if (expected.active) {
+ // Mark all other tabs inactive.
+ this.expectedTabs.forEach((v, k) => {
+ v.active = k == tabId;
+ });
+ }
+ // When we call this.check() to cycle thru all tabs, we do not specify
+ // an expected value. Do not update the tracker map in this case.
+ if (!expected.skip) {
+ this.expectedTabs.set(tabId, expected);
+ }
+
+ // Wait till the loaded url is as expected. Only checking the last part,
+ // since running this test with --verify causes multiple accounts to
+ // be created, changing the expected first part of message urls.
+ await window.waitForCondition(async () => {
+ let tab = await browser.tabs.get(tabId);
+ let expected = this.expectedTabs.get(tabId);
+ return tab.status == "complete" && tab.url.endsWith(expected.url);
+ }, `Should have loaded the correct URL in tab ${tabId}`);
+
+ // Check if all existing tabs match their expected values.
+ await this._verify();
+
+ // Cycle though all tabs, if there is more than one and run the check
+ // for each active tab.
+ if (!expected.skip && this.expectedTabs.size > 1) {
+ // Loop over all tabs, activate each and verify all of them. Test the currently active
+ // tab last, so we end up with the original condition.
+ let currentActiveTab = this._toArray().find(tab => tab.active);
+ let tabsToVerify = this._toArray()
+ .filter(tab => tab.id != currentActiveTab.id)
+ .concat(currentActiveTab);
+ for (let tab of tabsToVerify) {
+ await browser.tabs.update(tab.id, { active: true });
+ await this.check("Activating tab " + tab.id, tab.id, {
+ active: true,
+ skip: true,
+ });
+ }
+ }
+ }
+
+ // Return the expectedTabs Map as an array.
+ _toArray() {
+ return Array.from(this.expectedTabs.entries(), tab => {
+ return { id: tab[0], ...tab[1] };
+ });
+ }
+
+ // Verify that all tabs match their currently expected values.
+ async _verify() {
+ let tabs = await browser.tabs.query({});
+ browser.test.assertEq(
+ this.expectedTabs.size,
+ tabs.length,
+ `number of tabs should be correct`
+ );
+
+ for (let [tabId, expectedTab] of this.expectedTabs) {
+ let tab = await browser.tabs.get(tabId);
+ browser.test.assertEq(
+ expectedTab.active,
+ tab.active,
+ `${tab.type} tab (id:${tabId}) should have the correct active setting`
+ );
+
+ if (expectedTab.hasOwnProperty("message")) {
+ // Getthe currently displayed message.
+ let message = await browser.messageDisplay.getDisplayedMessage(
+ tabId
+ );
+
+ // Test message either being correct or not displayed if not
+ // expected.
+ if (expectedTab.message) {
+ browser.test.assertTrue(
+ !!message,
+ `${tab.type} tab (id:${tabId}) should have a message`
+ );
+ if (message) {
+ browser.test.assertEq(
+ expectedTab.message.id,
+ message.id,
+ `${tab.type} tab (id:${tabId}) should have the correct message`
+ );
+ }
+ } else {
+ browser.test.assertEq(
+ null,
+ message,
+ `${tab.type} tab (id:${tabId}) should not display a message`
+ );
+ }
+ }
+
+ // Testing url parameter.
+ if (expectedTab.url) {
+ browser.test.assertTrue(
+ tab.url.endsWith(expectedTab.url),
+ `${tab.type} tab (id:${tabId}) should display the correct url`
+ );
+ }
+ }
+ }
+ }
+
+ // Verify startup conditions.
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(
+ 1,
+ accounts.length,
+ `number of accounts should be correct`
+ );
+
+ let folder1 = accounts[0].folders.find(f => f.name == "test1");
+ browser.test.assertTrue(!!folder1, "folder should exist");
+ let { messages: messages1 } = await browser.messages.list(folder1);
+ browser.test.assertEq(
+ 5,
+ messages1.length,
+ `number of messages should be correct`
+ );
+
+ let folder2 = accounts[0].folders.find(f => f.name == "test2");
+ browser.test.assertTrue(!!folder2, "folder should exist");
+ let { messages: messages2 } = await browser.messages.list(folder2);
+ browser.test.assertEq(
+ 6,
+ messages2.length,
+ `number of messages should be correct`
+ );
+
+ // Test reject on invalid openProperties.
+ await browser.test.assertRejects(
+ browser.messageDisplay.open({ messageId: 578 }),
+ `Unknown or invalid messageId: 578.`,
+ "browser.messageDisplay.open() should reject, if invalid messageId is specified"
+ );
+
+ await browser.test.assertRejects(
+ browser.messageDisplay.open({ headerMessageId: "1" }),
+ `Unknown or invalid headerMessageId: 1.`,
+ "browser.messageDisplay.open() should reject, if invalid headerMessageId is specified"
+ );
+
+ await browser.test.assertRejects(
+ browser.messageDisplay.open({}),
+ "Exactly one of messageId, headerMessageId or file must be specified.",
+ "browser.messageDisplay.open() should reject, if no messageId and no headerMessageId is specified"
+ );
+
+ await browser.test.assertRejects(
+ browser.messageDisplay.open({ messageId: 578, headerMessageId: "1" }),
+ "Exactly one of messageId, headerMessageId or file must be specified.",
+ "browser.messageDisplay.open() should reject, if messageId and headerMessageId are specified"
+ );
+
+ // Create a TabTest to cycle through all existing tabs after each test to
+ // verify returned values under different active/inactive scenarios.
+ let tabTest = new TabTest();
+
+ // Load a content tab into the primary mail tab, to have a known startup
+ // condition.
+ let tabs = await browser.tabs.query({});
+ browser.test.assertEq(1, tabs.length);
+ let mailTab = tabs[0];
+ await browser.tabs.update(mailTab.id, {
+ url: "https://www.example.com/mailTab/1",
+ });
+ await tabTest.check(
+ "Load a url into the default mail tab.",
+ mailTab.id,
+ {
+ active: true,
+ url: "https://www.example.com/mailTab/1",
+ }
+ );
+
+ // Create an active content tab.
+ let tab1 = await browser.tabs.create({
+ url: "https://www.example.com/contentTab1/1",
+ });
+ await tabTest.check("Create a content tab #1.", tab1.id, {
+ active: true,
+ url: "https://www.example.com/contentTab1/1",
+ });
+
+ // Open an inactive message tab.
+ let tab2 = await browser.messageDisplay.open({
+ messageId: messages1[0].id,
+ location: "tab",
+ active: false,
+ });
+ await tabTest.check("messageDisplay.open with active: false", tab2.id, {
+ active: false,
+ message: messages1[0],
+ // To be able to run this test with --verify, specify only the last part
+ // of the expected message url, which is independent of the associated
+ // account.
+ url: "/localhost/test1?number=1",
+ });
+
+ // Open an active message tab.
+ let tab3 = await browser.messageDisplay.open({
+ messageId: messages1[0].id,
+ location: "tab",
+ active: true,
+ });
+ await tabTest.check(
+ "Opening the same message again should create a new tab.",
+ tab3.id,
+ {
+ active: true,
+ message: messages1[0],
+ url: "/localhost/test1?number=1",
+ }
+ );
+
+ // Open another content tab.
+ let tab4 = await browser.tabs.create({
+ url: "https://www.example.com/contentTab1/2",
+ });
+ await tabTest.check("Create a content tab #2.", tab4.id, {
+ active: true,
+ url: "https://www.example.com/contentTab1/2",
+ });
+
+ await browser.tabs.remove(tab1.id);
+ await browser.tabs.remove(tab2.id);
+ await browser.tabs.remove(tab3.id);
+ await browser.tabs.remove(tab4.id);
+
+ // Test opening multiple tabs.
+ let promisedTabs = [];
+ promisedTabs.push(
+ browser.messageDisplay.open({
+ messageId: messages1[0].id,
+ location: "tab",
+ })
+ );
+ promisedTabs.push(
+ browser.messageDisplay.open({
+ messageId: messages1[1].id,
+ location: "tab",
+ })
+ );
+ promisedTabs.push(
+ browser.messageDisplay.open({
+ messageId: messages1[2].id,
+ location: "tab",
+ })
+ );
+ promisedTabs.push(
+ browser.messageDisplay.open({
+ messageId: messages1[3].id,
+ location: "tab",
+ })
+ );
+ promisedTabs.push(
+ browser.messageDisplay.open({
+ messageId: messages1[4].id,
+ location: "tab",
+ })
+ );
+ let openedTabs = await Promise.allSettled(promisedTabs);
+ for (let i = 0; i < 5; i++) {
+ browser.test.assertEq(
+ "fulfilled",
+ openedTabs[i].status,
+ `Promise for the opened message should have been fulfilled for tab ${i}`
+ );
+ let msg = await browser.messageDisplay.getDisplayedMessage(
+ openedTabs[i].value.id
+ );
+ browser.test.assertEq(
+ messages1[i].id,
+ msg.id,
+ `Should see the correct message in window ${i}`
+ );
+ await browser.tabs.remove(openedTabs[i].value.id);
+ }
+
+ browser.test.notifyPass();
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead", "tabs"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
+
+add_task(async function testOpenMessagesInWindows() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ // Verify startup conditions.
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(
+ 1,
+ accounts.length,
+ `number of accounts should be correct`
+ );
+
+ let folder1 = accounts[0].folders.find(f => f.name == "test1");
+ browser.test.assertTrue(!!folder1, "folder should exist");
+ let { messages: messages1 } = await browser.messages.list(folder1);
+ browser.test.assertEq(
+ 5,
+ messages1.length,
+ `number of messages should be correct`
+ );
+
+ // Open multiple different windows.
+ {
+ let promisedTabs = [];
+ promisedTabs.push(
+ browser.messageDisplay.open({
+ messageId: messages1[0].id,
+ location: "window",
+ })
+ );
+ promisedTabs.push(
+ browser.messageDisplay.open({
+ messageId: messages1[1].id,
+ location: "window",
+ })
+ );
+ promisedTabs.push(
+ browser.messageDisplay.open({
+ messageId: messages1[2].id,
+ location: "window",
+ })
+ );
+ promisedTabs.push(
+ browser.messageDisplay.open({
+ messageId: messages1[3].id,
+ location: "window",
+ })
+ );
+ promisedTabs.push(
+ browser.messageDisplay.open({
+ messageId: messages1[4].id,
+ location: "window",
+ })
+ );
+ let openedTabs = await Promise.allSettled(promisedTabs);
+ let foundIds = new Set();
+ for (let i = 0; i < 5; i++) {
+ browser.test.assertEq(
+ "fulfilled",
+ openedTabs[i].status,
+ `Promise for the opened message should have been fulfilled for window ${i}`
+ );
+
+ browser.test.assertTrue(
+ !foundIds.has(openedTabs[i].value.id),
+ `Tab ${i} should have a unique id ${openedTabs[i].value.id}`
+ );
+ foundIds.add(openedTabs[i].value.id);
+
+ let msg = await browser.messageDisplay.getDisplayedMessage(
+ openedTabs[i].value.id
+ );
+ browser.test.assertEq(
+ messages1[i].id,
+ msg.id,
+ `Should see the correct message in window ${i}`
+ );
+ await browser.tabs.remove(openedTabs[i].value.id);
+ }
+ }
+
+ // Open multiple identical windows.
+ {
+ let promisedTabs = [];
+ promisedTabs.push(
+ browser.messageDisplay.open({
+ messageId: messages1[0].id,
+ location: "window",
+ })
+ );
+ promisedTabs.push(
+ browser.messageDisplay.open({
+ messageId: messages1[0].id,
+ location: "window",
+ })
+ );
+ promisedTabs.push(
+ browser.messageDisplay.open({
+ messageId: messages1[0].id,
+ location: "window",
+ })
+ );
+ promisedTabs.push(
+ browser.messageDisplay.open({
+ messageId: messages1[0].id,
+ location: "window",
+ })
+ );
+ promisedTabs.push(
+ browser.messageDisplay.open({
+ messageId: messages1[0].id,
+ location: "window",
+ })
+ );
+ let openedTabs = await Promise.allSettled(promisedTabs);
+ let foundIds = new Set();
+ for (let i = 0; i < 5; i++) {
+ browser.test.assertEq(
+ "fulfilled",
+ openedTabs[i].status,
+ `Promise for the opened message should have been fulfilled for window ${i}`
+ );
+
+ browser.test.assertTrue(
+ !foundIds.has(openedTabs[i].value.id),
+ `Tab ${i} should have a unique id ${openedTabs[i].value.id}`
+ );
+ foundIds.add(openedTabs[i].value.id);
+
+ let msg = await browser.messageDisplay.getDisplayedMessage(
+ openedTabs[i].value.id
+ );
+ browser.test.assertEq(
+ messages1[0].id,
+ msg.id,
+ `Should see the correct message in window ${i}`
+ );
+ await browser.tabs.remove(openedTabs[i].value.id);
+ }
+ }
+
+ browser.test.notifyPass();
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead", "tabs"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
+
+add_task(async function test_MV3_event_pages_onMessageDisplayed() {
+ let files = {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, hasFired is set to false. In
+ // case of a wake-up, the first fired event is the one that woke up the background.
+ let hasFired = false;
+
+ browser.messageDisplay.onMessageDisplayed.addListener((tab, message) => {
+ // Only send the first event after background wake-up, this should be
+ // the only one expected.
+ if (!hasFired) {
+ hasFired = true;
+ browser.test.sendMessage("onMessageDisplayed received", {
+ tab,
+ message,
+ });
+ }
+ });
+
+ browser.test.sendMessage("background started");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ manifest_version: 3,
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ browser_specific_settings: {
+ gecko: { id: "onMessageDisplayed@mochi.test" },
+ },
+ },
+ });
+
+ function checkPersistentListeners({ primed }) {
+ // A persistent event is referenced by its moduleName as defined in
+ // ext-mails.json, not by its actual namespace.
+ const persistent_events = ["messageDisplay.onMessageDisplayed"];
+
+ for (let event of persistent_events) {
+ let [moduleName, eventName] = event.split(".");
+ assertPersistentListeners(extension, moduleName, eventName, {
+ primed,
+ });
+ }
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ // Select a message.
+
+ {
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(gFolder);
+ about3Pane.threadTree.selectedIndex = 2;
+
+ let displayInfo = await extension.awaitMessage(
+ "onMessageDisplayed received"
+ );
+ Assert.equal(
+ displayInfo.message.subject,
+ "Huge Shindig Yesterday",
+ "The primed onMessageDisplayed event should return the correct message."
+ );
+ Assert.deepEqual(
+ {
+ active: true,
+ type: "mail",
+ },
+ {
+ active: displayInfo.tab.active,
+ type: displayInfo.tab.type,
+ },
+ "The primed onMessageDisplayed event should return the correct values"
+ );
+
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+ }
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ // Open a message in a window.
+
+ {
+ let messageWindow = await openMessageInWindow(gMessages[0]);
+ let displayInfo = await extension.awaitMessage(
+ "onMessageDisplayed received"
+ );
+ Assert.equal(
+ displayInfo.message.subject,
+ "Big Meeting Today",
+ "The primed onMessageDisplayed event should return the correct message."
+ );
+ Assert.deepEqual(
+ {
+ active: true,
+ type: "messageDisplay",
+ },
+ {
+ active: displayInfo.tab.active,
+ type: displayInfo.tab.type,
+ },
+ "The primed onMessageDisplayed event should return the correct values"
+ );
+
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+ messageWindow.close();
+ }
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ // Open a message in a tab.
+
+ {
+ await openMessageInTab(gMessages[1]);
+ let displayInfo = await extension.awaitMessage(
+ "onMessageDisplayed received"
+ );
+ Assert.equal(
+ displayInfo.message.subject,
+ "Small Party Tomorrow",
+ "The primed onMessageDisplayed event should return the correct message."
+ );
+ Assert.deepEqual(
+ {
+ active: true,
+ type: "messageDisplay",
+ },
+ {
+ active: displayInfo.tab.active,
+ type: displayInfo.tab.type,
+ },
+ "The primed onMessageDisplayed event should return the correct values"
+ );
+
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+ document.getElementById("tabmail").closeTab();
+ }
+
+ await extension.unload();
+});
+
+add_task(async function test_MV3_event_pages_onMessagesDisplayed() {
+ let files = {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, hasFired is set to false. In
+ // case of a wake-up, the first fired event is the one that woke up the background.
+ let hasFired = false;
+
+ browser.messageDisplay.onMessagesDisplayed.addListener(
+ (tab, messages) => {
+ // Only send the first event after background wake-up, this should be
+ // the only one expected.
+ if (!hasFired) {
+ hasFired = true;
+ browser.test.sendMessage("onMessagesDisplayed received", {
+ tab,
+ messages,
+ });
+ }
+ }
+ );
+
+ browser.test.sendMessage("background started");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ manifest_version: 3,
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ browser_specific_settings: {
+ gecko: { id: "onMessagesDisplayed@mochi.test" },
+ },
+ },
+ });
+
+ function checkPersistentListeners({ primed }) {
+ // A persistent event is referenced by its moduleName as defined in
+ // ext-mails.json, not by its actual namespace.
+ const persistent_events = ["messageDisplay.onMessagesDisplayed"];
+
+ for (let event of persistent_events) {
+ let [moduleName, eventName] = event.split(".");
+ assertPersistentListeners(extension, moduleName, eventName, {
+ primed,
+ });
+ }
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ // Select multiple messages.
+
+ {
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(gFolder);
+ about3Pane.threadTree.selectedIndices = [0, 1, 2, 3, 4];
+
+ let displayInfo = await extension.awaitMessage(
+ "onMessagesDisplayed received"
+ );
+ Assert.equal(
+ displayInfo.messages.length,
+ 5,
+ "The primed onMessagesDisplayed event should return the correct number of messages."
+ );
+ Assert.deepEqual(
+ [
+ "Big Meeting Today",
+ "Small Party Tomorrow",
+ "Huge Shindig Yesterday",
+ "Tiny Wedding In a Fortnight",
+ "Red Document Needs Attention",
+ ],
+ displayInfo.messages.map(e => e.subject),
+ "The primed onMessagesDisplayed event should return the correct messages."
+ );
+ Assert.deepEqual(
+ {
+ active: true,
+ type: "mail",
+ },
+ {
+ active: displayInfo.tab.active,
+ type: displayInfo.tab.type,
+ },
+ "The primed onMessagesDisplayed event should return the correct values"
+ );
+
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+ }
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ // Open a message in a window.
+
+ {
+ let messageWindow = await openMessageInWindow(gMessages[0]);
+ let displayInfo = await extension.awaitMessage(
+ "onMessagesDisplayed received"
+ );
+ Assert.equal(
+ displayInfo.messages.length,
+ 1,
+ "The primed onMessagesDisplayed event should return the correct number of messages."
+ );
+ Assert.equal(
+ displayInfo.messages[0].subject,
+ "Big Meeting Today",
+ "The primed onMessagesDisplayed event should return the correct message."
+ );
+ Assert.deepEqual(
+ {
+ active: true,
+ type: "messageDisplay",
+ },
+ {
+ active: displayInfo.tab.active,
+ type: displayInfo.tab.type,
+ },
+ "The primed onMessagesDisplayed event should return the correct values"
+ );
+
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+ messageWindow.close();
+ }
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ // Open a message in a tab.
+
+ {
+ await openMessageInTab(gMessages[1]);
+ let displayInfo = await extension.awaitMessage(
+ "onMessagesDisplayed received"
+ );
+ Assert.equal(
+ displayInfo.messages.length,
+ 1,
+ "The primed onMessagesDisplayed event should return the correct number of messages."
+ );
+ Assert.equal(
+ displayInfo.messages[0].subject,
+ "Small Party Tomorrow",
+ "The primed onMessagesDisplayed event should return the correct message."
+ );
+ Assert.deepEqual(
+ {
+ active: true,
+ type: "messageDisplay",
+ },
+ {
+ active: displayInfo.tab.active,
+ type: displayInfo.tab.type,
+ },
+ "The primed onMessagesDisplayed event should return the correct values"
+ );
+
+ await extension.awaitMessage("background started");
+ // The listeners should be persistent, but not primed.
+ checkPersistentListeners({ primed: false });
+ document.getElementById("tabmail").closeTab();
+ }
+
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction.js b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction.js
new file mode 100644
index 0000000000..4c48d835b4
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction.js
@@ -0,0 +1,337 @@
+/* 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 account;
+let messages;
+let tabmail = document.getElementById("tabmail");
+
+add_setup(async () => {
+ account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ let subFolders = rootFolder.subFolders;
+ createMessages(subFolders[0], 10);
+ messages = subFolders[0].messages;
+
+ let about3Pane = tabmail.currentAbout3Pane;
+ about3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: subFolders[0],
+ messagePaneVisible: true,
+ });
+ about3Pane.threadTree.selectedIndex = 0;
+ await awaitBrowserLoaded(
+ about3Pane.messageBrowser.contentWindow.getMessagePaneBrowser()
+ );
+});
+
+// This test uses a command from the menus API to open the popup.
+add_task(async function test_popup_open_with_menu_command() {
+ info("3-pane tab");
+ {
+ let testConfig = {
+ actionType: "message_display_action",
+ testType: "open-with-menu-command",
+ window: tabmail.currentAboutMessage,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+ }
+
+ info("Message tab");
+ {
+ await openMessageInTab(messages.getNext());
+ let testConfig = {
+ actionType: "message_display_action",
+ testType: "open-with-menu-command",
+ window: tabmail.currentAboutMessage,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+
+ document.getElementById("tabmail").closeTab();
+ }
+
+ info("Message window");
+ {
+ let messageWindow = await openMessageInWindow(messages.getNext());
+ let testConfig = {
+ actionType: "message_display_action",
+ testType: "open-with-menu-command",
+ window: messageWindow.messageBrowser.contentWindow,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+
+ messageWindow.close();
+ }
+});
+
+add_task(async function test_theme_icons() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: {
+ gecko: {
+ id: "message_display_action@mochi.test",
+ },
+ },
+ message_display_action: {
+ default_title: "default",
+ default_icon: "default.png",
+ theme_icons: [
+ {
+ dark: "dark.png",
+ light: "light.png",
+ size: 16,
+ },
+ ],
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let aboutMessage = tabmail.currentAboutMessage;
+ let uuid = extension.uuid;
+ let button = aboutMessage.document.getElementById(
+ "message_display_action_mochi_test-messageDisplayAction-toolbarbutton"
+ );
+
+ let dark_theme = await AddonManager.getAddonByID(
+ "thunderbird-compact-dark@mozilla.org"
+ );
+ await dark_theme.enable();
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ Assert.equal(
+ aboutMessage.getComputedStyle(button).listStyleImage,
+ `url("moz-extension://${uuid}/light.png")`,
+ `Dark theme should use light icon.`
+ );
+
+ let light_theme = await AddonManager.getAddonByID(
+ "thunderbird-compact-light@mozilla.org"
+ );
+ await light_theme.enable();
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ Assert.equal(
+ aboutMessage.getComputedStyle(button).listStyleImage,
+ `url("moz-extension://${uuid}/dark.png")`,
+ `Light theme should use dark icon.`
+ );
+
+ // Disabling a theme will enable the default theme.
+ await light_theme.disable();
+ Assert.equal(
+ aboutMessage.getComputedStyle(button).listStyleImage,
+ `url("moz-extension://${uuid}/default.png")`,
+ `Default theme should use default icon.`
+ );
+
+ await extension.unload();
+}).skip(); // TODO (Bug 1828322)
+
+add_task(async function test_button_order() {
+ info("3-pane tab");
+ await run_action_button_order_test(
+ [
+ {
+ name: "addon1",
+ toolbar: "header-view-toolbar",
+ },
+ {
+ name: "addon2",
+ toolbar: "header-view-toolbar",
+ },
+ ],
+ tabmail.currentAboutMessage,
+ "message_display_action"
+ );
+
+ info("Message tab");
+ await openMessageInTab(messages.getNext());
+ await run_action_button_order_test(
+ [
+ {
+ name: "addon1",
+ toolbar: "header-view-toolbar",
+ },
+ {
+ name: "addon2",
+ toolbar: "header-view-toolbar",
+ },
+ ],
+ tabmail.currentAboutMessage,
+ "message_display_action"
+ );
+ tabmail.closeTab();
+
+ info("Message window");
+ let messageWindow = await openMessageInWindow(messages.getNext());
+ await run_action_button_order_test(
+ [
+ {
+ name: "addon1",
+ toolbar: "header-view-toolbar",
+ },
+ {
+ name: "addon2",
+ toolbar: "header-view-toolbar",
+ },
+ ],
+ messageWindow.messageBrowser.contentWindow,
+ "message_display_action"
+ );
+ messageWindow.close();
+});
+
+add_task(async function test_upgrade() {
+ // Add a message_display_action, to make sure the currentSet has been initialized.
+ let extension1 = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ manifest_version: 2,
+ version: "1.0",
+ name: "Extension1",
+ applications: { gecko: { id: "Extension1@mochi.test" } },
+ message_display_action: {
+ default_title: "Extension1",
+ },
+ },
+ background() {
+ browser.test.sendMessage("Extension1 ready");
+ },
+ });
+ await extension1.startup();
+ await extension1.awaitMessage("Extension1 ready");
+
+ // Add extension without a message_display_action.
+ let extension2 = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ manifest_version: 2,
+ version: "1.0",
+ name: "Extension2",
+ applications: { gecko: { id: "Extension2@mochi.test" } },
+ },
+ background() {
+ browser.test.sendMessage("Extension2 ready");
+ },
+ });
+ await extension2.startup();
+ await extension2.awaitMessage("Extension2 ready");
+
+ // Update the extension, now including a message_display_action.
+ let updatedExtension2 = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ manifest_version: 2,
+ version: "2.0",
+ name: "Extension2",
+ applications: { gecko: { id: "Extension2@mochi.test" } },
+ message_display_action: {
+ default_title: "Extension2",
+ },
+ },
+ background() {
+ browser.test.sendMessage("Extension2 updated");
+ },
+ });
+ await updatedExtension2.startup();
+ await updatedExtension2.awaitMessage("Extension2 updated");
+
+ let aboutMessage = tabmail.currentAboutMessage;
+ let button = aboutMessage.document.getElementById(
+ "extension2_mochi_test-messageDisplayAction-toolbarbutton"
+ );
+
+ Assert.ok(button, "Button should exist");
+ await extension1.unload();
+ await extension2.unload();
+ await updatedExtension2.unload();
+});
+
+add_task(async function test_iconPath() {
+ // String values for the default_icon manifest entry have been tested in the
+ // theme_icons test already. Here we test imagePath objects for the manifest key
+ // and string values as well as objects for the setIcons() function.
+ let files = {
+ "background.js": async () => {
+ await window.sendMessage("checkState", "icon1.png");
+
+ // TODO: Figure out why this isn't working properly.
+ // await browser.messageDisplayAction.setIcon({ path: "icon2.png" });
+ // await window.sendMessage("checkState", "icon2.png");
+
+ // await browser.messageDisplayAction.setIcon({ path: { 16: "icon3.png" } });
+ // await window.sendMessage("checkState", "icon3.png");
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ applications: {
+ gecko: {
+ id: "message_display_action@mochi.test",
+ },
+ },
+ message_display_action: {
+ default_title: "default",
+ default_icon: { 16: "icon1.png" },
+ },
+ background: { scripts: ["utils.js", "background.js"] },
+ },
+ });
+
+ let aboutMessage = tabmail.currentAboutMessage;
+ extension.onMessage("checkState", async expected => {
+ let uuid = extension.uuid;
+ let button = aboutMessage.document.getElementById(
+ "message_display_action_mochi_test-messageDisplayAction-toolbarbutton"
+ );
+
+ Assert.equal(
+ aboutMessage.getComputedStyle(button).listStyleImage,
+ `url("moz-extension://${uuid}/${expected}")`,
+ `Icon path should be correct.`
+ );
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_popup_click.js b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_popup_click.js
new file mode 100644
index 0000000000..96493c475a
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_popup_click.js
@@ -0,0 +1,294 @@
+/* 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 account;
+let messages;
+let tabmail = document.getElementById("tabmail");
+
+add_setup(async () => {
+ account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ let subFolders = rootFolder.subFolders;
+ createMessages(subFolders[0], 10);
+ messages = subFolders[0].messages;
+
+ let about3Pane = tabmail.currentAbout3Pane;
+ about3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: subFolders[0],
+ messagePaneVisible: true,
+ });
+ about3Pane.threadTree.selectedIndex = 0;
+ await awaitBrowserLoaded(
+ about3Pane.messageBrowser.contentWindow.getMessagePaneBrowser()
+ );
+});
+
+// This test clicks on the action button to open the popup.
+add_task(async function test_popup_open_with_click() {
+ info("3-pane tab");
+ {
+ let testConfig = {
+ actionType: "message_display_action",
+ testType: "open-with-mouse-click",
+ window: tabmail.currentAboutMessage,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+ }
+
+ info("Message tab");
+ {
+ await openMessageInTab(messages.getNext());
+ let testConfig = {
+ actionType: "message_display_action",
+ testType: "open-with-mouse-click",
+ window: tabmail.currentAboutMessage,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+
+ document.getElementById("tabmail").closeTab();
+ }
+
+ info("Message window");
+ {
+ let messageWindow = await openMessageInWindow(messages.getNext());
+ let testConfig = {
+ actionType: "message_display_action",
+ testType: "open-with-mouse-click",
+ window: messageWindow.messageBrowser.contentWindow,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+
+ messageWindow.close();
+ }
+});
+
+// This test uses openPopup() to open the popup in a message window.
+add_task(async function test_popup_open_with_openPopup_in_message_window() {
+ let files = {
+ "background.js": async () => {
+ let windows = await browser.windows.getAll();
+ let mailWindow = windows.find(window => window.type == "normal");
+ let messageWindow = windows.find(
+ window => window.type == "messageDisplay"
+ );
+ browser.test.assertTrue(!!mailWindow, "should have found a mailWindow");
+ browser.test.assertTrue(
+ !!messageWindow,
+ "should have found a messageWindow"
+ );
+
+ let tabs = await browser.tabs.query({});
+ let mailTab = tabs.find(tab => tab.type == "mail");
+ browser.test.assertTrue(!!mailTab, "should have found a mailTab");
+
+ let msg = await browser.messageDisplay.getDisplayedMessage(mailTab.id);
+ browser.test.assertTrue(!!msg, "should display a message");
+
+ // The test starts with an opened messageWindow, the message_display_action
+ // is allowed there and should be visible, openPopup() should succeed.
+ browser.test.assertTrue(
+ (await browser.windows.get(messageWindow.id)).focused,
+ "messageWindow should be focused"
+ );
+ browser.test.assertTrue(
+ await browser.messageDisplayAction.openPopup(),
+ "openPopup() should have succeeded while the messageWindow is active"
+ );
+ await window.waitForMessage();
+
+ // Specifically open the message_display_action of the mailWindow, since we
+ // loaded a message, openPopup() should succeed.
+ browser.test.assertTrue(
+ await browser.messageDisplayAction.openPopup({
+ windowId: mailWindow.id,
+ }),
+ "openPopup() should have succeeded when explicitly requesting the mailWindow"
+ );
+ await window.waitForMessage();
+ // Mail window should have focus now.
+ browser.test.assertTrue(
+ (await browser.windows.get(mailWindow.id)).focused,
+ "mailWindow should be focused"
+ );
+
+ // Disable the message_display_action, openPopup() should fail.
+ await browser.messageDisplayAction.disable();
+ browser.test.assertFalse(
+ await browser.messageDisplayAction.openPopup(),
+ "openPopup() should have failed after the action_button was disabled"
+ );
+
+ // Enable the message_display_action, openPopup() should succeed.
+ await browser.messageDisplayAction.enable();
+ browser.test.assertTrue(
+ await browser.messageDisplayAction.openPopup(),
+ "openPopup() should have succeeded after the action_button was enabled again"
+ );
+ await window.waitForMessage();
+
+ // Create content tab, the message_display_action is not allowed there and
+ // should not be visible, openPopup() should fail.
+ let contentTab = await browser.tabs.create({
+ url: "https://www.example.com",
+ });
+ browser.test.assertFalse(
+ await browser.messageDisplayAction.openPopup(),
+ "openPopup() should have failed while the content tab is active"
+ );
+
+ // Close the content tab and return to the mail space, the message_display_action
+ // should be visible again, openPopup() should succeed.
+ await browser.tabs.remove(contentTab.id);
+ browser.test.assertTrue(
+ await browser.messageDisplayAction.openPopup(),
+ "openPopup() should have succeeded after the content tab was closed"
+ );
+ await window.waitForMessage();
+
+ // Load a webpage into the mailTab, the message_display_action should not
+ // be shown and openPopup() should fail
+ await browser.tabs.update(mailTab.id, { url: "https://www.example.com" });
+ browser.test.assertFalse(
+ await browser.messageDisplayAction.openPopup(),
+ "openPopup() should have failed while the mail tab shows a webpage"
+ );
+
+ // Open a message in a tab, the message_display_action should be shown and
+ // openPopup() should succeed.
+ let messageTab = await browser.messageDisplay.open({
+ active: true,
+ location: "tab",
+ messageId: msg.id,
+ windowId: mailWindow.id,
+ });
+ browser.test.assertTrue(
+ await browser.messageDisplayAction.openPopup(),
+ "openPopup() should have succeeded in a message tab"
+ );
+ await window.waitForMessage();
+
+ // Create a popup window, which does not have a message_display_action, openPopup()
+ // should fail.
+ let popupWindow = await browser.windows.create({
+ type: "popup",
+ url: "https://www.example.com",
+ });
+ browser.test.assertTrue(
+ (await browser.windows.get(popupWindow.id)).focused,
+ "popupWindow should be focused"
+ );
+ browser.test.assertFalse(
+ await browser.messageDisplayAction.openPopup(),
+ "openPopup() should have failed while the popup window is active"
+ );
+
+ // Specifically open the message_display_action of the messageWindow, should become
+ // focused and openPopup() should succeed.
+ browser.test.assertTrue(
+ await browser.messageDisplayAction.openPopup({
+ windowId: messageWindow.id,
+ }),
+ "openPopup() should have succeeded when explicitly requesting the messageWindow"
+ );
+ await window.waitForMessage();
+ browser.test.assertTrue(
+ (await browser.windows.get(messageWindow.id)).focused,
+ "messageWindow should be focused"
+ );
+
+ // The messageWindow is focused now, openPopup() should succeed.
+ browser.test.assertTrue(
+ await browser.messageDisplayAction.openPopup(),
+ "openPopup() should have succeeded while the messageWindow is active"
+ );
+ await window.waitForMessage();
+
+ // Close the popup window, the extra message tab and finish
+ await browser.windows.remove(popupWindow.id);
+ await browser.tabs.remove(messageTab.id);
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ "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 () {
+ browser.test.sendMessage("popup opened");
+ window.close();
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: {
+ id: "message_display_action_openPopup@mochi.test",
+ },
+ },
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["messagesRead"],
+ message_display_action: {
+ default_title: "default",
+ default_popup: "popup.html",
+ },
+ },
+ });
+
+ extension.onMessage("popup opened", async () => {
+ // Wait a moment to make sure the popup has closed.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => window.setTimeout(r, 150));
+ extension.sendMessage();
+ });
+
+ let messageWindow = await openMessageInWindow(messages.getNext());
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ messageWindow.close();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_popup_click_mv3_event_pages.js b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_popup_click_mv3_event_pages.js
new file mode 100644
index 0000000000..9f72bf4c99
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_popup_click_mv3_event_pages.js
@@ -0,0 +1,113 @@
+/* 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 account;
+let messages;
+let tabmail = document.getElementById("tabmail");
+
+add_setup(async () => {
+ account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ let subFolders = rootFolder.subFolders;
+ createMessages(subFolders[0], 10);
+ messages = subFolders[0].messages;
+
+ let about3Pane = tabmail.currentAbout3Pane;
+ about3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: subFolders[0],
+ messagePaneVisible: true,
+ });
+ about3Pane.threadTree.selectedIndex = 0;
+ await awaitBrowserLoaded(
+ about3Pane.messageBrowser.contentWindow.getMessagePaneBrowser()
+ );
+});
+
+async function subtest_popup_open_with_click_MV3_event_pages(
+ terminateBackground
+) {
+ info("3-pane tab");
+ {
+ let testConfig = {
+ manifest_version: 3,
+ terminateBackground,
+ actionType: "message_display_action",
+ testType: "open-with-mouse-click",
+ window: tabmail.currentAboutMessage,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+ }
+
+ info("Message tab");
+ {
+ await openMessageInTab(messages.getNext());
+ let testConfig = {
+ manifest_version: 3,
+ terminateBackground,
+ actionType: "message_display_action",
+ testType: "open-with-mouse-click",
+ window: tabmail.currentAboutMessage,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+
+ tabmail.closeTab();
+ }
+
+ info("Message window");
+ {
+ let messageWindow = await openMessageInWindow(messages.getNext());
+ let testConfig = {
+ manifest_version: 3,
+ terminateBackground,
+ actionType: "message_display_action",
+ testType: "open-with-mouse-click",
+ window: messageWindow.messageBrowser.contentWindow,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+
+ messageWindow.close();
+ }
+}
+// This MV3 test clicks on the action button to open the popup.
+add_task(async function test_event_pages_without_background_termination() {
+ await subtest_popup_open_with_click_MV3_event_pages(false);
+});
+// This MV3 test clicks on the action button to open the popup (background termination).
+add_task(async function test_event_pages_with_background_termination() {
+ await subtest_popup_open_with_click_MV3_event_pages(true);
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_properties.js b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_properties.js
new file mode 100644
index 0000000000..694d352090
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_properties.js
@@ -0,0 +1,184 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async () => {
+ let account = createAccount();
+ addIdentity(account);
+ let rootFolder = account.incomingServer.rootFolder;
+ rootFolder.createSubfolder("test", null);
+ let folder = rootFolder.getChildNamed("test");
+ createMessages(folder, 1);
+ let [message] = [...folder.messages];
+
+ let tabmail = document.getElementById("tabmail");
+ let about3Pane = tabmail.currentAbout3Pane;
+ about3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: folder.URI,
+ messagePaneVisible: true,
+ });
+ about3Pane.threadTree.selectedIndex = 0;
+ await awaitBrowserLoaded(
+ about3Pane.messageBrowser.contentWindow.getMessagePaneBrowser()
+ );
+
+ await openMessageInTab(message);
+ await openMessageInWindow(message);
+ await new Promise(resolve => executeSoon(resolve));
+
+ let files = {
+ "background.js": async () => {
+ async function checkProperty(property, expectedDefault, ...expected) {
+ browser.test.log(
+ `${property}: ${expectedDefault}, ${expected.join(", ")}`
+ );
+
+ browser.test.assertEq(
+ expectedDefault,
+ await browser.messageDisplayAction[property]({})
+ );
+ for (let i = 0; i < 3; i++) {
+ browser.test.assertEq(
+ expected[i],
+ await browser.messageDisplayAction[property]({ tabId: tabIDs[i] })
+ );
+ }
+
+ await window.sendMessage("checkProperty", property, expected);
+ }
+
+ let tabs = await browser.tabs.query({});
+ browser.test.assertEq(3, tabs.length);
+ let tabIDs = tabs.map(t => t.id);
+
+ await checkProperty("isEnabled", true, true, true, true);
+ await browser.messageDisplayAction.disable();
+ await checkProperty("isEnabled", false, false, false, false);
+ await browser.messageDisplayAction.enable(tabIDs[0]);
+ await checkProperty("isEnabled", false, true, false, false);
+ await browser.messageDisplayAction.enable();
+ await checkProperty("isEnabled", true, true, true, true);
+ await browser.messageDisplayAction.disable();
+ await checkProperty("isEnabled", false, true, false, false);
+ await browser.messageDisplayAction.disable(tabIDs[0]);
+ await checkProperty("isEnabled", false, false, false, false);
+ await browser.messageDisplayAction.enable();
+ await checkProperty("isEnabled", true, false, true, true);
+
+ await checkProperty(
+ "getTitle",
+ "default",
+ "default",
+ "default",
+ "default"
+ );
+ await browser.messageDisplayAction.setTitle({
+ tabId: tabIDs[2],
+ title: "tab2",
+ });
+ await checkProperty("getTitle", "default", "default", "default", "tab2");
+ await browser.messageDisplayAction.setTitle({ title: "new" });
+ await checkProperty("getTitle", "new", "new", "new", "tab2");
+ await browser.messageDisplayAction.setTitle({
+ tabId: tabIDs[1],
+ title: "tab1",
+ });
+ await checkProperty("getTitle", "new", "new", "tab1", "tab2");
+ await browser.messageDisplayAction.setTitle({
+ tabId: tabIDs[2],
+ title: null,
+ });
+ await checkProperty("getTitle", "new", "new", "tab1", "new");
+ await browser.messageDisplayAction.setTitle({ title: null });
+ await checkProperty("getTitle", "default", "default", "tab1", "default");
+ await browser.messageDisplayAction.setTitle({
+ tabId: tabIDs[1],
+ title: null,
+ });
+ await checkProperty(
+ "getTitle",
+ "default",
+ "default",
+ "default",
+ "default"
+ );
+
+ await browser.tabs.remove(tabIDs[0]);
+ await browser.tabs.remove(tabIDs[1]);
+ await browser.tabs.remove(tabIDs[2]);
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ applications: {
+ gecko: {
+ id: "message_display_action_properties@mochi.test",
+ },
+ },
+ background: { scripts: ["utils.js", "background.js"] },
+ message_display_action: {
+ default_title: "default",
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let mainWindowTabs = tabmail.tabInfo;
+ is(mainWindowTabs.length, 2);
+
+ let messageWindow = Services.wm.getMostRecentWindow("mail:messageWindow");
+ let messageWindowButton =
+ messageWindow.messageBrowser.contentDocument.getElementById(
+ "message_display_action_properties_mochi_test-messageDisplayAction-toolbarbutton"
+ );
+
+ extension.onMessage("checkProperty", async (property, expected) => {
+ function checkButton(button, expectedIndex) {
+ switch (property) {
+ case "isEnabled":
+ is(
+ button.disabled,
+ !expected[expectedIndex],
+ `button ${expectedIndex} enabled state`
+ );
+ break;
+ case "getTitle":
+ is(
+ button.getAttribute("label"),
+ expected[expectedIndex],
+ `button ${expectedIndex} label`
+ );
+ break;
+ }
+ }
+
+ for (let i = 0; i < 2; i++) {
+ tabmail.switchToTab(mainWindowTabs[i]);
+ let aboutMessage = mainWindowTabs[i].chromeBrowser.contentWindow;
+ if (aboutMessage.location.href == "about:3pane") {
+ aboutMessage = aboutMessage.messageBrowser.contentWindow;
+ }
+ await new Promise(resolve => aboutMessage.requestAnimationFrame(resolve));
+ checkButton(
+ aboutMessage.document.getElementById(
+ "message_display_action_properties_mochi_test-messageDisplayAction-toolbarbutton"
+ ),
+ i
+ );
+ }
+ checkButton(messageWindowButton, 2);
+
+ extension.sendMessage();
+ });
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ messageWindow.close();
+ tabmail.closeOtherTabs(0);
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_messageDisplayScripts.js b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplayScripts.js
new file mode 100644
index 0000000000..3f75bcb61c
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplayScripts.js
@@ -0,0 +1,636 @@
+/* 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 account, messages;
+let tabmail, about3Pane, messagePane;
+
+add_setup(async () => {
+ account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ rootFolder.createSubfolder("messageDisplayScripts", null);
+ let folder = rootFolder.getChildNamed("messageDisplayScripts");
+ createMessages(folder, 11);
+ messages = [...folder.messages];
+
+ tabmail = document.getElementById("tabmail");
+ about3Pane = tabmail.currentTabInfo.chromeBrowser.contentWindow;
+ about3Pane.displayFolder(folder.URI);
+ messagePane =
+ about3Pane.messageBrowser.contentDocument.getElementById("messagepane");
+});
+
+async function checkMessageBody(expected, message, browser) {
+ if (message && "textContent" in expected) {
+ let body = await new Promise(resolve => {
+ window.MsgHdrToMimeMessage(message, null, (msgHdr, mimeMessage) => {
+ resolve(mimeMessage.parts[0].body);
+ });
+ });
+ // Ignore Windows line-endings, they're not important here.
+ body = body.replace(/\r/g, "");
+ expected.textContent = body + expected.textContent;
+ }
+ if (!browser) {
+ browser = messagePane;
+ }
+
+ await checkContent(browser, expected);
+}
+
+/** Tests browser.tabs.insertCSS and browser.tabs.removeCSS. */
+add_task(async function testInsertRemoveCSS() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let [tab] = await browser.tabs.query({ mailTab: true });
+ await window.sendMessage();
+
+ await browser.tabs.insertCSS(tab.id, {
+ code: "body { background-color: lime; }",
+ });
+ await window.sendMessage();
+
+ await browser.tabs.removeCSS(tab.id, {
+ code: "body { background-color: lime; }",
+ });
+ await window.sendMessage();
+
+ await browser.tabs.insertCSS(tab.id, { file: "test.css" });
+ await window.sendMessage();
+
+ await browser.tabs.removeCSS(tab.id, { file: "test.css" });
+
+ browser.test.notifyPass("finished");
+ },
+ "test.css": "body { background-color: green; }",
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["messagesModify"],
+ },
+ });
+
+ about3Pane.threadTree.selectedIndex = 0;
+ await awaitBrowserLoaded(messagePane);
+
+ await extension.startup();
+
+ await extension.awaitMessage();
+ await checkMessageBody({ backgroundColor: "rgba(0, 0, 0, 0)" }, messages[0]);
+ extension.sendMessage();
+
+ await extension.awaitMessage();
+ await checkMessageBody({ backgroundColor: "rgb(0, 255, 0)" }, messages[0]);
+ extension.sendMessage();
+
+ await extension.awaitMessage();
+ await checkMessageBody({ backgroundColor: "rgba(0, 0, 0, 0)" }, messages[0]);
+ extension.sendMessage();
+
+ await extension.awaitMessage();
+ await checkMessageBody({ backgroundColor: "rgb(0, 128, 0)" }, messages[0]);
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await checkMessageBody({ backgroundColor: "rgba(0, 0, 0, 0)" }, messages[0]);
+
+ await extension.unload();
+});
+
+/** Tests browser.tabs.insertCSS fails without the "messagesModify" permission. */
+add_task(async function testInsertRemoveCSSNoPermissions() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let [tab] = await browser.tabs.query({ mailTab: true });
+
+ await browser.test.assertRejects(
+ browser.tabs.insertCSS(tab.id, {
+ code: "body { background-color: darkred; }",
+ }),
+ /Missing host permission for the tab/,
+ "insertCSS without permission should throw"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.insertCSS(tab.id, { file: "test.css" }),
+ /Missing host permission for the tab/,
+ "insertCSS without permission should throw"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.insertCSS(tab.id, {
+ file: "test.css",
+ matchAboutBlank: true,
+ }),
+ /Missing host permission for the tab/,
+ "insertCSS without permission should throw"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "test.css": "body { background-color: red; }",
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: [],
+ },
+ });
+
+ about3Pane.threadTree.selectedIndex = 1;
+ await awaitBrowserLoaded(messagePane);
+
+ await extension.startup();
+
+ await extension.awaitFinish("finished");
+ await checkMessageBody(
+ {
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ textContent: "",
+ },
+ messages[1]
+ );
+
+ await extension.unload();
+});
+
+/** Tests browser.tabs.executeScript. */
+add_task(async function testExecuteScript() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let [tab] = await browser.tabs.query({ mailTab: true });
+ await window.sendMessage();
+
+ await browser.tabs.executeScript(tab.id, {
+ code: `document.body.setAttribute("foo", "bar");`,
+ });
+ await window.sendMessage();
+
+ await browser.tabs.executeScript(tab.id, { file: "test.js" });
+
+ browser.test.notifyPass("finished");
+ },
+ "test.js": () => {
+ document.body.querySelector(".moz-text-flowed").textContent +=
+ "Hey look, the script ran!";
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["messagesModify"],
+ },
+ });
+
+ about3Pane.threadTree.selectedIndex = 2;
+ await awaitBrowserLoaded(messagePane);
+
+ await extension.startup();
+
+ await extension.awaitMessage();
+ await checkMessageBody({ textContent: "" }, messages[2]);
+ extension.sendMessage();
+
+ await extension.awaitMessage();
+ await checkMessageBody({ foo: "bar" }, messages[2]);
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await checkMessageBody(
+ {
+ foo: "bar",
+ textContent: "Hey look, the script ran!",
+ },
+ messages[2]
+ );
+
+ await extension.unload();
+});
+
+/** Tests browser.tabs.executeScript fails without the "messagesModify" permission. */
+add_task(async function testExecuteScriptNoPermissions() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let [tab] = await browser.tabs.query({ mailTab: true });
+
+ await browser.test.assertRejects(
+ browser.tabs.executeScript(tab.id, {
+ code: `document.body.setAttribute("foo", "bar");`,
+ }),
+ /Missing host permission for the tab/,
+ "executeScript without permission should throw"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.executeScript(tab.id, { file: "test.js" }),
+ /Missing host permission for the tab/,
+ "executeScript without permission should throw"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.executeScript(tab.id, {
+ file: "test.js",
+ matchAboutBlank: true,
+ }),
+ /Missing host permission for the tab/,
+ "executeScript without permission should throw"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "test.js": () => {
+ document.body.querySelector(".moz-text-flowed").textContent +=
+ "Hey look, the script ran!";
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: [],
+ },
+ });
+
+ about3Pane.threadTree.selectedIndex = 3;
+ await awaitBrowserLoaded(messagePane);
+
+ await extension.startup();
+
+ await extension.awaitFinish("finished");
+ await checkMessageBody({ foo: null, textContent: "" }, messages[3]);
+
+ await extension.unload();
+});
+
+/** Tests the messenger alias is available. */
+add_task(async function testExecuteScriptAlias() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let [tab] = await browser.tabs.query({ mailTab: true });
+ await window.sendMessage();
+
+ await browser.tabs.executeScript(tab.id, {
+ code: `document.body.querySelector(".moz-text-flowed").textContent +=
+ messenger.runtime.getManifest().applications.gecko.id;`,
+ });
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ applications: { gecko: { id: "message_display_scripts@mochitest" } },
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["messagesModify"],
+ },
+ });
+
+ about3Pane.threadTree.selectedIndex = 4;
+ await awaitBrowserLoaded(messagePane);
+
+ await extension.startup();
+
+ await extension.awaitMessage();
+ await checkMessageBody({ textContent: "" }, messages[4]);
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await checkMessageBody(
+ { textContent: "message_display_scripts@mochitest" },
+ messages[4]
+ );
+
+ await extension.unload();
+});
+
+/**
+ * Tests browser.messageDisplayScripts.register correctly adds CSS and
+ * JavaScript to message display windows. Also tests calling `unregister`
+ * on the returned object.
+ */
+add_task(async function testRegister() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ // Keep track of registered scrips being executed and ready.
+ browser.runtime.onMessage.addListener((message, sender) => {
+ if (message == "LOADED") {
+ window.sendMessage("ScriptLoaded", sender.tab.id);
+ }
+ });
+
+ let registeredScript = await browser.messageDisplayScripts.register({
+ css: [{ code: "body { color: white }" }, { file: "test.css" }],
+ js: [
+ { code: `document.body.setAttribute("foo", "bar");` },
+ { file: "test.js" },
+ ],
+ });
+
+ browser.test.onMessage.addListener(async (message, data) => {
+ switch (message) {
+ case "Unregister":
+ await registeredScript.unregister();
+ browser.test.notifyPass("finished");
+ break;
+
+ case "RuntimeMessageTest":
+ try {
+ browser.test.assertEq(
+ `Received: ${data.tabId}`,
+ await browser.tabs.sendMessage(data.tabId, data.tabId)
+ );
+ } catch (ex) {
+ browser.test.fail(
+ `Failed to send message to messageDisplayScript: ${ex}`
+ );
+ }
+ browser.test.sendMessage("RuntimeMessageTestDone");
+ break;
+ }
+ });
+
+ window.sendMessage("Ready");
+ },
+ "test.css": "body { background-color: green; }",
+ "test.js": () => {
+ document.body.querySelector(".moz-text-flowed").textContent +=
+ "Hey look, the script ran!";
+ browser.runtime.onMessage.addListener(async message => {
+ return `Received: ${message}`;
+ });
+ browser.runtime.sendMessage("LOADED");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["messagesModify", "<all_urls>"],
+ },
+ });
+
+ about3Pane.threadTree.selectedIndex = 5;
+ await awaitBrowserLoaded(messagePane);
+
+ extension.startup();
+ await extension.awaitMessage("Ready");
+
+ // Check a message that was already loaded. This tab has not loaded the
+ // registered scripts.
+ await checkMessageBody(
+ {
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ textContent: "",
+ },
+ messages[5]
+ );
+
+ // Load a new message and check it is modified.
+ let loadPromise = extension.awaitMessage("ScriptLoaded");
+ about3Pane.threadTree.selectedIndex = 6;
+ let tabId = await loadPromise;
+
+ await checkMessageBody(
+ {
+ backgroundColor: "rgb(0, 128, 0)",
+ color: "rgb(255, 255, 255)",
+ foo: "bar",
+ textContent: "Hey look, the script ran!",
+ },
+ messages[6]
+ );
+ // Check runtime messaging.
+ let testDonePromise = extension.awaitMessage("RuntimeMessageTestDone");
+ extension.sendMessage("RuntimeMessageTest", { tabId });
+ await testDonePromise;
+
+ // Open the message in a new tab.
+ loadPromise = extension.awaitMessage("ScriptLoaded");
+ let messageTab = await openMessageInTab(messages[6]);
+ let messageTabId = await loadPromise;
+ Assert.equal(tabmail.tabInfo.length, 2);
+
+ await checkMessageBody(
+ {
+ backgroundColor: "rgb(0, 128, 0)",
+ color: "rgb(255, 255, 255)",
+ foo: "bar",
+ textContent: "Hey look, the script ran!",
+ },
+ messages[6],
+ messageTab.browser
+ );
+ // Check runtime messaging.
+ testDonePromise = extension.awaitMessage("RuntimeMessageTestDone");
+ extension.sendMessage("RuntimeMessageTest", { tabId: messageTabId });
+ await testDonePromise;
+
+ // Open a content tab. The CSS and script shouldn't apply.
+ let contentTab = window.openContentTab("http://mochi.test:8888/");
+ // Let's wait a while and see if anything happens:
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ await checkMessageBody(
+ {
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ color: "rgb(0, 0, 0)",
+ foo: null,
+ },
+ undefined,
+ contentTab.browser
+ );
+
+ // Closing this tab should bring us back to the message in a tab.
+ tabmail.closeTab(contentTab);
+ Assert.equal(tabmail.currentTabInfo, messageTab);
+ await checkMessageBody(
+ {
+ backgroundColor: "rgb(0, 128, 0)",
+ color: "rgb(255, 255, 255)",
+ foo: "bar",
+ textContent: "Hey look, the script ran!",
+ },
+ messages[6],
+ messageTab.browser
+ );
+ // Check runtime messaging.
+ testDonePromise = extension.awaitMessage("RuntimeMessageTestDone");
+ extension.sendMessage("RuntimeMessageTest", { tabId: messageTabId });
+ await testDonePromise;
+
+ // Open the message in a new window.
+ loadPromise = extension.awaitMessage("ScriptLoaded");
+ let newWindow = await openMessageInWindow(messages[7]);
+ let newWindowMessagePane = newWindow.getBrowser();
+ let windowTabId = await loadPromise;
+
+ await checkMessageBody(
+ {
+ backgroundColor: "rgb(0, 128, 0)",
+ color: "rgb(255, 255, 255)",
+ foo: "bar",
+ textContent: "Hey look, the script ran!",
+ },
+ messages[7],
+ newWindowMessagePane
+ );
+ // Check runtime messaging.
+ testDonePromise = extension.awaitMessage("RuntimeMessageTestDone");
+ extension.sendMessage("RuntimeMessageTest", { tabId: windowTabId });
+ await testDonePromise;
+
+ // Unregister.
+ extension.sendMessage("Unregister");
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ // Check the CSS is unloaded from the message in a tab.
+ await checkMessageBody(
+ {
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ color: "rgb(0, 0, 0)",
+ foo: "bar",
+ textContent: "Hey look, the script ran!",
+ },
+ messages[6],
+ messageTab.browser
+ );
+
+ // Close the new tab.
+ tabmail.closeTab(messageTab);
+
+ await checkMessageBody(
+ {
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ color: "rgb(0, 0, 0)",
+ foo: "bar",
+ textContent: "Hey look, the script ran!",
+ },
+ messages[6]
+ );
+
+ // Check the CSS is unloaded from the message in a window.
+ await checkMessageBody(
+ {
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ color: "rgb(0, 0, 0)",
+ foo: "bar",
+ textContent: "Hey look, the script ran!",
+ },
+ messages[7],
+ newWindowMessagePane
+ );
+
+ await BrowserTestUtils.closeWindow(newWindow);
+});
+
+/** Tests content_scripts in the manifest do not affect message display. */
+async function subtestContentScriptManifest(message, ...permissions) {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "test.css": "body { background-color: red; }",
+ "test.js": () => {
+ document.body.textContent += "Hey look, the script ran!";
+ },
+ },
+ manifest: {
+ permissions,
+ content_scripts: [
+ {
+ matches: ["<all_urls>"],
+ css: ["test.css"],
+ js: ["test.js"],
+ match_about_blank: true,
+ match_origin_as_fallback: true,
+ },
+ ],
+ },
+ });
+
+ // match_origin_as_fallback is not implemented yet. Bug 1475831.
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ await extension.startup();
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+
+ await checkMessageBody(
+ {
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ textContent: "",
+ },
+ message
+ );
+
+ await extension.unload();
+}
+
+add_task(async function testContentScriptManifestNoPermission() {
+ about3Pane.threadTree.selectedIndex = 7;
+ await awaitBrowserLoaded(messagePane);
+ await subtestContentScriptManifest(messages[7]);
+});
+add_task(async function testContentScriptManifest() {
+ about3Pane.threadTree.selectedIndex = 8;
+ await awaitBrowserLoaded(messagePane);
+ await subtestContentScriptManifest(messages[8], "messagesModify");
+});
+
+/** Tests registered content scripts do not affect message display. */
+async function subtestContentScriptRegister(message, ...permissions) {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ await browser.contentScripts.register({
+ matches: ["<all_urls>"],
+ css: [{ file: "test.css" }],
+ js: [{ file: "test.js" }],
+ matchAboutBlank: true,
+ });
+
+ browser.test.notifyPass("finished");
+ },
+ "test.css": "body { background-color: red; }",
+ "test.js": () => {
+ document.body.querySelector(".moz-text-flowed").textContent +=
+ "Hey look, the script ran!";
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions,
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("finished");
+ await checkMessageBody(
+ {
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ textContent: "",
+ },
+ message
+ );
+
+ await extension.unload();
+}
+
+add_task(async function testContentScriptRegisterNoPermission() {
+ about3Pane.threadTree.selectedIndex = 9;
+ await awaitBrowserLoaded(messagePane);
+ await subtestContentScriptRegister(messages[9], "<all_urls>");
+});
+add_task(async function testContentScriptRegister() {
+ about3Pane.threadTree.selectedIndex = 10;
+ await awaitBrowserLoaded(messagePane);
+ await subtestContentScriptRegister(
+ messages[10],
+ "<all_urls>",
+ "messagesModify"
+ );
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_bug1827032.js b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_bug1827032.js
new file mode 100644
index 0000000000..ebae544585
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_bug1827032.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test to make sure messageDisplay.getDisplayedMessage() returns null for
+ * non-message tabs.
+ */
+add_task(async function testGetDisplayedMessageInComposeTab() {
+ let files = {
+ "background.js": async () => {
+ let composeTab = await browser.compose.beginNew();
+ browser.test.assertEq(
+ composeTab.type,
+ "messageCompose",
+ "Should have found a compose tab"
+ );
+
+ let msg = await browser.messageDisplay.getDisplayedMessage(composeTab.id);
+ browser.test.assertTrue(!msg, "Should not have found a message");
+
+ await browser.tabs.remove(composeTab.id);
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_bug1828056.js b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_bug1828056.js
new file mode 100644
index 0000000000..70b9670ac1
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_bug1828056.js
@@ -0,0 +1,212 @@
+var gAccount;
+var gMessages;
+var gFolder;
+
+add_setup(() => {
+ gAccount = createAccount();
+ addIdentity(gAccount);
+ let rootFolder = gAccount.incomingServer.rootFolder;
+ rootFolder.createSubfolder("test0", null);
+
+ let subFolders = {};
+ for (let folder of rootFolder.subFolders) {
+ subFolders[folder.name] = folder;
+ }
+ createMessages(subFolders.test0, 5);
+
+ gFolder = subFolders.test0;
+ gMessages = [...subFolders.test0.messages];
+});
+
+async function getTestExtension_open_msg() {
+ let files = {
+ "background.js": async () => {
+ let [location] = await window.waitForMessage();
+
+ let [mailTab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ browser.test.assertEq(
+ "mail",
+ mailTab.type,
+ "Should have found a mail tab."
+ );
+
+ // Get displayed message.
+ let message1 = await browser.messageDisplay.getDisplayedMessage(
+ mailTab.id
+ );
+ browser.test.assertTrue(
+ !!message1,
+ "We should have a displayed message."
+ );
+
+ // Open a message in the specified location and request the displayed
+ // message immediately.
+ let { message: message2, tab: messageTab } = await new Promise(
+ resolve => {
+ let createListener = async tab => {
+ browser.tabs.onCreated.removeListener(createListener);
+ let message = await browser.messageDisplay.getDisplayedMessage(
+ tab.id
+ );
+ resolve({ tab, message });
+ };
+ browser.tabs.onCreated.addListener(createListener);
+ browser.messageDisplay.open({
+ location,
+ messageId: message1.id,
+ });
+ }
+ );
+ browser.test.assertTrue(
+ !!message2,
+ "We should have a displayed message."
+ );
+ browser.test.assertTrue(
+ message1.id == message2?.id,
+ "We should see the same message."
+ );
+ browser.tabs.remove(messageTab.id);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ return ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead", "tabs"],
+ },
+ });
+}
+
+/**
+ * Open a message tab and request its message immediately.
+ */
+add_task(async function test_message_tab() {
+ let extension = await getTestExtension_open_msg();
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(gFolder);
+ about3Pane.threadTree.selectedIndex = 0;
+
+ await extension.startup();
+ extension.sendMessage("tab");
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+/**
+ * Open a message window and request its message immediately.
+ */
+add_task(async function test_message_window() {
+ let extension = await getTestExtension_open_msg();
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(gFolder);
+ about3Pane.threadTree.selectedIndex = 0;
+
+ await extension.startup();
+ extension.sendMessage("window");
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+async function getTestExtension_select_msg() {
+ let files = {
+ "background.js": async () => {
+ let [expected] = await window.waitForMessage();
+
+ let [mailTab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ browser.test.assertEq(
+ "mail",
+ mailTab.type,
+ "Should have found a mail tab."
+ );
+
+ // Get displayed message.
+ let message = await browser.messageDisplay.getDisplayedMessage(
+ mailTab.id
+ );
+ browser.test.assertTrue(!!message, "We should have a displayed message.");
+
+ await window.sendMessage("select");
+ let messages = await browser.messageDisplay.getDisplayedMessages(
+ mailTab.id
+ );
+ browser.test.assertEq(
+ expected,
+ messages.length,
+ "The returned number of messages should be correct."
+ );
+ for (let msg of messages) {
+ browser.test.assertTrue(
+ message.id != msg.id,
+ "The returned message must not be the original selected message."
+ );
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ return ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead", "tabs"],
+ },
+ });
+}
+
+/**
+ * Select a single message in a mail tab and request it immediately.
+ */
+add_task(async function test_single_message() {
+ let extension = await getTestExtension_select_msg();
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(gFolder);
+ about3Pane.threadTree.selectedIndex = 0;
+
+ extension.onMessage("select", () => {
+ about3Pane.threadTree.selectedIndex = 1;
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ extension.sendMessage(1);
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+/**
+ * Select multiple messages in a mail tab and request them immediately.
+ */
+add_task(async function test_multiple_message() {
+ let extension = await getTestExtension_select_msg();
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(gFolder);
+ about3Pane.threadTree.selectedIndex = 0;
+
+ extension.onMessage("select", () => {
+ about3Pane.threadTree.selectedIndices = [2, 3];
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ extension.sendMessage(2);
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_file.js b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_file.js
new file mode 100644
index 0000000000..a3b5c8cf0f
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_file.js
@@ -0,0 +1,221 @@
+/* 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/. */
+
+requestLongerTimeout(4);
+
+let gRootFolder;
+add_setup(async () => {
+ let account = createAccount();
+ gRootFolder = account.incomingServer.rootFolder;
+ gRootFolder.createSubfolder("testFolder", null);
+ gRootFolder.createSubfolder("otherFolder", null);
+ await createMessages(gRootFolder.getChildNamed("testFolder"), 5);
+});
+
+async function testOpenMessages(testConfig) {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ // Verify startup conditions.
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(
+ 1,
+ accounts.length,
+ `number of accounts should be correct`
+ );
+
+ let testFolder = accounts[0].folders.find(f => f.name == "testFolder");
+ browser.test.assertTrue(!!testFolder, "folder should exist");
+ let { messages } = await browser.messages.list(testFolder);
+ browser.test.assertEq(
+ 5,
+ messages.length,
+ `number of messages should be correct`
+ );
+
+ // Get test properties.
+ let [testConfig] = await window.sendMessage("getTestConfig");
+
+ async function open(message, testConfig) {
+ let properties = { ...testConfig };
+ if (properties.headerMessageId) {
+ properties.headerMessageId = message.headerMessageId;
+ } else if (properties.messageId) {
+ properties.messageId = message.id;
+ } else if (properties.file) {
+ properties.file = new File(
+ [await browser.messages.getRaw(message.id)],
+ "msgfile.eml"
+ );
+ }
+ return browser.messageDisplay.open(properties);
+ }
+
+ let expectedFail;
+ let additionalWindowIdToBeRemoved;
+ if (testConfig.windowType) {
+ switch (testConfig.windowType) {
+ case "normal":
+ {
+ let secondWindow = await browser.windows.create({
+ type: testConfig.windowType,
+ });
+ testConfig.windowId = secondWindow.id;
+ additionalWindowIdToBeRemoved = secondWindow.id;
+ }
+ break;
+ case "popup":
+ {
+ let secondWindow = await browser.windows.create({
+ type: testConfig.windowType,
+ });
+ testConfig.windowId = secondWindow.id;
+ additionalWindowIdToBeRemoved = secondWindow.id;
+ expectedFail = `Window with ID ${secondWindow.id} is not a normal window`;
+ }
+ break;
+ case "invalid":
+ testConfig.windowId = 1234;
+ expectedFail = `Invalid window ID: 1234`;
+ break;
+ }
+ delete testConfig.windowType;
+ }
+
+ if (expectedFail) {
+ await browser.test.assertRejects(
+ open(messages[0], testConfig),
+ `${expectedFail}`,
+ "browser.messageDisplay.open() should fail with invalid windowId"
+ );
+ } else {
+ // Open multiple messages.
+ let promisedTabs = [];
+ promisedTabs.push(open(messages[0], testConfig));
+ promisedTabs.push(open(messages[0], testConfig));
+ promisedTabs.push(open(messages[1], testConfig));
+ promisedTabs.push(open(messages[1], testConfig));
+ promisedTabs.push(open(messages[2], testConfig));
+ promisedTabs.push(open(messages[2], testConfig));
+ let openedTabs = await Promise.allSettled(promisedTabs);
+ for (let i = 0; i < openedTabs.length; i++) {
+ browser.test.assertEq(
+ "fulfilled",
+ openedTabs[i].status,
+ `Promise for the opened message should have been fulfilled for message ${i}`
+ );
+
+ let msg = await browser.messageDisplay.getDisplayedMessage(
+ openedTabs[i].value.id
+ );
+ if (testConfig.file) {
+ browser.test.assertTrue(
+ messages[Math.floor(i / 2)].id != msg.id,
+ `Opened file msg should have a new message id (${
+ msg.id
+ }) and should not equal the id of the source message (${
+ messages[Math.floor(i / 2)].id
+ }) in window ${i}`
+ );
+ } else {
+ browser.test.assertEq(
+ messages[Math.floor(i / 2)].id,
+ msg.id,
+ `Should see the correct message in window ${i}`
+ );
+ }
+ await browser.tabs.remove(openedTabs[i].value.id);
+ }
+ }
+
+ if (additionalWindowIdToBeRemoved) {
+ await browser.windows.remove(additionalWindowIdToBeRemoved);
+ }
+
+ browser.test.notifyPass();
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead", "tabs"],
+ },
+ });
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(gRootFolder.getChildNamed("otherFolder"));
+
+ extension.onMessage("getTestConfig", async () => {
+ extension.sendMessage(testConfig);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+}
+
+add_task(async function testMessageFileActiveDefault() {
+ await testOpenMessages({ file: true, active: true });
+});
+add_task(async function testMessageFileInactiveDefault() {
+ await testOpenMessages({ file: true, active: false });
+});
+add_task(async function testMessageFileActiveWindow() {
+ await testOpenMessages({
+ file: true,
+ active: true,
+ location: "window",
+ });
+});
+add_task(async function testMessageFileInactiveWindow() {
+ await testOpenMessages({
+ file: true,
+ active: false,
+ location: "window",
+ });
+});
+add_task(async function testMessageFileActiveTab() {
+ await testOpenMessages({
+ file: true,
+ active: true,
+ location: "tab",
+ });
+});
+add_task(async function testMessageFileInactiveTab() {
+ await testOpenMessages({
+ file: true,
+ active: false,
+ location: "tab",
+ });
+});
+add_task(async function testMessageFileOtherNormalWindowActiveTab() {
+ await testOpenMessages({
+ file: true,
+ active: true,
+ location: "tab",
+ windowType: "normal",
+ });
+});
+add_task(async function testMessageFileOtherNormalWindowInactiveTab() {
+ await testOpenMessages({
+ file: true,
+ active: false,
+ location: "tab",
+ windowType: "normal",
+ });
+});
+add_task(async function testMessageFileOtherPopupWindowFail() {
+ await testOpenMessages({
+ file: true,
+ location: "tab",
+ windowType: "popup",
+ });
+});
+add_task(async function testMessageFileInvalidWindowFail() {
+ await testOpenMessages({
+ file: true,
+ location: "tab",
+ windowType: "invalid",
+ });
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_headerMessageId.js b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_headerMessageId.js
new file mode 100644
index 0000000000..8be6aa4c2b
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_headerMessageId.js
@@ -0,0 +1,221 @@
+/* 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/. */
+
+requestLongerTimeout(4);
+
+let gRootFolder;
+add_setup(async () => {
+ let account = createAccount();
+ gRootFolder = account.incomingServer.rootFolder;
+ gRootFolder.createSubfolder("testFolder", null);
+ gRootFolder.createSubfolder("otherFolder", null);
+ await createMessages(gRootFolder.getChildNamed("testFolder"), 5);
+});
+
+async function testOpenMessages(testConfig) {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ // Verify startup conditions.
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(
+ 1,
+ accounts.length,
+ `number of accounts should be correct`
+ );
+
+ let testFolder = accounts[0].folders.find(f => f.name == "testFolder");
+ browser.test.assertTrue(!!testFolder, "folder should exist");
+ let { messages } = await browser.messages.list(testFolder);
+ browser.test.assertEq(
+ 5,
+ messages.length,
+ `number of messages should be correct`
+ );
+
+ // Get test properties.
+ let [testConfig] = await window.sendMessage("getTestConfig");
+
+ async function open(message, testConfig) {
+ let properties = { ...testConfig };
+ if (properties.headerMessageId) {
+ properties.headerMessageId = message.headerMessageId;
+ } else if (properties.messageId) {
+ properties.messageId = message.id;
+ } else if (properties.file) {
+ properties.file = new File(
+ [await browser.messages.getRaw(message.id)],
+ "msgfile.eml"
+ );
+ }
+ return browser.messageDisplay.open(properties);
+ }
+
+ let expectedFail;
+ let additionalWindowIdToBeRemoved;
+ if (testConfig.windowType) {
+ switch (testConfig.windowType) {
+ case "normal":
+ {
+ let secondWindow = await browser.windows.create({
+ type: testConfig.windowType,
+ });
+ testConfig.windowId = secondWindow.id;
+ additionalWindowIdToBeRemoved = secondWindow.id;
+ }
+ break;
+ case "popup":
+ {
+ let secondWindow = await browser.windows.create({
+ type: testConfig.windowType,
+ });
+ testConfig.windowId = secondWindow.id;
+ additionalWindowIdToBeRemoved = secondWindow.id;
+ expectedFail = `Window with ID ${secondWindow.id} is not a normal window`;
+ }
+ break;
+ case "invalid":
+ testConfig.windowId = 1234;
+ expectedFail = `Invalid window ID: 1234`;
+ break;
+ }
+ delete testConfig.windowType;
+ }
+
+ if (expectedFail) {
+ await browser.test.assertRejects(
+ open(messages[0], testConfig),
+ `${expectedFail}`,
+ "browser.messageDisplay.open() should fail with invalid windowId"
+ );
+ } else {
+ // Open multiple messages.
+ let promisedTabs = [];
+ promisedTabs.push(open(messages[0], testConfig));
+ promisedTabs.push(open(messages[0], testConfig));
+ promisedTabs.push(open(messages[1], testConfig));
+ promisedTabs.push(open(messages[1], testConfig));
+ promisedTabs.push(open(messages[2], testConfig));
+ promisedTabs.push(open(messages[2], testConfig));
+ let openedTabs = await Promise.allSettled(promisedTabs);
+ for (let i = 0; i < openedTabs.length; i++) {
+ browser.test.assertEq(
+ "fulfilled",
+ openedTabs[i].status,
+ `Promise for the opened message should have been fulfilled for message ${i}`
+ );
+
+ let msg = await browser.messageDisplay.getDisplayedMessage(
+ openedTabs[i].value.id
+ );
+ if (testConfig.file) {
+ browser.test.assertTrue(
+ messages[Math.floor(i / 2)].id != msg.id,
+ `Opened file msg should have a new message id (${
+ msg.id
+ }) and should not equal the id of the source message (${
+ messages[Math.floor(i / 2)].id
+ }) in window ${i}`
+ );
+ } else {
+ browser.test.assertEq(
+ messages[Math.floor(i / 2)].id,
+ msg.id,
+ `Should see the correct message in window ${i}`
+ );
+ }
+ await browser.tabs.remove(openedTabs[i].value.id);
+ }
+ }
+
+ if (additionalWindowIdToBeRemoved) {
+ await browser.windows.remove(additionalWindowIdToBeRemoved);
+ }
+
+ browser.test.notifyPass();
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead", "tabs"],
+ },
+ });
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(gRootFolder.getChildNamed("otherFolder"));
+
+ extension.onMessage("getTestConfig", async () => {
+ extension.sendMessage(testConfig);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+}
+
+add_task(async function testHeaderMessageIdActiveDefault() {
+ await testOpenMessages({ headerMessageId: true, active: true });
+});
+add_task(async function testHeaderMessageIdInactiveDefault() {
+ await testOpenMessages({ headerMessageId: true, active: false });
+});
+add_task(async function testHeaderMessageIdActiveWindow() {
+ await testOpenMessages({
+ headerMessageId: true,
+ active: true,
+ location: "window",
+ });
+});
+add_task(async function testHeaderMessageIdInactiveWindow() {
+ await testOpenMessages({
+ headerMessageId: true,
+ active: false,
+ location: "window",
+ });
+});
+add_task(async function testHeaderMessageIdActiveTab() {
+ await testOpenMessages({
+ headerMessageId: true,
+ active: true,
+ location: "tab",
+ });
+});
+add_task(async function testHeaderMessageIdInactiveTab() {
+ await testOpenMessages({
+ headerMessageId: true,
+ active: false,
+ location: "tab",
+ });
+});
+add_task(async function testHeaderMessageIdOtherNormalWindowActiveTab() {
+ await testOpenMessages({
+ headerMessageId: true,
+ active: true,
+ location: "tab",
+ windowType: "normal",
+ });
+});
+add_task(async function testHeaderMessageIdOtherNormalWindowInactiveTab() {
+ await testOpenMessages({
+ headerMessageId: true,
+ active: false,
+ location: "tab",
+ windowType: "normal",
+ });
+});
+add_task(async function testHeaderMessageIdOtherPopupWindowFail() {
+ await testOpenMessages({
+ headerMessageId: true,
+ location: "tab",
+ windowType: "popup",
+ });
+});
+add_task(async function testHeaderMessageIdInvalidWindowFail() {
+ await testOpenMessages({
+ headerMessageId: true,
+ location: "tab",
+ windowType: "invalid",
+ });
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_messageId.js b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_messageId.js
new file mode 100644
index 0000000000..47995d9ecd
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_messageId.js
@@ -0,0 +1,221 @@
+/* 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/. */
+
+requestLongerTimeout(4);
+
+let gRootFolder;
+add_setup(async () => {
+ let account = createAccount();
+ gRootFolder = account.incomingServer.rootFolder;
+ gRootFolder.createSubfolder("testFolder", null);
+ gRootFolder.createSubfolder("otherFolder", null);
+ await createMessages(gRootFolder.getChildNamed("testFolder"), 5);
+});
+
+async function testOpenMessages(testConfig) {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ // Verify startup conditions.
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(
+ 1,
+ accounts.length,
+ `number of accounts should be correct`
+ );
+
+ let testFolder = accounts[0].folders.find(f => f.name == "testFolder");
+ browser.test.assertTrue(!!testFolder, "folder should exist");
+ let { messages } = await browser.messages.list(testFolder);
+ browser.test.assertEq(
+ 5,
+ messages.length,
+ `number of messages should be correct`
+ );
+
+ // Get test properties.
+ let [testConfig] = await window.sendMessage("getTestConfig");
+
+ async function open(message, testConfig) {
+ let properties = { ...testConfig };
+ if (properties.headerMessageId) {
+ properties.headerMessageId = message.headerMessageId;
+ } else if (properties.messageId) {
+ properties.messageId = message.id;
+ } else if (properties.file) {
+ properties.file = new File(
+ [await browser.messages.getRaw(message.id)],
+ "msgfile.eml"
+ );
+ }
+ return browser.messageDisplay.open(properties);
+ }
+
+ let expectedFail;
+ let additionalWindowIdToBeRemoved;
+ if (testConfig.windowType) {
+ switch (testConfig.windowType) {
+ case "normal":
+ {
+ let secondWindow = await browser.windows.create({
+ type: testConfig.windowType,
+ });
+ testConfig.windowId = secondWindow.id;
+ additionalWindowIdToBeRemoved = secondWindow.id;
+ }
+ break;
+ case "popup":
+ {
+ let secondWindow = await browser.windows.create({
+ type: testConfig.windowType,
+ });
+ testConfig.windowId = secondWindow.id;
+ additionalWindowIdToBeRemoved = secondWindow.id;
+ expectedFail = `Window with ID ${secondWindow.id} is not a normal window`;
+ }
+ break;
+ case "invalid":
+ testConfig.windowId = 1234;
+ expectedFail = `Invalid window ID: 1234`;
+ break;
+ }
+ delete testConfig.windowType;
+ }
+
+ if (expectedFail) {
+ await browser.test.assertRejects(
+ open(messages[0], testConfig),
+ `${expectedFail}`,
+ "browser.messageDisplay.open() should fail with invalid windowId"
+ );
+ } else {
+ // Open multiple messages.
+ let promisedTabs = [];
+ promisedTabs.push(open(messages[0], testConfig));
+ promisedTabs.push(open(messages[0], testConfig));
+ promisedTabs.push(open(messages[1], testConfig));
+ promisedTabs.push(open(messages[1], testConfig));
+ promisedTabs.push(open(messages[2], testConfig));
+ promisedTabs.push(open(messages[2], testConfig));
+ let openedTabs = await Promise.allSettled(promisedTabs);
+ for (let i = 0; i < openedTabs.length; i++) {
+ browser.test.assertEq(
+ "fulfilled",
+ openedTabs[i].status,
+ `Promise for the opened message should have been fulfilled for message ${i}`
+ );
+
+ let msg = await browser.messageDisplay.getDisplayedMessage(
+ openedTabs[i].value.id
+ );
+ if (testConfig.file) {
+ browser.test.assertTrue(
+ messages[Math.floor(i / 2)].id != msg.id,
+ `Opened file msg should have a new message id (${
+ msg.id
+ }) and should not equal the id of the source message (${
+ messages[Math.floor(i / 2)].id
+ }) in window ${i}`
+ );
+ } else {
+ browser.test.assertEq(
+ messages[Math.floor(i / 2)].id,
+ msg.id,
+ `Should see the correct message in window ${i}`
+ );
+ }
+ await browser.tabs.remove(openedTabs[i].value.id);
+ }
+ }
+
+ if (additionalWindowIdToBeRemoved) {
+ await browser.windows.remove(additionalWindowIdToBeRemoved);
+ }
+
+ browser.test.notifyPass();
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead", "tabs"],
+ },
+ });
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(gRootFolder.getChildNamed("otherFolder"));
+
+ extension.onMessage("getTestConfig", async () => {
+ extension.sendMessage(testConfig);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+}
+
+add_task(async function testMessageIdActiveDefault() {
+ await testOpenMessages({ messageId: true, active: true });
+});
+add_task(async function testMessageIdInactiveDefault() {
+ await testOpenMessages({ messageId: true, active: false });
+});
+add_task(async function testMessageIdActiveWindow() {
+ await testOpenMessages({
+ messageId: true,
+ active: true,
+ location: "window",
+ });
+});
+add_task(async function testMessageIdInactiveWindow() {
+ await testOpenMessages({
+ messageId: true,
+ active: false,
+ location: "window",
+ });
+});
+add_task(async function testMessageIdActiveTab() {
+ await testOpenMessages({
+ messageId: true,
+ active: true,
+ location: "tab",
+ });
+});
+add_task(async function testMessageIdInActiveTab() {
+ await testOpenMessages({
+ messageId: true,
+ active: false,
+ location: "tab",
+ });
+});
+add_task(async function testMessageIdOtherNormalWindowActiveTab() {
+ await testOpenMessages({
+ messageId: true,
+ active: true,
+ location: "tab",
+ windowType: "normal",
+ });
+});
+add_task(async function testMessageIdOtherNormalWindowInactiveTab() {
+ await testOpenMessages({
+ messageId: true,
+ active: false,
+ location: "tab",
+ windowType: "normal",
+ });
+});
+add_task(async function testMessageIdOtherPopupWindowFail() {
+ await testOpenMessages({
+ messageId: true,
+ location: "tab",
+ windowType: "popup",
+ });
+});
+add_task(async function testMessageIdInvalidWindowFail() {
+ await testOpenMessages({
+ messageId: true,
+ location: "tab",
+ windowType: "invalid",
+ });
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_message_external.js b/comm/mail/components/extensions/test/browser/browser_ext_message_external.js
new file mode 100644
index 0000000000..8a4cf7ea30
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_message_external.js
@@ -0,0 +1,427 @@
+/* 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 gAccount;
+var gFolder;
+
+add_setup(() => {
+ gAccount = createAccount();
+ let rootFolder = gAccount.incomingServer.rootFolder;
+ rootFolder.createSubfolder("test0", null);
+ gFolder = rootFolder.getChildNamed("test0");
+ createMessages(gFolder, 5);
+});
+
+add_task(async function testExternalMessage() {
+ // Copy eml file into the profile folder, where we can delete it during the test.
+ let profileDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ profileDir.initWithPath(PathUtils.profileDir);
+ let messageFile = new FileUtils.File(
+ getTestFilePath("messages/attachedMessageSample.eml")
+ );
+ messageFile.copyTo(profileDir, "attachedMessageSample.eml");
+
+ let files = {
+ "background.js": async () => {
+ let platformInfo = await browser.runtime.getPlatformInfo();
+
+ const emlData = {
+ openExternalFileMessage: {
+ headerMessageId: "sample.eml@mime.sample",
+ author: "Batman <bruce@wayne-enterprises.com>",
+ ccList: ["Robin <damian@wayne-enterprises.com>"],
+ subject: "Attached message with attachments",
+ attachments: 2,
+ size: 9754,
+ external: true,
+ read: null,
+ recipients: ["Heinz <mueller@example.com>"],
+ date: 958796995000,
+ body: "This message has one normal attachment and one email attachment",
+ },
+ openExternalAttachedMessage: {
+ headerMessageId: "sample-attached.eml@mime.sample",
+ author: "Superman <clark.kent@dailyplanet.com>",
+ ccList: ["Jimmy <jimmy.Olsen@dailyplanet.com>"],
+ subject: "Test message",
+ attachments: 3,
+ size: platformInfo.os == "win" ? 6947 : 6825, // Line endings.
+ external: true,
+ read: null,
+ recipients: ["Heinz Müller <mueller@examples.com>"],
+ date: 958606367000,
+ body: "Die Hasen und die Frösche",
+ },
+ };
+
+ let [{ displayedFolder, windowId: mainWindowId }] =
+ await browser.mailTabs.query({
+ active: true,
+ currentWindow: true,
+ });
+
+ // Open an external file, either from file or via API.
+ async function openAndVerifyExternalMessage(
+ actionOrMessageId,
+ location,
+ expected
+ ) {
+ let tabPromise = window.waitForEvent("tabs.onCreated");
+ let messagePromise = window.waitForEvent(
+ "messageDisplay.onMessageDisplayed"
+ );
+
+ let returnedMsgTab;
+ if (Number.isInteger(actionOrMessageId)) {
+ returnedMsgTab = await browser.messageDisplay.open({
+ messageId: actionOrMessageId,
+ location,
+ });
+ } else {
+ await window.sendMessage(actionOrMessageId, location);
+ }
+ let [msgTab] = await tabPromise;
+ let [openedMsgTab, message] = await messagePromise;
+
+ if ("windowId" in expected) {
+ browser.test.assertEq(
+ expected.windowId,
+ msgTab.windowId,
+ "The opened tab should belong to the correct window"
+ );
+ } else {
+ browser.test.assertTrue(
+ msgTab.windowId != mainWindowId,
+ "The opened tab should not belong to the main window"
+ );
+ }
+ browser.test.assertEq(
+ msgTab.id,
+ openedMsgTab.id,
+ "The opened tab should match the onMessageDisplayed event tab"
+ );
+
+ if (Number.isInteger(actionOrMessageId)) {
+ browser.test.assertEq(
+ msgTab.id,
+ returnedMsgTab.id,
+ "The returned tab should match the onMessageDisplayed event tab"
+ );
+ }
+
+ if ("messageId" in expected) {
+ browser.test.assertEq(
+ expected.messageId,
+ message.id,
+ "The message should have the same ID as it did previously"
+ );
+ }
+
+ // Test the received message and the re-queried message.
+ for (let msg of [message, await browser.messages.get(message.id)]) {
+ browser.test.assertEq(
+ message.id,
+ msg.id,
+ "`The opened message should be correct."
+ );
+ browser.test.assertEq(
+ expected.author,
+ msg.author,
+ "The author should be correct"
+ );
+ browser.test.assertEq(
+ expected.headerMessageId,
+ msg.headerMessageId,
+ "The headerMessageId should be correct"
+ );
+ browser.test.assertEq(
+ expected.subject,
+ msg.subject,
+ "The subject should be correct"
+ );
+ browser.test.assertEq(
+ expected.size,
+ msg.size,
+ "The size should be correct"
+ );
+ browser.test.assertEq(
+ expected.external,
+ msg.external,
+ "The external flag should be correct"
+ );
+ browser.test.assertEq(
+ expected.date,
+ msg.date.getTime(),
+ "The date should be correct"
+ );
+ window.assertDeepEqual(
+ expected.recipients,
+ msg.recipients,
+ "The recipients should be correct"
+ );
+ window.assertDeepEqual(
+ expected.ccList,
+ msg.ccList,
+ "The carbon copy recipients should be correct"
+ );
+ }
+
+ let raw = await browser.messages.getRaw(message.id);
+ browser.test.assertTrue(
+ raw.startsWith(`Message-ID: <${expected.headerMessageId}>`),
+ "Raw msg should be correct"
+ );
+
+ let full = await browser.messages.getFull(message.id);
+ browser.test.assertTrue(
+ full.headers["message-id"].includes(`<${expected.headerMessageId}>`),
+ "Message-ID of full msg should be correct"
+ );
+ browser.test.assertTrue(
+ full.parts[0].parts[0].body.includes(expected.body),
+ "Body of full msg should be correct"
+ );
+
+ let attachments = await browser.messages.listAttachments(message.id);
+ browser.test.assertEq(
+ expected.attachments,
+ attachments.length,
+ "Should find the correct number of attachments"
+ );
+
+ await browser.tabs.remove(msgTab.id);
+ return message;
+ }
+
+ // Check API operations on the given message.
+ async function testMessageOperations(message) {
+ // Test copying a file message into Thunderbird.
+ let { messages: messagesBeforeCopy } = await browser.messages.list(
+ displayedFolder
+ );
+ await browser.messages.copy([message.id], displayedFolder);
+ let { messages: messagesAfterCopy } = await browser.messages.list(
+ displayedFolder
+ );
+ browser.test.assertEq(
+ messagesBeforeCopy.length + 1,
+ messagesAfterCopy.length,
+ "The file message should have been copied into the current folder"
+ );
+ let { messages } = await browser.messages.query({
+ folder: displayedFolder,
+ headerMessageId: message.headerMessageId,
+ });
+ browser.test.assertTrue(
+ messages.length == 1,
+ "A query should find the new copied file message in the current folder"
+ );
+
+ // All other operations should fail.
+ await browser.test.assertRejects(
+ browser.messages.update(message.id, {}),
+ `Error updating message: Operation not permitted for external messages`,
+ "Updating external messages should throw."
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.delete([message.id]),
+ `Error deleting message: Operation not permitted for external messages`,
+ "Deleting external messages should throw."
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.archive([message.id]),
+ `Error archiving message: Operation not permitted for external messages`,
+ "Archiving external messages should throw."
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.move([message.id], displayedFolder),
+ `Error moving message: Operation not permitted for external messages`,
+ "Moving external messages should throw."
+ );
+
+ return messages[0];
+ }
+
+ // Open an external message in a tab and check its details.
+ let externalMessage = await openAndVerifyExternalMessage(
+ "openExternalFileMessage",
+ "tab",
+ { ...emlData.openExternalFileMessage, windowId: mainWindowId }
+ );
+ // Open and check the same message in a window.
+ await openAndVerifyExternalMessage("openExternalFileMessage", "window", {
+ ...emlData.openExternalFileMessage,
+ messageId: externalMessage.id,
+ });
+ // Open and check the same message in a tab, using the API.
+ await openAndVerifyExternalMessage(externalMessage.id, "tab", {
+ ...emlData.openExternalFileMessage,
+ messageId: externalMessage.id,
+ windowId: mainWindowId,
+ });
+ // Open and check the same message in a window, using the API.
+ await openAndVerifyExternalMessage(externalMessage.id, "window", {
+ ...emlData.openExternalFileMessage,
+ messageId: externalMessage.id,
+ });
+
+ // Test operations on the external message. This will put a copy in a
+ // folder that we can use for the next step.
+ let copiedMessage = await testMessageOperations(externalMessage);
+ let messagePromise = window.waitForEvent(
+ "messageDisplay.onMessageDisplayed"
+ );
+ await browser.mailTabs.setSelectedMessages([copiedMessage.id]);
+ await messagePromise;
+
+ // Open an attached message in a tab and check its details.
+ let attachedMessage = await openAndVerifyExternalMessage(
+ "openExternalAttachedMessage",
+ "tab",
+ { ...emlData.openExternalAttachedMessage, windowId: mainWindowId }
+ );
+ // Open and check the same message in a window.
+ await openAndVerifyExternalMessage(
+ "openExternalAttachedMessage",
+ "window",
+ {
+ ...emlData.openExternalAttachedMessage,
+ messageId: attachedMessage.id,
+ }
+ );
+ // Open and check the same message in a tab, using the API.
+ await openAndVerifyExternalMessage(attachedMessage.id, "tab", {
+ ...emlData.openExternalAttachedMessage,
+ messageId: attachedMessage.id,
+ windowId: mainWindowId,
+ });
+ // Open and check the same message in a window, using the API.
+ await openAndVerifyExternalMessage(attachedMessage.id, "window", {
+ ...emlData.openExternalAttachedMessage,
+ messageId: attachedMessage.id,
+ });
+
+ // Test operations on the attached message.
+ await testMessageOperations(attachedMessage);
+
+ // Delete the local eml file to trigger access errors.
+ await window.sendMessage(`deleteExternalMessage`);
+
+ await browser.test.assertRejects(
+ browser.messages.update(externalMessage.id, {}),
+ `Error updating message: Message not found: ${externalMessage.id}.`,
+ "Updating a missing message should throw."
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.delete([externalMessage.id]),
+ `Error deleting message: Message not found: ${externalMessage.id}.`,
+ "Deleting a missing message should throw."
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.archive([externalMessage.id]),
+ `Error archiving message: Message not found: ${externalMessage.id}.`,
+ "Archiving a missing message should throw."
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.move([externalMessage.id], displayedFolder),
+ `Error moving message: Message not found: ${externalMessage.id}.`,
+ "Moving a missing message should throw."
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.copy([externalMessage.id], displayedFolder),
+ `Error copying message: Message not found: ${externalMessage.id}.`,
+ "Copying a missing message should throw."
+ );
+
+ await browser.test.assertRejects(
+ browser.messageDisplay.open({ messageId: externalMessage.id }),
+ `Unknown or invalid messageId: ${externalMessage.id}.`,
+ "Opening a missing message should throw."
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: [
+ "accountsRead",
+ "messagesRead",
+ "messagesMove",
+ "messagesDelete",
+ ],
+ },
+ });
+
+ let tabmail = document.getElementById("tabmail");
+ let about3Pane = tabmail.currentAbout3Pane;
+ about3Pane.displayFolder(gFolder.URI);
+ about3Pane.threadTree.selectedIndex = 0;
+
+ extension.onMessage("openExternalFileMessage", async location => {
+ let messagePath = PathUtils.join(
+ PathUtils.profileDir,
+ "attachedMessageSample.eml"
+ );
+ let messageFile = new FileUtils.File(messagePath);
+ let url = Services.io
+ .newFileURI(messageFile)
+ .mutate()
+ .setQuery("type=application/x-message-display")
+ .finalize();
+
+ Services.prefs.setIntPref(
+ "mail.openMessageBehavior",
+ MailConsts.OpenMessageBehavior[
+ location == "window" ? "NEW_WINDOW" : "NEW_TAB"
+ ]
+ );
+
+ MailUtils.openEMLFile(window, messageFile, url);
+ extension.sendMessage();
+ });
+
+ extension.onMessage("openExternalAttachedMessage", async location => {
+ Services.prefs.setIntPref(
+ "mail.openMessageBehavior",
+ MailConsts.OpenMessageBehavior[
+ location == "window" ? "NEW_WINDOW" : "NEW_TAB"
+ ]
+ );
+
+ // The message with attachment should be loaded in the 3-pane tab.
+ let aboutMessage = tabmail.currentAboutMessage;
+ aboutMessage.toggleAttachmentList(true);
+ EventUtils.synthesizeMouseAtCenter(
+ aboutMessage.document.querySelector(".attachmentItem"),
+ { clickCount: 2 },
+ aboutMessage
+ );
+ extension.sendMessage();
+ });
+
+ extension.onMessage("deleteExternalMessage", async () => {
+ let messagePath = PathUtils.join(
+ PathUtils.profileDir,
+ "attachedMessageSample.eml"
+ );
+ let messageFile = new FileUtils.File(messagePath);
+ messageFile.remove(false);
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_messages_open_attachment.js b/comm/mail/components/extensions/test/browser/browser_ext_messages_open_attachment.js
new file mode 100644
index 0000000000..c4e38465f7
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_messages_open_attachment.js
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_setup(async () => {
+ MailServices.accounts.createLocalMailAccount();
+ let localRoot =
+ MailServices.accounts.localFoldersServer.rootFolder.QueryInterface(
+ Ci.nsIMsgLocalMailFolder
+ );
+ let folder = localRoot.createLocalSubfolder("AttachmentA");
+ await createMessageFromFile(
+ folder,
+ getTestFilePath("messages/attachedMessageSample.eml")
+ );
+});
+
+add_task(async function testOpenAttachment() {
+ let files = {
+ "background.js": async () => {
+ let { messages } = await browser.messages.query({
+ headerMessageId: "sample.eml@mime.sample",
+ });
+
+ async function testTab(tab) {
+ let tabPromise = window.waitForEvent("tabs.onCreated");
+ let messagePromise = window.waitForEvent(
+ "messageDisplay.onMessageDisplayed"
+ );
+ await browser.messages.openAttachment(
+ messages[0].id,
+ // Open the eml attachment.
+ "1.2",
+ tab.id
+ );
+
+ let [msgTab] = await tabPromise;
+ let [openedMsgTab, message] = await messagePromise;
+
+ browser.test.assertEq(
+ msgTab.id,
+ openedMsgTab.id,
+ "The opened tab should match the onMessageDisplayed event tab"
+ );
+ browser.test.assertEq(
+ message.headerMessageId,
+ "sample-attached.eml@mime.sample",
+ "Should have opened the correct message"
+ );
+
+ await browser.tabs.remove(msgTab.id);
+ }
+
+ // Test using a mail tab.
+ let mailTab = await browser.mailTabs.getCurrent();
+ await testTab(mailTab);
+
+ // Test using a content tab.
+ let contentTab = await browser.tabs.create({ url: "test.html" });
+ await testTab(contentTab);
+ await browser.tabs.remove(contentTab.id);
+
+ // Test using a content window.
+ let contentWindow = await browser.windows.create({
+ type: "popup",
+ url: "test.html",
+ });
+ await testTab(contentWindow.tabs[0]);
+ await browser.windows.remove(contentWindow.id);
+
+ // Test using a message tab.
+ let messageTab = await browser.messageDisplay.open({
+ messageId: messages[0].id,
+ location: "tab",
+ });
+ await testTab(messageTab);
+ await browser.tabs.remove(messageTab.id);
+
+ // Test using a message window.
+ let messageWindowTab = await browser.messageDisplay.open({
+ messageId: messages[0].id,
+ location: "window",
+ });
+ await testTab(messageWindowTab);
+ await browser.tabs.remove(messageWindowTab.id);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: [
+ "accountsRead",
+ "messagesRead",
+ "messagesMove",
+ "messagesDelete",
+ ],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_quickFilter.js b/comm/mail/components/extensions/test/browser/browser_ext_quickFilter.js
new file mode 100644
index 0000000000..302486e31f
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_quickFilter.js
@@ -0,0 +1,132 @@
+/* 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 messages;
+let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+
+add_setup(async () => {
+ let account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ let subFolders = rootFolder.subFolders;
+ createMessages(subFolders[0], 10);
+
+ // Modify the messages so the filters can be checked against them.
+
+ messages = [...subFolders[0].messages];
+ messages[0].markRead(true);
+ messages[2].markRead(true);
+ messages[4].markRead(true);
+ messages[6].markRead(true);
+ messages[8].markRead(true);
+ messages[1].markFlagged(true);
+ messages[6].markFlagged(true);
+ messages[0].setStringProperty("keywords", "$label1");
+ messages[1].setStringProperty("keywords", "$label2");
+ messages[3].setStringProperty("keywords", "$label1 $label2");
+ messages[5].setStringProperty("keywords", "$label2");
+ messages[6].setStringProperty("keywords", "$label1");
+ messages[7].setStringProperty("keywords", "$label2 $label3");
+ messages[8].setStringProperty("keywords", "$label3");
+ messages[9].setStringProperty("keywords", "$label1 $label2 $label3");
+ messages[9].markHasAttachments(true);
+
+ // Add an author to the address book.
+
+ let author = messages[7].author.replace(/["<>]/g, "").split(" ");
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ card.setProperty("FirstName", author[0]);
+ card.setProperty("LastName", author[1]);
+ card.setProperty("DisplayName", `${author[0]} ${author[1]}`);
+ card.setProperty("PrimaryEmail", author[2]);
+ let ab = MailServices.ab.getDirectory("jsaddrbook://abook.sqlite");
+ let addedCard = ab.addCard(card);
+
+ about3Pane.displayFolder(subFolders[0]);
+
+ registerCleanupFunction(() => {
+ ab.deleteCards([addedCard]);
+ });
+});
+
+add_task(async () => {
+ async function background() {
+ browser.mailTabs.setQuickFilter({ unread: true });
+ await window.sendMessage("checkVisible", 1, 3, 5, 7, 9);
+
+ browser.mailTabs.setQuickFilter({ flagged: true });
+ await window.sendMessage("checkVisible", 1, 6);
+
+ browser.mailTabs.setQuickFilter({ flagged: true, unread: true });
+ await window.sendMessage("checkVisible", 1);
+
+ browser.mailTabs.setQuickFilter({ tags: true });
+ await window.sendMessage("checkVisible", 0, 1, 3, 5, 6, 7, 8, 9);
+
+ browser.mailTabs.setQuickFilter({
+ tags: { mode: "any", tags: { $label1: true } },
+ });
+ await window.sendMessage("checkVisible", 0, 3, 6, 9);
+
+ browser.mailTabs.setQuickFilter({
+ tags: { mode: "any", tags: { $label2: true } },
+ });
+ await window.sendMessage("checkVisible", 1, 3, 5, 7, 9);
+
+ browser.mailTabs.setQuickFilter({
+ tags: { mode: "any", tags: { $label1: true, $label2: true } },
+ });
+ await window.sendMessage("checkVisible", 0, 1, 3, 5, 6, 7, 9);
+
+ browser.mailTabs.setQuickFilter({
+ tags: { mode: "all", tags: { $label1: true, $label2: true } },
+ });
+ await window.sendMessage("checkVisible", 3, 9);
+
+ browser.mailTabs.setQuickFilter({
+ tags: { mode: "all", tags: { $label1: true, $label2: false } },
+ });
+ await window.sendMessage("checkVisible", 0, 6);
+
+ browser.mailTabs.setQuickFilter({ attachment: true });
+ await window.sendMessage("checkVisible", 9);
+
+ browser.mailTabs.setQuickFilter({ attachment: false });
+ await window.sendMessage("checkVisible", 0, 1, 2, 3, 4, 5, 6, 7, 8);
+
+ browser.mailTabs.setQuickFilter({ contact: true });
+ await window.sendMessage("checkVisible", 7);
+
+ browser.mailTabs.setQuickFilter({ contact: false });
+ await window.sendMessage("checkVisible", 0, 1, 2, 3, 4, 5, 6, 8, 9);
+
+ browser.test.notifyPass("quickFilter");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ },
+ });
+
+ extension.onMessage("checkVisible", async (...expected) => {
+ let actual = [];
+ let dbView = about3Pane.gDBView;
+ for (let i = 0; i < dbView.numMsgsInView; i++) {
+ actual.push(messages.indexOf(dbView.getMsgHdrAt(i)));
+ }
+
+ Assert.deepEqual(actual, expected);
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("quickFilter");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_sessions.js b/comm/mail/components/extensions/test/browser/browser_ext_sessions.js
new file mode 100644
index 0000000000..a77739c145
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_sessions.js
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async function test_sessions_data() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async () => {
+ let [mailTab] = await browser.tabs.query({ mailTab: true });
+ let contentTab = await browser.tabs.create({
+ url: "https://www.example.com",
+ });
+
+ // Check that there is no data at the beginning.
+ browser.test.assertEq(
+ await browser.sessions.getTabValue(mailTab.id, "aKey"),
+ undefined,
+ "Value for aKey should not exist"
+ );
+ browser.test.assertEq(
+ await browser.sessions.getTabValue(contentTab.id, "aKey"),
+ undefined,
+ "Value for aKey should not exist"
+ );
+
+ // Set some data.
+ await browser.sessions.setTabValue(mailTab.id, "aKey", "1234");
+ await browser.sessions.setTabValue(contentTab.id, "aKey", "4321");
+
+ // Check the data is correct.
+ browser.test.assertEq(
+ await browser.sessions.getTabValue(mailTab.id, "aKey"),
+ "1234",
+ "Value for aKey should exist"
+ );
+ browser.test.assertEq(
+ await browser.sessions.getTabValue(contentTab.id, "aKey"),
+ "4321",
+ "Value for aKey should exist"
+ );
+
+ // Update data.
+ await browser.sessions.setTabValue(mailTab.id, "aKey", "12345");
+ await browser.sessions.setTabValue(contentTab.id, "aKey", "54321");
+
+ // Check the data is correct.
+ browser.test.assertEq(
+ await browser.sessions.getTabValue(mailTab.id, "aKey"),
+ "12345",
+ "Value for aKey should exist"
+ );
+ browser.test.assertEq(
+ await browser.sessions.getTabValue(contentTab.id, "aKey"),
+ "54321",
+ "Value for aKey should exist"
+ );
+
+ // Clear data.
+ await browser.sessions.removeTabValue(mailTab.id, "aKey");
+ await browser.sessions.removeTabValue(contentTab.id, "aKey");
+
+ // Check the data is removed.
+ browser.test.assertEq(
+ await browser.sessions.getTabValue(mailTab.id, "aKey"),
+ undefined,
+ "Value for aKey should not exist"
+ );
+ browser.test.assertEq(
+ await browser.sessions.getTabValue(contentTab.id, "aKey"),
+ undefined,
+ "Value for aKey should not exist"
+ );
+
+ await browser.tabs.remove(contentTab.id);
+ browser.test.notifyPass();
+ },
+ manifest: {
+ manifest_version: 2,
+ browser_specific_settings: {
+ gecko: {
+ id: "sessions@mochi.test",
+ },
+ },
+ permissions: ["tabs"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_spaces.js b/comm/mail/components/extensions/test/browser/browser_ext_spaces.js
new file mode 100644
index 0000000000..16f6f4770e
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_spaces.js
@@ -0,0 +1,1047 @@
+/* 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/. */
+
+/**
+ * Helper Function, creates a test extension to verify expected button states.
+ *
+ * @param {Function} background - The background script executed by the test.
+ * @param {object} config - Additional config data for the test. Tests can
+ * include arbitrary data, but the following have a dedicated purpose:
+ * @param {string} selectedTheme - The selected theme (default, light or dark),
+ * used to select the expected button/menuitem icon.
+ * @param {?object} manifestIcons - The icons entry of the extension manifest.
+ * @param {?object} permissions - Permissions assigned to the extension.
+ */
+async function test_space(background, config = {}) {
+ let manifest = {
+ manifest_version: 3,
+ browser_specific_settings: {
+ gecko: {
+ id: "spaces_toolbar@mochi.test",
+ },
+ },
+ permissions: ["tabs"],
+ background: { scripts: ["utils.js", "background.js"] },
+ };
+
+ if (config.manifestIcons) {
+ manifest.icons = config.manifestIcons;
+ }
+
+ if (config.permissions) {
+ manifest.permissions = config.permissions;
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest,
+ });
+
+ extension.onMessage("checkTabs", async test => {
+ let tabmail = document.getElementById("tabmail");
+
+ if (test.action && test.spaceName && test.url) {
+ let tabPromise =
+ test.action == "switch"
+ ? BrowserTestUtils.waitForEvent(tabmail.tabContainer, "TabSelect")
+ : contentTabOpenPromise(tabmail, test.url);
+ let button = window.document.getElementById(
+ `spaces_toolbar_mochi_test-spacesButton-${test.spaceName}`
+ );
+ button.click();
+ await tabPromise;
+ }
+
+ let tabs = tabmail.tabInfo.filter(tabInfo => !!tabInfo.spaceButtonId);
+ Assert.equal(
+ test.openSpacesUrls.length,
+ tabs.length,
+ `Should have found the correct number of open add-on spaces tabs.`
+ );
+ for (let expectedUrl of test.openSpacesUrls) {
+ Assert.ok(
+ tabmail.tabInfo.find(
+ tabInfo =>
+ !!tabInfo.spaceButtonId &&
+ tabInfo.browser.currentURI.spec == expectedUrl
+ ),
+ `Should have found a spaces tab with the expected url.`
+ );
+ }
+ extension.sendMessage();
+ });
+
+ extension.onMessage("checkUI", async expected => {
+ let addonButtons = document.querySelectorAll(".spaces-addon-button");
+ Assert.equal(
+ expected.length,
+ addonButtons.length,
+ `Should have found the correct number of buttons.`
+ );
+
+ for (let {
+ name,
+ url,
+ title,
+ icons,
+ badgeText,
+ badgeBackgroundColor,
+ } of expected) {
+ // Check button.
+ let button = window.document.getElementById(
+ `spaces_toolbar_mochi_test-spacesButton-${name}`
+ );
+ Assert.ok(button, `Button for space ${name} should exist.`);
+ Assert.equal(
+ title,
+ button.title,
+ `Title of button for space ${name} should be correct.`
+ );
+
+ // Check button icon.
+ let imgStyles = window.getComputedStyle(button.querySelector("img"));
+ Assert.equal(
+ icons[config.selectedTheme],
+ imgStyles.content,
+ `Icon for button of space ${name} with theme ${config.selectedTheme} should be correct.`
+ );
+
+ // Check badge.
+ let badge = button.querySelector(".spaces-badge-container");
+ let badgeStyles = window.getComputedStyle(badge);
+ if (badgeText) {
+ Assert.equal(
+ "block",
+ badgeStyles.display,
+ `Button of space ${name} should have a badge.`
+ );
+ Assert.equal(
+ badgeText,
+ badge.textContent,
+ `Badge of button of space ${name} should have the correct content.`
+ );
+ if (badgeBackgroundColor) {
+ Assert.equal(
+ badgeBackgroundColor,
+ badgeStyles.backgroundColor,
+ `Badge of button of space ${name} should have the correct backgroundColor.`
+ );
+ }
+ } else {
+ Assert.equal(
+ "none",
+ badgeStyles.display,
+ `Button of space ${name} should not have a badge.`
+ );
+ }
+
+ let collapseButton = document.getElementById("collapseButton");
+ let revealButton = document.getElementById("spacesToolbarReveal");
+ let pinnedButton = document.getElementById("spacesPinnedButton");
+ let pinnedPopup = document.getElementById("spacesButtonMenuPopup");
+
+ Assert.ok(revealButton.hidden, "The status bar toggle button is hidden");
+ Assert.ok(pinnedButton.hidden, "The pinned titlebar button is hidden");
+ collapseButton.click();
+ Assert.ok(
+ !revealButton.hidden,
+ "The status bar toggle button is not hidden"
+ );
+ Assert.ok(
+ !pinnedButton.hidden,
+ "The pinned titlebar button is not hidden"
+ );
+ pinnedPopup.openPopup();
+
+ // Check menuitem.
+ let menuitem = window.document.getElementById(
+ `spaces_toolbar_mochi_test-spacesButton-${name}-menuitem`
+ );
+ Assert.ok(menuitem, `Menuitem for id ${name} should exist.`);
+ Assert.equal(
+ title,
+ menuitem.label,
+ `Label of menuitem of space ${name} should be correct.`
+ );
+
+ // Check menuitem icon.
+ let menuitemStyles = window.getComputedStyle(menuitem);
+ Assert.equal(
+ icons[config.selectedTheme],
+ menuitemStyles.listStyleImage,
+ `Icon of menuitem for space ${name} with theme ${config.selectedTheme} should be correct.`
+ );
+
+ pinnedPopup.hidePopup();
+ revealButton.click();
+ Assert.ok(revealButton.hidden, "The status bar toggle button is hidden");
+ Assert.ok(pinnedButton.hidden, "The pinned titlebar button is hidden");
+
+ //Check space and url.
+ let space = window.gSpacesToolbar.spaces.find(
+ space => space.name == `spaces_toolbar_mochi_test-spacesButton-${name}`
+ );
+ Assert.ok(space, "The space of this button should exists");
+ Assert.equal(
+ url,
+ space.url,
+ "The stored url of the space should be correct"
+ );
+ }
+ extension.sendMessage();
+ });
+
+ extension.onMessage("getConfig", async () => {
+ extension.sendMessage(config);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+}
+
+add_task(async function test_add_update_remove() {
+ async function background() {
+ let manifest = browser.runtime.getManifest();
+ let extensionIcon = manifest.icons
+ ? browser.runtime.getURL(manifest.icons[16])
+ : "chrome://messenger/content/extension.svg";
+
+ await window.sendMessage("checkUI", []);
+
+ // Test create().
+ browser.test.log("create(): Without id.");
+ await browser.test.assertThrows(
+ () => browser.spaces.create(),
+ /Incorrect argument types for spaces.create./,
+ "create() without name should throw."
+ );
+
+ browser.test.log("create(): Without default url.");
+ await browser.test.assertThrows(
+ () => browser.spaces.create("space_1"),
+ /Incorrect argument types for spaces.create./,
+ "create() without default url should throw."
+ );
+
+ browser.test.log("create(): With invalid default url.");
+ await browser.test.assertRejects(
+ browser.spaces.create("space_1", "invalid://url"),
+ /Failed to create space with name space_1: Invalid default url./,
+ "create() with an invalid default url should throw."
+ );
+
+ browser.test.log("create(): With default url only.");
+ let space_1 = await browser.spaces.create(
+ "space_1",
+ "https://test.invalid"
+ );
+ let expected_space_1 = {
+ name: "space_1",
+ title: "Generated extension",
+ url: "https://test.invalid",
+ icons: {
+ default: `url("${extensionIcon}")`,
+ },
+ };
+ await window.sendMessage("checkUI", [expected_space_1]);
+
+ browser.test.log("create(): With default url only, but existing id.");
+ await browser.test.assertRejects(
+ browser.spaces.create("space_1", "https://test.invalid"),
+ /Failed to create space with name space_1: Space already exists for this extension./,
+ "create() with existing id should throw."
+ );
+
+ browser.test.log("create(): With most properties.");
+ let space_2 = await browser.spaces.create("space_2", "/local/file.html", {
+ title: "Google",
+ defaultIcons: "default.png",
+ badgeText: "12",
+ badgeBackgroundColor: [50, 100, 150, 255],
+ });
+ let expected_space_2 = {
+ name: "space_2",
+ title: "Google",
+ url: browser.runtime.getURL("/local/file.html"),
+ icons: {
+ default: `url("${browser.runtime.getURL("default.png")}")`,
+ },
+ badgeText: "12",
+ badgeBackgroundColor: "rgb(50, 100, 150)",
+ };
+ await window.sendMessage("checkUI", [expected_space_1, expected_space_2]);
+
+ // Test update().
+ browser.test.log("update(): Without id.");
+ await browser.test.assertThrows(
+ () => browser.spaces.update(),
+ /Incorrect argument types for spaces.update./,
+ "update() without id should throw."
+ );
+
+ browser.test.log("update(): With invalid id.");
+ await browser.test.assertRejects(
+ browser.spaces.update(1234),
+ /Failed to update space with id 1234: Unknown id./,
+ "update() with invalid id should throw."
+ );
+
+ browser.test.log("update(): Without properties.");
+ await browser.spaces.update(space_1.id);
+ await window.sendMessage("checkUI", [expected_space_1, expected_space_2]);
+
+ browser.test.log("update(): Updating the badge.");
+ await browser.spaces.update(space_2.id, {
+ badgeText: "ok",
+ badgeBackgroundColor: "green",
+ });
+ expected_space_2.badgeText = "ok";
+ expected_space_2.badgeBackgroundColor = "rgb(0, 128, 0)";
+ await window.sendMessage("checkUI", [expected_space_1, expected_space_2]);
+
+ browser.test.log("update(): Removing the badge.");
+ await browser.spaces.update(space_2.id, {
+ badgeText: "",
+ });
+ delete expected_space_2.badgeText;
+ delete expected_space_2.badgeBackgroundColor;
+ await window.sendMessage("checkUI", [expected_space_1, expected_space_2]);
+
+ browser.test.log("update(): Changing the title.");
+ await browser.spaces.update(space_2.id, {
+ title: "Some other title",
+ });
+ expected_space_2.title = "Some other title";
+ await window.sendMessage("checkUI", [expected_space_1, expected_space_2]);
+
+ browser.test.log("update(): Removing the title.");
+ await browser.spaces.update(space_2.id, {
+ title: "",
+ });
+ expected_space_2.title = "Generated extension";
+ await window.sendMessage("checkUI", [expected_space_1, expected_space_2]);
+
+ browser.test.log("update(): Setting invalid default url.");
+ await browser.test.assertRejects(
+ browser.spaces.update(space_2.id, "invalid://url"),
+ `Failed to update space with id ${space_2.id}: Invalid default url.`,
+ "update() with invalid default url should throw."
+ );
+
+ await browser.spaces.update(space_2.id, "https://test.more.invalid", {
+ title: "Bing",
+ });
+ expected_space_2.title = "Bing";
+ expected_space_2.url = "https://test.more.invalid";
+ await window.sendMessage("checkUI", [expected_space_1, expected_space_2]);
+
+ // Test remove().
+ browser.test.log("remove(): Removing without id.");
+ await browser.test.assertThrows(
+ () => browser.spaces.remove(),
+ /Incorrect argument types for spaces.remove./,
+ "remove() without id should throw."
+ );
+
+ browser.test.log("remove(): Removing with invalid id.");
+ await browser.test.assertRejects(
+ browser.spaces.remove(1234),
+ /Failed to remove space with id 1234: Unknown id./,
+ "remove() with invalid id should throw."
+ );
+
+ browser.test.log("remove(): Removing space_1.");
+ await browser.spaces.remove(space_1.id);
+ await window.sendMessage("checkUI", [expected_space_2]);
+
+ browser.test.notifyPass();
+ }
+ await test_space(background, { selectedTheme: "default" });
+ await test_space(background, {
+ selectedTheme: "default",
+ manifestIcons: { 16: "manifest.png" },
+ });
+});
+
+add_task(async function test_open_reload_close() {
+ async function background() {
+ await window.sendMessage("checkTabs", { openSpacesUrls: [] });
+
+ // Add spaces.
+ let url1 = `http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data/content.html`;
+ let space_1 = await browser.spaces.create("space_1", url1);
+ let url2 = `http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data/content_body.html`;
+ let space_2 = await browser.spaces.create("space_2", url2);
+
+ // Open spaces.
+ await window.sendMessage("checkTabs", {
+ action: "open",
+ url: url1,
+ spaceName: "space_1",
+ openSpacesUrls: [url1],
+ });
+ await window.sendMessage("checkTabs", {
+ action: "open",
+ url: url2,
+ spaceName: "space_2",
+ openSpacesUrls: [url1, url2],
+ });
+
+ // Switch to open spaces tab.
+ await window.sendMessage("checkTabs", {
+ action: "switch",
+ url: url1,
+ spaceName: "space_1",
+ openSpacesUrls: [url1, url2],
+ });
+ await window.sendMessage("checkTabs", {
+ action: "switch",
+ url: url2,
+ spaceName: "space_2",
+ openSpacesUrls: [url1, url2],
+ });
+
+ // TODO: Add test for tab reloading, once this has been implemented.
+
+ // Remove spaces and check that related spaces tab are closed.
+ await browser.spaces.remove(space_1.id);
+ await window.sendMessage("checkTabs", { openSpacesUrls: [url2] });
+ await browser.spaces.remove(space_2.id);
+ await window.sendMessage("checkTabs", { openSpacesUrls: [] });
+
+ browser.test.notifyPass();
+ }
+ await test_space(background, { selectedTheme: "default" });
+});
+
+add_task(async function test_icons() {
+ async function background() {
+ let manifest = browser.runtime.getManifest();
+ let extensionIcon = manifest.icons
+ ? browser.runtime.getURL(manifest.icons[16])
+ : "chrome://messenger/content/extension.svg";
+
+ // Test 1: Setting defaultIcons and themeIcons.
+ browser.test.log("create(): Setting defaultIcons and themeIcons.");
+ let space_1 = await browser.spaces.create(
+ "space_1",
+ "https://test.invalid",
+ {
+ title: "Google",
+ defaultIcons: "default.png",
+ themeIcons: [
+ {
+ dark: "dark.png",
+ light: "light.png",
+ size: 16,
+ },
+ ],
+ }
+ );
+ let expected_space_1 = {
+ name: "space_1",
+ title: "Google",
+ url: "https://test.invalid",
+ icons: {
+ default: `url("${browser.runtime.getURL("default.png")}")`,
+ dark: `url("${browser.runtime.getURL("dark.png")}")`,
+ light: `url("${browser.runtime.getURL("light.png")}")`,
+ },
+ };
+ await window.sendMessage("checkUI", [expected_space_1]);
+
+ // Clearing defaultIcons.
+ await browser.spaces.update(space_1.id, {
+ defaultIcons: "",
+ });
+ expected_space_1.icons = {
+ default: `url("${browser.runtime.getURL("dark.png")}")`,
+ dark: `url("${browser.runtime.getURL("dark.png")}")`,
+ light: `url("${browser.runtime.getURL("light.png")}")`,
+ };
+ await window.sendMessage("checkUI", [expected_space_1]);
+
+ // Setting other defaultIcons.
+ await browser.spaces.update(space_1.id, {
+ defaultIcons: "other.png",
+ });
+ expected_space_1.icons = {
+ default: `url("${browser.runtime.getURL("other.png")}")`,
+ dark: `url("${browser.runtime.getURL("dark.png")}")`,
+ light: `url("${browser.runtime.getURL("light.png")}")`,
+ };
+ await window.sendMessage("checkUI", [expected_space_1]);
+
+ // Clearing themeIcons.
+ await browser.spaces.update(space_1.id, {
+ themeIcons: [],
+ });
+ expected_space_1.icons = {
+ default: `url("${browser.runtime.getURL("other.png")}")`,
+ dark: `url("${browser.runtime.getURL("other.png")}")`,
+ light: `url("${browser.runtime.getURL("other.png")}")`,
+ };
+ await window.sendMessage("checkUI", [expected_space_1]);
+
+ // Setting other themeIcons.
+ await browser.spaces.update(space_1.id, {
+ themeIcons: [
+ {
+ dark: "dark2.png",
+ light: "light2.png",
+ size: 16,
+ },
+ ],
+ });
+ expected_space_1.icons = {
+ default: `url("${browser.runtime.getURL("other.png")}")`,
+ dark: `url("${browser.runtime.getURL("dark2.png")}")`,
+ light: `url("${browser.runtime.getURL("light2.png")}")`,
+ };
+ await window.sendMessage("checkUI", [expected_space_1]);
+
+ // Test 2: Setting themeIcons only.
+ browser.test.log("create(): Setting themeIcons only.");
+ let space_2 = await browser.spaces.create(
+ "space_2",
+ "https://test.other.invalid",
+ {
+ title: "Wikipedia",
+ themeIcons: [
+ {
+ dark: "dark2.png",
+ light: "light2.png",
+ size: 16,
+ },
+ ],
+ }
+ );
+ // Not specifying defaultIcons but only themeIcons should always use the
+ // theme icons, even for the default theme (and not the extension icon).
+ let expected_space_2 = {
+ name: "space_2",
+ title: "Wikipedia",
+ url: "https://test.other.invalid",
+ icons: {
+ default: `url("${browser.runtime.getURL("dark2.png")}")`,
+ dark: `url("${browser.runtime.getURL("dark2.png")}")`,
+ light: `url("${browser.runtime.getURL("light2.png")}")`,
+ },
+ };
+ await window.sendMessage("checkUI", [expected_space_1, expected_space_2]);
+
+ // Clearing themeIcons.
+ await browser.spaces.update(space_2.id, {
+ themeIcons: [],
+ });
+ expected_space_2.icons = {
+ default: `url("${extensionIcon}")`,
+ dark: `url("${extensionIcon}")`,
+ light: `url("${extensionIcon}")`,
+ };
+ await window.sendMessage("checkUI", [expected_space_1, expected_space_2]);
+
+ // Test 3: Setting defaultIcons only.
+ browser.test.log("create(): Setting defaultIcons only.");
+ let space_3 = await browser.spaces.create(
+ "space_3",
+ "https://test.more.invalid",
+ {
+ title: "Bing",
+ defaultIcons: "default.png",
+ }
+ );
+ let expected_space_3 = {
+ name: "space_3",
+ title: "Bing",
+ url: "https://test.more.invalid",
+ icons: {
+ default: `url("${browser.runtime.getURL("default.png")}")`,
+ dark: `url("${browser.runtime.getURL("default.png")}")`,
+ light: `url("${browser.runtime.getURL("default.png")}")`,
+ },
+ };
+ await window.sendMessage("checkUI", [
+ expected_space_1,
+ expected_space_2,
+ expected_space_3,
+ ]);
+
+ // Clearing defaultIcons and setting themeIcons.
+ await browser.spaces.update(space_3.id, {
+ defaultIcons: "",
+ themeIcons: [
+ {
+ dark: "dark3.png",
+ light: "light3.png",
+ size: 16,
+ },
+ ],
+ });
+ expected_space_3.icons = {
+ default: `url("${browser.runtime.getURL("dark3.png")}")`,
+ dark: `url("${browser.runtime.getURL("dark3.png")}")`,
+ light: `url("${browser.runtime.getURL("light3.png")}")`,
+ };
+ await window.sendMessage("checkUI", [
+ expected_space_1,
+ expected_space_2,
+ expected_space_3,
+ ]);
+
+ // Test 4: Setting no icons.
+ browser.test.log("create(): Setting no icons.");
+ let space_4 = await browser.spaces.create(
+ "space_4",
+ "https://duckduckgo.com",
+ {
+ title: "DuckDuckGo",
+ }
+ );
+ let expected_space_4 = {
+ name: "space_4",
+ title: "DuckDuckGo",
+ url: "https://duckduckgo.com",
+ icons: {
+ default: `url("${extensionIcon}")`,
+ dark: `url("${extensionIcon}")`,
+ light: `url("${extensionIcon}")`,
+ },
+ };
+ await window.sendMessage("checkUI", [
+ expected_space_1,
+ expected_space_2,
+ expected_space_3,
+ expected_space_4,
+ ]);
+
+ // Setting and clearing default icons.
+ await browser.spaces.update(space_4.id, {
+ defaultIcons: "default.png",
+ });
+ expected_space_4.icons = {
+ default: `url("${browser.runtime.getURL("default.png")}")`,
+ dark: `url("${browser.runtime.getURL("default.png")}")`,
+ light: `url("${browser.runtime.getURL("default.png")}")`,
+ };
+ await window.sendMessage("checkUI", [
+ expected_space_1,
+ expected_space_2,
+ expected_space_3,
+ expected_space_4,
+ ]);
+ await browser.spaces.update(space_4.id, {
+ defaultIcons: "",
+ });
+ expected_space_4.icons = {
+ default: `url("${extensionIcon}")`,
+ dark: `url("${extensionIcon}")`,
+ light: `url("${extensionIcon}")`,
+ };
+ await window.sendMessage("checkUI", [
+ expected_space_1,
+ expected_space_2,
+ expected_space_3,
+ expected_space_4,
+ ]);
+
+ browser.test.notifyPass();
+ }
+
+ // Test with and without icons defined in the manifest.
+ for (let manifestIcons of [null, { 16: "manifest16.png" }]) {
+ let dark_theme = await AddonManager.getAddonByID(
+ "thunderbird-compact-dark@mozilla.org"
+ );
+ await dark_theme.enable();
+ await test_space(background, { selectedTheme: "light", manifestIcons });
+
+ let light_theme = await AddonManager.getAddonByID(
+ "thunderbird-compact-light@mozilla.org"
+ );
+ await light_theme.enable();
+ await test_space(background, { selectedTheme: "dark", manifestIcons });
+
+ // Disabling a theme will enable the default theme.
+ await light_theme.disable();
+ await test_space(background, { selectedTheme: "default", manifestIcons });
+ }
+});
+
+add_task(async function test_open_programmatically() {
+ async function background() {
+ await window.sendMessage("checkTabs", { openSpacesUrls: [] });
+
+ // Add spaces.
+ let url1 = `http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data/content.html`;
+ let space_1 = await browser.spaces.create("space_1", url1);
+ let url2 = `http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data/content_body.html`;
+ let space_2 = await browser.spaces.create("space_2", url2);
+ await window.sendMessage("checkTabs", { openSpacesUrls: [] });
+
+ async function openSpace(space, url) {
+ let loadPromise = new Promise(resolve => {
+ let urlSeen = false;
+ let listener = (tabId, changeInfo) => {
+ if (changeInfo.url && changeInfo.url == url) {
+ urlSeen = true;
+ }
+ if (changeInfo.status == "complete" && urlSeen) {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ };
+ browser.tabs.onUpdated.addListener(listener);
+ });
+ let tab = await browser.spaces.open(space.id);
+ await loadPromise;
+
+ browser.test.assertEq(
+ space.id,
+ tab.spaceId,
+ "The opened tab should belong to the correct space"
+ );
+
+ let queriedTabs = await browser.tabs.query({ spaceId: space.id });
+ browser.test.assertEq(
+ 1,
+ queriedTabs.length,
+ "browser.tabs.query() should find exactly one tab belonging to the opened space"
+ );
+ browser.test.assertEq(
+ tab.id,
+ queriedTabs[0].id,
+ "browser.tabs.query() should find the correct tab belonging to the opened space"
+ );
+ }
+
+ // Open space #1.
+ await openSpace(space_1, url1);
+ await window.sendMessage("checkTabs", {
+ spaceName: "space_1",
+ openSpacesUrls: [url1],
+ });
+
+ // Open space #2.
+ await openSpace(space_2, url2);
+ await window.sendMessage("checkTabs", {
+ spaceName: "space_2",
+ openSpacesUrls: [url1, url2],
+ });
+
+ // Switch to open space tab.
+ await window.sendMessage("checkTabs", {
+ action: "switch",
+ url: url1,
+ spaceName: "space_1",
+ openSpacesUrls: [url1, url2],
+ });
+
+ await window.sendMessage("checkTabs", {
+ action: "switch",
+ url: url2,
+ spaceName: "space_2",
+ openSpacesUrls: [url1, url2],
+ });
+
+ // Remove spaces and check that related spaces tab are closed.
+ await browser.spaces.remove(space_1.id);
+ await window.sendMessage("checkTabs", { openSpacesUrls: [url2] });
+ await browser.spaces.remove(space_2.id);
+ await window.sendMessage("checkTabs", { openSpacesUrls: [] });
+
+ browser.test.notifyPass();
+ }
+ await test_space(background, { selectedTheme: "default" });
+});
+
+// Load a second extension parallel to the standard space test, which creates
+// two additional spaces.
+async function test_query({ permissions }) {
+ async function query_background() {
+ function verify(description, expected, spaces) {
+ browser.test.assertEq(
+ expected.length,
+ spaces.length,
+ `${description}: Should find the correct number of spaces`
+ );
+ window.assertDeepEqual(
+ spaces,
+ expected,
+ `${description}: Should find the correct spaces`
+ );
+ }
+
+ async function query(queryInfo, expected) {
+ let spaces =
+ queryInfo === null
+ ? await browser.spaces.query()
+ : await browser.spaces.query(queryInfo);
+ verify(`Query ${JSON.stringify(queryInfo)}`, expected, spaces);
+ }
+
+ let builtIn = [
+ {
+ id: 1,
+ name: "mail",
+ isBuiltIn: true,
+ isSelfOwned: false,
+ },
+ {
+ id: 2,
+ isBuiltIn: true,
+ isSelfOwned: false,
+ name: "addressbook",
+ },
+ {
+ id: 3,
+ isBuiltIn: true,
+ isSelfOwned: false,
+ name: "calendar",
+ },
+ {
+ id: 4,
+ isBuiltIn: true,
+ isSelfOwned: false,
+ name: "tasks",
+ },
+ {
+ id: 5,
+ isBuiltIn: true,
+ isSelfOwned: false,
+ name: "chat",
+ },
+ {
+ id: 6,
+ isBuiltIn: true,
+ isSelfOwned: false,
+ name: "settings",
+ },
+ ];
+
+ await window.sendMessage("checkTabs", { openSpacesUrls: [] });
+ let [{ other_1, other_11, permissions }] = await window.sendMessage(
+ "getConfig"
+ );
+ let hasManagement = permissions && permissions.includes("management");
+
+ // Verify space_1 from other extension.
+ let expected_other_1 = {
+ name: "space_1",
+ isBuiltIn: false,
+ isSelfOwned: true,
+ };
+ if (hasManagement) {
+ expected_other_1.extensionId = "spaces_toolbar_other@mochi.test";
+ }
+ verify("Check space_1 from other extension", other_1, expected_other_1);
+
+ // Verify space_11 from other extension.
+ let expected_other_11 = {
+ name: "space_11",
+ isBuiltIn: false,
+ isSelfOwned: true,
+ };
+ if (hasManagement) {
+ expected_other_11.extensionId = "spaces_toolbar_other@mochi.test";
+ }
+ verify("Check space_11 from other extension", other_11, expected_other_11);
+
+ // Manipulate isSelfOwned, because we got those from the other extension.
+ other_1.isSelfOwned = false;
+ other_11.isSelfOwned = false;
+
+ await query(null, [...builtIn, other_1, other_11]);
+ await query({}, [...builtIn, other_1, other_11]);
+ await query({ isSelfOwned: false }, [...builtIn, other_1, other_11]);
+ await query({ isBuiltIn: true }, [...builtIn]);
+ await query({ isBuiltIn: false }, [other_1, other_11]);
+ await query({ isSelfOwned: true }, []);
+ await query(
+ { extensionId: "spaces_toolbar_other@mochi.test" },
+ hasManagement ? [other_1, other_11] : []
+ );
+
+ // Add spaces.
+ let url1 = `http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data/content.html`;
+ let space_1 = await browser.spaces.create("space_1", url1);
+ let url2 = `http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data/content_body.html`;
+ let space_2 = await browser.spaces.create("space_2", url2);
+
+ // Verify returned space_1
+ let expected_space_1 = {
+ name: "space_1",
+ isBuiltIn: false,
+ isSelfOwned: true,
+ };
+ if (hasManagement) {
+ expected_space_1.extensionId = "spaces_toolbar@mochi.test";
+ }
+ verify("Check space_1", space_1, expected_space_1);
+
+ // Verify returned space_2
+ let expected_space_2 = {
+ name: "space_2",
+ isBuiltIn: false,
+ isSelfOwned: true,
+ };
+ if (hasManagement) {
+ expected_space_2.extensionId = "spaces_toolbar@mochi.test";
+ }
+ verify("Check space_2", space_2, expected_space_2);
+
+ await query(null, [...builtIn, other_1, other_11, space_1, space_2]);
+ await query({ isSelfOwned: false }, [...builtIn, other_1, other_11]);
+ await query({ isBuiltIn: true }, [...builtIn]);
+ await query({ isBuiltIn: false }, [other_1, other_11, space_1, space_2]);
+ await query({ isSelfOwned: true }, [space_1, space_2]);
+ await query(
+ { extensionId: "spaces_toolbar_other@mochi.test" },
+ hasManagement ? [other_1, other_11] : []
+ );
+ await query(
+ { extensionId: "spaces_toolbar@mochi.test" },
+ hasManagement ? [space_1, space_2] : []
+ );
+
+ await query({ id: space_1.id }, [space_1]);
+ await query({ id: other_1.id }, [other_1]);
+ await query({ id: space_2.id }, [space_2]);
+ await query({ id: other_11.id }, [other_11]);
+ await query({ name: "space_1" }, [other_1, space_1]);
+ await query({ name: "space_2" }, [space_2]);
+ await query({ name: "space_11" }, [other_11]);
+
+ browser.test.notifyPass();
+ }
+
+ let otherExtension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let url = `http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data/content.html`;
+ let other_1 = await browser.spaces.create("space_1", url);
+ let other_11 = await browser.spaces.create("space_11", url);
+ browser.test.sendMessage("Done", { other_1, other_11 });
+ browser.test.notifyPass();
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ manifest_version: 3,
+ browser_specific_settings: {
+ gecko: {
+ id: "spaces_toolbar_other@mochi.test",
+ },
+ },
+ permissions,
+ background: { scripts: ["utils.js", "background.js"] },
+ },
+ });
+
+ await otherExtension.startup();
+ let { other_1, other_11 } = await otherExtension.awaitMessage("Done");
+
+ await test_space(query_background, {
+ selectedTheme: "default",
+ other_1,
+ other_11,
+ permissions,
+ });
+
+ await otherExtension.awaitFinish();
+ await otherExtension.unload();
+}
+
+add_task(async function test_query_no_management_permission() {
+ await test_query({ permissions: [] });
+});
+
+add_task(async function test_query_management_permission() {
+ await test_query({ permissions: ["management"] });
+});
+
+// Test built-in spaces to make sure the space definition of the spaceTracker in
+// ext-mails.js is matching the actual space definition in spacesToolbar.js
+add_task(async function test_builtIn_spaces() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ const checkSpace = async (spaceId, spaceName) => {
+ let spaces = await browser.spaces.query({ id: spaceId });
+ browser.test.assertEq(spaces.length, 1, "Should find a single space");
+ browser.test.assertEq(
+ spaces[0].isBuiltIn,
+ true,
+ "Should find a built-in space"
+ );
+ browser.test.assertEq(
+ spaces[0].name,
+ spaceName,
+ "Should find the correct space"
+ );
+ };
+
+ // Test the already open mail space.
+
+ let mailTabs = await browser.tabs.query({ type: "mail" });
+ browser.test.assertEq(
+ mailTabs.length,
+ 1,
+ "Should find a single mail tab"
+ );
+ await checkSpace(mailTabs[0].spaceId, "mail");
+
+ // Test all other spaces.
+
+ let builtInSpaces = [
+ "addressbook",
+ "calendar",
+ "tasks",
+ "chat",
+ "settings",
+ ];
+
+ for (let spaceName of builtInSpaces) {
+ await new Promise(resolve => {
+ const listener = async tab => {
+ await checkSpace(tab.spaceId, spaceName);
+ browser.tabs.remove(tab.id);
+ browser.tabs.onCreated.removeListener(listener);
+ resolve();
+ };
+ browser.tabs.onCreated.addListener(listener);
+ browser.test.sendMessage("openSpace", spaceName);
+ });
+ }
+
+ browser.test.notifyPass();
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ manifest_version: 2,
+ browser_specific_settings: {
+ gecko: {
+ id: "built-in-spaces@mochi.test",
+ },
+ },
+ background: { scripts: ["utils.js", "background.js"] },
+ },
+ });
+
+ extension.onMessage("openSpace", async spaceName => {
+ window.gSpacesToolbar.openSpace(
+ window.document.getElementById("tabmail"),
+ window.gSpacesToolbar.spaces.find(space => space.name == spaceName)
+ );
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_spacesToolbar.js b/comm/mail/components/extensions/test/browser/browser_ext_spacesToolbar.js
new file mode 100644
index 0000000000..15a2b2b999
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_spacesToolbar.js
@@ -0,0 +1,755 @@
+/* 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/. */
+
+/**
+ * Helper Function, creates a test extension to verify expected button states.
+ *
+ * @param {Function} background - The background script executed by the test.
+ * @param {string} selectedTheme - The selected theme (default, light or dark),
+ * used to select the expected button/menuitem icon.
+ * @param {?object} manifestIcons - The icons entry of the extension manifest.
+ */
+async function test_spaceToolbar(background, selectedTheme, manifestIcons) {
+ let manifest = {
+ manifest_version: 2,
+ applications: {
+ gecko: {
+ id: "spaces_toolbar@mochi.test",
+ },
+ },
+ permissions: ["tabs"],
+ background: { scripts: ["utils.js", "background.js"] },
+ };
+
+ if (manifestIcons) {
+ manifest.icons = manifestIcons;
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest,
+ });
+
+ extension.onMessage("checkTabs", async test => {
+ let tabmail = document.getElementById("tabmail");
+
+ if (test.action && test.buttonId && test.url) {
+ let tabPromise =
+ test.action == "switch"
+ ? BrowserTestUtils.waitForEvent(tabmail.tabContainer, "TabSelect")
+ : contentTabOpenPromise(tabmail, test.url);
+ let button = window.document.getElementById(
+ `spaces_toolbar_mochi_test-spacesButton-${test.buttonId}`
+ );
+ button.click();
+ await tabPromise;
+ }
+
+ let tabs = tabmail.tabInfo.filter(tabInfo => !!tabInfo.spaceButtonId);
+ Assert.equal(
+ test.openSpacesUrls.length,
+ tabs.length,
+ `Should have found the correct number of open add-on spaces tabs.`
+ );
+ for (let expectedUrl of test.openSpacesUrls) {
+ Assert.ok(
+ tabmail.tabInfo.find(
+ tabInfo =>
+ !!tabInfo.spaceButtonId &&
+ tabInfo.browser.currentURI.spec == expectedUrl
+ ),
+ `Should have found a spaces tab with the expected url.`
+ );
+ }
+ extension.sendMessage();
+ });
+
+ extension.onMessage("checkUI", async expected => {
+ let addonButtons = document.querySelectorAll(".spaces-addon-button");
+ Assert.equal(
+ expected.length,
+ addonButtons.length,
+ `Should have found the correct number of buttons.`
+ );
+
+ for (let {
+ id,
+ url,
+ title,
+ icons,
+ badgeText,
+ badgeBackgroundColor,
+ } of expected) {
+ // Check button.
+ let button = window.document.getElementById(
+ `spaces_toolbar_mochi_test-spacesButton-${id}`
+ );
+ Assert.ok(button, `Button for id ${id} should exist.`);
+ Assert.equal(
+ title,
+ button.title,
+ `Title of button ${id} should be correct.`
+ );
+
+ // Check button icon.
+ let imgStyles = window.getComputedStyle(button.querySelector("img"));
+ Assert.equal(
+ icons[selectedTheme],
+ imgStyles.content,
+ `Icon of button ${id} with theme ${selectedTheme} should be correct.`
+ );
+
+ // Check badge.
+ let badge = button.querySelector(".spaces-badge-container");
+ let badgeStyles = window.getComputedStyle(badge);
+ if (badgeText) {
+ Assert.equal(
+ "block",
+ badgeStyles.display,
+ `Button ${id} should have a badge.`
+ );
+ Assert.equal(
+ badgeText,
+ badge.textContent,
+ `Badge of button ${id} should have the correct content.`
+ );
+ if (badgeBackgroundColor) {
+ Assert.equal(
+ badgeBackgroundColor,
+ badgeStyles.backgroundColor,
+ `Badge of button ${id} should have the correct backgroundColor.`
+ );
+ }
+ } else {
+ Assert.equal(
+ "none",
+ badgeStyles.display,
+ `Button ${id} should not have a badge.`
+ );
+ }
+
+ let collapseButton = document.getElementById("collapseButton");
+ let revealButton = document.getElementById("spacesToolbarReveal");
+ let pinnedButton = document.getElementById("spacesPinnedButton");
+ let pinnedPopup = document.getElementById("spacesButtonMenuPopup");
+
+ Assert.ok(revealButton.hidden, "The status bar toggle button is hidden");
+ Assert.ok(pinnedButton.hidden, "The pinned titlebar button is hidden");
+ collapseButton.click();
+ Assert.ok(
+ !revealButton.hidden,
+ "The status bar toggle button is not hidden"
+ );
+ Assert.ok(
+ !pinnedButton.hidden,
+ "The pinned titlebar button is not hidden"
+ );
+ pinnedPopup.openPopup();
+
+ // Check menuitem.
+ let menuitem = window.document.getElementById(
+ `spaces_toolbar_mochi_test-spacesButton-${id}-menuitem`
+ );
+ Assert.ok(menuitem, `Menuitem for id ${id} should exist.`);
+ Assert.equal(
+ title,
+ menuitem.label,
+ `Label of menuitem ${id} should be correct.`
+ );
+
+ // Check menuitem icon.
+ let menuitemStyles = window.getComputedStyle(menuitem);
+ Assert.equal(
+ icons[selectedTheme],
+ menuitemStyles.listStyleImage,
+ `Icon of menuitem ${id} with theme ${selectedTheme} should be correct.`
+ );
+
+ pinnedPopup.hidePopup();
+ revealButton.click();
+ Assert.ok(revealButton.hidden, "The status bar toggle button is hidden");
+ Assert.ok(pinnedButton.hidden, "The pinned titlebar button is hidden");
+
+ //Check space and url.
+ let space = window.gSpacesToolbar.spaces.find(
+ space => space.name == `spaces_toolbar_mochi_test-spacesButton-${id}`
+ );
+ Assert.ok(space, "The space of this button should exists");
+ Assert.equal(
+ url,
+ space.url,
+ "The stored url of the space should be correct"
+ );
+ }
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+}
+
+add_task(async function test_add_update_remove() {
+ async function background() {
+ let manifest = browser.runtime.getManifest();
+ let extensionIcon = manifest.icons
+ ? browser.runtime.getURL(manifest.icons[16])
+ : "chrome://messenger/content/extension.svg";
+
+ // Test addButton().
+ browser.test.log("addButton(): Without id.");
+ await browser.test.assertThrows(
+ () => browser.spacesToolbar.addButton(),
+ /Incorrect argument types for spacesToolbar.addButton./,
+ "addButton() without id should throw."
+ );
+
+ browser.test.log("addButton(): Without properties.");
+ await browser.test.assertThrows(
+ () => browser.spacesToolbar.addButton("button_1"),
+ /Incorrect argument types for spacesToolbar.addButton./,
+ "addButton() without properties should throw."
+ );
+
+ browser.test.log("addButton(): With empty properties.");
+ await browser.test.assertRejects(
+ browser.spacesToolbar.addButton("button_1", {}),
+ /Failed to add button to the spaces toolbar: Invalid url./,
+ "addButton() without a url should throw."
+ );
+
+ browser.test.log("addButton(): With invalid url.");
+ await browser.test.assertRejects(
+ browser.spacesToolbar.addButton("button_1", {
+ url: "invalid://url",
+ }),
+ /Failed to add button to the spaces toolbar: Invalid url./,
+ "addButton() with an invalid url should throw."
+ );
+
+ browser.test.log("addButton(): With url only.");
+ await browser.spacesToolbar.addButton("button_1", {
+ url: "https://test.invalid",
+ });
+ let expected_button_1 = {
+ id: "button_1",
+ title: "Generated extension",
+ url: "https://test.invalid",
+ icons: {
+ default: `url("${extensionIcon}")`,
+ },
+ };
+ await window.sendMessage("checkUI", [expected_button_1]);
+
+ browser.test.log("addButton(): With url only, but existing id.");
+ await browser.test.assertRejects(
+ browser.spacesToolbar.addButton("button_1", {
+ url: "https://test.invalid",
+ }),
+ /Failed to add button to the spaces toolbar: The id button_1 is already used by this extension./,
+ "addButton() with existing id should throw."
+ );
+
+ browser.test.log("addButton(): With most properties.");
+ await browser.spacesToolbar.addButton("button_2", {
+ title: "Google",
+ url: "/local/file.html",
+ defaultIcons: "default.png",
+ badgeText: "12",
+ badgeBackgroundColor: [50, 100, 150, 255],
+ });
+ let expected_button_2 = {
+ id: "button_2",
+ title: "Google",
+ url: browser.runtime.getURL("/local/file.html"),
+ icons: {
+ default: `url("${browser.runtime.getURL("default.png")}")`,
+ },
+ badgeText: "12",
+ badgeBackgroundColor: "rgb(50, 100, 150)",
+ };
+ await window.sendMessage("checkUI", [expected_button_1, expected_button_2]);
+
+ // Test updateButton().
+ browser.test.log("updateButton(): Without id.");
+ await browser.test.assertThrows(
+ () => browser.spacesToolbar.updateButton(),
+ /Incorrect argument types for spacesToolbar.updateButton./,
+ "updateButton() without id should throw."
+ );
+
+ browser.test.log("updateButton(): Without properties.");
+ await browser.test.assertThrows(
+ () => browser.spacesToolbar.updateButton("InvalidId"),
+ /Incorrect argument types for spacesToolbar.updateButton./,
+ "updateButton() without properties should throw."
+ );
+
+ browser.test.log("updateButton(): With empty properties but invalid id.");
+ await browser.test.assertRejects(
+ browser.spacesToolbar.updateButton("InvalidId", {}),
+ /Failed to update button in the spaces toolbar: A button with id InvalidId does not exist for this extension./,
+ "updateButton() with invalid id should throw."
+ );
+
+ browser.test.log("updateButton(): With empty properties.");
+ await browser.spacesToolbar.updateButton("button_1", {});
+ await window.sendMessage("checkUI", [expected_button_1, expected_button_2]);
+
+ browser.test.log("updateButton(): Updating the badge.");
+ await browser.spacesToolbar.updateButton("button_2", {
+ badgeText: "ok",
+ badgeBackgroundColor: "green",
+ });
+ expected_button_2.badgeText = "ok";
+ expected_button_2.badgeBackgroundColor = "rgb(0, 128, 0)";
+ await window.sendMessage("checkUI", [expected_button_1, expected_button_2]);
+
+ browser.test.log("updateButton(): Removing the badge.");
+ await browser.spacesToolbar.updateButton("button_2", {
+ badgeText: "",
+ });
+ delete expected_button_2.badgeText;
+ delete expected_button_2.badgeBackgroundColor;
+ await window.sendMessage("checkUI", [expected_button_1, expected_button_2]);
+
+ browser.test.log("updateButton(): Changing the title.");
+ await browser.spacesToolbar.updateButton("button_2", {
+ title: "Some other title",
+ });
+ expected_button_2.title = "Some other title";
+ await window.sendMessage("checkUI", [expected_button_1, expected_button_2]);
+
+ browser.test.log("updateButton(): Removing the title.");
+ await browser.spacesToolbar.updateButton("button_2", {
+ title: "",
+ });
+ expected_button_2.title = "Generated extension";
+ await window.sendMessage("checkUI", [expected_button_1, expected_button_2]);
+
+ browser.test.log("updateButton(): Settings an invalid url.");
+ await browser.test.assertRejects(
+ browser.spacesToolbar.updateButton("button_2", {
+ url: "invalid://url",
+ }),
+ /Failed to update button in the spaces toolbar: Invalid url./,
+ "updateButton() with invalid url should throw."
+ );
+
+ await browser.spacesToolbar.updateButton("button_2", {
+ title: "Bing",
+ url: "https://test.more.invalid",
+ });
+ expected_button_2.title = "Bing";
+ expected_button_2.url = "https://test.more.invalid";
+ await window.sendMessage("checkUI", [expected_button_1, expected_button_2]);
+
+ // Test removeButton().
+ browser.test.log("removeButton(): Removing without id.");
+ await browser.test.assertThrows(
+ () => browser.spacesToolbar.removeButton(),
+ /Incorrect argument types for spacesToolbar.removeButton./,
+ "removeButton() without id should throw."
+ );
+
+ browser.test.log("removeButton(): Removing with invalid id.");
+ await browser.test.assertRejects(
+ browser.spacesToolbar.removeButton("InvalidId"),
+ /Failed to remove button from the spaces toolbar: A button with id InvalidId does not exist for this extension./,
+ "removeButton() with invalid id should throw."
+ );
+
+ browser.test.log("removeButton(): Removing button_1.");
+ await browser.spacesToolbar.removeButton("button_1");
+ await window.sendMessage("checkUI", [expected_button_2]);
+
+ browser.test.notifyPass();
+ }
+ await test_spaceToolbar(background, "default");
+ await test_spaceToolbar(background, "default", { 16: "manifest.png" });
+});
+
+add_task(async function test_open_reload_close() {
+ async function background() {
+ await window.sendMessage("checkTabs", { openSpacesUrls: [] });
+
+ // Add buttons.
+ let url1 = `http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data/content.html`;
+ await browser.spacesToolbar.addButton("button_1", {
+ url: url1,
+ });
+ let url2 = `http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data/content_body.html`;
+ await browser.spacesToolbar.addButton("button_2", {
+ url: url2,
+ });
+
+ // Open spaces.
+ await window.sendMessage("checkTabs", {
+ action: "open",
+ url: url1,
+ buttonId: "button_1",
+ openSpacesUrls: [url1],
+ });
+ await window.sendMessage("checkTabs", {
+ action: "open",
+ url: url2,
+ buttonId: "button_2",
+ openSpacesUrls: [url1, url2],
+ });
+
+ // Switch to open spaces tab.
+ await window.sendMessage("checkTabs", {
+ action: "switch",
+ url: url1,
+ buttonId: "button_1",
+ openSpacesUrls: [url1, url2],
+ });
+ await window.sendMessage("checkTabs", {
+ action: "switch",
+ url: url2,
+ buttonId: "button_2",
+ openSpacesUrls: [url1, url2],
+ });
+
+ // TODO: Add test for tab reloading, once this has been implemented.
+
+ // Remove buttons and check that related spaces tab are closed.
+ await browser.spacesToolbar.removeButton("button_1");
+ await window.sendMessage("checkTabs", { openSpacesUrls: [url2] });
+ await browser.spacesToolbar.removeButton("button_2");
+ await window.sendMessage("checkTabs", { openSpacesUrls: [] });
+
+ browser.test.notifyPass();
+ }
+ await test_spaceToolbar(background, "default");
+});
+
+add_task(async function test_icons() {
+ async function background() {
+ let manifest = browser.runtime.getManifest();
+ let extensionIcon = manifest.icons
+ ? browser.runtime.getURL(manifest.icons[16])
+ : "chrome://messenger/content/extension.svg";
+
+ // Test 1: Setting defaultIcons and themeIcons.
+ browser.test.log("addButton(): Setting defaultIcons and themeIcons.");
+ await browser.spacesToolbar.addButton("button_1", {
+ title: "Google",
+ url: "https://test.invalid",
+ defaultIcons: "default.png",
+ themeIcons: [
+ {
+ dark: "dark.png",
+ light: "light.png",
+ size: 16,
+ },
+ ],
+ });
+ let expected_button_1 = {
+ id: "button_1",
+ title: "Google",
+ url: "https://test.invalid",
+ icons: {
+ default: `url("${browser.runtime.getURL("default.png")}")`,
+ dark: `url("${browser.runtime.getURL("dark.png")}")`,
+ light: `url("${browser.runtime.getURL("light.png")}")`,
+ },
+ };
+ await window.sendMessage("checkUI", [expected_button_1]);
+
+ // Clearing defaultIcons.
+ await browser.spacesToolbar.updateButton("button_1", {
+ defaultIcons: "",
+ });
+ expected_button_1.icons = {
+ default: `url("${browser.runtime.getURL("dark.png")}")`,
+ dark: `url("${browser.runtime.getURL("dark.png")}")`,
+ light: `url("${browser.runtime.getURL("light.png")}")`,
+ };
+ await window.sendMessage("checkUI", [expected_button_1]);
+
+ // Setting other defaultIcons.
+ await browser.spacesToolbar.updateButton("button_1", {
+ defaultIcons: "other.png",
+ });
+ expected_button_1.icons = {
+ default: `url("${browser.runtime.getURL("other.png")}")`,
+ dark: `url("${browser.runtime.getURL("dark.png")}")`,
+ light: `url("${browser.runtime.getURL("light.png")}")`,
+ };
+ await window.sendMessage("checkUI", [expected_button_1]);
+
+ // Clearing themeIcons.
+ await browser.spacesToolbar.updateButton("button_1", {
+ themeIcons: [],
+ });
+ expected_button_1.icons = {
+ default: `url("${browser.runtime.getURL("other.png")}")`,
+ dark: `url("${browser.runtime.getURL("other.png")}")`,
+ light: `url("${browser.runtime.getURL("other.png")}")`,
+ };
+ await window.sendMessage("checkUI", [expected_button_1]);
+
+ // Setting other themeIcons.
+ await browser.spacesToolbar.updateButton("button_1", {
+ themeIcons: [
+ {
+ dark: "dark2.png",
+ light: "light2.png",
+ size: 16,
+ },
+ ],
+ });
+ expected_button_1.icons = {
+ default: `url("${browser.runtime.getURL("other.png")}")`,
+ dark: `url("${browser.runtime.getURL("dark2.png")}")`,
+ light: `url("${browser.runtime.getURL("light2.png")}")`,
+ };
+ await window.sendMessage("checkUI", [expected_button_1]);
+
+ // Test 2: Setting themeIcons only.
+ browser.test.log("addButton(): Setting themeIcons only.");
+ await browser.spacesToolbar.addButton("button_2", {
+ title: "Wikipedia",
+ url: "https://test.other.invalid",
+ themeIcons: [
+ {
+ dark: "dark2.png",
+ light: "light2.png",
+ size: 16,
+ },
+ ],
+ });
+ // Not specifying defaultIcons but only themeIcons should always use the
+ // theme icons, even for the default theme (and not the extension icon).
+ let expected_button_2 = {
+ id: "button_2",
+ title: "Wikipedia",
+ url: "https://test.other.invalid",
+ icons: {
+ default: `url("${browser.runtime.getURL("dark2.png")}")`,
+ dark: `url("${browser.runtime.getURL("dark2.png")}")`,
+ light: `url("${browser.runtime.getURL("light2.png")}")`,
+ },
+ };
+ await window.sendMessage("checkUI", [expected_button_1, expected_button_2]);
+
+ // Clearing themeIcons.
+ await browser.spacesToolbar.updateButton("button_2", {
+ themeIcons: [],
+ });
+ expected_button_2.icons = {
+ default: `url("${extensionIcon}")`,
+ dark: `url("${extensionIcon}")`,
+ light: `url("${extensionIcon}")`,
+ };
+ await window.sendMessage("checkUI", [expected_button_1, expected_button_2]);
+
+ // Test 3: Setting defaultIcons only.
+ browser.test.log("addButton(): Setting defaultIcons only.");
+ await browser.spacesToolbar.addButton("button_3", {
+ title: "Bing",
+ url: "https://test.more.invalid",
+ defaultIcons: "default.png",
+ });
+ let expected_button_3 = {
+ id: "button_3",
+ title: "Bing",
+ url: "https://test.more.invalid",
+ icons: {
+ default: `url("${browser.runtime.getURL("default.png")}")`,
+ dark: `url("${browser.runtime.getURL("default.png")}")`,
+ light: `url("${browser.runtime.getURL("default.png")}")`,
+ },
+ };
+ await window.sendMessage("checkUI", [
+ expected_button_1,
+ expected_button_2,
+ expected_button_3,
+ ]);
+
+ // Clearing defaultIcons and setting themeIcons.
+ await browser.spacesToolbar.updateButton("button_3", {
+ defaultIcons: "",
+ themeIcons: [
+ {
+ dark: "dark3.png",
+ light: "light3.png",
+ size: 16,
+ },
+ ],
+ });
+ expected_button_3.icons = {
+ default: `url("${browser.runtime.getURL("dark3.png")}")`,
+ dark: `url("${browser.runtime.getURL("dark3.png")}")`,
+ light: `url("${browser.runtime.getURL("light3.png")}")`,
+ };
+ await window.sendMessage("checkUI", [
+ expected_button_1,
+ expected_button_2,
+ expected_button_3,
+ ]);
+
+ // Test 4: Setting no icons.
+ browser.test.log("addButton(): Setting no icons.");
+ await browser.spacesToolbar.addButton("button_4", {
+ title: "DuckDuckGo",
+ url: "https://duckduckgo.com",
+ });
+ let expected_button_4 = {
+ id: "button_4",
+ title: "DuckDuckGo",
+ url: "https://duckduckgo.com",
+ icons: {
+ default: `url("${extensionIcon}")`,
+ dark: `url("${extensionIcon}")`,
+ light: `url("${extensionIcon}")`,
+ },
+ };
+ await window.sendMessage("checkUI", [
+ expected_button_1,
+ expected_button_2,
+ expected_button_3,
+ expected_button_4,
+ ]);
+
+ // Setting and clearing default icons.
+ await browser.spacesToolbar.updateButton("button_4", {
+ defaultIcons: "default.png",
+ });
+ expected_button_4.icons = {
+ default: `url("${browser.runtime.getURL("default.png")}")`,
+ dark: `url("${browser.runtime.getURL("default.png")}")`,
+ light: `url("${browser.runtime.getURL("default.png")}")`,
+ };
+ await window.sendMessage("checkUI", [
+ expected_button_1,
+ expected_button_2,
+ expected_button_3,
+ expected_button_4,
+ ]);
+ await browser.spacesToolbar.updateButton("button_4", {
+ defaultIcons: "",
+ });
+ expected_button_4.icons = {
+ default: `url("${extensionIcon}")`,
+ dark: `url("${extensionIcon}")`,
+ light: `url("${extensionIcon}")`,
+ };
+ await window.sendMessage("checkUI", [
+ expected_button_1,
+ expected_button_2,
+ expected_button_3,
+ expected_button_4,
+ ]);
+
+ browser.test.notifyPass();
+ }
+
+ // Test with and without icons defined in the manifest.
+ for (let manifestIcons of [null, { 16: "manifest16.png" }]) {
+ let dark_theme = await AddonManager.getAddonByID(
+ "thunderbird-compact-dark@mozilla.org"
+ );
+ await dark_theme.enable();
+ await test_spaceToolbar(background, "light", manifestIcons);
+
+ let light_theme = await AddonManager.getAddonByID(
+ "thunderbird-compact-light@mozilla.org"
+ );
+ await light_theme.enable();
+ await test_spaceToolbar(background, "dark", manifestIcons);
+
+ // Disabling a theme will enable the default theme.
+ await light_theme.disable();
+ await test_spaceToolbar(background, "default", manifestIcons);
+ }
+});
+
+add_task(async function test_open_programmatically() {
+ async function background() {
+ await window.sendMessage("checkTabs", { openSpacesUrls: [] });
+
+ // Add buttons.
+ let url1 = `http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data/content.html`;
+ await browser.spacesToolbar.addButton("button_1", {
+ url: url1,
+ });
+ let url2 = `http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data/content_body.html`;
+ await browser.spacesToolbar.addButton("button_2", {
+ url: url2,
+ });
+
+ async function clickSpaceButton(buttonId, url) {
+ let loadPromise = new Promise(resolve => {
+ let urlSeen = false;
+ let listener = (tabId, changeInfo) => {
+ if (changeInfo.url && changeInfo.url == url) {
+ urlSeen = true;
+ }
+ if (changeInfo.status == "complete" && urlSeen) {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ };
+ browser.tabs.onUpdated.addListener(listener);
+ });
+ let tab = await browser.spacesToolbar.clickButton(buttonId);
+ await loadPromise;
+
+ let queriedTabs = await browser.tabs.query({ spaceId: tab.spaceId });
+ browser.test.assertEq(
+ 1,
+ queriedTabs.length,
+ "browser.tabs.query() should find exactly one tab belonging to the opened space"
+ );
+ browser.test.assertEq(
+ tab.id,
+ queriedTabs[0].id,
+ "browser.tabs.query() should find the correct tab belonging to the opened space"
+ );
+ }
+
+ // Open space #1.
+ await clickSpaceButton("button_1", url1);
+ await window.sendMessage("checkTabs", {
+ buttonId: "button_1",
+ openSpacesUrls: [url1],
+ });
+
+ // Open space #2.
+ await clickSpaceButton("button_2", url2);
+ await window.sendMessage("checkTabs", {
+ buttonId: "button_2",
+ openSpacesUrls: [url1, url2],
+ });
+
+ // Switch to open space tab.
+ await window.sendMessage("checkTabs", {
+ action: "switch",
+ url: url1,
+ buttonId: "button_1",
+ openSpacesUrls: [url1, url2],
+ });
+
+ await window.sendMessage("checkTabs", {
+ action: "switch",
+ url: url2,
+ buttonId: "button_2",
+ openSpacesUrls: [url1, url2],
+ });
+
+ // Remove spaces and check that related spaces tab are closed.
+ await browser.spacesToolbar.removeButton("button_1");
+ await window.sendMessage("checkTabs", { openSpacesUrls: [url2] });
+ await browser.spacesToolbar.removeButton("button_2");
+ await window.sendMessage("checkTabs", { openSpacesUrls: [] });
+
+ browser.test.notifyPass();
+ }
+ await test_spaceToolbar(background, "default");
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_tabs_content.js b/comm/mail/components/extensions/test/browser/browser_ext_tabs_content.js
new file mode 100644
index 0000000000..fbc98ff09e
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_tabs_content.js
@@ -0,0 +1,336 @@
+/* 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/. */
+
+/**
+ * Common core of the test. This is complicated by how WebExtensions tests work.
+ *
+ * @param {Function} createTab - The code of this function is copied into the
+ * extension. It should assign a function to `window.createTab` that opens
+ * the tab to be tested and return the id of the tab.
+ * @param {Function} getBrowser - A function to get the <browser> associated
+ * with the tab.
+ */
+async function subTest(createTab, getBrowser, shouldRemove = true) {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "createTab.js": createTab,
+ "background.js": async () => {
+ // Open the tab to be tested.
+
+ let tabId = await window.createTab();
+
+ // Test insertCSS, removeCSS, and executeScript.
+
+ await window.sendMessage();
+ await browser.tabs.insertCSS(tabId, {
+ code: "body { background: lime }",
+ });
+ await window.sendMessage();
+ await browser.tabs.removeCSS(tabId, {
+ code: "body { background: lime }",
+ });
+ await window.sendMessage();
+ await browser.tabs.executeScript(tabId, {
+ code: `
+ document.body.textContent = "Hey look, the script ran!";
+ browser.runtime.onConnect.addListener(port =>
+ port.onMessage.addListener(message => {
+ browser.test.assertEq(message, "Sending a message.");
+ port.postMessage("Got your message.");
+ })
+ );
+ browser.runtime.onMessage.addListener(
+ (message, sender, sendResponse) => {
+ browser.test.assertEq(message, "Sending a message.");
+ sendResponse("Got your message.");
+ }
+ );
+ `,
+ });
+ await window.sendMessage();
+
+ // Test connect and sendMessage. The receivers were set up above.
+
+ let port = await browser.tabs.connect(tabId);
+ port.onMessage.addListener(message =>
+ browser.test.assertEq(message, "Got your message.")
+ );
+ port.postMessage("Sending a message.");
+
+ let response = await browser.tabs.sendMessage(
+ tabId,
+ "Sending a message."
+ );
+ browser.test.assertEq(response, "Got your message.");
+
+ // Remove the tab if required.
+
+ let [shouldRemove] = await window.sendMessage();
+ if (shouldRemove) {
+ await browser.tabs.remove(tabId);
+ }
+ browser.test.notifyPass();
+ },
+ "test.html": "<html><body>I'm a real page!</body></html>",
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "createTab.js", "background.js"] },
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage();
+ let browser = getBrowser();
+ await awaitBrowserLoaded(browser, url => url != "about:blank");
+
+ await checkContent(browser, {
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ textContent: "I'm a real page!",
+ });
+ extension.sendMessage();
+
+ await extension.awaitMessage();
+ await checkContent(browser, { backgroundColor: "rgb(0, 255, 0)" });
+ extension.sendMessage();
+
+ await extension.awaitMessage();
+ await checkContent(browser, { backgroundColor: "rgba(0, 0, 0, 0)" });
+ extension.sendMessage();
+
+ await extension.awaitMessage();
+ await checkContent(browser, { textContent: "Hey look, the script ran!" });
+ extension.sendMessage();
+
+ await extension.awaitMessage();
+ extension.sendMessage(shouldRemove);
+
+ await extension.awaitFinish();
+ await extension.unload();
+}
+
+add_task(async function testFirstTab() {
+ let createTab = async () => {
+ window.createTab = async function () {
+ let tabs = await browser.tabs.query({});
+ browser.test.assertEq(1, tabs.length);
+ await browser.tabs.update(tabs[0].id, { url: "test.html" });
+ return tabs[0].id;
+ };
+ };
+
+ let tabmail = document.getElementById("tabmail");
+ function getBrowser(expected) {
+ return tabmail.currentTabInfo.browser;
+ }
+
+ let gAccount = createAccount();
+ tabmail.currentAbout3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: gAccount.incomingServer.rootFolder.subFolders[0].URI,
+ });
+
+ return subTest(createTab, getBrowser, false);
+});
+
+add_task(async function testContentTab() {
+ let createTab = async () => {
+ window.createTab = async function () {
+ let tab = await browser.tabs.create({ url: "test.html" });
+ return tab.id;
+ };
+ };
+
+ function getBrowser(expected) {
+ let tabmail = document.getElementById("tabmail");
+ return tabmail.currentTabInfo.browser;
+ }
+
+ let tabmail = document.getElementById("tabmail");
+ Assert.equal(
+ tabmail.tabInfo.length,
+ 1,
+ "Should find the correct number of tabs before the test."
+ );
+ // Run the subtest without removing the created tab, to check if extension tabs
+ // are removed automatically, when the extension is removed.
+ let rv = await subTest(createTab, getBrowser, false);
+ Assert.equal(
+ tabmail.tabInfo.length,
+ 1,
+ "Should find the correct number of tabs after the test."
+ );
+ return rv;
+});
+
+add_task(async function testPopupWindow() {
+ let createTab = async () => {
+ window.createTab = async function () {
+ let popup = await browser.windows.create({
+ url: "test.html",
+ type: "popup",
+ });
+ browser.test.assertEq(1, popup.tabs.length);
+ return popup.tabs[0].id;
+ };
+ };
+
+ function getBrowser(expected) {
+ let popups = [...Services.wm.getEnumerator("mail:extensionPopup")];
+ Assert.equal(popups.length, 1);
+
+ let popup = popups[0];
+
+ let popupBrowser = popup.getBrowser();
+ Assert.ok(popupBrowser);
+
+ return popupBrowser;
+ }
+ let popups = [...Services.wm.getEnumerator("mail:extensionPopup")];
+ Assert.equal(
+ popups.length,
+ 0,
+ "Should find the no extension windows before the test."
+ );
+ // Run the subtest without removing the created window, to check if extension
+ // windows are removed automatically, when the extension is removed.
+ let rv = await subTest(createTab, getBrowser, false);
+ Assert.equal(
+ popups.length,
+ 0,
+ "Should find the no extension windows after the test."
+ );
+ return rv;
+});
+
+add_task(async function testMultipleContentTabs() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let tabs = [];
+ let tests = [
+ {
+ url: "test.html",
+ expectedUrl: browser.runtime.getURL("test.html"),
+ },
+ {
+ url: "test.html",
+ expectedUrl: browser.runtime.getURL("test.html"),
+ },
+ {
+ url: "https://www.example.com",
+ expectedUrl: "https://www.example.com/",
+ },
+ {
+ url: "https://www.example.com",
+ expectedUrl: "https://www.example.com/",
+ },
+ {
+ url: "https://www.example.com/",
+ expectedUrl: "https://www.example.com/",
+ },
+ {
+ url: "https://www.example.com/",
+ expectedUrl: "https://www.example.com/",
+ },
+ {
+ url: "https://www.example.com/",
+ expectedUrl: "https://www.example.com/",
+ },
+ ];
+
+ async function create(url, expectedUrl) {
+ let tabDonePromise = new Promise(resolve => {
+ let changeInfoStatus = false;
+ let changeInfoUrl = false;
+
+ let listener = (tabId, changeInfo) => {
+ if (!tab || tab.id != tabId) {
+ return;
+ }
+ // Looks like "complete" is reached sometimes before the url is done,
+ // so check for both.
+ if (changeInfo.status == "complete") {
+ changeInfoStatus = true;
+ }
+ if (changeInfo.url) {
+ changeInfoUrl = changeInfo.url;
+ }
+
+ if (changeInfoStatus && changeInfoUrl) {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve(changeInfoUrl);
+ }
+ };
+ browser.tabs.onUpdated.addListener(listener);
+ });
+
+ let tab = await browser.tabs.create({ url });
+ for (let otherTab of tabs) {
+ browser.test.assertTrue(
+ tab.id != otherTab.id,
+ "Id of created tab should be unique."
+ );
+ }
+ tabs.push(tab);
+
+ let changeInfoUrl = await tabDonePromise;
+ browser.test.assertEq(
+ expectedUrl,
+ changeInfoUrl,
+ "Should have seen the correct url."
+ );
+ }
+
+ for (let { url, expectedUrl } of tests) {
+ await create(url, expectedUrl);
+ }
+
+ browser.test.notifyPass();
+ },
+ "test.html": "<html><body>I'm a real page!</body></html>",
+ },
+ manifest: {
+ background: { scripts: ["background.js"] },
+ permissions: ["tabs"],
+ },
+ });
+
+ let tabmail = document.getElementById("tabmail");
+ Assert.equal(
+ tabmail.tabInfo.length,
+ 1,
+ "Should find the correct number of tabs before the test."
+ );
+
+ await extension.startup();
+ await extension.awaitFinish();
+ Assert.equal(
+ tabmail.tabInfo.length,
+ 8,
+ "Should find the correct number of tabs after the test."
+ );
+
+ await extension.unload();
+ // After unload, the two extension tabs should be closed.
+ Assert.equal(
+ tabmail.tabInfo.length,
+ 6,
+ "Should find the correct number of tabs after extension unload."
+ );
+
+ for (let i = tabmail.tabInfo.length; i > 0; i--) {
+ let nativeTabInfo = tabmail.tabInfo[i - 1];
+ let uri = nativeTabInfo.browser?.browsingContext.currentURI;
+ if (uri && ["https", "http"].includes(uri.scheme)) {
+ tabmail.closeTab(nativeTabInfo);
+ }
+ }
+ Assert.equal(
+ tabmail.tabInfo.length,
+ 1,
+ "Should find the correct number of tabs after test has finished."
+ );
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js b/comm/mail/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js
new file mode 100644
index 0000000000..48afe44ad7
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js
@@ -0,0 +1,275 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_setup(async function () {
+ // make sure userContext is enabled.
+ return SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+});
+
+add_task(async function () {
+ info("Start testing tabs.create with cookieStoreId");
+
+ let testCases = [
+ {
+ cookieStoreId: null,
+ expectedCookieStoreId: "firefox-default",
+ },
+ {
+ cookieStoreId: "firefox-default",
+ expectedCookieStoreId: "firefox-default",
+ },
+ {
+ cookieStoreId: "firefox-container-1",
+ expectedCookieStoreId: "firefox-container-1",
+ },
+ {
+ cookieStoreId: "firefox-container-2",
+ expectedCookieStoreId: "firefox-container-2",
+ },
+ { cookieStoreId: "firefox-container-42", failure: "exist" },
+ { cookieStoreId: "firefox-private", failure: "defaultToPrivate" },
+ { cookieStoreId: "wow", failure: "illegal" },
+ ];
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+
+ background() {
+ function testTab(data, tab) {
+ browser.test.assertTrue(!data.failure, "we want a success");
+ browser.test.assertTrue(!!tab, "we have a tab");
+ browser.test.assertEq(
+ data.expectedCookieStoreId,
+ tab.cookieStoreId,
+ "tab should have the correct cookieStoreId"
+ );
+ }
+
+ async function runTest(data) {
+ try {
+ // Tab Creation
+ let tab;
+ try {
+ tab = await browser.tabs.create({
+ windowId: this.defaultWindowId,
+ cookieStoreId: data.cookieStoreId,
+ });
+
+ browser.test.assertTrue(!data.failure, "we want a success");
+ } catch (error) {
+ browser.test.assertTrue(!!data.failure, "we want a failure");
+ if (data.failure == "illegal") {
+ browser.test.assertEq(
+ `Illegal cookieStoreId: ${data.cookieStoreId}`,
+ error.message,
+ "runtime.lastError should report the expected error message"
+ );
+ } else if (data.failure == "defaultToPrivate") {
+ browser.test.assertEq(
+ "Illegal to set private cookieStoreId in a non-private window",
+ error.message,
+ "runtime.lastError should report the expected error message"
+ );
+ } else if (data.failure == "privateToDefault") {
+ browser.test.assertEq(
+ "Illegal to set non-private cookieStoreId in a private window",
+ error.message,
+ "runtime.lastError should report the expected error message"
+ );
+ } else if (data.failure == "exist") {
+ browser.test.assertEq(
+ `No cookie store exists with ID ${data.cookieStoreId}`,
+ error.message,
+ "runtime.lastError should report the expected error message"
+ );
+ } else {
+ browser.test.fail("The test is broken");
+ }
+
+ browser.test.sendMessage("test-done");
+ return;
+ }
+
+ // Tests for tab creation
+ testTab(data, tab);
+
+ {
+ // Tests for tab querying
+ let [tab] = await browser.tabs.query({
+ windowId: this.defaultWindowId,
+ cookieStoreId: data.cookieStoreId,
+ });
+
+ browser.test.assertTrue(tab != undefined, "Tab found!");
+ testTab(data, tab);
+ }
+
+ let stores = await browser.cookies.getAllCookieStores();
+
+ let store = stores.find(store => store.id === tab.cookieStoreId);
+ browser.test.assertTrue(!!store, "We have a store for this tab.");
+ browser.test.assertTrue(
+ store.tabIds.includes(tab.id),
+ "tabIds includes this tab."
+ );
+
+ await browser.tabs.remove(tab.id);
+
+ browser.test.sendMessage("test-done");
+ } catch (e) {
+ browser.test.fail("An exception has been thrown");
+ }
+ }
+
+ async function initialize() {
+ let win = await browser.windows.getCurrent();
+ this.defaultWindowId = win.id;
+
+ browser.test.sendMessage("ready");
+ }
+
+ async function shutdown() {
+ browser.test.sendMessage("gone");
+ }
+
+ // Waiting for messages
+ browser.test.onMessage.addListener((msg, data) => {
+ if (msg == "be-ready") {
+ initialize();
+ } else if (msg == "test") {
+ runTest(data);
+ } else {
+ browser.test.assertTrue("finish", msg, "Shutting down");
+ shutdown();
+ }
+ });
+ },
+ });
+
+ await extension.startup();
+
+ info("Tests must be ready...");
+ extension.sendMessage("be-ready");
+ await extension.awaitMessage("ready");
+ info("Tests are ready to run!");
+
+ for (let test of testCases) {
+ info(`test tab.create with cookieStoreId: "${test.cookieStoreId}"`);
+ extension.sendMessage("test", test);
+ await extension.awaitMessage("test-done");
+ }
+
+ info("Waiting for shutting down...");
+ extension.sendMessage("finish");
+ await extension.awaitMessage("gone");
+
+ await extension.unload();
+});
+
+add_task(async function userContext_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", false]],
+ });
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.tabs.create({ cookieStoreId: "firefox-container-1" }),
+ /Contextual identities are currently disabled/,
+ "should refuse to open container tab when contextual identities are disabled"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function tabs_query_cookiestoreid_nocookiepermission() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ let tab = await browser.tabs.create({});
+ browser.test.assertEq(
+ "firefox-default",
+ tab.cookieStoreId,
+ "Expecting cookieStoreId for new tab"
+ );
+ let query = await browser.tabs.query({
+ index: tab.index,
+ cookieStoreId: tab.cookieStoreId,
+ });
+ browser.test.assertEq(
+ "firefox-default",
+ query[0].cookieStoreId,
+ "Expecting cookieStoreId for new tab through browser.tabs.query"
+ );
+ await browser.tabs.remove(tab.id);
+ browser.test.sendMessage("done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function tabs_query_multiple_cookiestoreId() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["cookies"],
+ },
+
+ async background() {
+ let tab1 = await browser.tabs.create({
+ cookieStoreId: "firefox-container-1",
+ });
+ browser.test.log(`Tab created for cookieStoreId:${tab1.cookieStoreId}`);
+
+ let tab2 = await browser.tabs.create({
+ cookieStoreId: "firefox-container-2",
+ });
+ browser.test.log(`Tab created for cookieStoreId:${tab2.cookieStoreId}`);
+
+ let tab3 = await browser.tabs.create({
+ cookieStoreId: "firefox-container-3",
+ });
+ browser.test.log(`Tab created for cookieStoreId:${tab3.cookieStoreId}`);
+
+ let tabs = await browser.tabs.query({
+ cookieStoreId: ["firefox-container-1", "firefox-container-2"],
+ });
+
+ browser.test.assertEq(
+ 2,
+ tabs.length,
+ "Expecting tabs for firefox-container-1 and firefox-container-2"
+ );
+
+ browser.test.assertEq(
+ "firefox-container-1",
+ tabs[0].cookieStoreId,
+ "Expecting tab for firefox-container-1 cookieStoreId"
+ );
+
+ browser.test.assertEq(
+ "firefox-container-2",
+ tabs[1].cookieStoreId,
+ "Expecting tab for firefox-container-2 cookieStoreId"
+ );
+
+ await browser.tabs.remove([tab1.id, tab2.id, tab3.id]);
+ browser.test.sendMessage("test-done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("test-done");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_tabs_events.js b/comm/mail/components/extensions/test/browser/browser_ext_tabs_events.js
new file mode 100644
index 0000000000..fa23482a3a
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_tabs_events.js
@@ -0,0 +1,591 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async () => {
+ let tabmail = document.getElementById("tabmail");
+
+ let account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ rootFolder.createSubfolder("tabsEvents", null);
+ let testFolder = rootFolder.findSubFolder("tabsEvents");
+ createMessages(testFolder, 5);
+ let messages = [...testFolder.messages];
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "page1.html": "<html><body>Page 1</body></html>",
+ "page2.html": "<html><body>Page 2</body></html>",
+ "background.js": async () => {
+ // Executes a command, but first loads a second extension with terminated
+ // background and waits for it to be restarted due to the executed command.
+ async function capturePrimedEvent(eventName, callback) {
+ let eventPageExtensionReadyPromise = window.waitForMessage();
+ browser.test.sendMessage("capturePrimedEvent", eventName);
+ await eventPageExtensionReadyPromise;
+ let eventPageExtensionFinishedPromise = window.waitForMessage();
+ callback();
+ return eventPageExtensionFinishedPromise;
+ }
+
+ let listener = {
+ events: [],
+ currentPromise: null,
+
+ pushEvent(...args) {
+ browser.test.log(JSON.stringify(args));
+ this.events.push(args);
+ if (this.currentPromise) {
+ let p = this.currentPromise;
+ this.currentPromise = null;
+ p.resolve(args);
+ }
+ },
+ onCreated(...args) {
+ this.pushEvent("onCreated", ...args);
+ },
+ onUpdated(...args) {
+ this.pushEvent("onUpdated", ...args);
+ },
+ onActivated(...args) {
+ this.pushEvent("onActivated", ...args);
+ },
+ onRemoved(...args) {
+ this.pushEvent("onRemoved", ...args);
+ },
+ async nextEvent() {
+ if (this.events.length == 0) {
+ return new Promise(
+ resolve => (this.currentPromise = { resolve })
+ );
+ }
+ return Promise.resolve(this.events[0]);
+ },
+ async checkEvent(expectedEvent, ...expectedArgs) {
+ await this.nextEvent();
+ let [actualEvent, ...actualArgs] = this.events.shift();
+ browser.test.assertEq(expectedEvent, actualEvent);
+ browser.test.assertEq(expectedArgs.length, actualArgs.length);
+ for (let i = 0; i < expectedArgs.length; i++) {
+ browser.test.assertEq(
+ typeof expectedArgs[i],
+ typeof actualArgs[i]
+ );
+ if (typeof expectedArgs[i] == "object") {
+ for (let key of Object.keys(expectedArgs[i])) {
+ browser.test.assertEq(
+ expectedArgs[i][key],
+ actualArgs[i][key]
+ );
+ }
+ } else {
+ browser.test.assertEq(expectedArgs[i], actualArgs[i]);
+ }
+ }
+ return actualArgs;
+ },
+ async pageLoad(tab, active = true) {
+ while (true) {
+ // Read the first event without consuming it.
+ let [actualEvent, actualTabId, actualInfo, actualTab] =
+ await this.nextEvent();
+ browser.test.assertEq("onUpdated", actualEvent);
+ browser.test.assertEq(tab, actualTabId);
+
+ if (
+ actualInfo.status == "loading" ||
+ actualTab.url == "about:blank"
+ ) {
+ // We're not interested in these events. Take them off the list.
+ browser.test.log("Skipping this event.");
+ this.events.shift();
+ } else {
+ break;
+ }
+ }
+ await this.checkEvent(
+ "onUpdated",
+ tab,
+ { status: "complete" },
+ {
+ id: tab,
+ windowId: initialWindow,
+ active,
+ mailTab: false,
+ }
+ );
+ },
+ };
+
+ browser.tabs.onCreated.addListener(listener.onCreated.bind(listener));
+ browser.tabs.onUpdated.addListener(listener.onUpdated.bind(listener), {
+ properties: ["status"],
+ });
+ browser.tabs.onActivated.addListener(
+ listener.onActivated.bind(listener)
+ );
+ browser.tabs.onRemoved.addListener(listener.onRemoved.bind(listener));
+
+ browser.test.log(
+ "Collect the ID of the initial tab (there must be only one) and window."
+ );
+
+ let initialTabs = await browser.tabs.query({});
+ browser.test.assertEq(1, initialTabs.length);
+ browser.test.assertEq(0, initialTabs[0].index);
+ browser.test.assertTrue(initialTabs[0].mailTab);
+ browser.test.assertEq("mail", initialTabs[0].type);
+ let [{ id: initialTab, windowId: initialWindow }] = initialTabs;
+
+ browser.test.log("Add a first content tab and wait for it to load.");
+
+ window.assertDeepEqual(
+ [
+ {
+ index: 1,
+ windowId: initialWindow,
+ active: true,
+ mailTab: false,
+ type: "content",
+ },
+ ],
+ await capturePrimedEvent("onCreated", () =>
+ browser.tabs.create({
+ url: browser.runtime.getURL("page1.html"),
+ })
+ )
+ );
+ let [{ id: contentTab1 }] = await listener.checkEvent("onCreated", {
+ index: 1,
+ windowId: initialWindow,
+ active: true,
+ mailTab: false,
+ type: "content",
+ });
+ browser.test.assertTrue(contentTab1 != initialTab);
+ await listener.pageLoad(contentTab1);
+ browser.test.assertEq(
+ "content",
+ (await browser.tabs.get(contentTab1)).type
+ );
+
+ browser.test.log("Add a second content tab and wait for it to load.");
+
+ // The external extension is looking for the onUpdated event, it either be
+ // a loading or completed event. Compare with whatever the local extension
+ // is getting.
+ let locContentTabUpdateInfoPromise = new Promise(resolve => {
+ let listener = (...args) => {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve(args);
+ };
+ browser.tabs.onUpdated.addListener(listener, {
+ properties: ["status"],
+ });
+ });
+ let primedContentTabUpdateInfo = await capturePrimedEvent(
+ "onUpdated",
+ () =>
+ browser.tabs.create({
+ url: browser.runtime.getURL("page2.html"),
+ })
+ );
+ let [{ id: contentTab2 }] = await listener.checkEvent("onCreated", {
+ index: 2,
+ windowId: initialWindow,
+ active: true,
+ mailTab: false,
+ type: "content",
+ });
+ let locContentTabUpdateInfo = await locContentTabUpdateInfoPromise;
+ window.assertDeepEqual(
+ locContentTabUpdateInfo,
+ primedContentTabUpdateInfo,
+ "primed onUpdated event and non-primed onUpdeated event should receive the same values",
+ { strict: true }
+ );
+
+ browser.test.assertTrue(
+ ![initialTab, contentTab1].includes(contentTab2)
+ );
+ await listener.pageLoad(contentTab2);
+ browser.test.assertEq(
+ "content",
+ (await browser.tabs.get(contentTab2)).type
+ );
+
+ browser.test.log("Add the calendar tab.");
+
+ window.assertDeepEqual(
+ [
+ {
+ index: 3,
+ windowId: initialWindow,
+ active: true,
+ mailTab: false,
+ type: "calendar",
+ },
+ ],
+ await capturePrimedEvent("onCreated", () =>
+ browser.test.sendMessage("openCalendarTab")
+ )
+ );
+ let [{ id: calendarTab }] = await listener.checkEvent("onCreated", {
+ index: 3,
+ windowId: initialWindow,
+ active: true,
+ mailTab: false,
+ type: "calendar",
+ });
+ browser.test.assertTrue(
+ ![initialTab, contentTab1, contentTab2].includes(calendarTab)
+ );
+
+ browser.test.log("Add the task tab.");
+
+ window.assertDeepEqual(
+ [
+ {
+ index: 4,
+ windowId: initialWindow,
+ active: true,
+ mailTab: false,
+ type: "tasks",
+ },
+ ],
+ await capturePrimedEvent("onCreated", () =>
+ browser.test.sendMessage("openTaskTab")
+ )
+ );
+ let [{ id: taskTab }] = await listener.checkEvent("onCreated", {
+ index: 4,
+ windowId: initialWindow,
+ active: true,
+ mailTab: false,
+ type: "tasks",
+ });
+ browser.test.assertTrue(
+ ![initialTab, contentTab1, contentTab2, calendarTab].includes(taskTab)
+ );
+
+ browser.test.log("Open a folder in a tab.");
+
+ window.assertDeepEqual(
+ [
+ {
+ index: 5,
+ windowId: initialWindow,
+ active: true,
+ mailTab: true,
+ type: "mail",
+ },
+ ],
+ await capturePrimedEvent("onCreated", () =>
+ browser.test.sendMessage("openFolderTab")
+ )
+ );
+ let [{ id: folderTab }] = await listener.checkEvent("onCreated", {
+ index: 5,
+ windowId: initialWindow,
+ active: true,
+ mailTab: true,
+ type: "mail",
+ });
+ browser.test.assertTrue(
+ ![
+ initialTab,
+ contentTab1,
+ contentTab2,
+ calendarTab,
+ taskTab,
+ ].includes(folderTab)
+ );
+
+ browser.test.log("Open a first message in a tab.");
+
+ window.assertDeepEqual(
+ [
+ {
+ index: 6,
+ windowId: initialWindow,
+ active: true,
+ mailTab: false,
+ type: "messageDisplay",
+ },
+ ],
+ await capturePrimedEvent("onCreated", () =>
+ browser.test.sendMessage("openMessageTab", false)
+ )
+ );
+
+ let [{ id: messageTab1 }] = await listener.checkEvent("onCreated", {
+ index: 6,
+ windowId: initialWindow,
+ active: true,
+ mailTab: false,
+ type: "messageDisplay",
+ });
+ browser.test.assertTrue(
+ ![
+ initialTab,
+ contentTab1,
+ contentTab2,
+ calendarTab,
+ taskTab,
+ folderTab,
+ ].includes(messageTab1)
+ );
+ await listener.pageLoad(messageTab1);
+
+ browser.test.log(
+ "Open a second message in a tab. In the background, just because."
+ );
+
+ window.assertDeepEqual(
+ [
+ {
+ index: 7,
+ windowId: initialWindow,
+ active: false,
+ mailTab: false,
+ type: "messageDisplay",
+ },
+ ],
+ await capturePrimedEvent("onCreated", () =>
+ browser.test.sendMessage("openMessageTab", true)
+ )
+ );
+ let [{ id: messageTab2 }] = await listener.checkEvent("onCreated", {
+ index: 7,
+ windowId: initialWindow,
+ active: false,
+ mailTab: false,
+ type: "messageDisplay",
+ });
+ browser.test.assertTrue(
+ ![
+ initialTab,
+ contentTab1,
+ contentTab2,
+ calendarTab,
+ taskTab,
+ folderTab,
+ messageTab1,
+ ].includes(messageTab2)
+ );
+ await listener.pageLoad(messageTab2, false);
+
+ browser.test.log(
+ "Activate each of the tabs in a somewhat random order to test the onActivated event."
+ );
+
+ let previousTabId = messageTab1;
+ for (let tab of [
+ initialTab,
+ calendarTab,
+ messageTab1,
+ taskTab,
+ contentTab1,
+ messageTab2,
+ folderTab,
+ contentTab2,
+ ]) {
+ window.assertDeepEqual(
+ [{ tabId: tab, windowId: initialWindow }],
+ await capturePrimedEvent("onActivated", () =>
+ browser.tabs.update(tab, { active: true })
+ )
+ );
+ await listener.checkEvent("onActivated", {
+ tabId: tab,
+ previousTabId,
+ windowId: initialWindow,
+ });
+ previousTabId = tab;
+ }
+
+ browser.test.log(
+ "Remove the first content tab. This was not active so no new tab should be activated."
+ );
+
+ window.assertDeepEqual(
+ [contentTab1, { windowId: initialWindow, isWindowClosing: false }],
+ await capturePrimedEvent("onRemoved", () =>
+ browser.tabs.remove(contentTab1)
+ )
+ );
+ await listener.checkEvent("onRemoved", contentTab1, {
+ windowId: initialWindow,
+ isWindowClosing: false,
+ });
+
+ browser.test.log(
+ "Remove the second content tab. This was active, and the calendar tab is after it, so that should be activated."
+ );
+
+ window.assertDeepEqual(
+ [contentTab2, { windowId: initialWindow, isWindowClosing: false }],
+ await capturePrimedEvent("onRemoved", () =>
+ browser.tabs.remove(contentTab2)
+ )
+ );
+ await listener.checkEvent("onRemoved", contentTab2, {
+ windowId: initialWindow,
+ isWindowClosing: false,
+ });
+ await listener.checkEvent("onActivated", {
+ tabId: calendarTab,
+ windowId: initialWindow,
+ });
+
+ browser.test.log("Remove the remaining tabs.");
+
+ for (let tab of [
+ taskTab,
+ messageTab1,
+ messageTab2,
+ folderTab,
+ calendarTab,
+ ]) {
+ window.assertDeepEqual(
+ [tab, { windowId: initialWindow, isWindowClosing: false }],
+ await capturePrimedEvent("onRemoved", () =>
+ browser.tabs.remove(tab)
+ )
+ );
+ await listener.checkEvent("onRemoved", tab, {
+ windowId: initialWindow,
+ isWindowClosing: false,
+ });
+ }
+
+ // Since the last tab was activated because all other tabs have been
+ // removed, previousTabId should be undefined.
+ await listener.checkEvent("onActivated", {
+ tabId: initialTab,
+ windowId: initialWindow,
+ previousTabId: undefined,
+ });
+
+ browser.test.assertEq(0, listener.events.length);
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["tabs"],
+ },
+ });
+
+ // Function to start an event page extension (MV3), which can be called whenever
+ // the main test is about to trigger an event. The extension terminates its
+ // background and listens for that single event, verifying it is waking up correctly.
+ async function event_page_extension(eventName, actionCallback) {
+ let ext = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, hasFired is set to false. In
+ // case of a wake-up, the first fired event is the one that woke up the background.
+ let hasFired = false;
+ let eventName = browser.runtime.getManifest().description;
+
+ if (["onCreated", "onActivated", "onRemoved"].includes(eventName)) {
+ browser.tabs[eventName].addListener(async (...args) => {
+ // Only send the first event after background wake-up, this should
+ // be the only one expected.
+ if (!hasFired) {
+ hasFired = true;
+ browser.test.sendMessage(`${eventName} received`, args);
+ }
+ });
+ }
+
+ if (eventName == "onUpdated") {
+ browser.tabs.onUpdated.addListener(
+ (...args) => {
+ // Only send the first event after background wake-up, this should
+ // be the only one expected.
+ if (!hasFired) {
+ hasFired = true;
+ browser.test.sendMessage("onUpdated received", args);
+ }
+ },
+ {
+ properties: ["status"],
+ }
+ );
+ }
+
+ browser.test.sendMessage("background started");
+ },
+ },
+ manifest: {
+ manifest_version: 3,
+ description: eventName,
+ background: { scripts: ["background.js"] },
+ permissions: ["tabs"],
+ browser_specific_settings: {
+ gecko: { id: `tabs.eventpage.${eventName}@mochi.test` },
+ },
+ },
+ });
+ await ext.startup();
+ await ext.awaitMessage("background started");
+ // The listener should be persistent, but not primed.
+ assertPersistentListeners(ext, "tabs", eventName, { primed: false });
+
+ await ext.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listener.
+ assertPersistentListeners(ext, "tabs", eventName, { primed: true });
+
+ await actionCallback();
+ let rv = await ext.awaitMessage(`${eventName} received`);
+ await ext.awaitMessage("background started");
+ // The listener should be persistent, but not primed.
+ assertPersistentListeners(ext, "tabs", eventName, { primed: false });
+
+ await ext.unload();
+ return rv;
+ }
+
+ extension.onMessage("openCalendarTab", () => {
+ let calendarTabButton = document.getElementById("calendarButton");
+ EventUtils.synthesizeMouseAtCenter(calendarTabButton, {
+ clickCount: 1,
+ });
+ });
+
+ extension.onMessage("openTaskTab", () => {
+ let taskTabButton = document.getElementById("tasksButton");
+ EventUtils.synthesizeMouseAtCenter(taskTabButton, { clickCount: 1 });
+ });
+
+ extension.onMessage("openFolderTab", () => {
+ tabmail.openTab("mail3PaneTab", {
+ folderURI: rootFolder.URI,
+ background: false,
+ });
+ });
+
+ extension.onMessage("openMessageTab", background => {
+ let msgHdr = messages.shift();
+ tabmail.openTab("mailMessageTab", {
+ messageURI: testFolder.getUriForMsg(msgHdr),
+ background,
+ });
+ });
+
+ extension.onMessage("capturePrimedEvent", async eventName => {
+ let primedEventData = await event_page_extension(eventName, () => {
+ // Resume execution in the main test, after the event page extension is
+ // ready to capture the event with deactivated background.
+ extension.sendMessage();
+ });
+ extension.sendMessage(...primedEventData);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_tabs_move.js b/comm/mail/components/extensions/test/browser/browser_ext_tabs_move.js
new file mode 100644
index 0000000000..9a249c62cb
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_tabs_move.js
@@ -0,0 +1,306 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_setup(async () => {
+ let account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ rootFolder.createSubfolder("testFolder", null);
+ await createMessages(rootFolder.getChildNamed("testFolder"), 5);
+});
+
+add_task(async function test_tabs_move() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ // Works as intended only if tabs are created one after the other.
+ async function createTab(url) {
+ let createdTab;
+ let loadPromise = new Promise(resolve => {
+ let urlSeen = false;
+ let listener = (tabId, changeInfo) => {
+ if (changeInfo.url && changeInfo.url == url) {
+ urlSeen = true;
+ }
+ if (changeInfo.status == "complete" && urlSeen) {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ };
+ browser.tabs.onUpdated.addListener(listener);
+ });
+ createdTab = await browser.tabs.create({ url });
+ await loadPromise;
+ return createdTab;
+ }
+
+ // Works as intended only if windows are created one after the other.
+ async function createWindow({ url, type }) {
+ let createdWindow;
+ let loadPromise = new Promise(resolve => {
+ if (!url) {
+ resolve();
+ } else {
+ let urlSeen = false;
+ let listener = async (tabId, changeInfo) => {
+ if (changeInfo.url && changeInfo.url == url) {
+ urlSeen = true;
+ }
+ if (changeInfo.status == "complete" && urlSeen) {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ };
+ browser.tabs.onUpdated.addListener(listener);
+ }
+ });
+ createdWindow = await browser.windows.create({ type, url });
+ await loadPromise;
+ return createdWindow;
+ }
+
+ let mailWindow = await browser.windows.getCurrent();
+
+ let tab1 = await createTab(browser.runtime.getURL("test1.html"));
+ let tab2 = await createTab(browser.runtime.getURL("test2.html"));
+ let tab3 = await createTab(browser.runtime.getURL("test3.html"));
+ let tab4 = await createTab(browser.runtime.getURL("test4.html"));
+
+ let tabs = await browser.tabs.query({ windowId: mailWindow.id });
+ browser.test.assertEq(5, tabs.length, "Number of tabs is correct");
+ browser.test.assertEq(
+ tab1.id,
+ tabs[1].id,
+ "Id of tab at index 1 should be that of tab1"
+ );
+ browser.test.assertEq(
+ tab2.id,
+ tabs[2].id,
+ "Id of tab at index 2 should be that of tab2"
+ );
+ browser.test.assertEq(
+ tab3.id,
+ tabs[3].id,
+ "Id of tab at index 3 should be that of tab3"
+ );
+ browser.test.assertEq(
+ tab4.id,
+ tabs[4].id,
+ "Id of tab at index 4 should be that of tab4"
+ );
+ browser.test.assertEq(1, tabs[1].index, "Index of tab1 is correct");
+ browser.test.assertEq(2, tabs[2].index, "Index of tab2 is correct");
+ browser.test.assertEq(3, tabs[3].index, "Index of tab3 is correct");
+ browser.test.assertEq(4, tabs[4].index, "Index of tab4 is correct");
+
+ // Move two tabs to the end of the current window.
+ await browser.tabs.move([tab2.id, tab1.id], { index: -1 });
+
+ tabs = await browser.tabs.query({ windowId: mailWindow.id });
+ browser.test.assertEq(
+ 5,
+ tabs.length,
+ "Number of tabs after move #1 is correct"
+ );
+ browser.test.assertEq(
+ tab3.id,
+ tabs[1].id,
+ "Id of tab at index 1 should be that of tab3 after move #1"
+ );
+ browser.test.assertEq(
+ tab4.id,
+ tabs[2].id,
+ "Id of tab at index 2 should be that of tab4 after move #1"
+ );
+ browser.test.assertEq(
+ tab2.id,
+ tabs[3].id,
+ "Id of tab at index 3 should be that of tab2 after move #1"
+ );
+ browser.test.assertEq(
+ tab1.id,
+ tabs[4].id,
+ "Id of tab at index 4 should be that of tab1 after move #1"
+ );
+ browser.test.assertEq(
+ 1,
+ tabs[1].index,
+ "Index of tab3 after move #1 is correct"
+ );
+ browser.test.assertEq(
+ 2,
+ tabs[2].index,
+ "Index of tab4 after move #1 is correct"
+ );
+ browser.test.assertEq(
+ 3,
+ tabs[3].index,
+ "Index of tab2 after move #1 is correct"
+ );
+ browser.test.assertEq(
+ 4,
+ tabs[4].index,
+ "Index of tab1 after move #1 is correct"
+ );
+
+ // Move a single tab to a specific location in current window.
+ await browser.tabs.move(tab3.id, { index: 3 });
+
+ tabs = await browser.tabs.query({ windowId: mailWindow.id });
+ browser.test.assertEq(
+ 5,
+ tabs.length,
+ "Number of tabs after move #2 is correct"
+ );
+ browser.test.assertEq(
+ tab4.id,
+ tabs[1].id,
+ "Id of tab at index 1 should be that of tab4 after move #2"
+ );
+ browser.test.assertEq(
+ tab3.id,
+ tabs[2].id,
+ "Id of tab at index 2 should be that of tab3 after move #2"
+ );
+ browser.test.assertEq(
+ tab2.id,
+ tabs[3].id,
+ "Id of tab at index 3 should be that of tab2 after move #2"
+ );
+ browser.test.assertEq(
+ tab1.id,
+ tabs[4].id,
+ "Id of tab at index 4 should be that of tab1 after move #2"
+ );
+ browser.test.assertEq(
+ 1,
+ tabs[1].index,
+ "Index of tab4 after move #2 is correct"
+ );
+ browser.test.assertEq(
+ 2,
+ tabs[2].index,
+ "Index of tab3 after move #2 is correct"
+ );
+ browser.test.assertEq(
+ 3,
+ tabs[3].index,
+ "Index of tab2 after move #2 is correct"
+ );
+ browser.test.assertEq(
+ 4,
+ tabs[4].index,
+ "Index of tab1 after move #2 is correct"
+ );
+
+ // Moving tabs to a popup should fail.
+ let popupWindow = await createWindow({
+ url: browser.runtime.getURL("test1.html"),
+ type: "popup",
+ });
+ await browser.test.assertRejects(
+ browser.tabs.move([tab3.id, tabs[4].id], {
+ windowId: popupWindow.id,
+ index: -1,
+ }),
+ `Window with ID ${popupWindow.id} is not a normal window`,
+ "Moving tabs to a popup window should fail."
+ );
+
+ // Moving a tab from a popup should fail.
+ let [popupTab] = await browser.tabs.query({ windowId: popupWindow.id });
+ await browser.test.assertRejects(
+ browser.tabs.move(popupTab.id, {
+ windowId: mailWindow.id,
+ index: -1,
+ }),
+ `Tab with ID ${popupTab.id} does not belong to a normal window`,
+ "Moving tabs from a popup window should fail."
+ );
+
+ // Moving a tab to an invalid window should fail.
+ await browser.test.assertRejects(
+ browser.tabs.move(popupTab.id, { windowId: 1234, index: -1 }),
+ `Invalid window ID: 1234`,
+ "Moving tabs to an invalid window should fail."
+ );
+
+ // Move tab between windows.
+ let secondMailWindow = await createWindow({ type: "normal" });
+ let [movedTab] = await browser.tabs.move(tab3.id, {
+ windowId: secondMailWindow.id,
+ index: -1,
+ });
+
+ tabs = await browser.tabs.query({ windowId: mailWindow.id });
+ browser.test.assertEq(
+ 4,
+ tabs.length,
+ "Number of tabs after move #3 is correct"
+ );
+ browser.test.assertEq(
+ tab4.id,
+ tabs[1].id,
+ "Id of tab at index 1 should be that of tab4 after move #3"
+ );
+ browser.test.assertEq(
+ tab2.id,
+ tabs[2].id,
+ "Id of tab at index 2 should be that of tab2 after move #3"
+ );
+ browser.test.assertEq(
+ tab1.id,
+ tabs[3].id,
+ "Id of tab at index 3 should be that of tab1 after move #3"
+ );
+ browser.test.assertEq(
+ 1,
+ tabs[1].index,
+ "Index of tab4 after move #3 is correct"
+ );
+ browser.test.assertEq(
+ 2,
+ tabs[2].index,
+ "Index of tab2 after move #3 is correct"
+ );
+ browser.test.assertEq(
+ 3,
+ tabs[3].index,
+ "Index of tab1 after move #3 is correct"
+ );
+
+ tabs = await browser.tabs.query({ windowId: secondMailWindow.id });
+ browser.test.assertEq(
+ 2,
+ tabs.length,
+ "Number of tabs in the second normal window after move #3 is correct"
+ );
+ browser.test.assertEq(
+ movedTab.id,
+ tabs[1].id,
+ "Id of tab at index 1 of the second normal window should be that of the moved tab"
+ );
+
+ await browser.tabs.remove(tab1.id);
+ await browser.tabs.remove(tab2.id);
+ await browser.tabs.remove(tab4.id);
+ await browser.windows.remove(popupWindow.id);
+ await browser.windows.remove(secondMailWindow.id);
+
+ browser.test.notifyPass();
+ },
+ "test1.html": "<html><body>I'm page #1!</body></html>",
+ "test2.html": "<html><body>I'm page #2!</body></html>",
+ "test3.html": "<html><body>I'm page #3!</body></html>",
+ "test4.html": "<html><body>I'm page #4!</body></html>",
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_tabs_onCreated_bug1817872.js b/comm/mail/components/extensions/test/browser/browser_ext_tabs_onCreated_bug1817872.js
new file mode 100644
index 0000000000..515c7695bf
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_tabs_onCreated_bug1817872.js
@@ -0,0 +1,226 @@
+var gAccount;
+var gMessages;
+var gFolder;
+
+add_setup(() => {
+ gAccount = createAccount();
+ addIdentity(gAccount);
+ let rootFolder = gAccount.incomingServer.rootFolder;
+ rootFolder.createSubfolder("test0", null);
+
+ let subFolders = {};
+ for (let folder of rootFolder.subFolders) {
+ subFolders[folder.name] = folder;
+ }
+ createMessages(subFolders.test0, 5);
+
+ gFolder = subFolders.test0;
+ gMessages = [...subFolders.test0.messages];
+});
+
+async function getTestExtension() {
+ let files = {
+ "background.js": async () => {
+ let [location] = await window.waitForMessage();
+
+ let [mailTab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ browser.test.assertEq(
+ "mail",
+ mailTab.type,
+ "Should have found a mail tab."
+ );
+
+ // Get displayed message.
+ let message1 = await browser.messageDisplay.getDisplayedMessage(
+ mailTab.id
+ );
+ browser.test.assertTrue(
+ !!message1,
+ "We should have a displayed message."
+ );
+
+ // Open message in a new tab, wait for onCreated and for onUpdated.
+ let messageTab = await new Promise(resolve => {
+ let createListener = tab => {
+ browser.tabs.onCreated.removeListener(createListener);
+ browser.test.assertEq(
+ "loading",
+ tab.status,
+ "The tab is expected to be still loading."
+ );
+ browser.tabs.onUpdated.addListener(updateListener, {
+ tabId: tab.id,
+ });
+ };
+ let updateListener = (tabId, changeInfo, tab) => {
+ if (changeInfo.status) {
+ browser.test.assertEq(
+ tab.status,
+ changeInfo.status,
+ "We should see the same status in tab and in changeInfo."
+ );
+ if (changeInfo.status == "complete") {
+ browser.tabs.onUpdated.removeListener(updateListener);
+ resolve(tab);
+ }
+ }
+ };
+ browser.tabs.onCreated.addListener(createListener);
+ browser.messageDisplay.open({
+ location,
+ messageId: message1.id,
+ });
+ });
+
+ // We should now be able to get the message.
+ let message2 = await browser.messageDisplay.getDisplayedMessage(
+ messageTab.id
+ );
+ browser.test.assertTrue(
+ !!message2,
+ "We should have a displayed message."
+ );
+ browser.test.assertTrue(
+ message1.id == message2?.id,
+ "We should see the same message."
+ );
+
+ // We should be able to get the message later as well.
+ await new Promise(resolve => window.setTimeout(resolve));
+ let message3 = await browser.messageDisplay.getDisplayedMessage(
+ messageTab.id
+ );
+ browser.test.assertTrue(
+ !!message3,
+ "We should have a displayed message."
+ );
+ browser.test.assertTrue(
+ message1.id == message3?.id,
+ "We should see the same message."
+ );
+
+ browser.tabs.remove(messageTab.id);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ return ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead", "tabs"],
+ },
+ });
+}
+
+/**
+ * Open a message tab and check its status, wait till loaded and get the message.
+ */
+add_task(async function test_onCreated_message_tab() {
+ let extension = await getTestExtension();
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(gFolder);
+ about3Pane.threadTree.selectedIndex = 0;
+
+ await extension.startup();
+ extension.sendMessage("tab");
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+/**
+ * Open a message window and check its status, wait till loaded and get the message.
+ */
+add_task(async function test_onCreated_message_window() {
+ let extension = await getTestExtension();
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(gFolder);
+ about3Pane.threadTree.selectedIndex = 0;
+
+ await extension.startup();
+ extension.sendMessage("window");
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+/**
+ * Open an address book tab and check its status.
+ */
+add_task(async function test_onCreated_addressBook_tab() {
+ let files = {
+ "background.js": async () => {
+ let [mailTab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ browser.test.assertEq(
+ "mail",
+ mailTab.type,
+ "Should have found a mail tab."
+ );
+
+ // Open ab tab, wait for onCreated and for onUpdated.
+ let abTab = await new Promise(resolve => {
+ let createListener = tab => {
+ browser.test.assertEq(
+ "loading",
+ tab.status,
+ "The tab is expected to be still loading."
+ );
+ browser.tabs.onUpdated.addListener(updateListener, {
+ tabId: tab.id,
+ });
+ };
+ let updateListener = (tabId, changeInfo, tab) => {
+ if (changeInfo.status) {
+ browser.test.assertEq(
+ tab.status,
+ changeInfo.status,
+ "We should see the same status in tab and in changeInfo."
+ );
+ if (changeInfo.status == "complete") {
+ browser.tabs.onUpdated.removeListener(updateListener);
+ resolve(tab);
+ }
+ }
+ };
+ browser.tabs.onCreated.addListener(createListener);
+ browser.addressBooks.openUI();
+ });
+ browser.test.assertEq(
+ "addressBook",
+ abTab.type,
+ "We should find an addressBook tab."
+ );
+ browser.tabs.remove(abTab.id);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["addressBooks"],
+ },
+ });
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(gFolder);
+ about3Pane.threadTree.selectedIndex = 0;
+
+ await extension.startup();
+ extension.sendMessage("window");
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_tabs_query.js b/comm/mail/components/extensions/test/browser/browser_ext_tabs_query.js
new file mode 100644
index 0000000000..6cecde63c7
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_tabs_query.js
@@ -0,0 +1,113 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async function testQuery() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ // There should be a single mailtab at startup.
+ let tabs = await browser.tabs.query({});
+
+ browser.test.assertEq(1, tabs.length, "Found one tab at startup");
+ browser.test.assertEq("mail", tabs[0].type, "Tab is mail tab");
+ let mailTab = tabs[0];
+
+ // Create a content tab.
+ let contentTab = await browser.tabs.create({ url: "test.html" });
+ browser.test.assertTrue(
+ contentTab.id != mailTab.id,
+ "Id of content tab is different from mail tab"
+ );
+
+ // Query spaces.
+ let spaces = await browser.spaces.query({ id: mailTab.spaceId });
+ browser.test.assertEq(1, spaces.length, "Found one matching space");
+ browser.test.assertEq(
+ "mail",
+ spaces[0].name,
+ "Space is the mail space"
+ );
+
+ // Query for all tabs.
+ tabs = await browser.tabs.query({});
+ browser.test.assertEq(2, tabs.length, "Found two tabs");
+
+ // Query for the content tab.
+ tabs = await browser.tabs.query({ type: "content" });
+ browser.test.assertEq(1, tabs.length, "Found one content tab");
+ browser.test.assertEq(
+ contentTab.id,
+ tabs[0].id,
+ "Id of content tab is correct"
+ );
+
+ // Query for the mail tab using spaceId.
+ tabs = await browser.tabs.query({ spaceId: mailTab.spaceId });
+ browser.test.assertEq(1, tabs.length, "Found one mail tab");
+ browser.test.assertEq(
+ mailTab.id,
+ tabs[0].id,
+ "Id of mail tab is correct"
+ );
+
+ // Query for the mail tab using type.
+ tabs = await browser.tabs.query({ type: "mail" });
+ browser.test.assertEq(1, tabs.length, "Found one mail tab");
+ browser.test.assertEq(
+ mailTab.id,
+ tabs[0].id,
+ "Id of mail tab is correct"
+ );
+
+ // Query for the mail tab using mailTab.
+ tabs = await browser.tabs.query({ mailTab: true });
+ browser.test.assertEq(1, tabs.length, "Found one mail tab");
+ browser.test.assertEq(
+ mailTab.id,
+ tabs[0].id,
+ "Id of mail tab is correct"
+ );
+
+ // Query for the content tab but also using mailTab.
+ tabs = await browser.tabs.query({ mailTab: true, type: "content" });
+ browser.test.assertEq(1, tabs.length, "Found one mail tab");
+ browser.test.assertEq(
+ mailTab.id,
+ tabs[0].id,
+ "Id of mail tab is correct"
+ );
+
+ // Query for active tab.
+ tabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(1, tabs.length, "Found one mail tab");
+ browser.test.assertEq(
+ contentTab.id,
+ tabs[0].id,
+ "Id of mail tab is correct"
+ );
+
+ // Query for highlighted tab.
+ tabs = await browser.tabs.query({ highlighted: true });
+ browser.test.assertEq(1, tabs.length, "Found one mail tab");
+ browser.test.assertEq(
+ contentTab.id,
+ tabs[0].id,
+ "Id of mail tab is correct"
+ );
+
+ await browser.tabs.remove(contentTab.id);
+ browser.test.notifyPass();
+ },
+ "test.html": "<html><body>I'm a real page!</body></html>",
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_tabs_update_reload.js b/comm/mail/components/extensions/test/browser/browser_ext_tabs_update_reload.js
new file mode 100644
index 0000000000..acd3bce0a7
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_tabs_update_reload.js
@@ -0,0 +1,578 @@
+/* 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 { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+/** @implements {nsIExternalProtocolService} */
+let mockExternalProtocolService = {
+ _loadedURLs: [],
+ externalProtocolHandlerExists(protocolScheme) {},
+ getApplicationDescription(scheme) {},
+ getProtocolHandlerInfo(protocolScheme) {
+ return {
+ possibleApplicationHandlers: Cc["@mozilla.org/array;1"].createInstance(
+ Ci.nsIMutableArray
+ ),
+ };
+ },
+ getProtocolHandlerInfoFromOS(protocolScheme, found) {},
+ isExposedProtocol(protocolScheme) {},
+ loadURI(uri, windowContext) {
+ this._loadedURLs.push(uri.spec);
+ },
+ setProtocolHandlerDefaults(handlerInfo, osHandlerExists) {},
+ QueryInterface: ChromeUtils.generateQI(["nsIExternalProtocolService"]),
+};
+
+var gAccount;
+var gMessages;
+var gFolder;
+
+add_setup(() => {
+ gAccount = createAccount();
+ addIdentity(gAccount);
+ let rootFolder = gAccount.incomingServer.rootFolder;
+ rootFolder.createSubfolder("test0", null);
+
+ let subFolders = {};
+ for (let folder of rootFolder.subFolders) {
+ subFolders[folder.name] = folder;
+ }
+ createMessages(subFolders.test0, 5);
+
+ gFolder = subFolders.test0;
+ gMessages = [...subFolders.test0.messages];
+});
+
+/**
+ * Update registered WebExtension protocol handler pages.
+ */
+add_task(async function testUpdateTabs_WebExtProtocolHandler() {
+ let files = {
+ "background.js": async () => {
+ // Test a mail tab.
+
+ let [mailTab] = await browser.mailTabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ browser.test.assertTrue(!!mailTab, "Should have found a mail tab.");
+
+ // Load a message.
+ let { messages } = await browser.messages.list(mailTab.displayedFolder);
+ await browser.mailTabs.setSelectedMessages(mailTab.id, [messages[0].id]);
+ let message1 = await browser.messageDisplay.getDisplayedMessage(
+ mailTab.id
+ );
+ browser.test.assertTrue(
+ !!message1,
+ "We should have a displayed message."
+ );
+ mailTab = await browser.tabs.get(mailTab.id);
+ browser.test.assertTrue(
+ mailTab.url.startsWith("mailbox:"),
+ "A message should be loaded"
+ );
+
+ // Update to a registered WebExtension protocol handler.
+ await new Promise(resolve => {
+ let urlSeen = false;
+ let updateListener = (tabId, changeInfo, tab) => {
+ if (
+ changeInfo.url &&
+ changeInfo.url.endsWith("handler.html#ext%2Btest%3A1234-1")
+ ) {
+ urlSeen = true;
+ }
+ if (urlSeen && changeInfo.status == "complete") {
+ resolve();
+ }
+ };
+ browser.tabs.onUpdated.addListener(updateListener);
+ browser.tabs.update(mailTab.id, { url: "ext+test:1234-1" });
+ });
+
+ mailTab = await browser.tabs.get(mailTab.id);
+ browser.test.assertTrue(
+ mailTab.url.endsWith("handler.html#ext%2Btest%3A1234-1"),
+ "Should have found the correct protocol handler url loaded"
+ );
+
+ // Test a message tab.
+
+ let messageTab = await browser.messageDisplay.open({
+ location: "tab",
+ messageId: message1.id,
+ });
+ browser.test.assertEq(
+ "messageDisplay",
+ messageTab.type,
+ "Should have found a message tab."
+ );
+ browser.test.assertTrue(
+ mailTab.windowId == messageTab.windowId,
+ "Tab should be in the main window."
+ );
+
+ // Updating a message tab to a registered WebExtension protocol handler
+ // should throw.
+ browser.test.assertRejects(
+ browser.tabs.update(messageTab.id, { url: "ext+test:1234-1" }),
+ /Loading a registered WebExtension protocol handler url is only supported for content tabs and mail tabs./,
+ "Updating a message tab to a registered WebExtension protocol handler should throw"
+ );
+ browser.tabs.remove(messageTab.id);
+
+ // Test a message window.
+
+ let messageWindowTab = await browser.messageDisplay.open({
+ location: "window",
+ messageId: message1.id,
+ });
+ browser.test.assertEq(
+ "messageDisplay",
+ messageWindowTab.type,
+ "Should have found a message tab."
+ );
+ browser.test.assertFalse(
+ mailTab.windowId == messageWindowTab.windowId,
+ "Tab should not be in the main window."
+ );
+
+ // Updating a message window to a registered WebExtension protocol handler
+ // should throw.
+ browser.test.assertRejects(
+ browser.tabs.update(messageWindowTab.id, { url: "ext+test:1234-1" }),
+ /Loading a registered WebExtension protocol handler url is only supported for content tabs and mail tabs./,
+ "Updating a message tab to a registered WebExtension protocol handler should throw"
+ );
+
+ browser.tabs.remove(messageWindowTab.id);
+
+ // Test a compose window.
+
+ let details1 = { to: ["Mr. Holmes <holmes@bakerstreet.invalid>"] };
+ let composeTab = await browser.compose.beginNew(details1);
+ browser.test.assertEq(
+ "messageCompose",
+ composeTab.type,
+ "Should have found a compose tab."
+ );
+ browser.test.assertFalse(
+ mailTab.windowId == composeTab.windowId,
+ "Tab should not be in the main window."
+ );
+ let details2 = await browser.compose.getComposeDetails(composeTab.id);
+ window.assertDeepEqual(
+ details1.to,
+ details2.to,
+ "We should see the correct compose details."
+ );
+
+ // Updating a message window to a registered WebExtension protocol handler
+ // should throw.
+ browser.test.assertRejects(
+ browser.tabs.update(composeTab.id, { url: "ext+test:1234-1" }),
+ /Loading a registered WebExtension protocol handler url is only supported for content tabs and mail tabs./,
+ "Updating a message tab to a registered WebExtension protocol handler should throw"
+ );
+
+ browser.tabs.remove(composeTab.id);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ "handler.html": "<html><body><p>Test Protocol Handler</p></body></html>",
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead", "tabs", "compose"],
+ protocol_handlers: [
+ {
+ protocol: "ext+test",
+ name: "Protocol Handler Example",
+ uriTemplate: "/handler.html#%s",
+ },
+ ],
+ },
+ });
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(gFolder);
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+/**
+ * Reload and update tabs and check if it fails for forbidden cases, keep track
+ * of urls opened externally.
+ */
+add_task(async function testUpdateReloadTabs() {
+ let mockExternalProtocolServiceCID = MockRegistrar.register(
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ mockExternalProtocolService
+ );
+ registerCleanupFunction(() => {
+ MockRegistrar.unregister(mockExternalProtocolServiceCID);
+ });
+
+ let files = {
+ "background.js": async () => {
+ // Test a mail tab.
+
+ let [mailTab] = await browser.mailTabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ browser.test.assertTrue(!!mailTab, "Should have found a mail tab.");
+
+ // Load a URL.
+ await new Promise(resolve => {
+ let urlSeen = false;
+ let updateListener = (tabId, changeInfo, tab) => {
+ if (changeInfo.url == "https://www.example.com/") {
+ urlSeen = true;
+ }
+ if (urlSeen && changeInfo.status == "complete") {
+ resolve();
+ }
+ };
+ browser.tabs.onUpdated.addListener(updateListener);
+ browser.tabs.update(mailTab.id, { url: "https://www.example.com/" });
+ });
+
+ browser.test.assertEq(
+ "https://www.example.com/",
+ (await browser.tabs.get(mailTab.id)).url,
+ "Should have found the correct url loaded"
+ );
+
+ // This should not throw.
+ await browser.tabs.reload(mailTab.id);
+
+ // Update a tel:// url.
+ await browser.tabs.update(mailTab.id, { url: "tel:1234-1" });
+ await window.sendMessage("check_external_loaded_url", "tel:1234-1");
+
+ // We should still have the same url displayed.
+ browser.test.assertEq(
+ "https://www.example.com/",
+ (await browser.tabs.get(mailTab.id)).url,
+ "Should have found the correct url loaded"
+ );
+
+ // Load a message.
+ let { messages } = await browser.messages.list(mailTab.displayedFolder);
+ await browser.mailTabs.setSelectedMessages(mailTab.id, [messages[1].id]);
+ let message1 = await browser.messageDisplay.getDisplayedMessage(
+ mailTab.id
+ );
+ browser.test.assertTrue(
+ !!message1,
+ "We should have a displayed message."
+ );
+ mailTab = await browser.tabs.get(mailTab.id);
+ browser.test.assertFalse(
+ "https://www.example.com/" == mailTab.url,
+ "Webpage should no longer be loaded"
+ );
+
+ // Reload should now fail.
+ browser.test.assertRejects(
+ browser.tabs.reload(mailTab.id),
+ /Reloading is only supported for tabs displaying a content page/,
+ "Reloading a mail tab not displaying a content page should throw"
+ );
+
+ // We should still see the same message.
+ let message2 = await browser.messageDisplay.getDisplayedMessage(
+ mailTab.id
+ );
+ browser.test.assertTrue(
+ !!message2,
+ "We should have a displayed message."
+ );
+ browser.test.assertTrue(
+ message1.id == message2.id,
+ "We should see the same message."
+ );
+
+ // Update a tel:// url.
+ await browser.tabs.update(mailTab.id, { url: "tel:1234-2" });
+ await window.sendMessage("check_external_loaded_url", "tel:1234-2");
+
+ // We should still see the same message.
+ message2 = await browser.messageDisplay.getDisplayedMessage(mailTab.id);
+ browser.test.assertTrue(
+ !!message2,
+ "We should have a displayed message."
+ );
+ browser.test.assertTrue(
+ message1.id == message2.id,
+ "We should see the same message."
+ );
+
+ // Update a non-registered WebExtension protocol handler.
+ await browser.tabs.update(mailTab.id, { url: "ext+test:1234-1" });
+ await window.sendMessage("check_external_loaded_url", "ext+test:1234-1");
+
+ // We should still see the same message.
+ message2 = await browser.messageDisplay.getDisplayedMessage(mailTab.id);
+ browser.test.assertTrue(
+ !!message2,
+ "We should have a displayed message."
+ );
+ browser.test.assertTrue(
+ message1.id == message2.id,
+ "We should see the same message."
+ );
+
+ // Test a message tab.
+
+ let messageTab = await browser.messageDisplay.open({
+ location: "tab",
+ messageId: message1.id,
+ });
+ browser.test.assertEq(
+ "messageDisplay",
+ messageTab.type,
+ "Should have found a message tab."
+ );
+ browser.test.assertTrue(
+ mailTab.windowId == messageTab.windowId,
+ "Tab should be in the main window."
+ );
+
+ browser.test.assertRejects(
+ browser.tabs.reload(messageTab.id),
+ /Reloading is only supported for tabs displaying a content page/,
+ "Reloading a message tab should throw"
+ );
+
+ // We should still see the same message.
+ message2 = await browser.messageDisplay.getDisplayedMessage(
+ messageTab.id
+ );
+ browser.test.assertTrue(
+ !!message2,
+ "We should have a displayed message."
+ );
+ browser.test.assertTrue(
+ message1.id == message2.id,
+ "We should see the same message."
+ );
+
+ // Update a tel:// url.
+ await browser.tabs.update(messageTab.id, { url: "tel:1234-3" });
+ await window.sendMessage("check_external_loaded_url", "tel:1234-3");
+
+ // We should still see the same message.
+ message2 = await browser.messageDisplay.getDisplayedMessage(
+ messageTab.id
+ );
+ browser.test.assertTrue(
+ !!message2,
+ "We should have a displayed message."
+ );
+ browser.test.assertTrue(
+ message1.id == message2.id,
+ "We should see the same message."
+ );
+
+ // Update a non-registered WebExtension protocol handler.
+ await browser.tabs.update(mailTab.id, { url: "ext+test:1234-2" });
+ await window.sendMessage("check_external_loaded_url", "ext+test:1234-2");
+
+ // We should still see the same message.
+ message2 = await browser.messageDisplay.getDisplayedMessage(
+ messageTab.id
+ );
+ browser.test.assertTrue(
+ !!message2,
+ "We should have a displayed message."
+ );
+ browser.test.assertTrue(
+ message1.id == message2.id,
+ "We should see the same message."
+ );
+
+ browser.tabs.remove(messageTab.id);
+
+ // Test a message window.
+
+ let messageWindowTab = await browser.messageDisplay.open({
+ location: "window",
+ messageId: message1.id,
+ });
+ browser.test.assertEq(
+ "messageDisplay",
+ messageWindowTab.type,
+ "Should have found a message tab."
+ );
+ browser.test.assertFalse(
+ mailTab.windowId == messageWindowTab.windowId,
+ "Tab should not be in the main window."
+ );
+
+ browser.test.assertRejects(
+ browser.tabs.reload(messageWindowTab.id),
+ /Reloading is only supported for tabs displaying a content page/,
+ "Reloading a message window should throw"
+ );
+
+ // We should still see the same message.
+ message2 = await browser.messageDisplay.getDisplayedMessage(
+ messageWindowTab.id
+ );
+ browser.test.assertTrue(
+ !!message2,
+ "We should have a displayed message."
+ );
+ browser.test.assertTrue(
+ message1.id == message2.id,
+ "We should see the same message."
+ );
+
+ // Update a tel:// url.
+ await browser.tabs.update(messageWindowTab.id, { url: "tel:1234-4" });
+ await window.sendMessage("check_external_loaded_url", "tel:1234-4");
+
+ // We should still see the same message.
+ message2 = await browser.messageDisplay.getDisplayedMessage(
+ messageWindowTab.id
+ );
+ browser.test.assertTrue(
+ !!message2,
+ "We should have a displayed message."
+ );
+ browser.test.assertTrue(
+ message1.id == message2.id,
+ "We should see the same message."
+ );
+
+ // Update a non-registered WebExtension protocol handler.
+ await browser.tabs.update(mailTab.id, { url: "ext+test:1234-3" });
+ await window.sendMessage("check_external_loaded_url", "ext+test:1234-3");
+
+ // We should still see the same message.
+ message2 = await browser.messageDisplay.getDisplayedMessage(
+ messageWindowTab.id
+ );
+ browser.test.assertTrue(
+ !!message2,
+ "We should have a displayed message."
+ );
+ browser.test.assertTrue(
+ message1.id == message2.id,
+ "We should see the same message."
+ );
+
+ browser.tabs.remove(messageWindowTab.id);
+
+ // Test a compose window.
+
+ let details1 = { to: ["Mr. Holmes <holmes@bakerstreet.invalid>"] };
+ let composeTab = await browser.compose.beginNew(details1);
+ browser.test.assertEq(
+ "messageCompose",
+ composeTab.type,
+ "Should have found a compose tab."
+ );
+ browser.test.assertFalse(
+ mailTab.windowId == composeTab.windowId,
+ "Tab should not be in the main window."
+ );
+ let details2 = await browser.compose.getComposeDetails(composeTab.id);
+ window.assertDeepEqual(
+ details1.to,
+ details2.to,
+ "We should see the correct compose details."
+ );
+
+ browser.test.assertRejects(
+ browser.tabs.reload(composeTab.id),
+ /Reloading is only supported for tabs displaying a content page/,
+ "Reloading a compose window should throw"
+ );
+
+ // We should still see the same composer.
+ details2 = await browser.compose.getComposeDetails(composeTab.id);
+ window.assertDeepEqual(
+ details1.to,
+ details2.to,
+ "We should see the correct compose details."
+ );
+
+ // Update a tel:// url.
+ await browser.tabs.update(composeTab.id, { url: "tel:1234-5" });
+ await window.sendMessage("check_external_loaded_url", "tel:1234-5");
+
+ // We should still see the same composer.
+ details2 = await browser.compose.getComposeDetails(composeTab.id);
+ window.assertDeepEqual(
+ details1.to,
+ details2.to,
+ "We should see the correct compose details."
+ );
+
+ // Update a non-registered WebExtension protocol handler.
+ await browser.tabs.update(mailTab.id, { url: "ext+test:1234-4" });
+ await window.sendMessage("check_external_loaded_url", "ext+test:1234-4");
+
+ // We should still see the same composer.
+ details2 = await browser.compose.getComposeDetails(composeTab.id);
+ window.assertDeepEqual(
+ details1.to,
+ details2.to,
+ "We should see the correct compose details."
+ );
+
+ browser.tabs.remove(composeTab.id);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead", "tabs", "compose"],
+ },
+ });
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(gFolder);
+
+ extension.onMessage("check_external_loaded_url", async expected => {
+ Assert.equal(
+ 1,
+ mockExternalProtocolService._loadedURLs.length,
+ "Should have found a single loaded url"
+ );
+ Assert.equal(
+ mockExternalProtocolService._loadedURLs[0],
+ expected,
+ "Should have found the expected url"
+ );
+ mockExternalProtocolService._loadedURLs = [];
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ Assert.equal(
+ 0,
+ mockExternalProtocolService._loadedURLs.length,
+ "Should not have any unexpected urls loaded externally"
+ );
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_themes_onUpdated.js b/comm/mail/components/extensions/test/browser/browser_ext_themes_onUpdated.js
new file mode 100644
index 0000000000..b2e6a40fb1
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_themes_onUpdated.js
@@ -0,0 +1,150 @@
+"use strict";
+
+// This test checks whether browser.theme.onUpdated works
+// when a static theme is applied
+
+const ACCENT_COLOR = "#a14040";
+const TEXT_COLOR = "#fac96e";
+const BACKGROUND =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0" +
+ "DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+
+add_task(async function test_on_updated() {
+ const theme = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.theme.onUpdated.addListener(updateInfo => {
+ browser.test.sendMessage("theme-updated", updateInfo);
+ });
+ },
+ });
+
+ await extension.startup();
+
+ info("Testing update event on static theme startup");
+ let updatedPromise = extension.awaitMessage("theme-updated");
+ await theme.startup();
+ const { theme: receivedTheme, windowId } = await updatedPromise;
+ Assert.ok(!windowId, "No window id in static theme update event");
+ Assert.ok(
+ receivedTheme.images.theme_frame.includes("image1.png"),
+ "Theme theme_frame image should be applied"
+ );
+ Assert.equal(
+ receivedTheme.colors.frame,
+ ACCENT_COLOR,
+ "Theme frame color should be applied"
+ );
+ Assert.equal(
+ receivedTheme.colors.tab_background_text,
+ TEXT_COLOR,
+ "Theme tab_background_text color should be applied"
+ );
+
+ info("Testing update event on static theme unload");
+ updatedPromise = extension.awaitMessage("theme-updated");
+ await theme.unload();
+ const updateInfo = await updatedPromise;
+ Assert.ok(!windowId, "No window id in static theme update event on unload");
+ Assert.equal(
+ Object.keys(updateInfo.theme),
+ 0,
+ "unloading theme sends empty theme in update event"
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_on_updated_eventpage() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.eventPages.enabled", true]],
+ });
+ const theme = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ const extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": () => {
+ // Whenever the extension starts or wakes up, the eventCounter is reset
+ // and allows to observe the order of events fired. In case of a wake-up,
+ // the first observed event is the one that woke up the background.
+ let eventCounter = 0;
+
+ browser.theme.onUpdated.addListener(async updateInfo => {
+ browser.test.sendMessage("theme-updated", {
+ eventCount: ++eventCounter,
+ ...updateInfo,
+ });
+ });
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ manifest_version: 3,
+ background: { scripts: ["utils.js", "background.js"] },
+ browser_specific_settings: { gecko: { id: "themes@mochi.test" } },
+ },
+ });
+
+ await extension.startup();
+ assertPersistentListeners(extension, "theme", "onUpdated", {
+ primed: false,
+ });
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ assertPersistentListeners(extension, "theme", "onUpdated", {
+ primed: true,
+ });
+
+ info("Testing update event on static theme startup");
+
+ await theme.startup();
+
+ const {
+ eventCount,
+ theme: receivedTheme,
+ windowId,
+ } = await extension.awaitMessage("theme-updated");
+ Assert.equal(eventCount, 1, "Event counter should be correct");
+ Assert.ok(!windowId, "No window id in static theme update event");
+ Assert.ok(
+ receivedTheme.images.theme_frame.includes("image1.png"),
+ "Theme theme_frame image should be applied"
+ );
+
+ await theme.unload();
+ await extension.awaitMessage("theme-updated");
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_tooltip_in_extension_pages.js b/comm/mail/components/extensions/test/browser/browser_ext_tooltip_in_extension_pages.js
new file mode 100644
index 0000000000..483482981e
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_tooltip_in_extension_pages.js
@@ -0,0 +1,685 @@
+/* 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 account;
+let subFolders;
+let messages;
+
+async function showTooltip(elementSelector, tooltip, browser, description) {
+ Assert.ok(!!tooltip, "tooltip element should exist");
+ tooltip.ownerGlobal.windowUtils.disableNonTestMouseEvents(true);
+ try {
+ while (tooltip.state != "open") {
+ // We first have to click on the element, otherwise a mousemove event will not
+ // trigger the tooltip.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => window.setTimeout(resolve, 125));
+ await synthesizeMouseAtCenterAndRetry(
+ elementSelector,
+ { button: 1 },
+ browser
+ );
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => window.setTimeout(resolve, 125));
+ await synthesizeMouseAtCenterAndRetry(
+ elementSelector,
+ { type: "mousemove" },
+ browser
+ );
+
+ try {
+ await TestUtils.waitForCondition(
+ () => tooltip.state == "open",
+ `Tooltip should have been shown for ${description}`
+ );
+ } catch (e) {
+ console.log(`Tooltip was not shown for ${description}, trying again.`);
+ }
+ }
+ } finally {
+ tooltip.ownerGlobal.windowUtils.disableNonTestMouseEvents(false);
+ }
+}
+
+add_setup(async () => {
+ account = createAccount();
+ addIdentity(account);
+ let rootFolder = account.incomingServer.rootFolder;
+ subFolders = rootFolder.subFolders;
+ createMessages(subFolders[0], 10);
+ await TestUtils.waitForCondition(
+ () => subFolders[0].messages.hasMoreElements(),
+ "Messages should be added to folder"
+ );
+ messages = subFolders[0].messages;
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.restoreState({
+ folderPaneVisible: true,
+ folderURI: subFolders[0],
+ messagePaneVisible: true,
+ });
+ about3Pane.threadTree.selectedIndex = 0;
+ await awaitBrowserLoaded(
+ about3Pane.messageBrowser.contentWindow.getMessagePaneBrowser()
+ );
+});
+
+add_task(async function test_browserAction_in_about3pane() {
+ let files = {
+ "background.js": async () => {
+ async function checkTooltip() {
+ // Trigger the tooltip and wait for the status.
+ let [state] = await window.sendMessage("check tooltip");
+ browser.test.assertEq("open", state, "Should find the tooltip open");
+ browser.test.notifyPass("finished");
+ }
+
+ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ if (message == "page loaded") {
+ sendResponse();
+ checkTooltip();
+ }
+ });
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => window.setTimeout(resolve, 125));
+ browser.browserAction.openPopup();
+ },
+ "page.js": async function () {
+ browser.runtime.sendMessage("page loaded");
+ },
+ "page.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <title>Page</title>
+ </head>
+ <body>
+ <h1>Tooltip test</h1>
+ <p title="Tooltip">I am an element with a tooltip</p>
+ <script src="page.js"></script>
+ </body>
+ </html>`,
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ browser_action: {
+ default_title: "default",
+ default_popup: "page.html",
+ },
+ },
+ });
+
+ extension.onMessage("check tooltip", async () => {
+ let popupBrowser = document.querySelector(".webextension-popup-browser");
+ let tooltip = document.getElementById("remoteBrowserTooltip");
+ await showTooltip(
+ "p",
+ tooltip,
+ popupBrowser,
+ "browserAction in about3pane"
+ );
+ extension.sendMessage(tooltip.state);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_browserAction_in_message_window() {
+ let files = {
+ "background.js": async () => {
+ async function checkTooltip() {
+ // Trigger the tooltip and wait for the status.
+ let [state] = await window.sendMessage("check tooltip");
+ browser.test.assertEq("open", state, "Should find the tooltip open");
+
+ // Close the message window.
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.windows.remove(tab.windowId);
+ browser.test.notifyPass("finished");
+ }
+
+ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ if (message == "page loaded") {
+ sendResponse();
+ checkTooltip();
+ }
+ });
+
+ // Open the popup after a message has been displayed.
+ browser.messageDisplay.onMessageDisplayed.addListener(
+ async (tab, message) => {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => window.setTimeout(resolve, 125));
+ browser.browserAction.openPopup({ windowId: tab.windowId });
+ }
+ );
+
+ // Open a message in a window.
+ let { messages } = await browser.messages.query({});
+ browser.messageDisplay.open({
+ location: "window",
+ messageId: messages[0].id,
+ });
+ },
+ "page.js": async function () {
+ browser.runtime.sendMessage("page loaded");
+ },
+ "page.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <title>Page</title>
+ </head>
+ <body>
+ <h1>Tooltip test</h1>
+ <p title="Tooltip">I am an element with a tooltip</p>
+ <script src="page.js"></script>
+ </body>
+ </html>`,
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["messagesRead", "accountsRead"],
+ browser_action: {
+ default_title: "default",
+ default_popup: "page.html",
+ default_windows: ["messageDisplay"],
+ },
+ },
+ });
+
+ extension.onMessage("check tooltip", async () => {
+ let messageWindow = Services.wm.getMostRecentWindow("mail:messageWindow");
+ let popupBrowser = messageWindow.document.querySelector(
+ ".webextension-popup-browser"
+ );
+ let tooltip = messageWindow.document.getElementById("remoteBrowserTooltip");
+ await showTooltip(
+ "p",
+ tooltip,
+ popupBrowser,
+ "browserAction in message window"
+ );
+ extension.sendMessage(tooltip.state);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_composeAction() {
+ let files = {
+ "background.js": async () => {
+ async function checkTooltip() {
+ // Trigger the tooltip and wait for the status.
+ let [state] = await window.sendMessage("check tooltip");
+ browser.test.assertEq("open", state, "Should find the tooltip open");
+
+ // Close the compose window.
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.windows.remove(tab.windowId);
+ browser.test.notifyPass("finished");
+ }
+
+ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ if (message == "page loaded") {
+ sendResponse();
+ checkTooltip();
+ }
+ });
+
+ let composeTab = await browser.compose.beginNew();
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => window.setTimeout(resolve, 125));
+ browser.composeAction.openPopup({ windowId: composeTab.windowId });
+ },
+ "page.js": async function () {
+ browser.runtime.sendMessage("page loaded");
+ },
+ "page.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <title>Page</title>
+ </head>
+ <body>
+ <h1>Tooltip test</h1>
+ <p title="Tooltip">I am an element with a tooltip</p>
+ <script src="page.js"></script>
+ </body>
+ </html>`,
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["compose"],
+ compose_action: {
+ default_title: "default",
+ default_popup: "page.html",
+ },
+ },
+ });
+
+ extension.onMessage("check tooltip", async () => {
+ let composeWindow = Services.wm.getMostRecentWindow("msgcompose");
+ let popupBrowser = composeWindow.document.querySelector(
+ ".webextension-popup-browser"
+ );
+ let tooltip = composeWindow.document.getElementById("remoteBrowserTooltip");
+ await showTooltip(
+ "p",
+ tooltip,
+ popupBrowser,
+ "composeAction in compose window"
+ );
+ extension.sendMessage(tooltip.state);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_messageDisplayAction_in_about3pane() {
+ let files = {
+ "background.js": async () => {
+ async function checkTooltip() {
+ // Trigger the tooltip and wait for the status.
+ let [state] = await window.sendMessage("check tooltip");
+ browser.test.assertEq("open", state, "Should find the tooltip open");
+ browser.test.notifyPass("finished");
+ }
+
+ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ if (message == "page loaded") {
+ sendResponse();
+ checkTooltip();
+ }
+ });
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => window.setTimeout(resolve, 125));
+ browser.messageDisplayAction.openPopup();
+ },
+ "page.js": async function () {
+ browser.runtime.sendMessage("page loaded");
+ },
+ "page.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <title>Page</title>
+ </head>
+ <body>
+ <h1>Tooltip test</h1>
+ <p title="Tooltip">I am an element with a tooltip</p>
+ <script src="page.js"></script>
+ </body>
+ </html>`,
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["messagesRead", "accountsRead"],
+ message_display_action: {
+ default_title: "default",
+ default_popup: "page.html",
+ },
+ },
+ });
+
+ extension.onMessage("check tooltip", async () => {
+ // The tooltip and the popup panel are defined in the top level messenger
+ // window, not in about:message.
+ let popupBrowser = document.querySelector(".webextension-popup-browser");
+ let tooltip = document.getElementById("remoteBrowserTooltip");
+ await showTooltip(
+ "p",
+ tooltip,
+ popupBrowser,
+ "messageDisplayAction in about3pane"
+ );
+ extension.sendMessage(tooltip.state);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_messageDisplayAction_in_message_tab() {
+ let files = {
+ "background.js": async () => {
+ async function checkTooltip() {
+ // Trigger the tooltip and wait for the status.
+ let [state] = await window.sendMessage("check tooltip");
+ browser.test.assertEq("open", state, "Should find the tooltip open");
+
+ // Close the message tab.
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.tabs.remove(tab.id);
+ browser.test.notifyPass("finished");
+ }
+
+ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ if (message == "page loaded") {
+ sendResponse();
+ checkTooltip();
+ }
+ });
+
+ // Open the popup after a message has been displayed.
+ browser.messageDisplay.onMessageDisplayed.addListener(
+ async (tab, message) => {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => window.setTimeout(resolve, 125));
+ browser.messageDisplayAction.openPopup({ windowId: tab.windowId });
+ }
+ );
+
+ // Open a message in a tab.
+ let { messages } = await browser.messages.query({});
+ browser.messageDisplay.open({
+ location: "tab",
+ messageId: messages[0].id,
+ });
+ },
+ "page.js": async function () {
+ browser.runtime.sendMessage("page loaded");
+ },
+ "page.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <title>Page</title>
+ </head>
+ <body>
+ <h1>Tooltip test</h1>
+ <p title="Tooltip">I am an element with a tooltip</p>
+ <script src="page.js"></script>
+ </body>
+ </html>`,
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["messagesRead", "accountsRead"],
+ message_display_action: {
+ default_title: "default",
+ default_popup: "page.html",
+ },
+ },
+ });
+
+ extension.onMessage("check tooltip", async () => {
+ // The tooltip and the popup panel are defined in the top level messenger
+ // window, not in about:message.
+ let popupBrowser = document.querySelector(".webextension-popup-browser");
+ let tooltip = document.getElementById("remoteBrowserTooltip");
+ await showTooltip(
+ "p",
+ tooltip,
+ popupBrowser,
+ "messageDisplayAction in message tab"
+ );
+ extension.sendMessage(tooltip.state);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_messageDisplayAction_in_message_window() {
+ let files = {
+ "background.js": async () => {
+ async function checkTooltip() {
+ // Trigger the tooltip and wait for the status.
+ let [state] = await window.sendMessage("check tooltip");
+ browser.test.assertEq("open", state, "Should find the tooltip open");
+
+ // Close the message window.
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.windows.remove(tab.windowId);
+ browser.test.notifyPass("finished");
+ }
+
+ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ if (message == "page loaded") {
+ sendResponse();
+ checkTooltip();
+ }
+ });
+
+ // Open the popup after a message has been displayed.
+ browser.messageDisplay.onMessageDisplayed.addListener(
+ async (tab, message) => {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => window.setTimeout(resolve, 125));
+ browser.messageDisplayAction.openPopup({ windowId: tab.windowId });
+ }
+ );
+
+ // Open a message in a window.
+ let { messages } = await browser.messages.query({});
+ browser.messageDisplay.open({
+ location: "window",
+ messageId: messages[0].id,
+ });
+ },
+ "page.js": async function () {
+ browser.runtime.sendMessage("page loaded");
+ },
+ "page.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <title>Page</title>
+ </head>
+ <body>
+ <h1>Tooltip test</h1>
+ <p title="Tooltip">I am an element with a tooltip</p>
+ <script src="page.js"></script>
+ </body>
+ </html>`,
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["messagesRead", "accountsRead"],
+ message_display_action: {
+ default_title: "default",
+ default_popup: "page.html",
+ },
+ },
+ });
+
+ extension.onMessage("check tooltip", async () => {
+ let messageWindow = Services.wm.getMostRecentWindow("mail:messageWindow");
+ let popupBrowser = messageWindow.document.querySelector(
+ ".webextension-popup-browser"
+ );
+ let tooltip = messageWindow.document.getElementById("remoteBrowserTooltip");
+ await showTooltip(
+ "p",
+ tooltip,
+ popupBrowser,
+ "messageDisplayAction in message window"
+ );
+ extension.sendMessage(tooltip.state);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_extension_window() {
+ let files = {
+ "background.js": async () => {
+ async function checkTooltip() {
+ // Trigger the tooltip and wait for the status.
+ let [state] = await window.sendMessage("check tooltip");
+ browser.test.assertEq("open", state, "Should find the tooltip open");
+
+ // Close the extension window.
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.windows.remove(tab.windowId);
+
+ browser.test.notifyPass("finished");
+ }
+
+ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ if (message == "page loaded") {
+ sendResponse();
+ checkTooltip();
+ }
+ });
+
+ // Open an extension window.
+ browser.windows.create({ type: "popup", url: "page.html" });
+ },
+ "page.js": async function () {
+ browser.runtime.sendMessage("page loaded");
+ },
+ "page.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <title>Page</title>
+ </head>
+ <body>
+ <h1>Tooltip test</h1>
+ <p title="Tooltip">I am an element with a tooltip</p>
+ <script src="page.js"></script>
+ </body>
+ </html>`,
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ },
+ });
+
+ extension.onMessage("check tooltip", async () => {
+ let extensionWindow = Services.wm.getMostRecentWindow(
+ "mail:extensionPopup"
+ );
+ let tooltip = extensionWindow.document.getElementById(
+ "remoteBrowserTooltip"
+ );
+ await showTooltip(
+ "p",
+ tooltip,
+ extensionWindow.browser,
+ "extension window"
+ );
+ extension.sendMessage(tooltip.state);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_extension_tab() {
+ let files = {
+ "background.js": async () => {
+ async function checkTooltip() {
+ // Trigger the tooltip and wait for the status.
+ let [state] = await window.sendMessage("check tooltip");
+ browser.test.assertEq("open", state, "Should find the tooltip open");
+
+ // Close the extension tab.
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.tabs.remove(tab.id);
+
+ browser.test.notifyPass("finished");
+ }
+
+ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ if (message == "page loaded") {
+ sendResponse();
+ checkTooltip();
+ }
+ });
+
+ // Open an extension tab.
+ browser.tabs.create({ url: "page.html" });
+ },
+ "page.js": async function () {
+ browser.runtime.sendMessage("page loaded");
+ },
+ "page.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <title>Page</title>
+ </head>
+ <body>
+ <h1>Tooltip test</h1>
+ <p title="Tooltip">I am an element with a tooltip</p>
+ <script src="page.js"></script>
+ </body>
+ </html>`,
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ },
+ });
+
+ extension.onMessage("check tooltip", async () => {
+ let tooltip = window.document.getElementById("remoteBrowserTooltip");
+ let browser = window.gTabmail.currentTabInfo.browser;
+ await showTooltip("p", tooltip, browser, "extension tab");
+ extension.sendMessage(tooltip.state);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_windows.js b/comm/mail/components/extensions/test/browser/browser_ext_windows.js
new file mode 100644
index 0000000000..6c996a8ca5
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_windows.js
@@ -0,0 +1,439 @@
+/* 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 { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+/** @implements {nsIExternalProtocolService} */
+let mockExternalProtocolService = {
+ _loadedURLs: [],
+ externalProtocolHandlerExists(protocolScheme) {},
+ getApplicationDescription(scheme) {},
+ getProtocolHandlerInfo(protocolScheme) {},
+ getProtocolHandlerInfoFromOS(protocolScheme, found) {},
+ isExposedProtocol(protocolScheme) {},
+ loadURI(uri, windowContext) {
+ this._loadedURLs.push(uri.spec);
+ },
+ setProtocolHandlerDefaults(handlerInfo, osHandlerExists) {},
+ urlLoaded(url) {
+ let found = this._loadedURLs.includes(url);
+ this._loadedURLs = this._loadedURLs.filter(e => e != url);
+ return found;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIExternalProtocolService"]),
+};
+
+let mockExternalProtocolServiceCID = MockRegistrar.register(
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ mockExternalProtocolService
+);
+
+registerCleanupFunction(() => {
+ MockRegistrar.unregister(mockExternalProtocolServiceCID);
+});
+
+add_task(async function test_openDefaultBrowser() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ const urls = {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://www.google.de/": true,
+ "https://www.google.de/": true,
+ "ftp://www.google.de/": false,
+ };
+
+ for (let [url, expected] of Object.entries(urls)) {
+ let rv = null;
+ try {
+ await browser.windows.openDefaultBrowser(url);
+ rv = true;
+ } catch (e) {
+ rv = false;
+ }
+ browser.test.assertEq(
+ rv,
+ expected,
+ `Checking result for browser.windows.openDefaultBrowser(${url})`
+ );
+ }
+ browser.test.sendMessage("ready", urls);
+ },
+ });
+
+ await extension.startup();
+ let urls = await extension.awaitMessage("ready");
+ for (let [url, expected] of Object.entries(urls)) {
+ Assert.equal(
+ mockExternalProtocolService.urlLoaded(url),
+ expected,
+ `Double check result for browser.windows.openDefaultBrowser(${url})`
+ );
+ }
+
+ await extension.unload();
+});
+
+add_task(async function test_focusWindows() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ let listener = {
+ waitingPromises: [],
+ waitForEvent() {
+ return new Promise(resolve => {
+ listener.waitingPromises.push(resolve);
+ });
+ },
+ checkWaiting() {
+ if (listener.waitingPromises.length < 1) {
+ browser.test.fail("Unexpected event fired");
+ }
+ },
+ created(win) {
+ listener.checkWaiting();
+ listener.waitingPromises.shift()(["onCreated", win]);
+ },
+ focusChanged(windowId) {
+ listener.checkWaiting();
+ listener.waitingPromises.shift()(["onFocusChanged", windowId]);
+ },
+ removed(windowId) {
+ listener.checkWaiting();
+ listener.waitingPromises.shift()(["onRemoved", windowId]);
+ },
+ };
+ browser.windows.onCreated.addListener(listener.created);
+ browser.windows.onFocusChanged.addListener(listener.focusChanged);
+ browser.windows.onRemoved.addListener(listener.removed);
+
+ let firstWindow = await browser.windows.getCurrent();
+ browser.test.assertEq("normal", firstWindow.type);
+
+ let currentWindows = await browser.windows.getAll();
+ browser.test.assertEq(1, currentWindows.length);
+ browser.test.assertEq(firstWindow.id, currentWindows[0].id);
+
+ // Open a new mail window.
+
+ let createdWindowPromise = listener.waitForEvent();
+ let focusChangedPromise1 = listener.waitForEvent();
+ let focusChangedPromise2 = listener.waitForEvent();
+ let eventName, createdWindow, windowId;
+
+ browser.test.sendMessage("openWindow");
+ [eventName, createdWindow] = await createdWindowPromise;
+ browser.test.assertEq("onCreated", eventName);
+ browser.test.assertEq("normal", createdWindow.type);
+
+ [eventName, windowId] = await focusChangedPromise1;
+ browser.test.assertEq("onFocusChanged", eventName);
+ browser.test.assertEq(browser.windows.WINDOW_ID_NONE, windowId);
+
+ [eventName, windowId] = await focusChangedPromise2;
+ browser.test.assertEq("onFocusChanged", eventName);
+ browser.test.assertEq(createdWindow.id, windowId);
+
+ currentWindows = await browser.windows.getAll();
+ browser.test.assertEq(2, currentWindows.length);
+ browser.test.assertEq(firstWindow.id, currentWindows[0].id);
+ browser.test.assertEq(createdWindow.id, currentWindows[1].id);
+
+ // Focus the first window.
+
+ let platformInfo = await browser.runtime.getPlatformInfo();
+
+ let focusChangedPromise3;
+ if (["mac", "win"].includes(platformInfo.os)) {
+ // Mac and Windows don't fire this event. Pretend they do.
+ focusChangedPromise3 = Promise.resolve([
+ "onFocusChanged",
+ browser.windows.WINDOW_ID_NONE,
+ ]);
+ } else {
+ focusChangedPromise3 = listener.waitForEvent();
+ }
+ let focusChangedPromise4 = listener.waitForEvent();
+
+ browser.test.sendMessage("switchWindows");
+ [eventName, windowId] = await focusChangedPromise3;
+ browser.test.assertEq("onFocusChanged", eventName);
+ browser.test.assertEq(browser.windows.WINDOW_ID_NONE, windowId);
+
+ [eventName, windowId] = await focusChangedPromise4;
+ browser.test.assertEq("onFocusChanged", eventName);
+ browser.test.assertEq(firstWindow.id, windowId);
+
+ // Close the first window.
+
+ let removedWindowPromise = listener.waitForEvent();
+
+ browser.test.sendMessage("closeWindow");
+ [eventName, windowId] = await removedWindowPromise;
+ browser.test.assertEq("onRemoved", eventName);
+ browser.test.assertEq(createdWindow.id, windowId);
+
+ currentWindows = await browser.windows.getAll();
+ browser.test.assertEq(1, currentWindows.length);
+ browser.test.assertEq(firstWindow.id, currentWindows[0].id);
+
+ browser.windows.onCreated.removeListener(listener.created);
+ browser.windows.onFocusChanged.removeListener(listener.focusChanged);
+ browser.windows.onRemoved.removeListener(listener.removed);
+
+ browser.test.notifyPass();
+ },
+ });
+
+ let account = createAccount();
+
+ await extension.startup();
+
+ await extension.awaitMessage("openWindow");
+ let newWindowPromise = BrowserTestUtils.domWindowOpened();
+ window.MsgOpenNewWindowForFolder(account.incomingServer.rootFolder.URI);
+ let newWindow = await newWindowPromise;
+
+ await extension.awaitMessage("switchWindows");
+ window.focus();
+
+ await extension.awaitMessage("closeWindow");
+ newWindow.close();
+
+ await extension.awaitFinish();
+ await extension.unload();
+});
+
+add_task(async function checkTitlePreface() {
+ let l10n = new Localization([
+ "branding/brand.ftl",
+ "messenger/extensions/popup.ftl",
+ ]);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "content.html": `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>A test document</title>
+ <script type="text/javascript" src="content.js"></script>
+ </head>
+ <body>
+ <p>This is text.</p>
+ </body>
+ </html>
+ `,
+ "content.js": `
+ browser.runtime.onMessage.addListener(
+ (data, sender) => {
+ if (data.command == "close") {
+ window.close();
+ }
+ }
+ );`,
+ "utils.js": await getUtilsJS(),
+ "background.js": async () => {
+ let popup;
+
+ // Test titlePreface during window creation.
+ {
+ let titlePreface = "PREFACE1";
+ let windowCreatePromise = window.waitForEvent("windows.onCreated");
+ // Do not await the create statement, but instead check if the onCreated
+ // event is delayed correctly to get the correct values.
+ browser.windows.create({
+ titlePreface,
+ url: "content.html",
+ type: "popup",
+ allowScriptsToClose: true,
+ });
+ popup = (await windowCreatePromise)[0];
+ let [expectedTitle] = await window.sendMessage(
+ "checkTitle",
+ titlePreface
+ );
+ browser.test.assertEq(
+ expectedTitle,
+ popup.title,
+ `Should find the correct title`
+ );
+ browser.test.assertEq(
+ true,
+ popup.focused,
+ `Should find the correct focus state`
+ );
+ }
+
+ // Test titlePreface during window update.
+ {
+ let titlePreface = "PREFACE2";
+ let updated = await browser.windows.update(popup.id, {
+ titlePreface,
+ });
+ let [expectedTitle] = await window.sendMessage(
+ "checkTitle",
+ titlePreface
+ );
+ browser.test.assertEq(
+ expectedTitle,
+ updated.title,
+ `Should find the correct title`
+ );
+ browser.test.assertEq(
+ true,
+ updated.focused,
+ `Should find the correct focus state`
+ );
+ }
+
+ // Finish
+ {
+ let windowRemovePromise = window.waitForEvent("windows.onRemoved");
+ browser.test.log(
+ "Testing allowScriptsToClose, waiting for window to close."
+ );
+ await browser.runtime.sendMessage({ command: "close" });
+ await windowRemovePromise;
+ }
+
+ // Test title after create without a preface.
+ {
+ let popup = await browser.windows.create({
+ url: "content.html",
+ type: "popup",
+ allowScriptsToClose: true,
+ });
+ let [expectedTitle] = await window.sendMessage("checkTitle", "");
+ browser.test.assertEq(
+ expectedTitle,
+ popup.title,
+ `Should find the correct title`
+ );
+ browser.test.assertEq(
+ true,
+ popup.focused,
+ `Should find the correct focus state`
+ );
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ },
+ });
+
+ extension.onMessage("checkTitle", async titlePreface => {
+ let win = Services.wm.getMostRecentWindow("mail:extensionPopup");
+
+ let defaultTitle = await l10n.formatValue("extension-popup-default-title");
+
+ let expectedTitle = titlePreface + "A test document";
+ // If we're on Mac, we don't display the separator and the app name (which
+ // is also used as default title).
+ if (AppConstants.platform != "macosx") {
+ expectedTitle += ` - ${defaultTitle}`;
+ }
+
+ Assert.equal(
+ win.document.title,
+ expectedTitle,
+ `Check if title is as expected.`
+ );
+ extension.sendMessage(expectedTitle);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_popupLayoutProperties() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "test.html": `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <title>TEST</title>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ </head>
+ <body>
+ <p>Test body</p>
+ </body>
+ </html>`,
+ "background.js": async () => {
+ async function checkWindow(windowId, expected, retries = 0) {
+ let win = await browser.windows.get(windowId);
+
+ if (
+ retries &&
+ Object.keys(expected).some(key => expected[key] != win[key])
+ ) {
+ browser.test.log(
+ `Got mismatched size (${JSON.stringify(
+ expected
+ )} != ${JSON.stringify(win)}). Retrying after a short delay.`
+ );
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 200));
+ return checkWindow(windowId, expected, retries - 1);
+ }
+
+ for (let [key, value] of Object.entries(expected)) {
+ browser.test.assertEq(
+ value,
+ win[key],
+ `Should find the correct updated value for ${key}`
+ );
+ }
+
+ return true;
+ }
+
+ let tests = [
+ { retries: 0, properties: { state: "minimized" } },
+ { retries: 0, properties: { state: "maximized" } },
+ { retries: 0, properties: { state: "fullscreen" } },
+ {
+ retries: 5,
+ properties: { width: 210, height: 220, left: 90, top: 80 },
+ },
+ ];
+
+ // Test create.
+ for (let test of tests) {
+ let win = await browser.windows.create({
+ type: "popup",
+ url: "test.html",
+ ...test.properties,
+ });
+ await checkWindow(win.id, test.properties, test.retries);
+ await browser.windows.remove(win.id);
+ }
+
+ // Test update.
+ for (let test of tests) {
+ let win = await browser.windows.create({
+ type: "popup",
+ url: "test.html",
+ });
+ await browser.windows.update(win.id, test.properties);
+ await checkWindow(win.id, test.properties, test.retries);
+ await browser.windows.remove(win.id);
+ }
+
+ browser.test.notifyPass();
+ },
+ },
+ manifest: {
+ background: { scripts: ["background.js"] },
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_windows_bug1732559.js b/comm/mail/components/extensions/test/browser/browser_ext_windows_bug1732559.js
new file mode 100644
index 0000000000..ee5acf743f
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_windows_bug1732559.js
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async function check_focus() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ // Create a promise which waits until the script in the window is loaded
+ // and the email field has focus, so we can send our fake keystrokes.
+ let loadPromise = new Promise(resolve => {
+ let listener = async (msg, sender) => {
+ if (msg == "loaded") {
+ browser.runtime.onMessage.removeListener(listener);
+ resolve(sender.tab.windowId);
+ }
+ };
+ browser.runtime.onMessage.addListener(listener);
+ });
+
+ let openedWin = await browser.windows.create({
+ url: "focus.html",
+ type: "popup",
+ allowScriptsToClose: true,
+ });
+ let loadedWinId = await loadPromise;
+
+ browser.test.assertEq(
+ openedWin.id,
+ loadedWinId,
+ "The correct window should have been loaded"
+ );
+
+ let removePromise = new Promise(resolve => {
+ browser.windows.onRemoved.addListener(id => {
+ if (id == openedWin.id) {
+ resolve();
+ }
+ });
+ });
+
+ window.sendMessage("sendKeyStrokes", openedWin.id);
+
+ await removePromise;
+ browser.test.notifyPass("finished");
+ },
+ "focus.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <script src="utils.js"></script>
+ <script src="focus.js"></script>
+ <title>Focus Test</title>
+ </head>
+ <body>
+ <input id="email" type="text"/>
+ <input id="delay" type="number" min="0" max="10" size="2"/>
+ </body>
+ </html>`,
+ "focus.js": () => {
+ async function load() {
+ let email = document.getElementById("email");
+ email.focus();
+
+ await new Promise(r => window.setTimeout(r));
+ await browser.runtime.sendMessage("loaded");
+
+ // Fails as expected if focus is not set in
+ // https://searchfox.org/comm-central/rev/be2751632bd695d17732ff590a71acb9b1ef920c/mail/components/extensions/extensionPopup.js#126-130
+ await window.waitForCondition(
+ () => email.value == "happy typing",
+ `Input field should have the correct value. Expected: "happy typing", actual: "${email.value}"`
+ );
+
+ window.close();
+ }
+ document.addEventListener("DOMContentLoaded", load, { once: true });
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ },
+ });
+
+ extension.onMessage("sendKeyStrokes", id => {
+ let window = Services.wm.getOuterWindowWithId(id);
+ EventUtils.sendString("happy typing", window);
+ extension.sendMessage("happy typing");
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_windows_create_normal_cookieStoreId.js b/comm/mail/components/extensions/test/browser/browser_ext_windows_create_normal_cookieStoreId.js
new file mode 100644
index 0000000000..19d34c26c5
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_windows_create_normal_cookieStoreId.js
@@ -0,0 +1,116 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Supported for creating normal windows is very limited in Thunderbird, a url
+// in the createData is ignored for example. This test only verifies that all the
+// things that are officially not supported, fail.
+
+add_task(async function no_cookies_permission() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ await browser.test.assertRejects(
+ browser.windows.create({ cookieStoreId: "firefox-container-1" }),
+ /No permission for cookieStoreId/,
+ "cookieStoreId requires cookies permission"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function invalid_cookieStoreId() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["cookies"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.windows.create({ cookieStoreId: "not-firefox-container-1" }),
+ /Illegal cookieStoreId/,
+ "cookieStoreId must be valid"
+ );
+
+ await browser.test.assertRejects(
+ browser.windows.create({ cookieStoreId: "firefox-private" }),
+ /Illegal to set private cookieStoreId in a non-private window/,
+ "cookieStoreId cannot be private in a non-private window"
+ );
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function userContext_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", false]],
+ });
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.windows.create({ cookieStoreId: "firefox-container-1" }),
+ /Contextual identities are currently disabled/,
+ "cookieStoreId cannot be a container tab ID when contextual identities are disabled"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function cookieStoreId_and_tabId() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["cookies"],
+ },
+ async background() {
+ for (let cookieStoreId of ["firefox-default", "firefox-container-1"]) {
+ let { id: normalTabId } = await browser.tabs.create({ cookieStoreId });
+
+ await browser.test.assertRejects(
+ browser.windows.create({
+ cookieStoreId: "firefox-container-2",
+ tabId: normalTabId,
+ }),
+ /`tabId` may not be used in conjunction with `cookieStoreId`/,
+ "Cannot use cookieStoreId for pre-existing tabs"
+ );
+
+ await browser.tabs.remove(normalTabId);
+ }
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_windows_create_popup_cookieStoreId.js b/comm/mail/components/extensions/test/browser/browser_ext_windows_create_popup_cookieStoreId.js
new file mode 100644
index 0000000000..e547fb6b9b
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_windows_create_popup_cookieStoreId.js
@@ -0,0 +1,255 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function no_cookies_permission() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ await browser.test.assertRejects(
+ browser.windows.create({
+ type: "popup",
+ cookieStoreId: "firefox-container-1",
+ }),
+ /No permission for cookieStoreId/,
+ "cookieStoreId requires cookies permission"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function invalid_cookieStoreId() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["cookies"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.windows.create({
+ type: "popup",
+ cookieStoreId: "not-firefox-container-1",
+ }),
+ /Illegal cookieStoreId/,
+ "cookieStoreId must be valid"
+ );
+
+ await browser.test.assertRejects(
+ browser.windows.create({
+ type: "popup",
+ cookieStoreId: "firefox-private",
+ }),
+ /Illegal to set private cookieStoreId in a non-private window/,
+ "cookieStoreId cannot be private in a non-private window"
+ );
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function userContext_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", false]],
+ });
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.windows.create({
+ type: "popup",
+ cookieStoreId: "firefox-container-1",
+ }),
+ /Contextual identities are currently disabled/,
+ "cookieStoreId cannot be a container tab ID when contextual identities are disabled"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function valid_cookieStoreId() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ const testCases = [
+ {
+ description: "one URL",
+ createParams: {
+ type: "popup",
+ url: "about:blank",
+ cookieStoreId: "firefox-container-1",
+ },
+ expectedCookieStoreIds: ["firefox-container-1"],
+ expectedExecuteScriptResult: ["about:blank - null"],
+ },
+ {
+ description: "one URL in an array",
+ createParams: {
+ type: "popup",
+ url: ["about:blank"],
+ cookieStoreId: "firefox-container-1",
+ },
+ expectedCookieStoreIds: ["firefox-container-1"],
+ expectedExecuteScriptResult: ["about:blank - null"],
+ },
+ ];
+
+ async function background(testCases) {
+ let readyTabs = new Map();
+ let tabReadyCheckers = new Set();
+ browser.webNavigation.onCompleted.addListener(({ url, tabId, frameId }) => {
+ if (frameId === 0) {
+ readyTabs.set(tabId, url);
+ browser.test.log(`Detected navigation in tab ${tabId} to ${url}.`);
+
+ for (let check of tabReadyCheckers) {
+ check(tabId, url);
+ }
+ }
+ });
+ async function awaitTabReady(tabId, expectedUrl) {
+ if (readyTabs.get(tabId) === expectedUrl) {
+ browser.test.log(`Tab ${tabId} was ready with URL ${expectedUrl}.`);
+ return;
+ }
+ await new Promise(resolve => {
+ browser.test.log(
+ `Waiting for tab ${tabId} to load URL ${expectedUrl}...`
+ );
+ tabReadyCheckers.add(function check(completedTabId, completedUrl) {
+ if (completedTabId === tabId && completedUrl === expectedUrl) {
+ tabReadyCheckers.delete(check);
+ resolve();
+ }
+ });
+ });
+ browser.test.log(`Tab ${tabId} is ready with URL ${expectedUrl}.`);
+ }
+
+ async function executeScriptAndGetResult(tabId) {
+ try {
+ return (
+ await browser.tabs.executeScript(tabId, {
+ matchAboutBlank: true,
+ code: "`${document.URL} - ${origin}`",
+ })
+ )[0];
+ } catch (e) {
+ return e.message;
+ }
+ }
+ for (let {
+ description,
+ createParams,
+ expectedCookieStoreIds,
+ expectedExecuteScriptResult,
+ } of testCases) {
+ let win = await browser.windows.create(createParams);
+
+ browser.test.assertEq(
+ expectedCookieStoreIds.length,
+ win.tabs.length,
+ "Expected number of tabs"
+ );
+
+ for (let [i, expectedCookieStoreId] of Object.entries(
+ expectedCookieStoreIds
+ )) {
+ browser.test.assertEq(
+ expectedCookieStoreId,
+ win.tabs[i].cookieStoreId,
+ `expected cookieStoreId for tab ${i} (${description})`
+ );
+ }
+
+ for (let [i, expectedResult] of Object.entries(
+ expectedExecuteScriptResult
+ )) {
+ // Wait until the the tab can process the tabs.executeScript calls.
+ // TODO: Remove this when bug 1418655 and bug 1397667 are fixed.
+ let expectedUrl = Array.isArray(createParams.url)
+ ? createParams.url[i]
+ : createParams.url || "about:home";
+ await awaitTabReady(win.tabs[i].id, expectedUrl);
+
+ let result = await executeScriptAndGetResult(win.tabs[i].id);
+ browser.test.assertEq(
+ expectedResult,
+ result,
+ `expected executeScript result for tab ${i} (${description})`
+ );
+ }
+
+ await browser.windows.remove(win.id);
+ }
+ browser.test.sendMessage("done");
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["cookies", "webNavigation"],
+ },
+ background: `(${background})(${JSON.stringify(testCases)})`,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function cookieStoreId_and_tabId() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["cookies"],
+ },
+ async background() {
+ for (let cookieStoreId of ["firefox-default", "firefox-container-1"]) {
+ let { id: normalTabId } = await browser.tabs.create({ cookieStoreId });
+
+ await browser.test.assertRejects(
+ browser.windows.create({
+ type: "popup",
+ cookieStoreId: "firefox-container-2",
+ tabId: normalTabId,
+ }),
+ /`tabId` may not be used in conjunction with `cookieStoreId`/,
+ "Cannot use cookieStoreId for pre-existing tabs"
+ );
+
+ await browser.tabs.remove(normalTabId);
+ }
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_windows_events.js b/comm/mail/components/extensions/test/browser/browser_ext_windows_events.js
new file mode 100644
index 0000000000..89cbd77e55
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_windows_events.js
@@ -0,0 +1,405 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async () => {
+ let account = createAccount();
+ addIdentity(account);
+ let rootFolder = account.incomingServer.rootFolder;
+ rootFolder.createSubfolder("windowsEvents", null);
+ let testFolder = rootFolder.findSubFolder("windowsEvents");
+ createMessages(testFolder, 5);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ // Executes a command, but first loads a second extension with terminated
+ // background and waits for it to be restarted due to the executed command.
+ async function capturePrimedEvent(eventName, callback) {
+ let eventPageExtensionReadyPromise = window.waitForMessage();
+ browser.test.sendMessage("capturePrimedEvent", eventName);
+ await eventPageExtensionReadyPromise;
+ let eventPageExtensionFinishedPromise = window.waitForMessage();
+ callback();
+ return eventPageExtensionFinishedPromise;
+ }
+
+ let listener = {
+ tabEvents: [],
+ windowEvents: [],
+ currentPromise: null,
+
+ pushEvent(...args) {
+ browser.test.log(JSON.stringify(args));
+ let queue = args[0].startsWith("windows.")
+ ? this.windowEvents
+ : this.tabEvents;
+ queue.push(args);
+ if (queue.currentPromise) {
+ let p = queue.currentPromise;
+ queue.currentPromise = null;
+ p.resolve();
+ }
+ },
+ windowsOnCreated(...args) {
+ this.pushEvent("windows.onCreated", ...args);
+ },
+ windowsOnRemoved(...args) {
+ this.pushEvent("windows.onRemoved", ...args);
+ },
+ tabsOnCreated(...args) {
+ this.pushEvent("tabs.onCreated", ...args);
+ },
+ tabsOnRemoved(...args) {
+ this.pushEvent("tabs.onRemoved", ...args);
+ },
+ async checkEvent(expectedEvent, ...expectedArgs) {
+ let queue = expectedEvent.startsWith("windows.")
+ ? this.windowEvents
+ : this.tabEvents;
+ if (queue.length == 0) {
+ await new Promise(
+ resolve => (queue.currentPromise = { resolve })
+ );
+ }
+ let [actualEvent, ...actualArgs] = queue.shift();
+ browser.test.assertEq(expectedEvent, actualEvent);
+ browser.test.assertEq(expectedArgs.length, actualArgs.length);
+
+ for (let i = 0; i < expectedArgs.length; i++) {
+ browser.test.assertEq(
+ typeof expectedArgs[i],
+ typeof actualArgs[i]
+ );
+ if (typeof expectedArgs[i] == "object") {
+ for (let key of Object.keys(expectedArgs[i])) {
+ browser.test.assertEq(
+ expectedArgs[i][key],
+ actualArgs[i][key]
+ );
+ }
+ } else {
+ browser.test.assertEq(expectedArgs[i], actualArgs[i]);
+ }
+ }
+
+ return actualArgs;
+ },
+ };
+ browser.tabs.onCreated.addListener(
+ listener.tabsOnCreated.bind(listener)
+ );
+ browser.tabs.onRemoved.addListener(
+ listener.tabsOnRemoved.bind(listener)
+ );
+ browser.windows.onCreated.addListener(
+ listener.windowsOnCreated.bind(listener)
+ );
+ browser.windows.onRemoved.addListener(
+ listener.windowsOnRemoved.bind(listener)
+ );
+
+ browser.test.log(
+ "Collect the ID of the initial window (there must be only one) and tab."
+ );
+
+ let initialWindows = await browser.windows.getAll({ populate: true });
+ browser.test.assertEq(1, initialWindows.length);
+ let [{ id: initialWindow, tabs: initialTabs }] = initialWindows;
+ browser.test.assertEq(1, initialTabs.length);
+ browser.test.assertEq(0, initialTabs[0].index);
+ browser.test.assertTrue(initialTabs[0].mailTab);
+ let [{ id: initialTab }] = initialTabs;
+
+ browser.test.log("Open a new main window (messenger.xhtml).");
+
+ let primedMainWindowInfo = await window.sendMessage("openMainWindow");
+ let [{ id: mainWindow }] = await listener.checkEvent(
+ "windows.onCreated",
+ { type: "normal" }
+ );
+ let [{ id: mainTab }] = await listener.checkEvent("tabs.onCreated", {
+ index: 0,
+ windowId: mainWindow,
+ active: true,
+ mailTab: true,
+ });
+ window.assertDeepEqual(
+ [
+ {
+ id: mainWindow,
+ type: "normal",
+ },
+ ],
+ primedMainWindowInfo
+ );
+
+ browser.test.log("Open a compose window (messengercompose.xhtml).");
+
+ let primedComposeWindowInfo = await capturePrimedEvent(
+ "onCreated",
+ () => browser.compose.beginNew()
+ );
+ let [{ id: composeWindow }] = await listener.checkEvent(
+ "windows.onCreated",
+ {
+ type: "messageCompose",
+ }
+ );
+ let [{ id: composeTab }] = await listener.checkEvent("tabs.onCreated", {
+ index: 0,
+ windowId: composeWindow,
+ active: true,
+ mailTab: false,
+ });
+ window.assertDeepEqual(
+ [
+ {
+ id: composeWindow,
+ type: "messageCompose",
+ },
+ ],
+ primedComposeWindowInfo
+ );
+
+ browser.test.log("Open a message in a window (messageWindow.xhtml).");
+
+ let primedDisplayWindowInfo = await window.sendMessage(
+ "openDisplayWindow"
+ );
+ let [{ id: displayWindow }] = await listener.checkEvent(
+ "windows.onCreated",
+ {
+ type: "messageDisplay",
+ }
+ );
+ let [{ id: displayTab }] = await listener.checkEvent("tabs.onCreated", {
+ index: 0,
+ windowId: displayWindow,
+ active: true,
+ mailTab: false,
+ });
+ window.assertDeepEqual(
+ [
+ {
+ id: displayWindow,
+ type: "messageDisplay",
+ },
+ ],
+ primedDisplayWindowInfo
+ );
+
+ browser.test.log("Open a page in a popup window.");
+
+ let primedPopupWindowInfo = await capturePrimedEvent("onCreated", () =>
+ browser.windows.create({
+ url: "test.html",
+ type: "popup",
+ width: 800,
+ height: 500,
+ })
+ );
+ let [{ id: popupWindow }] = await listener.checkEvent(
+ "windows.onCreated",
+ {
+ type: "popup",
+ width: 800,
+ height: 500,
+ }
+ );
+ let [{ id: popupTab }] = await listener.checkEvent("tabs.onCreated", {
+ index: 0,
+ windowId: popupWindow,
+ active: true,
+ mailTab: false,
+ });
+ window.assertDeepEqual(
+ [
+ {
+ id: popupWindow,
+ type: "popup",
+ width: 800,
+ height: 500,
+ },
+ ],
+ primedPopupWindowInfo
+ );
+
+ browser.test.log("Pause to let windows load properly.");
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 2500));
+
+ browser.test.log("Change focused window.");
+
+ let focusInfoPromise = new Promise(resolve => {
+ let listener = windowId => {
+ browser.windows.onFocusChanged.removeListener(listener);
+ resolve(windowId);
+ };
+ browser.windows.onFocusChanged.addListener(listener);
+ });
+ let [primedFocusInfo] = await capturePrimedEvent("onFocusChanged", () =>
+ browser.windows.update(composeWindow, { focused: true })
+ );
+ let focusInfo = await focusInfoPromise;
+ let platformInfo = await browser.runtime.getPlatformInfo();
+
+ let expectedWindow = ["mac", "win"].includes(platformInfo.os)
+ ? composeWindow
+ : browser.windows.WINDOW_ID_NONE;
+ window.assertDeepEqual(expectedWindow, primedFocusInfo);
+ window.assertDeepEqual(expectedWindow, focusInfo);
+
+ browser.test.log("Close the new main window.");
+
+ let primedMainWindowRemoveInfo = await capturePrimedEvent(
+ "onRemoved",
+ () => browser.windows.remove(mainWindow)
+ );
+ await listener.checkEvent("windows.onRemoved", mainWindow);
+ await listener.checkEvent("tabs.onRemoved", mainTab, {
+ windowId: mainWindow,
+ isWindowClosing: true,
+ });
+ window.assertDeepEqual([mainWindow], primedMainWindowRemoveInfo);
+
+ browser.test.log("Close the compose window.");
+
+ let primedComposWindowRemoveInfo = await capturePrimedEvent(
+ "onRemoved",
+ () => browser.windows.remove(composeWindow)
+ );
+ await listener.checkEvent("windows.onRemoved", composeWindow);
+ await listener.checkEvent("tabs.onRemoved", composeTab, {
+ windowId: composeWindow,
+ isWindowClosing: true,
+ });
+ window.assertDeepEqual([composeWindow], primedComposWindowRemoveInfo);
+
+ browser.test.log("Close the message window.");
+
+ let primedDisplayWindowRemoveInfo = await capturePrimedEvent(
+ "onRemoved",
+ () => browser.windows.remove(displayWindow)
+ );
+ await listener.checkEvent("windows.onRemoved", displayWindow);
+ await listener.checkEvent("tabs.onRemoved", displayTab, {
+ windowId: displayWindow,
+ isWindowClosing: true,
+ });
+ window.assertDeepEqual([displayWindow], primedDisplayWindowRemoveInfo);
+
+ browser.test.log("Close the popup window.");
+
+ let primedPopupWindowRemoveInfo = await capturePrimedEvent(
+ "onRemoved",
+ () => browser.windows.remove(popupWindow)
+ );
+ await listener.checkEvent("windows.onRemoved", popupWindow);
+ await listener.checkEvent("tabs.onRemoved", popupTab, {
+ windowId: popupWindow,
+ isWindowClosing: true,
+ });
+ window.assertDeepEqual([popupWindow], primedPopupWindowRemoveInfo);
+
+ let finalWindows = await browser.windows.getAll({ populate: true });
+ browser.test.assertEq(1, finalWindows.length);
+ browser.test.assertEq(initialWindow, finalWindows[0].id);
+ browser.test.assertEq(1, finalWindows[0].tabs.length);
+ browser.test.assertEq(initialTab, finalWindows[0].tabs[0].id);
+
+ browser.test.assertEq(0, listener.tabEvents.length);
+ browser.test.assertEq(0, listener.windowEvents.length);
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["addressBooks"],
+ },
+ });
+
+ // Function to start an event page extension (MV3), which can be called whenever
+ // the main test is about to trigger an event. The extension terminates its
+ // background and listens for that single event, verifying it is waking up correctly.
+ async function event_page_extension(eventName, actionCallback) {
+ let ext = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, hasFired is set to false. In
+ // case of a wake-up, the first fired event is the one that woke up the background.
+ let hasFired = false;
+ let eventName = browser.runtime.getManifest().description;
+
+ if (
+ ["onCreated", "onFocusChanged", "onRemoved"].includes(eventName)
+ ) {
+ browser.windows[eventName].addListener(async (...args) => {
+ // Only send the first event after background wake-up, this should
+ // be the only one expected.
+ if (!hasFired) {
+ hasFired = true;
+ browser.test.sendMessage(`${eventName} received`, args);
+ }
+ });
+ }
+
+ browser.test.sendMessage("background started");
+ },
+ },
+ manifest: {
+ manifest_version: 3,
+ description: eventName,
+ background: { scripts: ["background.js"] },
+ browser_specific_settings: {
+ gecko: { id: `windows.eventpage.${eventName}@mochi.test` },
+ },
+ },
+ });
+ await ext.startup();
+ await ext.awaitMessage("background started");
+ // The listener should be persistent, but not primed.
+ assertPersistentListeners(ext, "windows", eventName, { primed: false });
+
+ await ext.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listener.
+ assertPersistentListeners(ext, "windows", eventName, { primed: true });
+
+ await actionCallback();
+ let rv = await ext.awaitMessage(`${eventName} received`);
+ await ext.awaitMessage("background started");
+ // The listener should be persistent, but not primed.
+ assertPersistentListeners(ext, "windows", eventName, { primed: false });
+
+ await ext.unload();
+ return rv;
+ }
+
+ extension.onMessage("openMainWindow", async () => {
+ let primedEventData = await event_page_extension("onCreated", () => {
+ return window.MsgOpenNewWindowForFolder(testFolder.URI);
+ });
+ extension.sendMessage(...primedEventData);
+ });
+
+ extension.onMessage("openDisplayWindow", async () => {
+ let primedEventData = await event_page_extension("onCreated", () => {
+ return openMessageInWindow([...testFolder.messages][0]);
+ });
+ extension.sendMessage(...primedEventData);
+ });
+
+ extension.onMessage("capturePrimedEvent", async eventName => {
+ let primedEventData = await event_page_extension(eventName, () => {
+ // Resume execution of the main test, after the event page extension has
+ // primed its event listeners.
+ extension.sendMessage();
+ });
+ extension.sendMessage(...primedEventData);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_windows_types.js b/comm/mail/components/extensions/test/browser/browser_ext_windows_types.js
new file mode 100644
index 0000000000..af9ad35f8a
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/browser_ext_windows_types.js
@@ -0,0 +1,121 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async () => {
+ let files = {
+ "background.js": async () => {
+ // Message compose window.
+
+ let createdWindowPromise = window.waitForEvent("windows.onCreated");
+ await browser.compose.beginNew();
+ let [createdWindow] = await createdWindowPromise;
+ browser.test.assertEq("messageCompose", createdWindow.type);
+
+ let windowDetail = await browser.windows.get(createdWindow.id, {
+ populate: true,
+ });
+ browser.test.assertEq("messageCompose", windowDetail.type);
+ browser.test.assertEq(1, windowDetail.tabs.length);
+ browser.test.assertEq("messageCompose", windowDetail.tabs[0].type);
+ // These three properties should not be present, but not fail either.
+ browser.test.assertEq(undefined, windowDetail.tabs[0].favIconUrl);
+ browser.test.assertEq(undefined, windowDetail.tabs[0].title);
+ browser.test.assertEq(undefined, windowDetail.tabs[0].url);
+
+ let removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ await browser.tabs.remove(windowDetail.tabs[0].id);
+ await removedWindowPromise;
+
+ // Message display window.
+
+ createdWindowPromise = window.waitForEvent("windows.onCreated");
+ browser.test.sendMessage("openMessage");
+ [createdWindow] = await createdWindowPromise;
+ browser.test.assertEq("messageDisplay", createdWindow.type);
+
+ windowDetail = await browser.windows.get(createdWindow.id, {
+ populate: true,
+ });
+ browser.test.assertEq("messageDisplay", windowDetail.type);
+ browser.test.assertEq(1, windowDetail.tabs.length);
+ browser.test.assertEq("messageDisplay", windowDetail.tabs[0].type);
+ browser.test.assertEq("about:blank", windowDetail.tabs[0].url);
+ // These properties should not be present, but not fail either.
+ browser.test.assertEq(undefined, windowDetail.tabs[0].favIconUrl);
+ browser.test.assertEq(undefined, windowDetail.tabs[0].title);
+
+ removedWindowPromise = window.waitForEvent("windows.onRemoved");
+ browser.test.sendMessage("closeMessage");
+ await removedWindowPromise;
+
+ browser.test.notifyPass();
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["addressBooks", "tabs"],
+ },
+ });
+
+ let account = createAccount();
+ addIdentity(account);
+ let rootFolder = account.incomingServer.rootFolder;
+ rootFolder.createSubfolder("test1", null);
+ let subFolders = {};
+ for (let folder of rootFolder.subFolders) {
+ subFolders[folder.name] = folder;
+ }
+ createMessages(subFolders.test1, 1);
+
+ await extension.startup();
+
+ await extension.awaitMessage("openMessage");
+ let newWindow = await openMessageInWindow([...subFolders.test1.messages][0]);
+
+ await extension.awaitMessage("closeMessage");
+ newWindow.close();
+
+ await extension.awaitFinish();
+ await extension.unload();
+});
+
+add_task(async function test_tabs_of_second_tabmail() {
+ let files = {
+ "background.js": async () => {
+ let testWindow = await browser.windows.create({ type: "normal" });
+ browser.test.assertEq("normal", testWindow.type);
+
+ let tabs = await await browser.tabs.query({ windowId: testWindow.id });
+ browser.test.assertEq(1, tabs.length);
+ browser.test.assertEq("mail", tabs[0].type);
+
+ await browser.windows.remove(testWindow.id);
+
+ browser.test.notifyPass();
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["background.js"] },
+ },
+ });
+
+ let account = createAccount();
+ addIdentity(account);
+ let rootFolder = account.incomingServer.rootFolder;
+ rootFolder.createSubfolder("test1", null);
+ let subFolders = {};
+ for (let folder of rootFolder.subFolders) {
+ subFolders[folder.name] = folder;
+ }
+ createMessages(subFolders.test1, 1);
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/browser/data/cloudFile1.txt b/comm/mail/components/extensions/test/browser/data/cloudFile1.txt
new file mode 100644
index 0000000000..42c5dbfae0
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/data/cloudFile1.txt
@@ -0,0 +1 @@
+you got the moves!
diff --git a/comm/mail/components/extensions/test/browser/data/cloudFile2.txt b/comm/mail/components/extensions/test/browser/data/cloudFile2.txt
new file mode 100644
index 0000000000..42c5dbfae0
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/data/cloudFile2.txt
@@ -0,0 +1 @@
+you got the moves!
diff --git a/comm/mail/components/extensions/test/browser/data/content.html b/comm/mail/components/extensions/test/browser/data/content.html
new file mode 100644
index 0000000000..6a56ee6a5a
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/data/content.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8"/>
+ <title>A test document</title>
+</head>
+<body>
+ <p id="description">This is text.</p>
+ <p><a href="http://mochi.test:8888/">This is a link with text.</a></p>
+ <p><img src="http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data/tb-logo.png" width="304" height="84" /></p>
+</body>
+</html>
diff --git a/comm/mail/components/extensions/test/browser/data/content_body.html b/comm/mail/components/extensions/test/browser/data/content_body.html
new file mode 100644
index 0000000000..7652f2d84d
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/data/content_body.html
@@ -0,0 +1 @@
+<p>This is text.</p><p><a href="http://mochi.test:8888/">This is a link with text.</a></p><p><img src="http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data/tb-logo.png" width="304" height="84" /></p>
diff --git a/comm/mail/components/extensions/test/browser/data/linktest.html b/comm/mail/components/extensions/test/browser/data/linktest.html
new file mode 100644
index 0000000000..f8b49156d8
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/data/linktest.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8"/>
+ <title>A test document</title>
+</head>
+<body>
+ <p><a id="linkExt1" href="https://example.org/browser/comm/mail/components/extensions/test/browser/data/content.html">self</a></p>
+ <p><a id="linkExt2" href="https://mozilla.org/">other</a></p>
+</body>
+</html>
diff --git a/comm/mail/components/extensions/test/browser/data/tb-logo.png b/comm/mail/components/extensions/test/browser/data/tb-logo.png
new file mode 100644
index 0000000000..aac56e2546
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/data/tb-logo.png
Binary files differ
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"
+ );
+}
diff --git a/comm/mail/components/extensions/test/browser/head_menus.js b/comm/mail/components/extensions/test/browser/head_menus.js
new file mode 100644
index 0000000000..346c4ca044
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/head_menus.js
@@ -0,0 +1,733 @@
+/* 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/. */
+
+/* globals synthesizeMouseAtCenterAndRetry, awaitBrowserLoaded */
+
+"use strict";
+
+const { ExtensionPermissions } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+
+const { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+
+const treeClick = mailTestUtils.treeClick.bind(null, EventUtils, window);
+
+var URL_BASE =
+ "http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data";
+
+/**
+ * Left-click on something and wait for the context menu to appear.
+ * For elements in the parent process only.
+ *
+ * @param {Element} menu - The <menu> that should appear.
+ * @param {Element} element - The element to be clicked on.
+ * @returns {Promise} A promise that resolves when the menu appears.
+ */
+function leftClick(menu, element) {
+ let shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(element, {}, element.ownerGlobal);
+ return shownPromise;
+}
+/**
+ * Right-click on something and wait for the context menu to appear.
+ * For elements in the parent process only.
+ *
+ * @param {Element} menu - The <menu> that should appear.
+ * @param {Element} element - The element to be clicked on.
+ * @returns {Promise} A promise that resolves when the menu appears.
+ */
+function rightClick(menu, element) {
+ let shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ element,
+ { type: "contextmenu" },
+ element.ownerGlobal
+ );
+ return shownPromise;
+}
+
+/**
+ * Right-click on something in a content document and wait for the context
+ * menu to appear.
+ *
+ * @param {Element} menu - The <menu> that should appear.
+ * @param {string} selector - CSS selector of the element to be clicked on.
+ * @param {Element} browser - <browser> containing the element.
+ * @returns {Promise} A promise that resolves when the menu appears.
+ */
+async function rightClickOnContent(menu, selector, browser) {
+ let shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ await synthesizeMouseAtCenterAndRetry(
+ selector,
+ { type: "contextmenu" },
+ browser
+ );
+ return shownPromise;
+}
+
+/**
+ * Check the parameters of a browser.onShown event was fired.
+ *
+ * @see mail/components/extensions/schemas/menus.json
+ *
+ * @param extension
+ * @param {object} expectedInfo
+ * @param {Array?} expectedInfo.menuIds
+ * @param {Array?} expectedInfo.contexts
+ * @param {Array?} expectedInfo.attachments
+ * @param {object?} expectedInfo.displayedFolder
+ * @param {object?} expectedInfo.selectedFolder
+ * @param {Array?} expectedInfo.selectedMessages
+ * @param {RegExp?} expectedInfo.pageUrl
+ * @param {string?} expectedInfo.selectionText
+ * @param {object} expectedTab
+ * @param {boolean} expectedTab.active
+ * @param {integer} expectedTab.index
+ * @param {boolean} expectedTab.mailTab
+ */
+async function checkShownEvent(extension, expectedInfo, expectedTab) {
+ let [info, tab] = await extension.awaitMessage("onShown");
+ Assert.deepEqual(info.menuIds, expectedInfo.menuIds);
+ Assert.deepEqual(info.contexts, expectedInfo.contexts);
+
+ Assert.equal(
+ !!info.attachments,
+ !!expectedInfo.attachments,
+ "attachments in info"
+ );
+ if (expectedInfo.attachments) {
+ Assert.equal(info.attachments.length, expectedInfo.attachments.length);
+ for (let i = 0; i < expectedInfo.attachments.length; i++) {
+ Assert.equal(info.attachments[i].name, expectedInfo.attachments[i].name);
+ Assert.equal(info.attachments[i].size, expectedInfo.attachments[i].size);
+ }
+ }
+
+ for (let infoKey of ["displayedFolder", "selectedFolder"]) {
+ Assert.equal(
+ !!info[infoKey],
+ !!expectedInfo[infoKey],
+ `${infoKey} in info`
+ );
+ if (expectedInfo[infoKey]) {
+ Assert.equal(info[infoKey].accountId, expectedInfo[infoKey].accountId);
+ Assert.equal(info[infoKey].path, expectedInfo[infoKey].path);
+ Assert.ok(Array.isArray(info[infoKey].subFolders));
+ }
+ }
+
+ Assert.equal(
+ !!info.selectedMessages,
+ !!expectedInfo.selectedMessages,
+ "selectedMessages in info"
+ );
+ if (expectedInfo.selectedMessages) {
+ Assert.equal(info.selectedMessages.id, null);
+ Assert.equal(
+ info.selectedMessages.messages.length,
+ expectedInfo.selectedMessages.messages.length
+ );
+ for (let i = 0; i < expectedInfo.selectedMessages.messages.length; i++) {
+ Assert.equal(
+ info.selectedMessages.messages[i].subject,
+ expectedInfo.selectedMessages.messages[i].subject
+ );
+ }
+ }
+
+ Assert.equal(!!info.pageUrl, !!expectedInfo.pageUrl, "pageUrl in info");
+ if (expectedInfo.pageUrl) {
+ if (typeof expectedInfo.pageUrl == "string") {
+ Assert.equal(info.pageUrl, expectedInfo.pageUrl);
+ } else {
+ Assert.ok(info.pageUrl.match(expectedInfo.pageUrl));
+ }
+ }
+
+ Assert.equal(
+ !!info.selectionText,
+ !!expectedInfo.selectionText,
+ "selectionText in info"
+ );
+ if (expectedInfo.selectionText) {
+ Assert.equal(info.selectionText, expectedInfo.selectionText);
+ }
+
+ Assert.equal(tab.active, expectedTab.active, "tab is active");
+ Assert.equal(tab.index, expectedTab.index, "tab index");
+ Assert.equal(tab.mailTab, expectedTab.mailTab, "tab is mailTab");
+}
+
+/**
+ * Check the parameters of a browser.onClicked event was fired.
+ *
+ * @see mail/components/extensions/schemas/menus.json
+ *
+ * @param extension
+ * @param {object} expectedInfo
+ * @param {string?} expectedInfo.selectionText
+ * @param {string?} expectedInfo.linkText
+ * @param {RegExp?} expectedInfo.pageUrl
+ * @param {RegExp?} expectedInfo.linkUrl
+ * @param {RegExp?} expectedInfo.srcUrl
+ * @param {object} expectedTab
+ * @param {boolean} expectedTab.active
+ * @param {integer} expectedTab.index
+ * @param {boolean} expectedTab.mailTab
+ */
+async function checkClickedEvent(extension, expectedInfo, expectedTab) {
+ let [info, tab] = await extension.awaitMessage("onClicked");
+
+ Assert.equal(info.selectionText, expectedInfo.selectionText, "selectionText");
+ Assert.equal(info.linkText, expectedInfo.linkText, "linkText");
+ if (expectedInfo.menuItemId) {
+ Assert.equal(info.menuItemId, expectedInfo.menuItemId, "menuItemId");
+ }
+
+ for (let infoKey of ["pageUrl", "linkUrl", "srcUrl"]) {
+ Assert.equal(
+ !!info[infoKey],
+ !!expectedInfo[infoKey],
+ `${infoKey} in info`
+ );
+ if (expectedInfo[infoKey]) {
+ if (typeof expectedInfo[infoKey] == "string") {
+ Assert.equal(info[infoKey], expectedInfo[infoKey]);
+ } else {
+ Assert.ok(info[infoKey].match(expectedInfo[infoKey]));
+ }
+ }
+ }
+
+ Assert.equal(tab.active, expectedTab.active, "tab is active");
+ Assert.equal(tab.index, expectedTab.index, "tab index");
+ Assert.equal(tab.mailTab, expectedTab.mailTab, "tab is mailTab");
+}
+
+async function getMenuExtension(manifest) {
+ let details = {
+ files: {
+ "background.js": async () => {
+ let contexts = [
+ "audio",
+ "compose_action",
+ "compose_action_menu",
+ "message_display_action",
+ "message_display_action_menu",
+ "editable",
+ "frame",
+ "image",
+ "link",
+ "page",
+ "password",
+ "selection",
+ "tab",
+ "video",
+ "message_list",
+ "folder_pane",
+ "compose_attachments",
+ "compose_body",
+ "tools_menu",
+ ];
+ if (browser.runtime.getManifest().manifest_version > 2) {
+ contexts.push("action", "action_menu");
+ } else {
+ contexts.push("browser_action", "browser_action_menu");
+ }
+
+ for (let context of contexts) {
+ browser.menus.create({
+ id: context,
+ title: context,
+ contexts: [context],
+ });
+ }
+
+ browser.menus.onShown.addListener((...args) => {
+ browser.test.sendMessage("onShown", args);
+ });
+
+ browser.menus.onClicked.addListener((...args) => {
+ browser.test.sendMessage("onClicked", args);
+ });
+ browser.test.sendMessage("menus-created");
+ },
+ },
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "menus@mochi.test",
+ },
+ },
+ background: { scripts: ["background.js"] },
+ ...manifest,
+ },
+ useAddonManager: "temporary",
+ };
+
+ if (!details.manifest.permissions) {
+ details.manifest.permissions = [];
+ }
+ details.manifest.permissions.push("menus");
+ console.log(JSON.stringify(details, 2));
+ let extension = ExtensionTestUtils.loadExtension(details);
+ if (details.manifest.host_permissions) {
+ // MV3 has to manually grant the requested permission.
+ await ExtensionPermissions.add("menus@mochi.test", {
+ permissions: [],
+ origins: details.manifest.host_permissions,
+ });
+ }
+ return extension;
+}
+
+async function subtest_content(
+ extension,
+ extensionHasPermission,
+ browser,
+ pageUrl,
+ tab
+) {
+ await awaitBrowserLoaded(browser, url => url != "about:blank");
+
+ let menuId = browser.getAttribute("context");
+ let ownerDocument;
+ if (browser.ownerGlobal.parent.location.href == "about:3pane") {
+ ownerDocument = browser.ownerGlobal.parent.document;
+ } else if (menuId == "browserContext") {
+ ownerDocument = browser.ownerGlobal.top.document;
+ } else {
+ ownerDocument = browser.ownerDocument;
+ }
+ let menu = ownerDocument.getElementById(menuId);
+
+ await synthesizeMouseAtCenterAndRetry("body", {}, browser);
+
+ info("Test a part of the page with no content.");
+
+ await rightClickOnContent(menu, "body", browser);
+ Assert.ok(menu.querySelector("#menus_mochi_test-menuitem-_page"));
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.hidePopup();
+ await hiddenPromise;
+ // 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 => setTimeout(r, 250));
+
+ await checkShownEvent(
+ extension,
+ {
+ menuIds: ["page"],
+ contexts: ["page", "all"],
+ pageUrl: extensionHasPermission ? pageUrl : undefined,
+ },
+ tab
+ );
+
+ info("Test selection.");
+
+ await SpecialPowers.spawn(browser, [], () => {
+ let text = content.document.querySelector("p");
+ content.getSelection().selectAllChildren(text);
+ });
+ await rightClickOnContent(menu, "p", browser);
+ Assert.ok(menu.querySelector("#menus_mochi_test-menuitem-_selection"));
+ await checkShownEvent(
+ extension,
+ {
+ pageUrl: extensionHasPermission ? pageUrl : undefined,
+ selectionText: extensionHasPermission ? "This is text." : undefined,
+ menuIds: ["selection"],
+ contexts: ["selection", "all"],
+ },
+ tab
+ );
+
+ hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ let clickedPromise = checkClickedEvent(
+ extension,
+ {
+ pageUrl,
+ selectionText: "This is text.",
+ },
+ tab
+ );
+ menu.activateItem(
+ menu.querySelector("#menus_mochi_test-menuitem-_selection")
+ );
+ await clickedPromise;
+ await hiddenPromise;
+
+ // 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 => setTimeout(r, 250));
+
+ await synthesizeMouseAtCenterAndRetry("body", {}, browser); // Select nothing.
+
+ info("Test link.");
+
+ await rightClickOnContent(menu, "a", browser);
+ Assert.ok(menu.querySelector("#menus_mochi_test-menuitem-_link"));
+ await checkShownEvent(
+ extension,
+ {
+ pageUrl: extensionHasPermission ? pageUrl : undefined,
+ menuIds: ["link"],
+ contexts: ["link", "all"],
+ },
+ tab
+ );
+
+ hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ clickedPromise = checkClickedEvent(
+ extension,
+ {
+ pageUrl,
+ linkUrl: "http://mochi.test:8888/",
+ linkText: "This is a link with text.",
+ },
+ tab
+ );
+ menu.activateItem(menu.querySelector("#menus_mochi_test-menuitem-_link"));
+ await clickedPromise;
+ await hiddenPromise;
+ // 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 => setTimeout(r, 250));
+
+ info("Test image.");
+
+ await rightClickOnContent(menu, "img", browser);
+ Assert.ok(menu.querySelector("#menus_mochi_test-menuitem-_image"));
+ await checkShownEvent(
+ extension,
+ {
+ pageUrl: extensionHasPermission ? pageUrl : undefined,
+ menuIds: ["image"],
+ contexts: ["image", "all"],
+ },
+ tab
+ );
+
+ hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ clickedPromise = checkClickedEvent(
+ extension,
+ {
+ pageUrl,
+ srcUrl: `${URL_BASE}/tb-logo.png`,
+ },
+ tab
+ );
+ menu.activateItem(menu.querySelector("#menus_mochi_test-menuitem-_image"));
+ await clickedPromise;
+ await hiddenPromise;
+ // 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 => setTimeout(r, 250));
+}
+
+async function openExtensionSubMenu(menu) {
+ // The extension submenu ends with a number, which increases over time, but it
+ // does not have a underscore.
+ let submenu;
+ for (let item of menu.querySelectorAll("[id^=menus_mochi_test-menuitem-]")) {
+ if (!item.id.includes("-_")) {
+ submenu = item;
+ break;
+ }
+ }
+ Assert.ok(submenu, `Found submenu: ${submenu.id}`);
+
+ // Open submenu.
+ let submenuPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ submenu.openMenu(true);
+ await submenuPromise;
+
+ return submenu;
+}
+
+async function subtest_compose_body(
+ extension,
+ extensionHasPermission,
+ browser,
+ pageUrl,
+ tab
+) {
+ await awaitBrowserLoaded(browser, url => url != "about:blank");
+
+ let ownerDocument = browser.ownerDocument;
+ let menu = ownerDocument.getElementById(browser.getAttribute("context"));
+
+ await synthesizeMouseAtCenterAndRetry("body", {}, browser);
+
+ info("Test a part of the page with no content.");
+ {
+ await rightClickOnContent(menu, "body", browser);
+ Assert.ok(menu.querySelector(`#menus_mochi_test-menuitem-_compose_body`));
+ Assert.ok(menu.querySelector(`#menus_mochi_test-menuitem-_editable`));
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.hidePopup();
+ await hiddenPromise;
+ // 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 => setTimeout(r, 250));
+
+ await checkShownEvent(
+ extension,
+ {
+ menuIds: ["editable", "compose_body"],
+ contexts: ["editable", "compose_body", "all"],
+ pageUrl: extensionHasPermission ? pageUrl : undefined,
+ },
+ tab
+ );
+ }
+
+ info("Test selection.");
+ {
+ await SpecialPowers.spawn(browser, [], () => {
+ let text = content.document.querySelector("p");
+ content.getSelection().selectAllChildren(text);
+ });
+
+ await rightClickOnContent(menu, "p", browser);
+ let submenu = await openExtensionSubMenu(menu);
+
+ await checkShownEvent(
+ extension,
+ {
+ pageUrl: extensionHasPermission ? pageUrl : undefined,
+ selectionText: extensionHasPermission ? "This is text." : undefined,
+ menuIds: ["editable", "selection", "compose_body"],
+ contexts: ["editable", "selection", "compose_body", "all"],
+ },
+ tab
+ );
+ Assert.ok(submenu.querySelector("#menus_mochi_test-menuitem-_selection"));
+ Assert.ok(
+ submenu.querySelector("#menus_mochi_test-menuitem-_compose_body")
+ );
+ Assert.ok(submenu.querySelector("#menus_mochi_test-menuitem-_editable"));
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(submenu, "popuphidden");
+ let clickedPromise = checkClickedEvent(
+ extension,
+ {
+ pageUrl,
+ selectionText: "This is text.",
+ },
+ tab
+ );
+ menu.activateItem(
+ submenu.querySelector("#menus_mochi_test-menuitem-_selection")
+ );
+ await clickedPromise;
+ await hiddenPromise;
+
+ // 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 => setTimeout(r, 250));
+ await synthesizeMouseAtCenterAndRetry("body", {}, browser); // Select nothing.
+ }
+
+ info("Test link.");
+ {
+ await rightClickOnContent(menu, "a", browser);
+ let submenu = await openExtensionSubMenu(menu);
+
+ await checkShownEvent(
+ extension,
+ {
+ pageUrl: extensionHasPermission ? pageUrl : undefined,
+ menuIds: ["editable", "link", "compose_body"],
+ contexts: ["editable", "link", "compose_body", "all"],
+ },
+ tab
+ );
+ Assert.ok(submenu.querySelector("#menus_mochi_test-menuitem-_link"));
+ Assert.ok(submenu.querySelector("#menus_mochi_test-menuitem-_editable"));
+ Assert.ok(
+ submenu.querySelector("#menus_mochi_test-menuitem-_compose_body")
+ );
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(submenu, "popuphidden");
+ let clickedPromise = checkClickedEvent(
+ extension,
+ {
+ pageUrl,
+ linkUrl: "http://mochi.test:8888/",
+ linkText: "This is a link with text.",
+ },
+ tab
+ );
+ menu.activateItem(
+ submenu.querySelector("#menus_mochi_test-menuitem-_link")
+ );
+ await clickedPromise;
+ await hiddenPromise;
+
+ // 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 => setTimeout(r, 250));
+ await synthesizeMouseAtCenterAndRetry("body", {}, browser); // Select nothing.
+ }
+
+ info("Test image.");
+ {
+ await rightClickOnContent(menu, "img", browser);
+ let submenu = await openExtensionSubMenu(menu);
+
+ await checkShownEvent(
+ extension,
+ {
+ pageUrl: extensionHasPermission ? pageUrl : undefined,
+ menuIds: ["editable", "image", "compose_body"],
+ contexts: ["editable", "image", "compose_body", "all"],
+ },
+ tab
+ );
+ Assert.ok(submenu.querySelector("#menus_mochi_test-menuitem-_image"));
+ Assert.ok(submenu.querySelector("#menus_mochi_test-menuitem-_editable"));
+ Assert.ok(
+ submenu.querySelector("#menus_mochi_test-menuitem-_compose_body")
+ );
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ let clickedPromise = checkClickedEvent(
+ extension,
+ {
+ pageUrl,
+ srcUrl: `${URL_BASE}/tb-logo.png`,
+ },
+ tab
+ );
+ menu.activateItem(
+ submenu.querySelector("#menus_mochi_test-menuitem-_image")
+ );
+ await clickedPromise;
+ await hiddenPromise;
+
+ // 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 => setTimeout(r, 250));
+ await synthesizeMouseAtCenterAndRetry("body", {}, browser); // Select nothing.
+ }
+}
+
+// Test UI elements which have been made accessible for the menus API.
+// Assumed to be run after subtest_content, so we know everything has finished
+// loading.
+async function subtest_element(
+ extension,
+ extensionHasPermission,
+ element,
+ pageUrl,
+ tab
+) {
+ for (let selectedTest of [false, true]) {
+ element.focus();
+ if (selectedTest) {
+ element.value = "This is selected text.";
+ element.select();
+ } else {
+ element.value = "";
+ }
+
+ let event = await rightClick(element.ownerGlobal, element);
+ let menu = event.target;
+ let trigger = menu.triggerNode;
+ let menuitem = menu.querySelector("#menus_mochi_test-menuitem-_editable");
+ Assert.equal(
+ element.id,
+ trigger.id,
+ "Contextmenu of correct element has been triggered."
+ );
+ Assert.equal(
+ menuitem.id,
+ "menus_mochi_test-menuitem-_editable",
+ "Contextmenu includes menu."
+ );
+
+ await checkShownEvent(
+ extension,
+ {
+ menuIds: selectedTest ? ["editable", "selection"] : ["editable"],
+ contexts: selectedTest
+ ? ["editable", "selection", "all"]
+ : ["editable", "all"],
+ pageUrl: extensionHasPermission ? pageUrl : undefined,
+ selectionText:
+ extensionHasPermission && selectedTest
+ ? "This is selected text."
+ : undefined,
+ },
+ tab
+ );
+
+ // With text being selected, there will be two "context" entries in an
+ // extension submenu. Open the submenu.
+ let submenu = null;
+ if (selectedTest) {
+ for (let foundMenu of menu.querySelectorAll(
+ "[id^='menus_mochi_test-menuitem-']"
+ )) {
+ if (!foundMenu.id.startsWith("menus_mochi_test-menuitem-_")) {
+ submenu = foundMenu;
+ }
+ }
+ Assert.ok(submenu, "Submenu found.");
+ let submenuPromise = BrowserTestUtils.waitForEvent(
+ element.ownerGlobal,
+ "popupshown"
+ );
+ submenu.openMenu(true);
+ await submenuPromise;
+ }
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(
+ element.ownerGlobal,
+ "popuphidden"
+ );
+ let clickedPromise = checkClickedEvent(
+ extension,
+ {
+ pageUrl,
+ selectionText: selectedTest ? "This is selected text." : undefined,
+ },
+ tab
+ );
+ if (submenu) {
+ submenu.menupopup.activateItem(menuitem);
+ } else {
+ menu.activateItem(menuitem);
+ }
+ await clickedPromise;
+ await hiddenPromise;
+
+ // 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 => setTimeout(r, 250));
+ }
+}
diff --git a/comm/mail/components/extensions/test/browser/messages/attachedMessageSample.eml b/comm/mail/components/extensions/test/browser/messages/attachedMessageSample.eml
new file mode 100644
index 0000000000..0575e8542c
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/messages/attachedMessageSample.eml
@@ -0,0 +1,186 @@
+Message-ID: <sample.eml@mime.sample>
+Date: Fri, 20 May 2000 00:29:55 -0400
+To: Heinz <mueller@example.com>
+Cc: Robin <damian@wayne-enterprises.com>
+From: Batman <bruce@wayne-enterprises.com>
+Subject: Attached message with attachments
+Mime-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------49CVLb1N6p6Spdka4qq7Naeg"
+
+This is a multi-part message in MIME format.
+--------------49CVLb1N6p6Spdka4qq7Naeg
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+
+<html>
+ <head>
+
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ </head>
+ <body>
+ <p>This message has one normal attachment and one email attachment,
+ which itself has 3 attachments.<br>
+ </p>
+ </body>
+</html>
+--------------49CVLb1N6p6Spdka4qq7Naeg
+Content-Type: message/rfc822; charset=UTF-8; name="sample02.eml"
+Content-Disposition: attachment; filename="sample02.eml"
+Content-Transfer-Encoding: 7bit
+
+Message-ID: <sample-attached.eml@mime.sample>
+From: Superman <clark.kent@dailyplanet.com>
+To: =?iso-8859-1?Q?Heinz_M=FCller?= <mueller@examples.com>
+Cc: Jimmy <jimmy.Olsen@dailyplanet.com>
+Subject: Test message
+Date: Wed, 17 May 2000 19:32:47 -0400
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="----=_NextPart_000_0002_01BFC036.AE309650"
+X-Priority: 3 (Normal)
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0)
+Importance: Normal
+X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_000_0002_01BFC036.AE309650
+Content-Type: text/plain;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+
+Die Hasen und die Fr=F6sche=20
+=20
+
+------=_NextPart_000_0002_01BFC036.AE309650
+Content-Type: image/png;
+ name="blueball1.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="blueball2.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgAAAAA
+CCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQ
+MYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO
+5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaMvee1
+5++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAADBMg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu
+MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbssceb
+L5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18P
+yqqWUw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC
+UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/MjRxm
+T6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1CSYoOiMOS
+GwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B
+1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD
+/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZz
+O7wAAAAASUVORK5CYII=
+
+------=_NextPart_000_0002_01BFC036.AE309650
+Content-Type: image/png;
+ name="greenball.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment
+
+iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAAAEAAAGAAAIQAA
+CAAAMQAAQgAAUgAAWgAASgAIYwAIcwAIewAQjAAIawAAOQAAYwAQlAAQnAAhpQAQpQAhrQBCvRhj
+xjFjxjlSxiEpzgAYvQAQrQAYrQAhvQCU1mOt1nuE1lJK3hgh1gAYxgAYtQAAKQBCzhDO55Te563G
+55SU52NS5yEh3gAYzgBS3iGc52vW75y974yE71JC7xCt73ul3nNa7ykh5wAY1gAx5wBS7yFr7zlK
+7xgp5wAp7wAx7wAIhAAQtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAp1fnZAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu
+MT1evmgAAAFtSURBVHicddJtV8IgFAdwD2zIgMEE1+NcqdsoK+m5tCyz7/+ZiLmHsyzvq53zO/cy
++N9ery1bVe9PWQA9z4MQ+H8Yoj7GASZ95IHfaBGmLOSchyIgyOu22mgQSjUcDuNYcoGjLiLK1cHh
+0fHJaTKKOcMItgYxT89OzsfjyTTLC8UF0c2ZNmKquJhczq6ub+YmSVUYRF59GeDastu7+9nD41Nm
+kiJ2jc2J3kAWZ9Pr55fH18XSmRuKUTXUaqHy7O19tfr4NFle/w3YDrWRUIlZrL/W86XJkyJVG9Ea
+EjIx2XyZmZJGioeUaL+2AY8TY8omR6nkLKhu70zjUKVJXsp3quS2DVSJWNh3zzJKCyexI0ZxBP3a
+fE0ElyqOlZJyw8r3BE2SFiJCyxA434SCkg65RhdeQBljQtCg39LWrA90RDDG1EWrYUO23hMANUKR
+Rl61E529cR++D2G5LK002dr/qrcfu9u0V3bxn/XdhR/NYeeN0ggsLAAAACV0RVh0Q29tbWVudABj
+bGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII=
+
+------=_NextPart_000_0002_01BFC036.AE309650
+Content-Type: image/png
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="redball.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAVAAAa
+AAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACHAAB9AAB0
+AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABM
+AAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHfhITm
+f3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5PlrKzpmZntZWXvJSXXAADB
+AACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2
+AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABH
+AAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABPAAASAAAC
+AABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADIAADTAADNAACzAACDAABuAAAe
+AAB+AADAAACkAACNAAB/AABpAABQAAAwAACRAACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACs
+AACvAACtAACmAACJAAB6AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABV
+AACOAACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAM
+AAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAD8LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu
+MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iK
+iUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ
+29ja2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW
+SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2YnOAj+
+d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/uXLzVJ2q
+m6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqV
+tWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZw
+HBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/joOyYed5
+QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms1y9evXid7QZacgOxmSxktNzd
+tSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5
+IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg==
+
+------=_NextPart_000_0002_01BFC036.AE309650--
+--------------49CVLb1N6p6Spdka4qq7Naeg
+Content-Type: image/png;
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="yellowball.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgA
+AAAACCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQ
+MZQAGFIQMYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYY
+QsYQMaUAACHO5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9K
+e+8YOaUYSsaMvee15++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADB
+Mg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAGI
+SURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbsscebL5xznTsh
+5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18PyqqW
+Uw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC
+UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/M
+jRxmT6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1C
+SYoOiMOSGwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIom
+H3NCKX0lnI+B1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0N
+xW62p+lT+Yi747sD/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBi
+eSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII=
+
+--------------49CVLb1N6p6Spdka4qq7Naeg--
diff --git a/comm/mail/components/extensions/test/browser/messages/messageWithLink.eml b/comm/mail/components/extensions/test/browser/messages/messageWithLink.eml
new file mode 100644
index 0000000000..469a799f05
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/messages/messageWithLink.eml
@@ -0,0 +1,26 @@
+Message-ID: <sample.eml@mime.sample>
+Date: Fri, 20 May 2000 00:29:55 -0400
+To: Heinz <mueller@example.com>
+Cc: Robin <damian@wayne-enterprises.com>
+From: Batman <bruce@wayne-enterprises.com>
+Subject: Message with a link
+Mime-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------49CVLb1N6p6Spdka4qq7Naeg"
+
+This is a multi-part message in MIME format.
+--------------49CVLb1N6p6Spdka4qq7Naeg
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+
+<html>
+ <head>
+
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ </head>
+ <body>
+ <p>This is an interesting <a id="link" href="https://www.example.de/messageLink.html">link</a></p>
+ </body>
+</html>
+
+--------------49CVLb1N6p6Spdka4qq7Naeg--
diff --git a/comm/mail/components/extensions/test/browser/test_browserAction.js b/comm/mail/components/extensions/test/browser/test_browserAction.js
new file mode 100644
index 0000000000..209c701168
--- /dev/null
+++ b/comm/mail/components/extensions/test/browser/test_browserAction.js
@@ -0,0 +1,845 @@
+/* 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 account;
+let messages;
+
+add_setup(async () => {
+ account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ let subFolders = rootFolder.subFolders;
+ createMessages(subFolders[0], 10);
+ messages = subFolders[0].messages;
+});
+
+// This test uses a command from the menus API to open the popup.
+add_task(async function test_popup_open_with_menu_command_mv2() {
+ info("3-pane tab");
+ let testConfig = {
+ actionType: "browser_action",
+ testType: "open-with-menu-command",
+ window,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+
+ info("Message window");
+ {
+ let messageWindow = await openMessageInWindow(messages.getNext());
+ let testConfig = {
+ actionType: "browser_action",
+ testType: "open-with-menu-command",
+ default_windows: ["messageDisplay"],
+ window: messageWindow,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+ messageWindow.close();
+ }
+});
+
+add_task(async function test_popup_open_with_menu_command_mv3() {
+ info("3-pane tab");
+ let testConfig = {
+ manifest_version: 3,
+ actionType: "action",
+ testType: "open-with-menu-command",
+ window,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+
+ info("Message window");
+ {
+ let messageWindow = await openMessageInWindow(messages.getNext());
+ let testConfig = {
+ manifest_version: 3,
+ actionType: "action",
+ testType: "open-with-menu-command",
+ default_windows: ["messageDisplay"],
+ window: messageWindow,
+ };
+
+ await run_popup_test({
+ ...testConfig,
+ });
+ await run_popup_test({
+ ...testConfig,
+ use_default_popup: true,
+ });
+ await run_popup_test({
+ ...testConfig,
+ disable_button: true,
+ });
+ messageWindow.close();
+ }
+});
+
+add_task(async function test_theme_icons() {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: {
+ id: "browser_action_properties@mochi.test",
+ },
+ },
+ browser_action: {
+ default_title: "default",
+ default_icon: "default.png",
+ theme_icons: [
+ {
+ dark: "dark.png",
+ light: "light.png",
+ size: 16,
+ },
+ ],
+ },
+ },
+ });
+
+ let unifiedToolbarUpdate = TestUtils.topicObserved(
+ "unified-toolbar-state-change"
+ );
+ await extension.startup();
+ await unifiedToolbarUpdate;
+ await TestUtils.waitForCondition(
+ () =>
+ document.querySelector(
+ `#unifiedToolbarContent [extension="browser_action_properties@mochi.test"]`
+ ),
+ "Button added to unified toolbar"
+ );
+
+ let uuid = extension.uuid;
+ let icon = document.querySelector(
+ `#unifiedToolbarContent [extension="browser_action_properties@mochi.test"] .button-icon`
+ );
+
+ let dark_theme = await AddonManager.getAddonByID(
+ "thunderbird-compact-dark@mozilla.org"
+ );
+ await dark_theme.enable();
+ Assert.equal(
+ window.getComputedStyle(icon).content,
+ `url("moz-extension://${uuid}/light.png")`,
+ `Dark theme should use light icon.`
+ );
+
+ let light_theme = await AddonManager.getAddonByID(
+ "thunderbird-compact-light@mozilla.org"
+ );
+ await light_theme.enable();
+ Assert.equal(
+ window.getComputedStyle(icon).content,
+ `url("moz-extension://${uuid}/dark.png")`,
+ `Light theme should use dark icon.`
+ );
+
+ // Disabling a theme will enable the default theme.
+ await light_theme.disable();
+ Assert.equal(
+ window.getComputedStyle(icon).content,
+ `url("moz-extension://${uuid}/default.png")`,
+ `Default theme should use default icon.`
+ );
+ await extension.unload();
+});
+
+add_task(async function test_theme_icons_messagewindow() {
+ let messageWindow = await openMessageInWindow(messages.getNext());
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: {
+ id: "browser_action_properties@mochi.test",
+ },
+ },
+ browser_action: {
+ default_title: "default",
+ default_icon: "default.png",
+ default_windows: ["messageDisplay"],
+ theme_icons: [
+ {
+ dark: "dark.png",
+ light: "light.png",
+ size: 16,
+ },
+ ],
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let uuid = extension.uuid;
+ let button = messageWindow.document.getElementById(
+ "browser_action_properties_mochi_test-browserAction-toolbarbutton"
+ );
+
+ let dark_theme = await AddonManager.getAddonByID(
+ "thunderbird-compact-dark@mozilla.org"
+ );
+ await dark_theme.enable();
+ Assert.equal(
+ window.getComputedStyle(button).listStyleImage,
+ `url("moz-extension://${uuid}/light.png")`,
+ `Dark theme should use light icon.`
+ );
+
+ let light_theme = await AddonManager.getAddonByID(
+ "thunderbird-compact-light@mozilla.org"
+ );
+ await light_theme.enable();
+ Assert.equal(
+ window.getComputedStyle(button).listStyleImage,
+ `url("moz-extension://${uuid}/dark.png")`,
+ `Light theme should use dark icon.`
+ );
+
+ // Disabling a theme will enable the default theme.
+ await light_theme.disable();
+ Assert.equal(
+ window.getComputedStyle(button).listStyleImage,
+ `url("moz-extension://${uuid}/default.png")`,
+ `Default theme should use default icon.`
+ );
+
+ await extension.unload();
+ messageWindow.close();
+});
+
+add_task(async function test_button_order() {
+ info("3-pane tab");
+ await run_action_button_order_test(
+ [
+ {
+ name: "addon1",
+ toolbar: "unified-toolbar",
+ },
+ {
+ name: "addon2",
+ toolbar: "unified-toolbar",
+ },
+ {
+ name: "addon3",
+ toolbar: "unified-toolbar",
+ },
+ {
+ name: "addon4",
+ toolbar: "unified-toolbar",
+ },
+ ],
+ window,
+ "browser_action"
+ );
+
+ info("Message window");
+ let messageWindow = await openMessageInWindow(messages.getNext());
+ await run_action_button_order_test(
+ [
+ {
+ name: "addon1",
+ toolbar: "mail-bar3",
+ default_windows: ["messageDisplay"],
+ },
+ {
+ name: "addon2",
+ toolbar: "mail-bar3",
+ default_windows: ["messageDisplay"],
+ },
+ ],
+ messageWindow,
+ "browser_action"
+ );
+ messageWindow.close();
+});
+
+add_task(async function test_upgrade() {
+ // Add a browser_action, to make sure the currentSet has been initialized.
+ let extension1 = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ manifest_version: 2,
+ version: "1.0",
+ name: "Extension1",
+ applications: { gecko: { id: "Extension1@mochi.test" } },
+ browser_action: {
+ default_title: "Extension1",
+ },
+ },
+ background() {
+ browser.test.sendMessage("Extension1 ready");
+ },
+ });
+ await extension1.startup();
+ await extension1.awaitMessage("Extension1 ready");
+
+ // Add extension without a browser_action.
+ let extension2 = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ manifest_version: 2,
+ version: "1.0",
+ name: "Extension2",
+ applications: { gecko: { id: "Extension2@mochi.test" } },
+ },
+ background() {
+ browser.test.sendMessage("Extension2 ready");
+ },
+ });
+ await extension2.startup();
+ await extension2.awaitMessage("Extension2 ready");
+
+ // Update the extension, now including a browser_action.
+ let updatedExtension2 = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ manifest_version: 2,
+ version: "2.0",
+ name: "Extension2",
+ applications: { gecko: { id: "Extension2@mochi.test" } },
+ browser_action: {
+ default_title: "Extension2",
+ },
+ },
+ background() {
+ browser.test.sendMessage("Extension2 updated");
+ },
+ });
+ await updatedExtension2.startup();
+ await updatedExtension2.awaitMessage("Extension2 updated");
+
+ let button = document.querySelector(
+ `.unified-toolbar [extension="Extension2@mochi.test"]`
+ );
+
+ Assert.ok(button, "Button should exist");
+
+ await extension1.unload();
+ await extension2.unload();
+ await updatedExtension2.unload();
+});
+
+add_task(async function test_iconPath() {
+ // String values for the default_icon manifest entry have been tested in the
+ // theme_icons test already. Here we test imagePath objects for the manifest key
+ // and string values as well as objects for the setIcons() function.
+ let files = {
+ "background.js": async () => {
+ await window.sendMessage("checkState", "icon1.png");
+
+ await browser.browserAction.setIcon({ path: "icon2.png" });
+ await window.sendMessage("checkState", "icon2.png");
+
+ await browser.browserAction.setIcon({ path: { 16: "icon3.png" } });
+ await window.sendMessage("checkState", "icon3.png");
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: {
+ id: "browser_action@mochi.test",
+ },
+ },
+ browser_action: {
+ default_title: "default",
+ default_icon: { 16: "icon1.png" },
+ },
+ background: { scripts: ["utils.js", "background.js"] },
+ },
+ });
+
+ extension.onMessage("checkState", async expected => {
+ let uuid = extension.uuid;
+ let icon = document.querySelector(
+ `.unified-toolbar [extension="browser_action@mochi.test"] .button-icon`
+ );
+
+ Assert.equal(
+ window.getComputedStyle(icon).content,
+ `url("moz-extension://${uuid}/${expected}")`,
+ `Icon path should be correct.`
+ );
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_allowedSpaces() {
+ let tabmail = document.getElementById("tabmail");
+ let unifiedToolbar = document.querySelector("unified-toolbar");
+
+ function buttonInUnifiedToolbar() {
+ let button = unifiedToolbar.querySelector(
+ '[item-id="ext-browser_action_spaces@mochi.test"]'
+ );
+ if (!button) {
+ return false;
+ }
+ return BrowserTestUtils.is_visible(button);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: {
+ id: "browser_action_spaces@mochi.test",
+ },
+ },
+ browser_action: {
+ allowed_spaces: ["calendar", "default"],
+ },
+ },
+ });
+
+ let mailSpace = window.gSpacesToolbar.spaces.find(
+ space => space.name == "mail"
+ );
+ window.gSpacesToolbar.openSpace(tabmail, mailSpace);
+
+ let unifiedToolbarUpdate = TestUtils.topicObserved(
+ "unified-toolbar-state-change"
+ );
+
+ await extension.startup();
+ await unifiedToolbarUpdate;
+
+ ok(
+ !buttonInUnifiedToolbar(),
+ "Button shouldn't be in the mail space toolbar"
+ );
+
+ let toolbarMutation = BrowserTestUtils.waitForMutationCondition(
+ unifiedToolbar,
+ { childList: true },
+ () => true
+ );
+ window.gSpacesToolbar.openSpace(
+ tabmail,
+ window.gSpacesToolbar.spaces.find(space => space.name == "calendar")
+ );
+ await toolbarMutation;
+
+ ok(
+ buttonInUnifiedToolbar(),
+ "Button should be in the calendar space toolbar"
+ );
+
+ tabmail.closeTab();
+ toolbarMutation = BrowserTestUtils.waitForMutationCondition(
+ unifiedToolbar,
+ { childList: true },
+ () => true
+ );
+ tabmail.openTab("contentTab", { url: "about:blank" });
+ await toolbarMutation;
+
+ ok(buttonInUnifiedToolbar(), "Button should be in the default space toolbar");
+
+ tabmail.closeTab();
+ toolbarMutation = BrowserTestUtils.waitForMutationCondition(
+ unifiedToolbar,
+ { childList: true },
+ () => true
+ );
+ window.gSpacesToolbar.openSpace(tabmail, mailSpace);
+ await toolbarMutation;
+
+ ok(
+ !buttonInUnifiedToolbar(),
+ "Button should be hidden again in the mail space toolbar"
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_allowedInAllSpaces() {
+ let tabmail = document.getElementById("tabmail");
+ let unifiedToolbar = document.querySelector("unified-toolbar");
+
+ function buttonInUnifiedToolbar() {
+ let button = unifiedToolbar.querySelector(
+ '[item-id="ext-browser_action_all_spaces@mochi.test"]'
+ );
+ if (!button) {
+ return false;
+ }
+ return BrowserTestUtils.is_visible(button);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: {
+ id: "browser_action_all_spaces@mochi.test",
+ },
+ },
+ browser_action: {
+ allowed_spaces: [],
+ },
+ },
+ });
+
+ let mailSpace = window.gSpacesToolbar.spaces.find(
+ space => space.name == "mail"
+ );
+ window.gSpacesToolbar.openSpace(tabmail, mailSpace);
+
+ let unifiedToolbarUpdate = TestUtils.topicObserved(
+ "unified-toolbar-state-change"
+ );
+
+ await extension.startup();
+ await unifiedToolbarUpdate;
+
+ ok(buttonInUnifiedToolbar(), "Button should be in the mail space toolbar");
+
+ let toolbarMutation = BrowserTestUtils.waitForMutationCondition(
+ unifiedToolbar,
+ { childList: true },
+ () => true
+ );
+ window.gSpacesToolbar.openSpace(
+ tabmail,
+ window.gSpacesToolbar.spaces.find(space => space.name == "calendar")
+ );
+ await toolbarMutation;
+
+ ok(
+ buttonInUnifiedToolbar(),
+ "Button should be in the calendar space toolbar"
+ );
+
+ tabmail.closeTab();
+ toolbarMutation = BrowserTestUtils.waitForMutationCondition(
+ unifiedToolbar,
+ { childList: true },
+ () => true
+ );
+ tabmail.openTab("contentTab", { url: "about:blank" });
+ await toolbarMutation;
+
+ ok(buttonInUnifiedToolbar(), "Button should be in the default space toolbar");
+
+ tabmail.closeTab();
+ toolbarMutation = BrowserTestUtils.waitForMutationCondition(
+ unifiedToolbar,
+ { childList: true },
+ () => true
+ );
+ window.gSpacesToolbar.openSpace(tabmail, mailSpace);
+ await toolbarMutation;
+
+ ok(
+ buttonInUnifiedToolbar(),
+ "Button should still be in the mail space toolbar"
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_allowedSpacesDefault() {
+ let tabmail = document.getElementById("tabmail");
+ let unifiedToolbar = document.querySelector("unified-toolbar");
+
+ function buttonInUnifiedToolbar() {
+ let button = unifiedToolbar.querySelector(
+ '[item-id="ext-browser_action_default_spaces@mochi.test"]'
+ );
+ if (!button) {
+ return false;
+ }
+ return BrowserTestUtils.is_visible(button);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: {
+ id: "browser_action_default_spaces@mochi.test",
+ },
+ },
+ browser_action: {
+ default_title: "Test Action",
+ },
+ },
+ });
+
+ let mailSpace = window.gSpacesToolbar.spaces.find(
+ space => space.name == "mail"
+ );
+ window.gSpacesToolbar.openSpace(tabmail, mailSpace);
+
+ let unifiedToolbarUpdate = TestUtils.topicObserved(
+ "unified-toolbar-state-change"
+ );
+
+ await extension.startup();
+ await unifiedToolbarUpdate;
+
+ ok(buttonInUnifiedToolbar(), "Button should be in the mail space toolbar");
+
+ let toolbarMutation = BrowserTestUtils.waitForMutationCondition(
+ unifiedToolbar,
+ { childList: true },
+ () => true
+ );
+ window.gSpacesToolbar.openSpace(
+ tabmail,
+ window.gSpacesToolbar.spaces.find(space => space.name == "calendar")
+ );
+ await toolbarMutation;
+
+ ok(
+ !buttonInUnifiedToolbar(),
+ "Button should not be in the calendar space toolbar"
+ );
+
+ tabmail.closeTab();
+ toolbarMutation = BrowserTestUtils.waitForMutationCondition(
+ unifiedToolbar,
+ { childList: true },
+ () => true
+ );
+ tabmail.openTab("contentTab", { url: "about:blank" });
+ await toolbarMutation;
+
+ ok(
+ !buttonInUnifiedToolbar(),
+ "Button should not be in the default space toolbar"
+ );
+
+ tabmail.closeTab();
+ toolbarMutation = BrowserTestUtils.waitForMutationCondition(
+ unifiedToolbar,
+ { childList: true },
+ () => true
+ );
+ window.gSpacesToolbar.openSpace(tabmail, mailSpace);
+ await toolbarMutation;
+
+ ok(
+ buttonInUnifiedToolbar(),
+ "Button should still be in the mail space toolbar again"
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_update_allowedSpaces() {
+ let tabmail = document.getElementById("tabmail");
+ let unifiedToolbar = document.querySelector("unified-toolbar");
+
+ function buttonInUnifiedToolbar() {
+ let button = unifiedToolbar.querySelector(
+ '[item-id="ext-browser_action_spaces@mochi.test"]'
+ );
+ if (!button) {
+ return false;
+ }
+ return BrowserTestUtils.is_visible(button);
+ }
+
+ async function closeSpaceTab() {
+ let toolbarMutation = BrowserTestUtils.waitForMutationCondition(
+ unifiedToolbar,
+ { childList: true },
+ () => true
+ );
+ tabmail.closeTab();
+ await toolbarMutation;
+ }
+
+ async function ensureActiveMailSpace() {
+ let mailSpace = window.gSpacesToolbar.spaces.find(
+ space => space.name == "mail"
+ );
+ if (window.gSpacesToolbar.currentSpace != mailSpace) {
+ let toolbarMutation = BrowserTestUtils.waitForMutationCondition(
+ unifiedToolbar,
+ { childList: true },
+ () => true
+ );
+ window.gSpacesToolbar.openSpace(tabmail, mailSpace);
+ await toolbarMutation;
+ }
+ }
+
+ async function checkUnifiedToolbar(extension, expectedSpaces) {
+ // Make sure the mail space is open.
+ await ensureActiveMailSpace();
+
+ let unifiedToolbarUpdate = TestUtils.topicObserved(
+ "unified-toolbar-state-change"
+ );
+ await extension.startup();
+ await unifiedToolbarUpdate;
+
+ // Test mail space.
+ {
+ let expected = expectedSpaces.includes("mail");
+ Assert.equal(
+ buttonInUnifiedToolbar(),
+ expected,
+ `Button should${expected ? " " : " not "}be in the mail space toolbar`
+ );
+ }
+
+ // Test calendar space.
+ {
+ let toolbarMutation = BrowserTestUtils.waitForMutationCondition(
+ unifiedToolbar,
+ { childList: true },
+ () => true
+ );
+ window.gSpacesToolbar.openSpace(
+ tabmail,
+ window.gSpacesToolbar.spaces.find(space => space.name == "calendar")
+ );
+ await toolbarMutation;
+
+ let expected = expectedSpaces.includes("calendar");
+ Assert.equal(
+ buttonInUnifiedToolbar(),
+ expected,
+ `Button should${
+ expected ? " " : " not "
+ }be in the calendar space toolbar`
+ );
+ await closeSpaceTab();
+ }
+
+ // Test default space.
+ {
+ let toolbarMutation = BrowserTestUtils.waitForMutationCondition(
+ unifiedToolbar,
+ { childList: true },
+ () => true
+ );
+ tabmail.openTab("contentTab", { url: "about:blank" });
+ await toolbarMutation;
+
+ let expected = expectedSpaces.includes("default");
+ Assert.equal(
+ buttonInUnifiedToolbar(),
+ expected,
+ `Button should${
+ expected ? " " : " not "
+ }be in the default space toolbar`
+ );
+ await closeSpaceTab();
+ }
+
+ // Test mail space again.
+ {
+ await ensureActiveMailSpace();
+ let expected = expectedSpaces.includes("mail");
+ Assert.equal(
+ buttonInUnifiedToolbar(),
+ expected,
+ `Button should${expected ? " " : " not "}be in the mail space toolbar`
+ );
+ }
+ }
+
+ // Install extension and test that the button is shown in the default space and
+ // in the calendar space.
+ let extension1 = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ applications: {
+ gecko: {
+ id: "browser_action_spaces@mochi.test",
+ },
+ },
+ browser_action: {
+ allowed_spaces: ["calendar", "default"],
+ },
+ },
+ });
+ await checkUnifiedToolbar(extension1, ["calendar", "default"]);
+
+ // Update extension by installing a newer version on top. Verify that it is now
+ // also shown in the mail space.
+ let extension2 = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ applications: {
+ gecko: {
+ id: "browser_action_spaces@mochi.test",
+ },
+ },
+ browser_action: {
+ allowed_spaces: ["mail", "calendar", "default"],
+ },
+ },
+ });
+ await checkUnifiedToolbar(extension2, ["mail", "calendar", "default"]);
+
+ // Update extension by installing a newer version on top. Verify that it is now
+ // no longer shown in the calendar space.
+ let extension3 = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ applications: {
+ gecko: {
+ id: "browser_action_spaces@mochi.test",
+ },
+ },
+ browser_action: {
+ allowed_spaces: ["mail", "default"],
+ },
+ },
+ });
+ await checkUnifiedToolbar(extension3, ["mail", "default"]);
+
+ await extension1.unload();
+ await extension2.unload();
+ await extension3.unload();
+});